Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-24 21:30:53 +03:00
parent 999ae1359f
commit 8212dffeb6
12 changed files with 243 additions and 122 deletions

View file

@ -0,0 +1,40 @@
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.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.routing.AppRouter
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent
import com.tangem.tap.features.details.ui.resetcard.model.ResetCardModel
import com.tangem.tap.store
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultResetCardComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: ResetCardComponent.Params,
) : ResetCardComponent, AppComponentContext by appComponentContext {
private val model: ResetCardModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.screenState.collectAsStateWithLifecycle()
ResetCardScreen(
modifier = modifier,
state = state,
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
)
}
@AssistedFactory
interface Factory : ResetCardComponent.Factory {
override fun create(context: AppComponentContext, params: ResetCardComponent.Params): DefaultResetCardComponent
}
}

View file

@ -1,34 +0,0 @@
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
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.store
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class ResetCardFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
private val viewModel: ResetCardViewModel by viewModels()
@Composable
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.screenState.collectAsStateWithLifecycle()
ResetCardScreen(
state = state,
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
modifier = modifier,
)
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.tap.features.details.ui.resetcard.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.wallets.models.UserWalletId
interface ResetCardComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val cardId: String,
val isActiveBackupStatus: Boolean,
val backupCardsCount: Int,
)
interface Factory : ComponentFactory<Params, ResetCardComponent>
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.features.details.ui.resetcard.di
import com.tangem.core.decompose.model.Model
import com.tangem.tap.features.details.ui.resetcard.DefaultResetCardComponent
import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent
import com.tangem.tap.features.details.ui.resetcard.model.ResetCardModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(SingletonComponent::class)
internal interface ResetCardFeatureModule {
@Binds
fun bindFactory(impl: DefaultResetCardComponent.Factory): ResetCardComponent.Factory
@Binds
@IntoMap
@ClassKey(ResetCardModel::class)
fun bindModel(model: ResetCardModel): Model
}

View file

@ -1,21 +1,19 @@
package com.tangem.tap.features.details.ui.resetcard
package com.tangem.tap.features.details.ui.resetcard.model
import android.os.Bundle
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.compose.runtime.Stable
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.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
import com.tangem.domain.card.ResetCardUseCase
import com.tangem.domain.card.ResetCardUserCodeParams
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
@ -24,9 +22,12 @@ import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor
import com.tangem.tap.features.details.ui.common.utils.getResetToFactoryDescription
import com.tangem.tap.features.details.ui.resetcard.ResetCardDialog
import com.tangem.tap.features.details.ui.resetcard.ResetCardScreenState
import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
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
@ -35,8 +36,11 @@ import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@HiltViewModel
internal class ResetCardViewModel @Inject constructor(
@Stable
@ComponentScoped
internal class ResetCardModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer,
getUserWalletUseCase: GetUserWalletUseCase,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val resetCardUseCase: ResetCardUseCase,
@ -45,13 +49,12 @@ internal class ResetCardViewModel @Inject constructor(
private val userWalletsListManager: UserWalletsListManager,
private val analyticsEventHandler: AnalyticsEventHandler,
private val cardSettingsInteractor: CardSettingsInteractor,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
) : Model() {
private val params = paramsContainer.require<ResetCardComponent.Params>()
// region Card-set specific data. All cards from single set have the same userWalletId and cardTypesResolver
private val currentUserWalletId = savedStateHandle.get<Bundle>(AppRoute.ResetToFactory.USER_WALLET_ID)
?.unbundle(UserWalletId.serializer())
?: error("UserWalletId must be provided for ResetCardViewModel")
private val currentUserWalletId = params.userWalletId
// Use only for card-specific data
private val userWallet = getUserWalletUseCase(userWalletId = currentUserWalletId)
@ -65,15 +68,11 @@ internal class ResetCardViewModel @Inject constructor(
// endregion
// region Data of card that was scanned on CardSettings
private val primaryCardId: String = savedStateHandle.get<String>(AppRoute.ResetToFactory.CARD_ID)
?: error("CardId must be provided for ResetCardViewModel")
private val primaryCardId: String = params.cardId
private val isActiveBackupPrimaryCard =
savedStateHandle.get<Boolean>(AppRoute.ResetToFactory.IS_ACTIVE_BACKUP_STATUS)
?: error("IsActiveBackupCard must be provided for ResetCardViewModel")
private val isActiveBackupPrimaryCard = params.isActiveBackupStatus
private val primaryBackupCardsCount = savedStateHandle.get<Int>(AppRoute.ResetToFactory.BACKUP_CARDS_COUNT)
?: error("CardCount must be provided for ResetCardViewModel")
private val primaryBackupCardsCount = params.backupCardsCount
// endregion
// TODO: move logic to separate domain entity
@ -177,7 +176,7 @@ internal class ResetCardViewModel @Inject constructor(
}
private fun makeFullReset() {
viewModelScope.launch {
modelScope.launch {
resetCardUseCase(cardId = primaryCardId, params = currentUserCodeParams).onRight {
deleteSavedAccessCodesUseCase(cardId = primaryCardId)
val hasUserWallets = deleteWalletUseCase(userWalletId = currentUserWalletId).getOrElse {
@ -203,7 +202,7 @@ internal class ResetCardViewModel @Inject constructor(
private fun onContinueResetClick() {
dismissDialog()
viewModelScope.launch {
modelScope.launch {
resetCardUseCase(
cardNumber = resetBackupCardCount + 1,
params = currentUserCodeParams,

View file

@ -0,0 +1,43 @@
package com.tangem.tap.features.details.ui.securitymode
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.routing.AppRouter
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent
import com.tangem.tap.features.details.ui.securitymode.model.SecurityModeModel
import com.tangem.tap.store
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultSecurityModeComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: SecurityModeComponent.Params,
) : SecurityModeComponent, AppComponentContext by appComponentContext {
private val model: SecurityModeModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.screenState.collectAsStateWithLifecycle()
SecurityModeScreen(
modifier = modifier,
state = state,
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
)
}
@AssistedFactory
interface Factory : SecurityModeComponent.Factory {
override fun create(
context: AppComponentContext,
params: SecurityModeComponent.Params,
): DefaultSecurityModeComponent
}
}

View file

@ -1,34 +0,0 @@
package com.tangem.tap.features.details.ui.securitymode
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
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.store
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class SecurityModeFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
private val viewModel: SecurityModeViewModel by viewModels()
@Composable
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.screenState.collectAsStateWithLifecycle()
SecurityModeScreen(
modifier = modifier,
state = state,
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
)
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.tap.features.details.ui.securitymode.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.wallets.models.UserWalletId
interface SecurityModeComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
)
interface Factory : ComponentFactory<Params, SecurityModeComponent>
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.features.details.ui.securitymode.di
import com.tangem.core.decompose.model.Model
import com.tangem.tap.features.details.ui.securitymode.DefaultSecurityModeComponent
import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent
import com.tangem.tap.features.details.ui.securitymode.model.SecurityModeModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(SingletonComponent::class)
internal interface SecurityModeFeatureModule {
@Binds
fun bindFactory(impl: DefaultSecurityModeComponent.Factory): SecurityModeComponent.Factory
@Binds
@IntoMap
@ClassKey(SecurityModeModel::class)
fun bindModel(model: SecurityModeModel): Model
}

View file

@ -1,11 +1,12 @@
package com.tangem.tap.features.details.ui.securitymode
package com.tangem.tap.features.details.ui.securitymode.model
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.compose.runtime.Stable
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -15,18 +16,21 @@ import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor
import com.tangem.tap.features.details.ui.common.utils.getAllowedSecurityOptions
import com.tangem.tap.features.details.ui.common.utils.getCurrentSecurityOption
import com.tangem.tap.features.details.ui.securitymode.SecurityModeScreenState
import com.tangem.tap.store
import dagger.hilt.android.lifecycle.HiltViewModel
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
internal class SecurityModeViewModel @Inject constructor(
@Stable
@ComponentScoped
internal class SecurityModeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val tangemSdkManager: TangemSdkManager,
private val cardSettingsInteractor: CardSettingsInteractor,
) : ViewModel() {
) : Model() {
private val scannedScanResponse = cardSettingsInteractor.scannedScanResponse.value
?: error("Scan response is null")
@ -62,7 +66,7 @@ internal class SecurityModeViewModel @Inject constructor(
val cardId = scannedScanResponse.card.cardId
val selectedOption = screenState.value.selectedSecurityMode
viewModelScope.launch {
modelScope.launch {
val result = when (selectedOption) {
SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId)
SecurityOption.PassCode -> tangemSdkManager.setPasscode(cardId)

View file

@ -24,8 +24,8 @@ import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorCom
import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.api.AccessCodeRecoveryComponent
import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment
import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment
import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent
import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent
import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent
import com.tangem.tap.features.home.api.HomeComponent
import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment
@ -69,6 +69,8 @@ internal class ChildFactory @Inject constructor(
private val cardSettingsComponentFactory: CardSettingsComponent.Factory,
private val appCurrencySelectorComponentFactory: AppCurrencySelectorComponent.Factory,
private val appSettingsComponentFactory: AppSettingsComponent.Factory,
private val securityModeComponentFactory: SecurityModeComponent.Factory,
private val resetCardComponentFactory: ResetCardComponent.Factory,
private val walletRouter: WalletRouter,
private val testerRouter: TesterRouter,
private val pushNotificationRouter: PushNotificationsRouter,
@ -319,14 +321,33 @@ internal class ChildFactory @Inject constructor(
componentFactory = appSettingsComponentFactory,
)
}
is AppRoute.DetailsSecurity -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SecurityModeComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = securityModeComponentFactory,
)
}
is AppRoute.ResetToFactory -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = ResetCardComponent.Params(
userWalletId = route.userWalletId,
cardId = route.cardId,
isActiveBackupStatus = route.isActiveBackupStatus,
backupCardsCount = route.backupCardsCount,
),
componentFactory = resetCardComponentFactory,
)
}
is AppRoute.SaveWallet,
is AppRoute.DetailsSecurity,
is AppRoute.OnboardingNote,
is AppRoute.OnboardingOther,
is AppRoute.OnboardingTwins,
is AppRoute.OnboardingWallet,
is AppRoute.ReferralProgram,
is AppRoute.ResetToFactory,
is AppRoute.Wallet,
is AppRoute.PushNotification,
-> error("Unsupported route: $route")
@ -396,7 +417,13 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.DetailsSecurity -> {
route.asFragmentChild(Provider { SecurityModeFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SecurityModeComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = securityModeComponentFactory,
)
}
is AppRoute.Disclaimer -> {
route.asComponentChild(
@ -451,7 +478,16 @@ internal class ChildFactory @Inject constructor(
route.asFragmentChild(Provider { ReferralFragment() })
}
is AppRoute.ResetToFactory -> {
route.asFragmentChild(Provider { ResetCardFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = ResetCardComponent.Params(
userWalletId = route.userWalletId,
cardId = route.cardId,
isActiveBackupStatus = route.isActiveBackupStatus,
backupCardsCount = route.backupCardsCount,
),
componentFactory = resetCardComponentFactory,
)
}
is AppRoute.Swap -> {
route.asComponentChild(

View file

@ -103,10 +103,7 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class DetailsSecurity(
val userWalletId: UserWalletId,
) : AppRoute(path = "/details/security"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
}
) : AppRoute(path = "/details/security")
@Serializable
data class CardSettings(
@ -136,18 +133,7 @@ sealed class AppRoute(val path: String) : Route {
"/$cardId" +
"/$isActiveBackupStatus" +
"/$backupCardsCount",
),
RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val USER_WALLET_ID = "userWalletId"
const val CARD_ID = "cardId"
const val IS_ACTIVE_BACKUP_STATUS = "isActiveBackupStatus"
const val BACKUP_CARDS_COUNT = "backupCardsCount"
}
}
)
@Serializable
data object AccessCodeRecovery : AppRoute(path = "/access_code_recovery"), RouteBundleParams {