Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-21 12:07:59 +03:00
parent 08a78738cb
commit 3477c8f591
13 changed files with 215 additions and 101 deletions

View file

@ -1,27 +0,0 @@
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.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class CardSettingsFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
private val viewModel: CardSettingsViewModel by viewModels()
@Composable
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.screenState.collectAsStateWithLifecycle()
CardSettingsScreen(modifier = modifier, state = state)
}
}

View file

@ -0,0 +1,36 @@
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.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.model.CardSettingsModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultCardSettingsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: CardSettingsComponent.Params,
) : CardSettingsComponent, AppComponentContext by appComponentContext {
private val model: CardSettingsModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.screenState.collectAsStateWithLifecycle()
CardSettingsScreen(modifier = modifier, state = state)
}
@AssistedFactory
interface Factory : CardSettingsComponent.Factory {
override fun create(
context: AppComponentContext,
params: CardSettingsComponent.Params,
): DefaultCardSettingsComponent
}
}

View file

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

View file

@ -1,33 +0,0 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
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
class AccessCodeRecoveryFragment : ComposeFragment() {
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 = state,
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
)
}
}

View file

@ -0,0 +1,40 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
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.cardsettings.coderecovery.api.AccessCodeRecoveryComponent
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.model.AccessCodeRecoveryModel
import com.tangem.tap.store
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("UnusedPrivateMember")
internal class DefaultAccessCodeRecoveryComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
) : AccessCodeRecoveryComponent, AppComponentContext by appComponentContext {
private val model: AccessCodeRecoveryModel = getOrCreateModel()
@Composable
override fun Content(modifier: Modifier) {
val state by model.screenState.collectAsStateWithLifecycle()
AccessCodeRecoveryScreen(
state = state,
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
)
}
@AssistedFactory
interface Factory : AccessCodeRecoveryComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultAccessCodeRecoveryComponent
}
}

View file

@ -0,0 +1,9 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface AccessCodeRecoveryComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Unit, AccessCodeRecoveryComponent>
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery.di
import com.tangem.core.decompose.model.Model
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.DefaultAccessCodeRecoveryComponent
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.api.AccessCodeRecoveryComponent
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.model.AccessCodeRecoveryModel
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 AccessCodeRecoveryFeatureModule {
@Binds
fun bindFactory(impl: DefaultAccessCodeRecoveryComponent.Factory): AccessCodeRecoveryComponent.Factory
@Binds
@IntoMap
@ClassKey(AccessCodeRecoveryModel::class)
fun bindModel(model: AccessCodeRecoveryModel): Model
}

View file

@ -1,29 +1,33 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
package com.tangem.tap.features.details.ui.cardsettings.coderecovery.model
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.compose.runtime.Stable
import com.tangem.common.doOnSuccess
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
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryScreenState
import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor
import com.tangem.tap.features.details.ui.common.utils.isAccessCodeRecoveryEnabled
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 AccessCodeRecoveryViewModel @Inject constructor(
@Stable
@ComponentScoped
internal class AccessCodeRecoveryModel @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")
@ -47,7 +51,7 @@ internal class AccessCodeRecoveryViewModel @Inject constructor(
)
}
private fun saveChanges() = viewModelScope.launch {
private fun saveChanges() = modelScope.launch {
val isEnabled = screenState.value.enabledSelection
tangemSdkManager

View file

@ -0,0 +1,25 @@
package com.tangem.tap.features.details.ui.cardsettings.di
import com.tangem.core.decompose.model.Model
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.DefaultCardSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.model.CardSettingsModel
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 CardSettingsFeatureModule {
@Binds
fun bindFactory(impl: DefaultCardSettingsComponent.Factory): CardSettingsComponent.Factory
@Binds
@IntoMap
@ClassKey(CardSettingsModel::class)
fun bindModel(model: CardSettingsModel): Model
}

View file

@ -1,16 +1,15 @@
package com.tangem.tap.features.details.ui.cardsettings
package com.tangem.tap.features.details.ui.cardsettings.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.CompletionResult
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.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.CardTypesResolver
@ -20,7 +19,6 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -29,13 +27,16 @@ 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.features.details.ui.cardsettings.CardInfo
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsScreenState
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor
import com.tangem.tap.features.details.ui.common.utils.*
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.R
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -43,22 +44,24 @@ import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@HiltViewModel
internal class CardSettingsViewModel @Inject constructor(
@Stable
@ComponentScoped
internal class CardSettingsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val scanCardProcessor: ScanCardProcessor,
private val tangemSdkManager: TangemSdkManager,
private val cardSettingsInteractor: CardSettingsInteractor,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
) : Model() {
private val params = paramsContainer.require<CardSettingsComponent.Params>()
private var previousBiometricsRequestPolicy: Boolean = false
private val userWalletId = savedStateHandle.get<Bundle>(AppRoute.CardSettings.USER_WALLET_ID_KEY)
?.unbundle(UserWalletId.serializer())
?: error("User wallet ID is required for CardSettingsViewModel")
private val userWalletId = params.userWalletId
val screenState: MutableStateFlow<CardSettingsScreenState> = MutableStateFlow(getInitialState())
@ -68,10 +71,11 @@ internal class CardSettingsViewModel @Inject constructor(
cardSettingsInteractor.scannedScanResponse
.filterNotNull()
.onEach(::updateCardDetails)
.launchIn(viewModelScope)
.launchIn(modelScope)
}
override fun onCleared() {
override fun onDestroy() {
super.onDestroy()
// Restore the previous value of access code request policy
cardSdkConfigRepository.isBiometricsRequestPolicy = previousBiometricsRequestPolicy
}
@ -96,7 +100,7 @@ internal class CardSettingsViewModel @Inject constructor(
onBackClick = ::onBackClick,
)
private fun scanCard() = viewModelScope.launch {
private fun scanCard() = modelScope.launch {
scanCardProcessor.scan(
analyticsSource = com.tangem.core.analytics.models.AnalyticsParam.ScreensSources.Settings,
allowsRequestAccessCodeFromRepository = true,
@ -217,7 +221,7 @@ internal class CardSettingsViewModel @Inject constructor(
}
}
private fun changeAccessCode() = viewModelScope.launch {
private fun changeAccessCode() = modelScope.launch {
val scanResponse = requireNotNull(cardSettingsInteractor.scannedScanResponse.value) { "Scan response is null" }
when (val result = tangemSdkManager.setAccessCode(scanResponse.card.cardId)) {

View file

@ -22,8 +22,8 @@ import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.navigation.WalletRouter
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorFragment
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment
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.walletconnect.api.WalletConnectComponent
@ -65,6 +65,8 @@ internal class ChildFactory @Inject constructor(
private val tokenDetailsComponentFactory: TokenDetailsComponent.Factory,
private val walletConnectComponentFactory: WalletConnectComponent.Factory,
private val qrScanningComponentFactory: QrScanningComponent.Factory,
private val accessCodeRecoveryComponentFactory: AccessCodeRecoveryComponent.Factory,
private val cardSettingsComponentFactory: CardSettingsComponent.Factory,
private val walletRouter: WalletRouter,
private val testerRouter: TesterRouter,
private val pushNotificationRouter: PushNotificationsRouter,
@ -285,11 +287,23 @@ internal class ChildFactory @Inject constructor(
componentFactory = qrScanningComponentFactory,
)
}
is AppRoute.AccessCodeRecovery,
is AppRoute.AccessCodeRecovery -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = accessCodeRecoveryComponentFactory,
)
}
is AppRoute.CardSettings -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = CardSettingsComponent.Params(userWalletId = route.userWalletId),
componentFactory = cardSettingsComponentFactory,
)
}
is AppRoute.AppCurrencySelector,
is AppRoute.SaveWallet,
is AppRoute.AppSettings,
is AppRoute.CardSettings,
is AppRoute.DetailsSecurity,
is AppRoute.OnboardingNote,
is AppRoute.OnboardingOther,
@ -314,7 +328,11 @@ internal class ChildFactory @Inject constructor(
Child.Initial
}
is AppRoute.AccessCodeRecovery -> {
route.asFragmentChild(Provider { AccessCodeRecoveryFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = accessCodeRecoveryComponentFactory,
)
}
is AppRoute.AppCurrencySelector -> {
route.asFragmentChild(Provider { AppCurrencySelectorFragment() })
@ -340,7 +358,11 @@ internal class ChildFactory @Inject constructor(
route.asFragmentChild(Provider { AppSettingsFragment() })
}
is AppRoute.CardSettings -> {
route.asFragmentChild(Provider { CardSettingsFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = CardSettingsComponent.Params(userWalletId = route.userWalletId),
componentFactory = cardSettingsComponentFactory,
)
}
is AppRoute.Details -> {
route.asComponentChild(

View file

@ -111,14 +111,7 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
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"
}
}
) : AppRoute(path = "/card_settings/${userWalletId.stringValue}")
@Serializable
data object AppSettings : AppRoute(path = "/app_settings")

View file

@ -1,9 +1,11 @@
package com.tangem.domain.visa.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import kotlinx.serialization.Serializable
@Serializable
@JsonClass(generateAdapter = true)
data class VisaActivationOrderInfo(
@Json(name = "orderId") val orderId: String,
@Json(name = "customer_id") val customerId: String,