Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-18 15:06:23 +04:00
commit ab914dd37a
46 changed files with 740 additions and 1006 deletions

View file

@ -16,7 +16,6 @@ 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.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.details.redux.SecurityOption
import org.rekotlin.Action
sealed class GlobalAction : Action {
@ -75,7 +74,6 @@ sealed class GlobalAction : Action {
) : GlobalAction()
data class HideWarningMessage(val warning: WarningMessage) : GlobalAction()
data class UpdateSecurityOptions(val securityOption: SecurityOption) : GlobalAction()
data class SetConfigManager(val configManager: ConfigManager) : GlobalAction()
data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction()

View file

@ -4,7 +4,6 @@ import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
@ -21,7 +20,6 @@ object DemoHelper {
private val disabledActionFeatures = listOf(
WalletConnectAction.StartWalletConnect::class.java,
BackupAction.StartBackup::class.java,
DetailsAction.ResetToFactory.Start::class.java,
)
fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId)

View file

@ -14,26 +14,6 @@ sealed class DetailsAction : Action {
val shouldSaveUserWallets: Boolean,
) : DetailsAction()
data object ReCreateTwinsWallet : DetailsAction()
sealed class ResetToFactory : DetailsAction() {
data object Start : ResetToFactory()
data object Proceed : ResetToFactory()
data class AcceptCondition1(val accepted: Boolean) : ResetToFactory()
data class AcceptCondition2(val accepted: Boolean) : ResetToFactory()
data object Failure : ResetToFactory()
data object Success : ResetToFactory()
data class ShowDialog(val dialog: CardSettingsState.Dialog) : ResetToFactory()
data object DismissDialog : ResetToFactory()
}
data object ScanCard : DetailsAction()
data class PrepareCardSettingsData(val scanResponse: ScanResponse) : DetailsAction()
data object ResetCardSettingsData : DetailsAction()
data object ScanAndSaveUserWallet : DetailsAction() {
data object Success : DetailsAction()
@ -43,26 +23,6 @@ sealed class DetailsAction : Action {
data object DismissError : DetailsAction()
sealed class AccessCodeRecovery : DetailsAction() {
object Open : AccessCodeRecovery()
data class SaveChanges(val enabled: Boolean) : AccessCodeRecovery() {
data class Success(val enabled: Boolean) : AccessCodeRecovery()
}
data class SelectOption(val enabled: Boolean) : AccessCodeRecovery()
}
sealed class ManageSecurity : DetailsAction() {
data object OpenSecurity : ManageSecurity()
data class SelectOption(val option: SecurityOption) : ManageSecurity()
data object SaveChanges : ManageSecurity() {
data object Success : ManageSecurity()
data object Failure : ManageSecurity()
}
data object ChangeAccessCode : ManageSecurity()
}
sealed class AppSettings : DetailsAction() {
data class SwitchPrivacySetting(
val enable: Boolean,

View file

@ -1,40 +1,36 @@
package com.tangem.tap.features.details.redux
import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.common.*
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
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.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
@ -42,17 +38,13 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
class DetailsMiddleware {
private val eraseWalletMiddleware = EraseWalletMiddleware()
private val manageSecurityMiddleware = ManageSecurityMiddleware()
private val appSettingsMiddleware = AppSettingsMiddleware()
private val accessCodeRecoveryMiddleware = AccessCodeRecoveryMiddleware()
val detailsMiddleware: Middleware<AppState> = { _, stateProvider ->
{ next ->
{ action ->
@ -69,153 +61,11 @@ class DetailsMiddleware {
private fun handleAction(state: DetailsState, action: Action) {
when (action) {
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action, state)
is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, action)
is DetailsAction.ReCreateTwinsWallet -> {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) }
}
is DetailsAction.AccessCodeRecovery -> accessCodeRecoveryMiddleware.handle(state, action)
is DetailsAction.ScanCard -> scanCard(state)
is DetailsAction.ScanAndSaveUserWallet -> scanAndSaveUserWallet()
}
}
class EraseWalletMiddleware {
@Suppress("CyclomaticComplexMethod")
fun handle(action: DetailsAction.ResetToFactory) {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
when (action) {
is DetailsAction.ResetToFactory.Start -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
if (card.isTangemTwins) {
store.dispatch(DetailsAction.ReCreateTwinsWallet)
return
} else {
store.dispatchNavigationAction { push(AppRoute.ResetToFactory) }
}
}
is DetailsAction.ResetToFactory.Proceed -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
scope.launch {
val userWalletId = UserWalletIdBuilder.card(card).build()
// we must require a password regardless of biometric settings
val policy = tangemSdkManager.userCodeRequestPolicy
val doBeforeErase = {
val type = if (card.isAccessCodeSet) {
UserCodeType.AccessCode
} else if (card.isPasscodeSet == true) {
UserCodeType.Passcode
} else {
null
}
type?.let {
tangemSdkManager.setUserCodeRequestPolicy(UserCodeRequestPolicy.Always(type))
}
}
val doAfterErase = {
tangemSdkManager.setUserCodeRequestPolicy(policy)
}
doBeforeErase()
tangemSdkManager.resetToFactorySettings(card.cardId, true)
.flatMap {
userWalletsListManager.delete(listOfNotNull(userWalletId))
}
.flatMap {
tangemSdkManager.deleteSavedUserCodes(setOf(card.cardId))
}
.doOnSuccess {
Analytics.send(Settings.CardSettings.FactoryResetFinished())
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
store.onUserWalletSelected(selectedUserWallet)
} else {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync }
.fold(onSuccess = { true }, onFailure = { false })
if (isLocked && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
} else {
store.dispatchNavigationAction { replaceAll(AppRoute.Home) }
}
}
}
.doOnFailure { error ->
if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
Analytics.send(Settings.CardSettings.FactoryResetFinished(error = error))
}
}
.doOnResult {
doAfterErase()
}
}
}
else -> Unit
}
}
}
class ManageSecurityMiddleware {
@Suppress("ComplexMethod")
fun handle(action: DetailsAction.ManageSecurity, detailsState: DetailsState) {
when (action) {
is DetailsAction.ManageSecurity.OpenSecurity -> {
store.dispatchNavigationAction { push(AppRoute.DetailsSecurity) }
}
is DetailsAction.ManageSecurity.SaveChanges -> {
val cardSettingsState = detailsState.cardSettingsState
val cardId = cardSettingsState?.card?.cardId
val selectedOption = cardSettingsState?.manageSecurityState?.selectedOption
scope.launch {
val result = when (selectedOption) {
SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId)
SecurityOption.PassCode -> tangemSdkManager.setPasscode(cardId)
SecurityOption.AccessCode -> tangemSdkManager.setAccessCode(cardId)
else -> return@launch
}
withContext(Dispatchers.Main) {
val paramValue = AnalyticsParam.SecurityMode.from(selectedOption)
when (result) {
is CompletionResult.Success -> {
Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue))
store.dispatch(GlobalAction.UpdateSecurityOptions(selectedOption))
store.dispatchNavigationAction(AppRouter::pop)
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Success)
}
is CompletionResult.Failure -> {
val error = result.error
if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue, error))
}
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure)
}
else -> Unit
}
}
}
}
is DetailsAction.ManageSecurity.ChangeAccessCode -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
scope.launch {
when (tangemSdkManager.setAccessCode(card.cardId)) {
is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged())
is CompletionResult.Failure -> {}
}
}
}
else -> Unit
}
}
}
class AppSettingsMiddleware {
private val checkBiometricsStatusJobHolder = JobHolder()
@ -431,63 +281,6 @@ class DetailsMiddleware {
}
}
class AccessCodeRecoveryMiddleware {
fun handle(state: DetailsState, action: DetailsAction.AccessCodeRecovery) {
when (action) {
is DetailsAction.AccessCodeRecovery.Open -> {
Analytics.send(Settings.CardSettings.AccessCodeRecoveryButton())
store.dispatchNavigationAction { push(AppRoute.AccessCodeRecovery) }
}
is DetailsAction.AccessCodeRecovery.SaveChanges -> {
scope.launch {
tangemSdkManager
.setAccessCodeRecoveryEnabled(state.cardSettingsState?.card?.cardId, action.enabled)
.doOnSuccess {
Analytics.send(
Settings.CardSettings.AccessCodeRecoveryChanged(
AnalyticsParam.AccessCodeRecoveryStatus.from(action.enabled),
),
)
store.dispatchNavigationAction(AppRouter::pop)
store.dispatchOnMain(
DetailsAction.AccessCodeRecovery.SaveChanges.Success(action.enabled),
)
}
}
}
is DetailsAction.AccessCodeRecovery.SelectOption -> Unit
is DetailsAction.AccessCodeRecovery.SaveChanges.Success -> Unit
}
}
}
private fun scanCard(state: DetailsState) = scope.launch {
store.inject(DaggerGraphState::scanCardProcessor)
.scan(allowsRequestAccessCodeFromRepository = true)
.doOnSuccess { scanResponse ->
// if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
val currentUserWalletId = state.scanResponse
?.let { UserWalletIdBuilder.scanResponse(it).build() }
val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse)
.build()
val isSameWallet = currentUserWalletId == scannedUserWalletId
if (isSameWallet) {
store.dispatchOnMain(
DetailsAction.PrepareCardSettingsData(scanResponse = scanResponse),
)
} else {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
headerId = R.string.common_warning,
messageId = R.string.error_wrong_wallet_tapped,
),
)
}
}
}
private fun scanAndSaveUserWallet() = scope.launch(Dispatchers.IO) {
val cardSdkConfigRepository = store.inject(DaggerGraphState::cardSdkConfigRepository)

View file

@ -1,21 +1,15 @@
package com.tangem.tap.features.details.redux
import com.tangem.common.routing.AppRoute
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.runBlocking
import org.rekotlin.Action
import java.util.EnumSet
object DetailsReducer {
fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state)
@ -29,16 +23,6 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
is DetailsAction.PrepareScreen -> {
handlePrepareScreen(action, state)
}
is DetailsAction.PrepareCardSettingsData -> {
handlePrepareCardSettingsScreen(scanResponse = action.scanResponse, state = detailsState)
}
is DetailsAction.ResetCardSettingsData -> detailsState.copy(cardSettingsState = null)
is DetailsAction.ResetToFactory -> {
handleEraseWallet(action, detailsState)
}
is DetailsAction.ManageSecurity -> {
handleSecurityAction(action, detailsState)
}
is DetailsAction.AppSettings -> {
handlePrivacyAction(action, detailsState)
}
@ -47,7 +31,6 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
selectedAppCurrency = action.currency,
),
)
is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState)
is DetailsAction.ScanAndSaveUserWallet -> detailsState.copy(
isScanningInProgress = true,
)
@ -61,21 +44,12 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
is DetailsAction.DismissError -> detailsState.copy(
error = null,
)
else -> detailsState
}
}
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppState): DetailsState {
val router = store.inject(DaggerGraphState::appRouter)
return DetailsState(
scanResponse = action.scanResponse,
// If the current screen is ResetToFactory, we must save the ResetToFactory's state
cardSettingsState = if (router.stack.lastOrNull() is AppRoute.ResetToFactory) {
state.detailsState.cardSettingsState
} else {
null
},
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
appSettingsState = AppSettingsState(
isBiometricsAvailable = runBlocking {
@ -98,139 +72,6 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppS
)
}
private fun handlePrepareCardSettingsScreen(scanResponse: ScanResponse, state: DetailsState): DetailsState {
val cardTypesResolver = scanResponse.cardTypesResolver
val card = scanResponse.card
val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2()
val isShowPasswordResetRadioButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active
val cardSettingsState = CardSettingsState(
cardInfo = card.toCardInfo(cardTypesResolver),
scanResponse = scanResponse,
manageSecurityState = prepareSecurityOptions(card, cardTypesResolver),
card = scanResponse.card,
resetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver),
resetButtonEnabled = false,
condition1Checked = false,
condition2Checked = false,
accessCodeRecovery = if (cardTypesResolver.isWallet2()) {
val enabled = card.userSettings?.isUserCodeRecoveryAllowed ?: false
AccessCodeRecoveryState(
enabledOnCard = enabled,
enabledSelection = enabled,
)
} else {
null
},
isShowPasswordResetRadioButton = isShowPasswordResetRadioButton,
dialog = null,
)
return state.copy(cardSettingsState = cardSettingsState)
}
private fun prepareSecurityOptions(card: CardDTO, cardTypesResolver: CardTypesResolver): ManageSecurityState {
val securityOption = when {
card.isAccessCodeSet -> {
SecurityOption.AccessCode
}
card.isPasscodeSet == true -> {
SecurityOption.PassCode
}
else -> {
SecurityOption.LongTap
}
}
val allowedSecurityOptions = when {
cardTypesResolver.isStart2Coin() || cardTypesResolver.isTangemNote() -> {
EnumSet.of(SecurityOption.LongTap)
}
card.settings.isBackupAllowed -> {
EnumSet.of(securityOption)
}
else ->
prepareAllowedSecurityOptions(cardTypesResolver = cardTypesResolver, currentSecurityOption = securityOption)
}
return ManageSecurityState(
currentOption = securityOption,
allowedOptions = allowedSecurityOptions,
selectedOption = securityOption,
)
}
private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: CardTypesResolver): Boolean {
val hasPermanentWallet = card.wallets.any { it.settings.isPermanent }
val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin()
return !isNotAllowed
}
private fun handleEraseWallet(action: DetailsAction.ResetToFactory, state: DetailsState): DetailsState {
val cardSettingsState = state.cardSettingsState
return when (action) {
is DetailsAction.ResetToFactory.AcceptCondition1 -> {
val warning1Checked = action.accepted
val resetButtonEnabled = if (cardSettingsState?.isShowPasswordResetRadioButton == true) {
warning1Checked && cardSettingsState.condition2Checked
} else {
warning1Checked
}
state.copy(
cardSettingsState = cardSettingsState?.copy(
condition1Checked = action.accepted,
resetButtonEnabled = resetButtonEnabled,
),
)
}
is DetailsAction.ResetToFactory.AcceptCondition2 -> {
val warning2Checked = action.accepted
state.copy(
cardSettingsState = cardSettingsState?.copy(
condition2Checked = action.accepted,
resetButtonEnabled = warning2Checked && cardSettingsState.condition1Checked,
),
)
}
is DetailsAction.ResetToFactory.ShowDialog -> {
state.copy(cardSettingsState = cardSettingsState?.copy(dialog = action.dialog))
}
is DetailsAction.ResetToFactory.DismissDialog -> {
state.copy(cardSettingsState = cardSettingsState?.copy(dialog = null))
}
else -> state
}
}
private fun handleSecurityAction(action: DetailsAction.ManageSecurity, state: DetailsState): DetailsState {
return when (action) {
is DetailsAction.ManageSecurity.SelectOption -> {
val manageSecurityState = state.cardSettingsState?.manageSecurityState?.copy(
selectedOption = action.option,
)
state.copy(cardSettingsState = state.cardSettingsState?.copy(manageSecurityState = manageSecurityState))
}
is DetailsAction.ManageSecurity.SaveChanges.Success -> {
// Setting options to show only LongTap from now on for non-twins
val manageSecurityState = state.cardSettingsState?.manageSecurityState?.copy(
currentOption = state.cardSettingsState.manageSecurityState.selectedOption,
allowedOptions = state.scanResponse?.cardTypesResolver?.let {
prepareAllowedSecurityOptions(
cardTypesResolver = it,
currentSecurityOption = state.cardSettingsState.manageSecurityState.selectedOption,
)
} ?: EnumSet.of(SecurityOption.LongTap),
)
state.copy(
cardSettingsState = state.cardSettingsState?.copy(
manageSecurityState = manageSecurityState,
),
)
}
else -> state
}
}
private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState {
return when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy(
@ -290,65 +131,4 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
is DetailsAction.AppSettings.CheckBiometricsStatus,
-> state
}
}
private fun handleAccessCodeRecoveryAction(
action: DetailsAction.AccessCodeRecovery,
state: DetailsState,
): DetailsState {
return when (action) {
DetailsAction.AccessCodeRecovery.Open -> {
val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy(
enabledSelection = state.cardSettingsState.accessCodeRecovery.enabledOnCard,
)
state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery))
}
is DetailsAction.AccessCodeRecovery.SaveChanges -> state
is DetailsAction.AccessCodeRecovery.SelectOption -> {
val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy(
enabledSelection = action.enabled,
)
state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery))
}
is DetailsAction.AccessCodeRecovery.SaveChanges.Success -> {
val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy(
enabledOnCard = action.enabled,
enabledSelection = action.enabled,
)
state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery))
}
}
}
private fun prepareAllowedSecurityOptions(
cardTypesResolver: CardTypesResolver,
currentSecurityOption: SecurityOption?,
): EnumSet<SecurityOption> {
val allowedSecurityOptions = EnumSet.of(SecurityOption.LongTap)
if (cardTypesResolver.isTangemTwins()) {
allowedSecurityOptions.add(SecurityOption.PassCode)
}
if (currentSecurityOption == SecurityOption.AccessCode) {
allowedSecurityOptions.add(SecurityOption.AccessCode)
}
if (currentSecurityOption == SecurityOption.PassCode) {
allowedSecurityOptions.add(SecurityOption.PassCode)
}
return allowedSecurityOptions
}
@Suppress("MagicNumber")
private fun CardDTO.toCardInfo(cardTypesResolver: CardTypesResolver): CardInfo {
val cardId = this.cardId.chunked(4).joinToString(separator = " ")
val issuer = this.issuer.name
val signedHashes = this.signedHashesCount()
return CardInfo(
cardId = cardId,
issuer = issuer,
signedHashes = signedHashes,
isTwin = cardTypesResolver.isTangemTwins(),
hasBackup = backupStatus?.isActive == true,
)
}

View file

@ -3,15 +3,11 @@ package com.tangem.tap.features.details.redux
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.entities.Button
import org.rekotlin.StateType
import java.util.EnumSet
data class DetailsState(
val scanResponse: ScanResponse? = null,
val cardSettingsState: CardSettingsState? = null,
val privacyPolicyUrl: String? = null,
val createBackupAllowed: Boolean = false,
val isScanningInProgress: Boolean = false,
@ -19,52 +15,13 @@ data class DetailsState(
val appSettingsState: AppSettingsState = AppSettingsState(),
) : StateType
data class CardInfo(
val cardId: String,
val issuer: String,
val signedHashes: Int,
val isTwin: Boolean,
val hasBackup: Boolean,
)
/**
* @property enabledOnCard whether access code recovery is enabled on card
* @property enabledSelection current selected option in app (not saved on card yet)
*/
data class AccessCodeRecoveryState(
val enabledOnCard: Boolean,
val enabledSelection: Boolean,
)
data class CardSettingsState(
val cardInfo: CardInfo,
val card: CardDTO,
val scanResponse: ScanResponse,
val manageSecurityState: ManageSecurityState?,
val resetCardAllowed: Boolean,
val resetButtonEnabled: Boolean,
val condition1Checked: Boolean,
val condition2Checked: Boolean,
val accessCodeRecovery: AccessCodeRecoveryState? = null,
val isShowPasswordResetRadioButton: Boolean,
val dialog: Dialog?,
) {
sealed interface Dialog {
data object StartResetDialog : Dialog
data object ContinueResetDialog : Dialog
data object InterruptedResetDialog : Dialog
data object CompletedResetDialog : Dialog
}
sealed class ResetCardDialog {
data object StartResetDialog : ResetCardDialog()
data object ContinueResetDialog : ResetCardDialog()
data object InterruptedResetDialog : ResetCardDialog()
data object CompletedResetDialog : ResetCardDialog()
}
data class ManageSecurityState(
val currentOption: SecurityOption = SecurityOption.LongTap,
val selectedOption: SecurityOption = currentOption,
val allowedOptions: EnumSet<SecurityOption> = EnumSet.allOf(SecurityOption::class.java),
val buttonProceed: Button = Button(true),
)
data class AppSettingsState(
val saveWallets: Boolean = false,
val saveAccessCodes: Boolean = false,

View file

@ -7,12 +7,10 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@ -37,7 +35,6 @@ internal class AppSettingsFragment : ComposeFragment() {
modifier = modifier,
state = state,
onBackClick = {
store.dispatch(DetailsAction.ResetCardSettingsData)
store.dispatchNavigationAction(AppRouter::pop)
},
)

View file

@ -1,15 +1,14 @@
package com.tangem.tap.features.details.ui.cardsettings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@ -24,13 +23,12 @@ internal class CardSettingsFragment : ComposeFragment() {
@Composable
override fun ScreenContent(modifier: Modifier) {
LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
val state by viewModel.screenState.collectAsStateWithLifecycle()
CardSettingsScreen(
modifier = modifier,
state = viewModel.screenState.value,
state = state,
onBackClick = {
store.dispatch(DetailsAction.ResetCardSettingsData)
store.dispatchNavigationAction(AppRouter::pop)
},
)

View file

@ -4,16 +4,12 @@ import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.stringResource
import com.tangem.tap.features.details.redux.AccessCodeRecoveryState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.securitymode.toTitleRes
import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
import com.tangem.wallet.R
import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo
internal data class CardSettingsScreenState(
val cardDetails: List<CardInfo>? = null,
val accessCodeRecoveryState: AccessCodeRecoveryState? = null,
val onScanCardClick: () -> Unit,
val onElementClick: (CardInfo) -> Unit,
)
@ -44,7 +40,7 @@ internal sealed class CardInfo(
clickable = clickable,
)
object ChangeAccessCode : CardInfo(
data object ChangeAccessCode : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_change_access_code),
subtitle = TextReference.Res(R.string.card_settings_change_access_code_footer),
clickable = true,
@ -60,9 +56,9 @@ internal sealed class CardInfo(
clickable = true,
)
class ResetToFactorySettings(cardInfo: ReduxCardInfo) : CardInfo(
class ResetToFactorySettings(description: TextReference) : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_reset_card_to_factory),
subtitle = cardInfo.toResetCardDescriptionText(),
subtitle = description,
clickable = true,
)
}

View file

@ -1,105 +1,124 @@
package com.tangem.tap.features.details.ui.cardsettings
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import android.os.Bundle
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import arrow.core.Either
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.getTwinCardIdForUser
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.features.details.redux.CardSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.features.details.ui.common.utils.*
import com.tangem.tap.store
import com.tangem.wallet.R
import dagger.hilt.android.lifecycle.HiltViewModel
import org.rekotlin.StoreSubscriber
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
internal class CardSettingsViewModel @Inject constructor(
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
) :
ViewModel(), DefaultLifecycleObserver, StoreSubscriber<DetailsState> {
private val scanCardProcessor: ScanCardProcessor,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val tangemSdkManager: TangemSdkManager,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
var screenState: MutableState<CardSettingsScreenState> =
mutableStateOf(updateState(store.state.detailsState.cardSettingsState))
private val userWalletId = savedStateHandle.get<Bundle>(AppRoute.CardSettings.USER_WALLET_ID_KEY)
?.unbundle(UserWalletId.serializer())
?: error("User wallet ID is required for CardSettingsViewModel")
override fun onStart(owner: LifecycleOwner) {
when (val selectedWalletEither = getSelectedWalletSyncUseCase()) {
is Either.Left -> {
Timber.e(selectedWalletEither.value.toString())
val screenState: MutableStateFlow<CardSettingsScreenState> = MutableStateFlow(getInitialState())
private fun getInitialState() = CardSettingsScreenState(
cardDetails = null,
onElementClick = ::handleClickingItem,
onScanCardClick = ::scanCard,
)
private fun scanCard() = viewModelScope.launch {
scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true)
.doOnSuccess { scanResponse ->
val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
val isCorrectUserWalletScanned = scannedUserWalletId == userWalletId
if (isCorrectUserWalletScanned) {
val userWallet = getUserWallet()
updateCardDetails(userWallet)
} else {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
headerId = R.string.common_warning,
messageId = R.string.error_wrong_wallet_tapped,
),
)
}
}
}
private fun updateCardDetails(userWallet: UserWallet) {
val card = userWallet.scanResponse.card
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
val cardId = cardTypesResolver.getCardId()
val currentSecurityOption = getCurrentSecurityOption(card)
val allowedSecurityOptions = getAllowedSecurityOptions(
card = card,
cardTypesResolver = cardTypesResolver,
currentSecurityOption = currentSecurityOption,
)
val isResetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver)
val cardDetails = buildList {
CardInfo.CardId(cardId).let(::add)
CardInfo.Issuer(card.issuer.name).let(::add)
if (!cardTypesResolver.isTangemTwins()) {
CardInfo.SignedHashes(card.signedHashesCount().toString()).let(::add)
}
CardInfo.SecurityMode(
currentSecurityOption,
clickable = allowedSecurityOptions.size > 1,
).let(::add)
if (card.backupStatus?.isActive == true && card.isAccessCodeSet) {
CardInfo.ChangeAccessCode.let(::add)
}
if (isAccessCodeRecoveryAllowed(cardTypesResolver)) {
CardInfo.AccessCodeRecovery(isAccessCodeRecoveryEnabled(cardTypesResolver, card)).let(::add)
}
if (isResetCardAllowed) {
CardInfo.ResetToFactorySettings(
description = getResetToFactoryDescription(card, cardTypesResolver),
).let(::add)
}
is Either.Right -> Unit
}
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop(owner: LifecycleOwner) {
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
screenState.value = updateState(state.cardSettingsState)
}
private fun updateState(state: CardSettingsState?): CardSettingsScreenState {
return if (state?.manageSecurityState == null) {
CardSettingsScreenState(
cardDetails = null,
accessCodeRecoveryState = null,
onElementClick = {},
onScanCardClick = {
store.dispatch(DetailsAction.ScanCard)
},
)
} else {
val cardId = if (state.card.isTangemTwins) {
state.card.getTwinCardIdForUser()
} else {
state.cardInfo.cardId
}
val cardDetails: MutableList<CardInfo> = mutableListOf(
CardInfo.CardId(cardId),
CardInfo.Issuer(state.cardInfo.issuer),
)
if (!state.card.isTangemTwins) {
cardDetails.add(CardInfo.SignedHashes(state.cardInfo.signedHashes.toString()))
}
cardDetails.add(
CardInfo.SecurityMode(
securityOption = state.manageSecurityState.currentOption,
clickable = state.manageSecurityState.allowedOptions.size > 1,
),
)
if (state.card.backupStatus?.isActive == true && state.card.isAccessCodeSet) {
cardDetails.add(CardInfo.ChangeAccessCode)
}
if (state.accessCodeRecovery != null) {
cardDetails.add(CardInfo.AccessCodeRecovery(state.accessCodeRecovery.enabledOnCard))
}
if (state.resetCardAllowed) {
cardDetails.add(CardInfo.ResetToFactorySettings(state.cardInfo))
}
CardSettingsScreenState(
cardDetails = cardDetails,
accessCodeRecoveryState = state.accessCodeRecovery,
onScanCardClick = { },
onElementClick = {
handleClickingItem(it)
},
)
screenState.update { state ->
state.copy(cardDetails = cardDetails)
}
}
@ -107,20 +126,43 @@ internal class CardSettingsViewModel @Inject constructor(
when (item) {
is CardInfo.ChangeAccessCode -> {
Analytics.send(Settings.CardSettings.ButtonChangeUserCode(AnalyticsParam.UserCode.AccessCode))
store.dispatch(DetailsAction.ManageSecurity.ChangeAccessCode)
changeAccessCode()
}
is CardInfo.ResetToFactorySettings -> {
Analytics.send(Settings.CardSettings.ButtonFactoryReset())
store.dispatch(DetailsAction.ResetToFactory.Start)
store.dispatchNavigationAction { AppRoute.ResetToFactory(userWalletId) }
}
is CardInfo.SecurityMode -> {
Analytics.send(Settings.CardSettings.ButtonChangeSecurityMode())
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)
store.dispatchNavigationAction { AppRoute.DetailsSecurity(userWalletId) }
}
is CardInfo.AccessCodeRecovery -> {
store.dispatch(DetailsAction.AccessCodeRecovery.Open)
store.dispatchNavigationAction { AppRoute.AccessCodeRecovery(userWalletId) }
}
else -> {}
}
}
private fun changeAccessCode() = viewModelScope.launch {
val card = getUserWallet().scanResponse.card
when (val result = tangemSdkManager.setAccessCode(card.cardId)) {
is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged())
is CompletionResult.Failure -> {
Timber.e("Failed to change access code: ${result.error}")
}
}
}
private fun getUserWallet(): UserWallet {
return getUserWalletUseCase(userWalletId).getOrElse {
error("Failed to get user wallet $userWalletId: $it")
}
}
private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: CardTypesResolver): Boolean {
val hasPermanentWallet = card.wallets.any { it.settings.isPermanent }
val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin()
return !isNotAllowed
}
}

View file

@ -1,55 +1,33 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
class AccessCodeRecoveryFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
class AccessCodeRecoveryFragment : ComposeFragment() {
private val viewModel = AccessCodeRecoveryViewModel(store)
private var screenState: MutableState<AccessCodeRecoveryScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery))
private val viewModel: AccessCodeRecoveryViewModel by viewModels()
@Inject
override lateinit var uiDependencies: UiDependencies
@Composable
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.screenState.collectAsStateWithLifecycle()
AccessCodeRecoveryScreen(
state = screenState.value,
state = state,
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
screenState.value =
viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery)
}
}

View file

@ -1,10 +1,6 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
@ -57,7 +53,7 @@ fun AccessCodeRecoveryOptions(state: AccessCodeRecoveryScreenState) {
DetailsMainButton(
title = stringResource(id = R.string.common_save_changes),
enabled = state.isSaveChangesEnabled,
onClick = { state.onSaveChangesClick(state.enabledSelection) },
onClick = { state.onSaveChangesClick() },
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing20),
)
}

View file

@ -11,6 +11,6 @@ data class AccessCodeRecoveryScreenState(
val enabledOnCard: Boolean,
val enabledSelection: Boolean,
val isSaveChangesEnabled: Boolean,
val onSaveChangesClick: (Boolean) -> Unit,
val onSaveChangesClick: () -> Unit,
val onOptionClick: (Boolean) -> Unit,
)

View file

@ -1,30 +1,91 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.AccessCodeRecoveryState
import com.tangem.tap.features.details.redux.DetailsAction
import org.rekotlin.Store
import android.os.Bundle
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.common.doOnSuccess
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.bundle.unbundle
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.features.details.ui.common.utils.isAccessCodeRecoveryEnabled
import com.tangem.tap.store
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
class AccessCodeRecoveryViewModel(val store: Store<AppState>) {
@HiltViewModel
internal class AccessCodeRecoveryViewModel @Inject constructor(
private val getUserWalletUseCase: GetUserWalletUseCase,
private val tangemSdkManager: TangemSdkManager,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
fun updateState(state: AccessCodeRecoveryState?): AccessCodeRecoveryScreenState {
// We shouldn't get to this screen here when this state is null
return if (state == null) {
AccessCodeRecoveryScreenState(
enabledOnCard = false,
enabledSelection = false,
isSaveChangesEnabled = false,
onSaveChangesClick = {},
onOptionClick = {},
)
} else {
AccessCodeRecoveryScreenState(
enabledOnCard = state.enabledOnCard,
enabledSelection = state.enabledSelection,
isSaveChangesEnabled = state.enabledOnCard != state.enabledSelection,
onSaveChangesClick = { store.dispatch(DetailsAction.AccessCodeRecovery.SaveChanges(it)) },
onOptionClick = { store.dispatch(DetailsAction.AccessCodeRecovery.SelectOption(it)) },
private val userWalletId = savedStateHandle.get<Bundle>(AppRoute.AccessCodeRecovery.USER_WALLET_ID_KEY)
?.unbundle(UserWalletId.serializer())
?: error("UserWalletId is required for AccessCodeRecoveryViewModel")
val screenState = MutableStateFlow(
value = getInitialState(),
)
private fun getInitialState(): AccessCodeRecoveryScreenState {
val userWallet = getUserWallet()
val isEnabled = isAccessCodeRecoveryEnabled(
typeResolver = userWallet.scanResponse.cardTypesResolver,
card = userWallet.scanResponse.card,
)
return AccessCodeRecoveryScreenState(
enabledOnCard = isEnabled,
enabledSelection = isEnabled,
isSaveChangesEnabled = false,
onSaveChangesClick = ::saveChanges,
onOptionClick = ::selectOption,
)
}
private fun saveChanges() = viewModelScope.launch {
val userWallet = getUserWallet()
val isEnabled = screenState.value.enabledSelection
tangemSdkManager
.setAccessCodeRecoveryEnabled(userWallet.cardId, isEnabled)
.doOnSuccess {
Analytics.send(
Settings.CardSettings.AccessCodeRecoveryChanged(
AnalyticsParam.AccessCodeRecoveryStatus.from(isEnabled),
),
)
store.dispatchNavigationAction(AppRouter::pop)
}
}
private fun selectOption(isEnabled: Boolean) {
screenState.update {
it.copy(
isSaveChangesEnabled = isEnabled != it.enabledOnCard,
)
}
}
private fun getUserWallet(): UserWallet {
return getUserWalletUseCase(userWalletId).getOrElse {
error("Unable to get user wallet $userWalletId: $it")
}
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.tap.features.details.ui.common.utils
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.models.scan.CardDTO
internal fun isAccessCodeRecoveryAllowed(typeResolver: CardTypesResolver): Boolean = typeResolver.isWallet2()
internal fun isAccessCodeRecoveryEnabled(typeResolver: CardTypesResolver, card: CardDTO): Boolean =
if (typeResolver.isWallet2()) {
card.userSettings?.isUserCodeRecoveryAllowed ?: false
} else {
false
}

View file

@ -0,0 +1,14 @@
package com.tangem.tap.features.details.ui.common.utils
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
internal fun getResetToFactoryDescription(card: CardDTO, typesResolver: CardTypesResolver): TextReference {
return if (card.backupStatus?.isActive != true || typesResolver.isTangemTwins()) {
TextReference.Res(R.string.reset_card_without_backup_to_factory_message)
} else {
TextReference.Res(R.string.reset_card_with_backup_to_factory_message)
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.tap.features.details.ui.common.utils
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.features.details.redux.SecurityOption
import java.util.EnumSet
internal fun getCurrentSecurityOption(card: CardDTO): SecurityOption = when {
card.isAccessCodeSet -> SecurityOption.AccessCode
card.isPasscodeSet == true -> SecurityOption.PassCode
else -> SecurityOption.LongTap
}
internal fun getAllowedSecurityOptions(
card: CardDTO,
cardTypesResolver: CardTypesResolver,
currentSecurityOption: SecurityOption,
): EnumSet<SecurityOption> = when {
cardTypesResolver.isStart2Coin() || cardTypesResolver.isTangemNote() -> EnumSet.of(SecurityOption.LongTap)
card.settings.isBackupAllowed -> EnumSet.of(currentSecurityOption)
else -> prepareAllowedSecurityOptions(
cardTypesResolver = cardTypesResolver,
currentSecurityOption = currentSecurityOption,
)
}
private fun prepareAllowedSecurityOptions(
cardTypesResolver: CardTypesResolver,
currentSecurityOption: SecurityOption?,
): EnumSet<SecurityOption> {
val allowedSecurityOptions = EnumSet.of(SecurityOption.LongTap)
if (cardTypesResolver.isTangemTwins()) {
allowedSecurityOptions.add(SecurityOption.PassCode)
}
if (currentSecurityOption == SecurityOption.AccessCode) {
allowedSecurityOptions.add(SecurityOption.AccessCode)
}
if (currentSecurityOption == SecurityOption.PassCode) {
allowedSecurityOptions.add(SecurityOption.PassCode)
}
return allowedSecurityOptions
}

View file

@ -39,7 +39,6 @@ import kotlinx.coroutines.flow.onEach
import org.rekotlin.Store
import timber.log.Timber
// TODO: change to Android ViewModel [REDACTED_JIRA]
internal class DetailsViewModel(
private val store: Store<AppState>,
private val walletsRepository: WalletsRepository,
@ -90,10 +89,6 @@ internal class DetailsViewModel(
SettingsItem.AppSettings(::navigateToAppSettings)
.let(::add)
// removed chat in task [REDACTED_TASK_KEY]
// SettingsItem.Chat(::navigateToChat)
// .let(::add)
SettingsItem.SendFeedback(::sendFeedback)
.let(::add)
@ -137,7 +132,10 @@ internal class DetailsViewModel(
}
private fun navigateToReferralProgram() {
store.dispatchNavigationAction { push(AppRoute.ReferralProgram) }
val userWallet = userWalletsListManager.selectedUserWalletSync
?: error("Selected wallet must be not null")
store.dispatchNavigationAction { push(AppRoute.ReferralProgram(userWallet.walletId)) }
}
private fun sendFeedback() {
@ -157,8 +155,10 @@ internal class DetailsViewModel(
}
private fun navigateToCardSettings() {
val userWalletId = userWalletsListManager.selectedUserWalletSync?.walletId
?: error("UserWalletId must be not null")
Analytics.send(Settings.ButtonCardSettings())
store.dispatchNavigationAction { push(AppRoute.CardSettings) }
store.dispatchNavigationAction { push(AppRoute.CardSettings(userWalletId)) }
}
private fun linkMoreCards() {

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.details.ui.resetcard
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -8,26 +9,21 @@ import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.MutableStateFlow
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
internal class ResetCardFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
internal class ResetCardFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
private val viewModel: ResetCardViewModel by viewModels()
private var screenState = MutableStateFlow<ResetCardScreenState>(ResetCardScreenState.InitialState)
@Composable
override fun ScreenContent(modifier: Modifier) {
val state = screenState.collectAsStateWithLifecycle().value
val state by viewModel.screenState.collectAsStateWithLifecycle()
ResetCardScreen(
state = state,
@ -35,23 +31,4 @@ internal class ResetCardFragment : ComposeFragment(), StoreSubscriber<DetailsSta
modifier = modifier,
)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
screenState.value = viewModel.updateState(state.cardSettingsState)
}
}

View file

@ -20,24 +20,19 @@ import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
import com.tangem.tap.features.details.ui.resetcard.ResetCardScreenState.ResetCardScreenContent.Dialog as ResetCardDialog
import com.tangem.tap.features.details.ui.resetcard.ResetCardScreenState.Dialog as ResetCardDialog
@Composable
internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
SettingsScreensScaffold(
modifier = modifier,
content = {
when (state) {
is ResetCardScreenState.ResetCardScreenContent -> ResetCardView(state = state)
ResetCardScreenState.InitialState -> {
// do nothing for now, just white screen
}
}
ResetCardView(state = state)
},
onBackClick = onBackClick,
)
when (val dialog = (state as? ResetCardScreenState.ResetCardScreenContent)?.dialog) {
when (val dialog = state.dialog) {
is ResetCardDialog.StartReset,
is ResetCardDialog.ContinueReset,
is ResetCardDialog.InterruptedReset,
@ -48,7 +43,7 @@ internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Uni
}
@Composable
private fun ResetCardView(state: ResetCardScreenState.ResetCardScreenContent) {
private fun ResetCardView(state: ResetCardScreenState) {
val scrollState = rememberScrollState()
Column(
@ -110,7 +105,7 @@ private fun Description(text: TextReference) {
}
@Composable
private fun Conditions(state: ResetCardScreenState.ResetCardScreenContent) {
private fun Conditions(state: ResetCardScreenState) {
state.warningsToShow.forEach {
when (it) {
ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS -> {
@ -120,7 +115,6 @@ private fun Conditions(state: ResetCardScreenState.ResetCardScreenContent) {
description = TextReference.Res(R.string.reset_card_to_factory_condition_1),
)
}
ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE -> {
ConditionCheckBox(
checkedState = state.acceptCondition2Checked,
@ -192,7 +186,7 @@ private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) {
}
@Composable
private fun CommonResetDialog(dialog: ResetCardScreenState.ResetCardScreenContent.Dialog) {
private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) {
BasicDialog(
title = stringResource(dialog.titleResId),
message = stringResource(dialog.messageResId),
@ -231,10 +225,13 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) {
.background(TangemTheme.colors.background.secondary),
) {
ResetCardScreen(
state = ResetCardScreenState.ResetCardScreenContent(
accepted = true,
state = ResetCardScreenState(
resetButtonEnabled = true,
showResetPasswordButton = false,
warningsToShow = listOf(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS),
descriptionText = TextReference.Res(R.string.reset_card_with_backup_to_factory_message),
acceptCondition1Checked = false,
acceptCondition2Checked = false,
onAcceptCondition1ToggleClick = {},
onAcceptCondition2ToggleClick = {},
onResetButtonClick = {},

View file

@ -4,63 +4,57 @@ import androidx.annotation.StringRes
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
internal sealed class ResetCardScreenState {
internal data class ResetCardScreenState(
val resetButtonEnabled: Boolean,
val descriptionText: TextReference,
val warningsToShow: List<WarningsToReset>,
val showResetPasswordButton: Boolean,
val acceptCondition1Checked: Boolean,
val acceptCondition2Checked: Boolean,
val onAcceptCondition1ToggleClick: (Boolean) -> Unit,
val onAcceptCondition2ToggleClick: (Boolean) -> Unit,
val onResetButtonClick: () -> Unit,
val dialog: Dialog?,
) {
object InitialState : ResetCardScreenState()
sealed class Dialog(
@StringRes val titleResId: Int,
@StringRes val messageResId: Int,
) {
data class ResetCardScreenContent(
val accepted: Boolean = false,
val descriptionText: TextReference,
val warningsToShow: List<WarningsToReset>,
val acceptCondition1Checked: Boolean = false,
val acceptCondition2Checked: Boolean = false,
val onAcceptCondition1ToggleClick: (Boolean) -> Unit,
val onAcceptCondition2ToggleClick: (Boolean) -> Unit,
val onResetButtonClick: () -> Unit,
val dialog: Dialog? = null,
) : ResetCardScreenState() {
val resetButtonEnabled: Boolean
get() = accepted
abstract val onConfirmClick: () -> Unit
abstract val onDismiss: () -> Unit
sealed class Dialog(
@StringRes val titleResId: Int,
@StringRes val messageResId: Int,
data class StartReset(
override val onConfirmClick: () -> Unit,
override val onDismiss: () -> Unit,
) : Dialog(
titleResId = R.string.common_attention,
messageResId = R.string.card_settings_action_sheet_title,
)
data class ContinueReset(
override val onConfirmClick: () -> Unit,
override val onDismiss: () -> Unit,
) : Dialog(
titleResId = R.string.card_settings_continue_reset_alert_title,
messageResId = R.string.card_settings_continue_reset_alert_message,
)
data class InterruptedReset(
override val onConfirmClick: () -> Unit,
override val onDismiss: () -> Unit,
) : Dialog(
titleResId = R.string.card_settings_interrupted_reset_alert_title,
messageResId = R.string.card_settings_interrupted_reset_alert_message,
)
data class CompletedReset(override val onConfirmClick: () -> Unit) : Dialog(
titleResId = R.string.card_settings_completed_reset_alert_title,
messageResId = R.string.card_settings_completed_reset_alert_message,
) {
abstract val onConfirmClick: () -> Unit
abstract val onDismiss: () -> Unit
data class StartReset(
override val onConfirmClick: () -> Unit,
override val onDismiss: () -> Unit,
) : Dialog(
titleResId = R.string.common_attention,
messageResId = R.string.card_settings_action_sheet_title,
)
data class ContinueReset(
override val onConfirmClick: () -> Unit,
override val onDismiss: () -> Unit,
) : Dialog(
titleResId = R.string.card_settings_continue_reset_alert_title,
messageResId = R.string.card_settings_continue_reset_alert_message,
)
data class InterruptedReset(
override val onConfirmClick: () -> Unit,
override val onDismiss: () -> Unit,
) : Dialog(
titleResId = R.string.card_settings_interrupted_reset_alert_title,
messageResId = R.string.card_settings_interrupted_reset_alert_message,
)
data class CompletedReset(override val onConfirmClick: () -> Unit) : Dialog(
titleResId = R.string.card_settings_completed_reset_alert_title,
messageResId = R.string.card_settings_completed_reset_alert_message,
) {
override val onDismiss: () -> Unit = {}
}
override val onDismiss: () -> Unit = {}
}
}

View file

@ -1,8 +1,12 @@
package com.tangem.tap.features.details.ui.resetcard
import android.os.Bundle
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
@ -10,96 +14,132 @@ import com.tangem.domain.card.ResetCardUseCase
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.features.details.redux.CardSettingsState
import com.tangem.tap.features.details.redux.DetailsAction.ResetToFactory
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.details.ui.resetcard.featuretoggles.ResetCardFeatureToggles
import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
import com.tangem.tap.features.details.redux.ResetCardDialog
import com.tangem.tap.features.details.ui.common.utils.getResetToFactoryDescription
import com.tangem.tap.store
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import com.tangem.tap.features.details.redux.CardSettingsState.Dialog as CardSettingsDialog
@Suppress("LongParameterList")
@HiltViewModel
internal class ResetCardViewModel @Inject constructor(
private val resetCardFeatureToggles: ResetCardFeatureToggles,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val resetCardUseCase: ResetCardUseCase,
private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase,
private val deleteWalletUseCase: DeleteWalletUseCase,
private val userWalletsListManager: UserWalletsListManager,
private val analyticsEventHandler: AnalyticsEventHandler,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val firstCardScanResponse = store.state.detailsState.cardSettingsState?.scanResponse
?: error("ScanResponse can't be null")
private val currentUserWalletId = createUserWalletId(firstCardScanResponse)
private val userWalletId = savedStateHandle.get<Bundle>(AppRoute.ResetToFactory.USER_WALLET_ID)
?.unbundle(UserWalletId.serializer())
?: error("User wallet ID must be provided for ResetCardViewModel")
// TODO: move logic to separate domain entity
private var resetBackupCardCount = 0
fun updateState(state: CardSettingsState?): ResetCardScreenState.ResetCardScreenContent {
val descriptionText = state?.cardInfo
?.toResetCardDescriptionText()
?: TextReference.Str(value = "")
val screenState: MutableStateFlow<ResetCardScreenState> = MutableStateFlow(
value = getInitialState(),
)
private fun getInitialState(): ResetCardScreenState {
val userWallet = getUserWalletUseCase(userWalletId).getOrElse {
error("Failed to get user wallet $userWalletId: $it")
}
val card = userWallet.scanResponse.card
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
val descriptionText = getResetToFactoryDescription(card, cardTypesResolver)
val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2()
val showResetPasswordButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active
val warningsToShow = buildList {
add(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS)
if (state?.isShowPasswordResetRadioButton == true) {
if (showResetPasswordButton) {
add(ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE)
}
}
return ResetCardScreenState.ResetCardScreenContent(
accepted = state?.resetButtonEnabled ?: false,
return ResetCardScreenState(
resetButtonEnabled = false,
descriptionText = descriptionText,
warningsToShow = warningsToShow,
acceptCondition1Checked = state?.condition1Checked ?: false,
acceptCondition2Checked = state?.condition2Checked ?: false,
onAcceptCondition1ToggleClick = { store.dispatch(ResetToFactory.AcceptCondition1(it)) },
onAcceptCondition2ToggleClick = { store.dispatch(ResetToFactory.AcceptCondition2(it)) },
onResetButtonClick = { showDialog(CardSettingsDialog.StartResetDialog) },
dialog = state?.dialog?.let(::createDialog),
showResetPasswordButton = showResetPasswordButton,
acceptCondition1Checked = false,
acceptCondition2Checked = false,
onAcceptCondition1ToggleClick = ::toggleFirstCondition,
onAcceptCondition2ToggleClick = ::toggleSecondCondition,
onResetButtonClick = { showDialog(ResetCardDialog.StartResetDialog) },
dialog = null,
)
}
private fun createDialog(dialog: CardSettingsDialog): ResetCardScreenState.ResetCardScreenContent.Dialog {
private fun toggleFirstCondition(isAccepted: Boolean) {
screenState.update { prevState ->
val resetButtonEnabled = if (prevState.showResetPasswordButton) {
isAccepted && prevState.acceptCondition2Checked
} else {
isAccepted
}
prevState.copy(
acceptCondition1Checked = isAccepted,
resetButtonEnabled = resetButtonEnabled,
)
}
}
private fun toggleSecondCondition(isAccepted: Boolean) {
screenState.update { prevState ->
val resetButtonEnabled = prevState.acceptCondition1Checked && isAccepted
prevState.copy(
acceptCondition2Checked = isAccepted,
resetButtonEnabled = resetButtonEnabled,
)
}
}
private fun createDialog(dialog: ResetCardDialog): ResetCardScreenState.Dialog {
return when (dialog) {
CardSettingsDialog.StartResetDialog -> {
ResetCardScreenState.ResetCardScreenContent.Dialog.StartReset(
ResetCardDialog.StartResetDialog -> {
ResetCardScreenState.Dialog.StartReset(
onConfirmClick = ::onStartResetClick,
onDismiss = ::dismissDialog,
)
}
CardSettingsDialog.ContinueResetDialog -> {
ResetCardScreenState.ResetCardScreenContent.Dialog.ContinueReset(
ResetCardDialog.ContinueResetDialog -> {
ResetCardScreenState.Dialog.ContinueReset(
onConfirmClick = ::onContinueResetClick,
onDismiss = ::onContinueResetDialogDismiss,
)
}
CardSettingsDialog.InterruptedResetDialog -> {
ResetCardScreenState.ResetCardScreenContent.Dialog.InterruptedReset(
ResetCardDialog.InterruptedResetDialog -> {
ResetCardScreenState.Dialog.InterruptedReset(
onConfirmClick = ::onContinueResetClick,
onDismiss = ::onInterruptedResetDialogDismiss,
)
}
CardSettingsDialog.CompletedResetDialog -> {
ResetCardScreenState.ResetCardScreenContent.Dialog.CompletedReset(
ResetCardDialog.CompletedResetDialog -> {
ResetCardScreenState.Dialog.CompletedReset(
onConfirmClick = ::dismissAndFinishFullReset,
)
}
@ -109,21 +149,26 @@ internal class ResetCardViewModel @Inject constructor(
private fun onStartResetClick() {
dismissDialog()
if (resetCardFeatureToggles.isFullResetEnabled) {
makeFullReset()
} else {
store.dispatch(ResetToFactory.Proceed)
}
makeFullReset()
}
private fun makeFullReset() {
viewModelScope.launch {
resetCardUseCase(card = firstCardScanResponse.card).onRight {
deleteSavedAccessCodesUseCase(firstCardScanResponse.card.cardId)
deleteWalletUseCase(currentUserWalletId)
val userWallet = getUserWallet()
val scanResponse = userWallet.scanResponse
resetCardUseCase(card = scanResponse.card).onRight {
deleteSavedAccessCodesUseCase(scanResponse.card.cardId)
val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse {
Timber.e("Unable to delete user wallet: $it")
return@launch
}
if (hasUserWallets) {
val newSelectedWallet = getSelectedWalletSyncUseCase().getOrElse {
error("Failed to get selected wallet: $it")
}
val newSelectedWallet = getSelectedWalletSyncUseCase().getOrNull()
if (newSelectedWallet != null) {
store.onUserWalletSelected(newSelectedWallet)
}
@ -138,10 +183,11 @@ internal class ResetCardViewModel @Inject constructor(
dismissDialog()
viewModelScope.launch {
val userWallet = getUserWallet()
resetCardUseCase(
cardNumber = resetBackupCardCount + 1,
card = firstCardScanResponse.card,
userWalletId = currentUserWalletId,
card = userWallet.scanResponse.card,
userWalletId = userWalletId,
)
.onRight {
resetBackupCardCount++
@ -150,14 +196,14 @@ internal class ResetCardViewModel @Inject constructor(
checkRemainingBackupCards()
}
.onLeft { showDialog(CardSettingsDialog.InterruptedResetDialog) }
.onLeft { showDialog(ResetCardDialog.InterruptedResetDialog) }
}
}
private fun onContinueResetDialogDismiss() {
dismissDialog()
showDialog(CardSettingsDialog.InterruptedResetDialog)
showDialog(ResetCardDialog.InterruptedResetDialog)
}
private fun onInterruptedResetDialogDismiss() {
@ -167,20 +213,26 @@ internal class ResetCardViewModel @Inject constructor(
}
private fun checkRemainingBackupCards() {
val backupCardsCount = firstCardScanResponse.getBackupCardsCount()
val backupCardsCount = getUserWallet().scanResponse.getBackupCardsCount()
when {
backupCardsCount > resetBackupCardCount -> showDialog(CardSettingsDialog.ContinueResetDialog)
backupCardsCount > resetBackupCardCount -> showDialog(ResetCardDialog.ContinueResetDialog)
backupCardsCount == resetBackupCardCount -> {
analyticsEventHandler.send(
event = Settings.CardSettings.FactoryResetFinished(cardsCount = resetBackupCardCount + 1),
)
showDialog(CardSettingsDialog.CompletedResetDialog)
showDialog(ResetCardDialog.CompletedResetDialog)
}
else -> finishFullReset()
}
}
private fun getUserWallet(): UserWallet {
return getUserWalletUseCase(userWalletId).getOrElse {
error("Failed to get user wallet $userWalletId: $it")
}
}
private fun dismissAndFinishFullReset() {
dismissDialog()
@ -202,12 +254,12 @@ internal class ResetCardViewModel @Inject constructor(
}
}
private fun showDialog(dialog: CardSettingsDialog) {
store.dispatch(ResetToFactory.ShowDialog(dialog))
private fun showDialog(dialog: ResetCardDialog) {
screenState.update { it.copy(dialog = createDialog(dialog)) }
}
private fun dismissDialog() {
store.dispatch(ResetToFactory.DismissDialog)
screenState.update { it.copy(dialog = null) }
}
private fun ScanResponse.getBackupCardsCount(): Int {
@ -221,9 +273,4 @@ internal class ResetCardViewModel @Inject constructor(
-> 0
}
}
private fun createUserWalletId(scanResponse: ScanResponse): UserWalletId {
return UserWalletIdBuilder.scanResponse(scanResponse).build()
?: error("UserWalletId can't be null")
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.tap.features.details.ui.resetcard.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.tap.features.details.ui.resetcard.featuretoggles.DefaultResetCardFeatureToggles
import com.tangem.tap.features.details.ui.resetcard.featuretoggles.ResetCardFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object ResetCardModule {
@Provides
@Singleton
fun provideResetCardFeatureToggles(featureTogglesManager: FeatureTogglesManager): ResetCardFeatureToggles {
return DefaultResetCardFeatureToggles(featureTogglesManager)
}
}

View file

@ -1,11 +0,0 @@
package com.tangem.tap.features.details.ui.resetcard.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
internal class DefaultResetCardFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : ResetCardFeatureToggles {
override val isFullResetEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "FULL_RESET_ENABLED")
}

View file

@ -1,6 +0,0 @@
package com.tangem.tap.features.details.ui.resetcard.featuretoggles
interface ResetCardFeatureToggles {
val isFullResetEnabled: Boolean
}

View file

@ -1,55 +1,34 @@
package com.tangem.tap.features.details.ui.securitymode
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
internal class SecurityModeFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
internal class SecurityModeFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
private val viewModel = SecurityModeViewModel(store)
private var screenState: MutableState<SecurityModeScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.manageSecurityState))
private val viewModel: SecurityModeViewModel by viewModels()
@Composable
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.screenState.collectAsStateWithLifecycle()
SecurityModeScreen(
modifier = modifier,
state = screenState.value,
state = state,
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
screenState.value = viewModel.updateState(state.cardSettingsState?.manageSecurityState)
}
}

View file

@ -1,29 +1,109 @@
package com.tangem.tap.features.details.ui.securitymode
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.ManageSecurityState
import android.os.Bundle
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.bundle.unbundle
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.features.details.redux.SecurityOption
import org.rekotlin.Store
import com.tangem.tap.features.details.ui.common.utils.getAllowedSecurityOptions
import com.tangem.tap.features.details.ui.common.utils.getCurrentSecurityOption
import com.tangem.tap.store
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
internal class SecurityModeViewModel(val store: Store<AppState>) {
@HiltViewModel
internal class SecurityModeViewModel @Inject constructor(
private val getUserWalletUseCase: GetUserWalletUseCase,
private val tangemSdkManager: TangemSdkManager,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val userWalletId = savedStateHandle.get<Bundle>(AppRoute.DetailsSecurity.USER_WALLET_ID_KEY)
?.unbundle(UserWalletId.serializer())
?: error("UserWalletId is required for SecurityModeViewModel")
val screenState = MutableStateFlow(
value = getInitialState(),
)
private fun getInitialState(): SecurityModeScreenState {
val userWallet = getUserWallet()
val scanResponse = userWallet.scanResponse
val card = scanResponse.card
val cardTypesResolver = scanResponse.cardTypesResolver
val currentSecurityOption = getCurrentSecurityOption(card)
val allowedSecurityOptions = getAllowedSecurityOptions(card, cardTypesResolver, currentSecurityOption)
fun updateState(state: ManageSecurityState?): SecurityModeScreenState {
if (state == null) {
return SecurityModeScreenState(
availableOptions = emptyList(),
selectedSecurityMode = SecurityOption.LongTap,
isSaveChangesEnabled = false,
onNewModeSelected = {},
onSaveChangesClicked = {},
)
}
return SecurityModeScreenState(
availableOptions = state.allowedOptions.toList(),
selectedSecurityMode = state.selectedOption,
isSaveChangesEnabled = state.selectedOption != state.currentOption,
onNewModeSelected = { store.dispatch(DetailsAction.ManageSecurity.SelectOption(it)) },
onSaveChangesClicked = { store.dispatch(DetailsAction.ManageSecurity.SaveChanges) },
availableOptions = allowedSecurityOptions.toList(),
selectedSecurityMode = currentSecurityOption,
isSaveChangesEnabled = false,
onNewModeSelected = ::selectOption,
onSaveChangesClicked = ::saveChanges,
)
}
private fun selectOption(securityOption: SecurityOption) {
screenState.update { state ->
state.copy(
selectedSecurityMode = securityOption,
isSaveChangesEnabled = securityOption != getCurrentSecurityOption(getUserWallet().scanResponse.card),
)
}
}
private fun saveChanges() {
val userWallet = getUserWallet()
val cardId = userWallet.cardId
val selectedOption = screenState.value.selectedSecurityMode
viewModelScope.launch {
val result = when (selectedOption) {
SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId)
SecurityOption.PassCode -> tangemSdkManager.setPasscode(cardId)
SecurityOption.AccessCode -> tangemSdkManager.setAccessCode(cardId)
}
val paramValue = AnalyticsParam.SecurityMode.from(selectedOption)
when (result) {
is CompletionResult.Success -> {
Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue))
store.dispatchNavigationAction(AppRouter::pop)
}
is CompletionResult.Failure -> {
val error = result.error
if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue, error))
}
}
else -> Unit
}
}
}
private fun getUserWallet(): UserWallet {
return getUserWalletUseCase(userWalletId).getOrElse {
error("Unable to get user wallet $userWalletId: $it")
}
}
}

View file

@ -1,13 +0,0 @@
package com.tangem.tap.features.details.ui.utils
import com.tangem.tap.features.details.redux.CardInfo
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
internal fun CardInfo.toResetCardDescriptionText(): TextReference {
return if (!this.hasBackup || this.isTwin) {
TextReference.Res(R.string.reset_card_without_backup_to_factory_message)
} else {
TextReference.Res(R.string.reset_card_with_backup_to_factory_message)
}
}

View file

@ -30,7 +30,7 @@ internal class WelcomeViewModel @Inject constructor(
DefaultLifecycleObserver {
private val initialIntent: SerializableIntent? = savedStateHandle.get<Bundle>(AppRoute.Welcome.INITIAL_INTENT_KEY)
?.let { it.unbundle(SerializableIntent.serializer()) }
?.unbundle(SerializableIntent.serializer())
private val stateInternal = MutableStateFlow(WelcomeScreenState())
val state: StateFlow<WelcomeScreenState> = stateInternal

View file

@ -117,19 +117,55 @@ sealed class AppRoute(val path: String) : Route {
) : AppRoute(path = "/details/${userWalletId.stringValue}")
@Serializable
data object DetailsSecurity : AppRoute(path = "/details/security")
data class DetailsSecurity(
val userWalletId: UserWalletId,
) : AppRoute(path = "/details/security"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val USER_WALLET_ID_KEY = "userWalletId"
}
}
@Serializable
data object CardSettings : AppRoute(path = "/card_settings")
data class CardSettings(
val userWalletId: UserWalletId,
) : AppRoute(path = "/card_settings/${userWalletId.stringValue}"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val USER_WALLET_ID_KEY = "userWalletId"
}
}
@Serializable
data object AppSettings : AppRoute(path = "/app_settings")
@Serializable
data object ResetToFactory : AppRoute(path = "/reset_to_factory")
data class ResetToFactory(
val userWalletId: UserWalletId,
) : AppRoute(path = "/reset_to_factory/${userWalletId.stringValue}"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val USER_WALLET_ID = "userWalletId"
}
}
@Serializable
data object AccessCodeRecovery : AppRoute(path = "/access_code_recovery")
data class AccessCodeRecovery(
val userWalletId: UserWalletId,
) : AppRoute(path = "/access_code_recovery/${userWalletId.stringValue}"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val USER_WALLET_ID_KEY = "userWalletId"
}
}
@Serializable
data object ManageTokens : AppRoute(path = "/manage_tokens")
@ -155,7 +191,16 @@ sealed class AppRoute(val path: String) : Route {
}
@Serializable
data object ReferralProgram : AppRoute(path = "/referral_program")
data class ReferralProgram(
val userWalletId: UserWalletId,
) : AppRoute(path = "/referral_program"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val USER_WALLET_ID_KEY = "userWalletId"
}
}
@Serializable
data class Swap(

View file

@ -27,10 +27,6 @@
"name": "STAKING_ENABLED",
"version": "undefined"
},
{
"name": "FULL_RESET_ENABLED",
"version": "5.12.0"
},
{
"name": "DETAILS_REDESIGN_ENABLED",
"version": "5.13.0"

View file

@ -143,7 +143,13 @@ internal class TangemCardTypesResolver(
override fun getRemainingSignatures(): Int? = card.wallets.firstOrNull()?.remainingSignatures
override fun getCardId(): String = card.cardId
override fun getCardId(): String {
return if (isTangemTwins()) {
card.getTwinCardIdForUser()
} else {
card.cardId
}
}
override fun isTestCard(): Boolean = card.isTestCard

View file

@ -13,6 +13,7 @@ import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.configs.Wallet2CardConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWallet
val ScanResponse.cardTypesResolver: CardTypesResolver
get() = TangemCardTypesResolver(
@ -27,6 +28,9 @@ val ScanResponse.derivationStyleProvider: DerivationStyleProvider
card,
)
val UserWallet.cardTypesResolver: CardTypesResolver
get() = scanResponse.cardTypesResolver
fun ScanResponse.twinsIsTwinned(): Boolean = card.isTangemTwins && walletData != null && secondTwinPublicKey != null
fun ScanResponse.supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed
fun ScanResponse.supportsBackup(): Boolean = card.settings.isBackupAllowed

View file

@ -16,12 +16,14 @@ interface UserWalletsListManager {
val userWallets: Flow<List<UserWallet>>
/** [Flow] with selected [UserWallet] updates */
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
val selectedUserWallet: Flow<UserWallet>
/** [List] with all saved [UserWallet]s updates */
val userWalletsSync: List<UserWallet>
/** Selected [UserWallet] */
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
val selectedUserWalletSync: UserWallet?
/** Indicates that the [UserWalletsListManager] contains at least one saved [UserWallet] */

View file

@ -17,6 +17,7 @@ import com.tangem.domain.wallets.models.UserWallet
*/
class GetSelectedWalletSyncUseCase(private val userWalletsListManager: UserWalletsListManager) {
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
operator fun invoke(): Either<GetUserWalletError, UserWallet> {
return either {
ensureNotNull(

View file

@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.Flow
*/
class GetSelectedWalletUseCase(private val userWalletsListManager: UserWalletsListManager) {
@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features")
operator fun invoke(): Either<GetUserWalletError, Flow<UserWallet>> {
return either {
userWalletsListManager.selectedUserWallet

View file

@ -1,12 +1,13 @@
package com.tangem.feature.referral.domain
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.referral.domain.models.ReferralData
interface ReferralInteractor {
val isDemoMode: Boolean
suspend fun getReferralStatus(): ReferralData
suspend fun getReferralStatus(userWalletId: UserWalletId): ReferralData
suspend fun startReferral(): ReferralData
suspend fun startReferral(userWalletId: UserWalletId): ReferralData
}

View file

@ -3,7 +3,8 @@ package com.tangem.feature.referral.domain
import arrow.core.getOrElse
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.referral.domain.models.ReferralData
import com.tangem.feature.referral.domain.models.TokenData
import com.tangem.lib.crypto.UserWalletManager
@ -14,7 +15,7 @@ internal class ReferralInteractorImpl(
private val repository: ReferralRepository,
private val userWalletManager: UserWalletManager,
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
) : ReferralInteractor {
@ -22,20 +23,20 @@ internal class ReferralInteractorImpl(
override val isDemoMode: Boolean get() = repository.isDemoMode
override suspend fun getReferralStatus(): ReferralData {
val referralData = repository.getReferralData(userWalletManager.getWalletId())
override suspend fun getReferralStatus(userWalletId: UserWalletId): ReferralData {
val referralData = repository.getReferralData(userWalletId.stringValue)
saveReferralTokens(referralData.tokens)
return referralData
}
override suspend fun startReferral(): ReferralData {
override suspend fun startReferral(userWalletId: UserWalletId): ReferralData {
if (tokensForReferral.isEmpty()) error("Tokens for ref is empty")
val tokenData = tokensForReferral.first()
val userWallet = getSelectedWalletSyncUseCase().getOrElse {
error("Failed to get selected wallet: $it")
val userWallet = getUserWalletUseCase(userWalletId).getOrElse {
error("Failed to get user wallet $userWalletId: $it")
}
val cryptoCurrency = repository.getCryptoCurrency(userWalletId = userWallet.walletId, tokenData = tokenData)

View file

@ -2,7 +2,7 @@ package com.tangem.feature.referral.domain.di
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.referral.domain.ReferralInteractor
import com.tangem.feature.referral.domain.ReferralInteractorImpl
import com.tangem.feature.referral.domain.ReferralRepository
@ -23,14 +23,14 @@ class ReferralDomainModule {
referralRepository: ReferralRepository,
userWalletManager: UserWalletManager,
derivePublicKeysUseCase: DerivePublicKeysUseCase,
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
getUserWalletUseCase: GetUserWalletUseCase,
addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
): ReferralInteractor {
return ReferralInteractorImpl(
repository = referralRepository,
userWalletManager = userWalletManager,
derivePublicKeysUseCase = derivePublicKeysUseCase,
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
getUserWalletUseCase = getUserWalletUseCase,
addCryptoCurrenciesUseCase = addCryptoCurrenciesUseCase,
)
}

View file

@ -31,6 +31,7 @@ dependencies {
implementation(deps.compose.ui.tooling)
/** Domain */
implementation(projects.domain.wallets.models)
implementation(projects.features.referral.domain)
/** Other libraries */

View file

@ -1,11 +1,16 @@
package com.tangem.feature.referral.viewmodels
import android.os.Bundle
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.referral.analytics.ReferralEvents
import com.tangem.feature.referral.domain.ReferralInteractor
import com.tangem.feature.referral.domain.models.DiscountType
@ -29,15 +34,20 @@ internal class ReferralViewModel @Inject constructor(
private val referralInteractor: ReferralInteractor,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
var uiState: ReferralStateHolder by mutableStateOf(createInitiallyUiState())
private set
private val userWalletId = savedStateHandle.get<Bundle>(AppRoute.ReferralProgram.USER_WALLET_ID_KEY)
?.unbundle(UserWalletId.serializer())
?: error("User wallet ID is required for Referral screen")
private var referralRouter: ReferralRouter by Delegates.notNull()
private var lastReferralData: ReferralData? = null
var uiState: ReferralStateHolder by mutableStateOf(createInitiallyUiState())
private set
init {
loadReferralData()
}
@ -66,7 +76,7 @@ internal class ReferralViewModel @Inject constructor(
uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading)
viewModelScope.launch(dispatchers.main) {
runCatching(dispatchers.io) {
referralInteractor.getReferralStatus().apply {
referralInteractor.getReferralStatus(userWalletId).apply {
lastReferralData = this
}
}
@ -82,7 +92,7 @@ internal class ReferralViewModel @Inject constructor(
analyticsEventHandler.send(ReferralEvents.ClickParticipate)
uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading)
viewModelScope.launch(dispatchers.main) {
runCatching(dispatchers.io) { referralInteractor.startReferral() }
runCatching(dispatchers.io) { referralInteractor.startReferral(userWalletId) }
.onSuccess(::showContent)
.onFailure { throwable ->
if (throwable is UserCancelledException) {

View file

@ -25,6 +25,7 @@ dependencies {
implementation(projects.common.routing)
/* Project - Domain */
implementation(projects.domain.legacy)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)

View file

@ -3,6 +3,7 @@ package com.tangem.feature.walletsettings.component.preview
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.navigation.DummyRouter
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.feature.walletsettings.entity.WalletSettingsUM
import com.tangem.feature.walletsettings.ui.WalletSettingsScreen
@ -15,7 +16,9 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent {
items = ItemsBuilder(
router = DummyRouter(),
).buildItems(
walletName = "My wallet",
userWalletId = UserWalletId("011"),
userWalletName = "My Wallet",
isReferralAvailable = true,
renameWallet = {},
forgetWallet = {},
),

View file

@ -11,6 +11,7 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
@ -75,7 +76,9 @@ internal class WalletSettingsModel @Inject constructor(
userWallet: UserWallet,
dialogNavigation: SlotNavigation<DialogConfig>,
): PersistentList<WalletSettingsItemUM> = itemsBuilder.buildItems(
walletName = userWallet.name,
userWalletId = userWallet.walletId,
userWalletName = userWallet.name,
isReferralAvailable = userWallet.cardTypesResolver.isTangemWallet(),
renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) },
forgetWallet = { forgetWallet(userWallet.walletId) },
)

View file

@ -6,9 +6,12 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.block.model.BlockUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM
import com.tangem.feature.walletsettings.impl.R
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import javax.inject.Inject
@ComponentScoped
@ -16,9 +19,15 @@ internal class ItemsBuilder @Inject constructor(
private val router: Router,
) {
fun buildItems(walletName: String, forgetWallet: () -> Unit, renameWallet: () -> Unit) = persistentListOf(
buildNameItem(walletName, renameWallet),
buildCardItem(),
fun buildItems(
userWalletId: UserWalletId,
userWalletName: String,
isReferralAvailable: Boolean,
forgetWallet: () -> Unit,
renameWallet: () -> Unit,
): PersistentList<WalletSettingsItemUM> = persistentListOf(
buildNameItem(userWalletName, renameWallet),
buildCardItem(userWalletId, isReferralAvailable),
buildForgetItem(forgetWallet),
)
@ -29,37 +38,37 @@ internal class ItemsBuilder @Inject constructor(
onClick = renameWallet,
)
private fun buildCardItem() = WalletSettingsItemUM.WithItems(
id = "card",
blocks = buildCardBlocks(),
description = resourceReference(R.string.settings_card_settings_footer),
)
private fun buildCardItem(userWalletId: UserWalletId, isReferralAvailable: Boolean) =
WalletSettingsItemUM.WithItems(
id = "card",
description = resourceReference(R.string.settings_card_settings_footer),
blocks = buildList {
BlockUM(
text = resourceReference(R.string.card_settings_title),
iconRes = R.drawable.ic_card_settings_24,
onClick = { router.push(AppRoute.CardSettings(userWalletId)) },
).let(::add)
private fun buildCardBlocks() = persistentListOf(
BlockUM(
text = resourceReference(R.string.card_settings_title),
iconRes = R.drawable.ic_card_settings_24,
onClick = { router.push(AppRoute.CardSettings) },
),
BlockUM(
text = resourceReference(R.string.referral_title),
iconRes = R.drawable.ic_add_friends_24,
onClick = { router.push(AppRoute.ReferralProgram) },
),
)
if (isReferralAvailable) {
BlockUM(
text = resourceReference(R.string.referral_title),
iconRes = R.drawable.ic_add_friends_24,
onClick = { router.push(AppRoute.ReferralProgram(userWalletId)) },
).let(::add)
}
}.toImmutableList(),
)
private fun buildForgetItem(forgetWallet: () -> Unit) = WalletSettingsItemUM.WithItems(
id = "forget",
blocks = buildForgetBlocks(forgetWallet),
description = resourceReference(R.string.settings_forget_wallet_footer),
)
private fun buildForgetBlocks(forgetWallet: () -> Unit) = persistentListOf(
BlockUM(
text = resourceReference(R.string.settings_forget_wallet),
iconRes = R.drawable.ic_card_foget_24,
onClick = forgetWallet,
accentType = BlockUM.AccentType.WARNING,
blocks = persistentListOf(
BlockUM(
text = resourceReference(R.string.settings_forget_wallet),
iconRes = R.drawable.ic_card_foget_24,
onClick = forgetWallet,
accentType = BlockUM.AccentType.WARNING,
),
),
)
}

View file

@ -176,6 +176,8 @@ internal class WalletViewModel @Inject constructor(
}
}
// It's okay here because we need to be able to observe the selected wallet changes
@Suppress("DEPRECATION")
private fun subscribeOnSelectedWalletFlow() {
getSelectedWalletUseCase().onRight {
it