Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-26 13:52:26 +00:00
commit 1d5e9692cf
382 changed files with 6038 additions and 4437 deletions

View file

@ -22,9 +22,14 @@ android {
jniLibs {
useLegacyPackaging = true
}
resources.excludes.add("META-INF/DEPENDENCIES")
resources.excludes.add("META-INF/LICENSE.md")
resources.excludes.add("META-INF/NOTICE.md")
resources.excludes.add("META-INF/DISCLAIMER")
resources.excludes.add("META-INF/DEPENDENCIES")
resources.excludes.add("META-INF/FastDoubleParser-NOTICE")
resources.excludes.add("META-INF/FastDoubleParser-LICENSE")
resources.excludes.add("META-INF/io.netty.versions.properties")
resources.excludes.add("META-INF/INDEX.LIST")
}
androidResources {
generateLocaleConfig = true
@ -141,7 +146,7 @@ dependencies {
/** Features */
implementation(projects.features.onboarding)
implementation(projects.features.referral.presentation)
implementation(projects.features.referral.impl)
implementation(projects.features.referral.domain)
implementation(projects.features.referral.data)
implementation(projects.features.swap.api)

@ -1 +1 @@
Subproject commit 89dac3cd1e171d5801596ad151aa3af3fbc6c140
Subproject commit af8b453fd25b29d949c05302ec282511dc2614b8

View file

@ -46,6 +46,7 @@ import com.tangem.core.ui.message.EventMessageEffect
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.data.balancehiding.DefaultDeviceFlipDetector
import com.tangem.data.card.sdk.CardSdkOwner
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
@ -59,13 +60,9 @@ import com.tangem.domain.staking.SendUnsubmittedHashesUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import com.tangem.features.wallet.navigation.WalletRouter
import com.tangem.google.GoogleServicesHelper
import com.tangem.operations.backup.BackupService
@ -141,18 +138,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var walletRouter: WalletRouter
@Inject
lateinit var tokenDetailsRouter: TokenDetailsRouter
@Inject
lateinit var walletConnectInteractor: WalletConnectInteractor
@Inject
lateinit var sendRouter: SendRouter
@Inject
lateinit var qrScanningRouter: QrScanningRouter
@Inject
lateinit var deepLinksRegistry: DeepLinksRegistry
@ -187,9 +175,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
internal lateinit var routingComponentFactory: RoutingComponent.Factory
@Inject
lateinit var pushNotificationsRouter: PushNotificationsRouter
@Inject
lateinit var cardRepository: CardRepository
@ -217,6 +202,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
internal lateinit var uiDependencies: UiDependencies
@Inject
internal lateinit var defaultDeviceFlipDetector: DefaultDeviceFlipDetector
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
@ -283,6 +271,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
lifecycle.addObserver(WindowObscurationObserver)
lifecycle.addObserver(defaultDeviceFlipDetector)
}
private fun installEventMessageEffect() {

View file

@ -5,7 +5,7 @@ import androidx.appcompat.app.AppCompatActivity
import com.arkivanov.decompose.defaultComponentContext
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.DefaultAppComponentContext
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.decompose.ui.UiMessageSender
@ -27,7 +27,7 @@ internal object RootAppComponentContextModule {
fun provideRootAppComponentContext(
@ActivityContext context: Context,
dispatchers: CoroutineDispatcherProvider,
componentBuilder: DecomposeComponent.Builder,
componentBuilder: ModelComponent.Builder,
@GlobalUiMessageSender messageSender: UiMessageSender,
): AppComponentContext {
return DefaultAppComponentContext(

View file

@ -490,6 +490,7 @@ internal class DefaultTangemSdkManager(
activationInput = activationInput,
coroutineScope = this,
),
cardId = activationInput.cardId,
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
)
}
@ -502,6 +503,7 @@ internal class DefaultTangemSdkManager(
runnable = VisaCustomerWalletApproveTask(
visaDataForApprove = visaDataForApprove,
),
cardId = visaDataForApprove.customerWalletCardId,
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
)
}

View file

@ -4,12 +4,15 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.models.UserWalletId
@JsonClass(generateAdapter = true)
internal data class UserWalletSensitiveInformation(
@Json(name = "wallets")
val wallets: List<CardDTO.Wallet>,
@Json(name = "visaCardActivationStatus")
val visaCardActivationStatus: VisaCardActivationStatus? = null,
)
@JsonClass(generateAdapter = true)

View file

@ -6,7 +6,10 @@ import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
internal val UserWallet.sensitiveInformation: UserWalletSensitiveInformation
get() = UserWalletSensitiveInformation(scanResponse.card.wallets)
get() = UserWalletSensitiveInformation(
wallets = scanResponse.card.wallets,
visaCardActivationStatus = scanResponse.visaCardActivationStatus,
)
internal val UserWallet.publicInformation: UserWalletPublicInformation
get() = UserWalletPublicInformation(
@ -19,6 +22,7 @@ internal val UserWallet.publicInformation: UserWalletPublicInformation
card = scanResponse.card.copy(
wallets = emptyList(),
),
visaCardActivationStatus = null,
),
hasBackupError = hasBackupError,
)
@ -45,6 +49,7 @@ internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInfo
card = scanResponse.card.copy(
wallets = sensitiveInformation.wallets,
),
visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus,
),
)
}
@ -70,5 +75,6 @@ internal fun UserWallet.lock(): UserWallet = copy(
card = scanResponse.card.copy(
wallets = emptyList(),
),
visaCardActivationStatus = null,
),
)

View file

@ -1,9 +1,9 @@
package com.tangem.tap.features.details.redux
import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
sealed class DetailsAction : Action {
@ -27,7 +27,7 @@ sealed class DetailsAction : Action {
}
data class CheckBiometricsStatus(
val lifecycleScope: LifecycleCoroutineScope,
val coroutineScope: CoroutineScope,
) : AppSettings()
data object EnrollBiometrics : AppSettings()

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.details.redux
import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.common.CompletionResult
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
@ -22,6 +21,7 @@ import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
@ -67,7 +67,7 @@ class DetailsMiddleware {
}
}
is DetailsAction.AppSettings.CheckBiometricsStatus -> {
observeBiometricsStatusChanges(action.lifecycleScope)
observeBiometricsStatusChanges(action.coroutineScope)
}
is DetailsAction.AppSettings.EnrollBiometrics -> {
enrollBiometrics()
@ -90,7 +90,7 @@ class DetailsMiddleware {
}
}
private fun observeBiometricsStatusChanges(lifecycleScope: LifecycleCoroutineScope) {
private fun observeBiometricsStatusChanges(scope: CoroutineScope) {
val needEnrollBiometricsFlow = flow {
do {
val needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull()
@ -108,7 +108,7 @@ class DetailsMiddleware {
.onEach { needEnrollBiometrics ->
store.dispatchWithMain(DetailsAction.AppSettings.BiometricsStatusChanged(needEnrollBiometrics))
}
.launchIn(lifecycleScope)
.launchIn(scope)
.saveIn(checkBiometricsStatusJobHolder)
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.features.details.ui.appcurrency
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.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.screen.ComposeFragment
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class AppCurrencySelectorFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
private val viewModel: AppCurrencySelectorViewModel by viewModels()
@Composable
override fun ScreenContent(modifier: Modifier) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
NavigationBar3ButtonsScrim()
AppCurrencySelectorScreen(
modifier = modifier,
state = uiState,
)
}
}

View file

@ -0,0 +1,38 @@
package com.tangem.tap.features.details.ui.appcurrency
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.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent
import com.tangem.tap.features.details.ui.appcurrency.model.AppCurrencySelectorModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("UnusedPrivateMember")
internal class DefaultAppCurrencySelectorComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
) : AppCurrencySelectorComponent, AppComponentContext by appComponentContext {
private val model: AppCurrencySelectorModel = getOrCreateModel()
@Composable
override fun Content(modifier: Modifier) {
val uiState by model.uiState.collectAsStateWithLifecycle()
NavigationBar3ButtonsScrim()
AppCurrencySelectorScreen(
modifier = modifier,
state = uiState,
)
}
@AssistedFactory
interface Factory : AppCurrencySelectorComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultAppCurrencySelectorComponent
}
}

View file

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

View file

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

View file

@ -1,35 +1,38 @@
package com.tangem.tap.features.details.ui.appcurrency
package com.tangem.tap.features.details.ui.appcurrency.model
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.compose.runtime.Stable
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.appcurrency.GetAvailableCurrenciesUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.SelectAppCurrencyUseCase
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorIntents
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState
import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorStateHolder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
internal class AppCurrencySelectorViewModel @Inject constructor(
@Stable
@ModelScoped
internal class AppCurrencySelectorModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getAvailableCurrenciesUseCase: GetAvailableCurrenciesUseCase,
private val selectAppCurrencyUseCase: SelectAppCurrencyUseCase,
private val router: AppRouter,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel(), AppCurrencySelectorIntents {
) : Model(), AppCurrencySelectorIntents {
private val stateController = AppCurrencySelectorStateHolder(
intents = this,
onSubscription = { fetchCurrencies() },
stateFlowScope = viewModelScope,
stateFlowScope = modelScope,
)
val uiState: StateFlow<AppCurrencySelectorState> = stateController.stateFlow
@ -47,7 +50,7 @@ internal class AppCurrencySelectorViewModel @Inject constructor(
}
override fun onCurrencyClick(currency: AppCurrencySelectorState.Currency) {
viewModelScope.launch(dispatchers.io) {
modelScope.launch(dispatchers.io) {
selectAppCurrencyUseCase(currency.id)
.onRight {
analyticsEventHandler.send(
@ -63,7 +66,7 @@ internal class AppCurrencySelectorViewModel @Inject constructor(
}
private fun fetchCurrencies() {
viewModelScope.launch(dispatchers.io) {
modelScope.launch(dispatchers.io) {
val availableCurrencies = getAvailableCurrenciesUseCase()
.onRight(stateController::updateStateWithAvailableCurrencies)
.getOrNull()

View file

@ -1,42 +0,0 @@
package com.tangem.tap.features.details.ui.appsettings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
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.store
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class AppSettingsFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
@Inject
lateinit var appCurrencyRepository: AppCurrencyRepository
@Composable
override fun ScreenContent(modifier: Modifier) {
val viewModel = hiltViewModel<AppSettingsViewModel>().apply {
LocalLifecycleOwner.current.lifecycle.addObserver(observer = this)
}
val state by viewModel.uiState.collectAsStateWithLifecycle()
AppSettingsScreen(
modifier = modifier,
state = state,
onBackClick = {
store.dispatchNavigationAction(AppRouter::pop)
},
)
}
}

View file

@ -0,0 +1,49 @@
package com.tangem.tap.features.details.ui.appsettings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.essenty.lifecycle.doOnResume
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.appsettings.api.AppSettingsComponent
import com.tangem.tap.features.details.ui.appsettings.model.AppSettingsModel
import com.tangem.tap.store
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("UnusedPrivateMember")
internal class DefaultAppSettingsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
) : AppSettingsComponent, AppComponentContext by appComponentContext {
private val model: AppSettingsModel = getOrCreateModel()
init {
doOnResume { model.onResume() }
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
AppSettingsScreen(
modifier = modifier,
state = state,
onBackClick = {
store.dispatchNavigationAction(AppRouter::pop)
},
)
}
@AssistedFactory
interface Factory : AppSettingsComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultAppSettingsComponent
}
}

View file

@ -2,13 +2,13 @@ package com.tangem.tap.features.details.ui.appsettings.analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState
import dagger.hilt.android.scopes.ViewModelScoped
import javax.inject.Inject
@ViewModelScoped
@ModelScoped
internal class AppSettingsItemsAnalyticsSender @Inject constructor(
private val analyticsHandler: AnalyticsEventHandler,
) {

View file

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

View file

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

View file

@ -1,8 +1,10 @@
package com.tangem.tap.features.details.ui.appsettings
package com.tangem.tap.features.details.ui.appsettings.model
import androidx.lifecycle.*
import androidx.compose.runtime.Stable
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.model.AppThemeMode
@ -20,12 +22,15 @@ import com.tangem.tap.features.details.redux.AppSetting
import com.tangem.tap.features.details.redux.AppSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState
import com.tangem.tap.features.details.ui.appsettings.analytics.AppSettingsItemsAnalyticsSender
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
@ -34,8 +39,10 @@ import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@Suppress("LongParameterList")
@HiltViewModel
internal class AppSettingsViewModel @Inject constructor(
@Stable
@ModelScoped
internal class AppSettingsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val appCurrencyRepository: AppCurrencyRepository,
private val walletsRepository: WalletsRepository,
private val canUseBiometryUseCase: CanUseBiometryUseCase,
@ -44,9 +51,7 @@ internal class AppSettingsViewModel @Inject constructor(
private val appThemeModeRepository: AppThemeModeRepository,
private val settingsRepository: SettingsRepository,
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
) : ViewModel(),
StoreSubscriber<DetailsState>,
DefaultLifecycleObserver {
) : Model(), StoreSubscriber<DetailsState> {
private val itemsFactory = AppSettingsItemsFactory()
private val dialogsFactory = AppSettingsDialogsFactory()
@ -82,11 +87,12 @@ internal class AppSettingsViewModel @Inject constructor(
}
}
override fun onResume(owner: LifecycleOwner) {
store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(owner.lifecycleScope))
fun onResume() {
store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(modelScope))
}
override fun onCleared() {
override fun onDestroy() {
super.onDestroy()
store.unsubscribe(subscriber = this)
}
@ -225,7 +231,7 @@ internal class AppSettingsViewModel @Inject constructor(
.saveIn(appCurrencyUpdatesJobHolder)
}
private fun bootstrapBiometricsUpdates() = viewModelScope.launch {
private fun bootstrapBiometricsUpdates() = modelScope.launch {
val state = AppSettingsState(
saveWallets = walletsRepository.shouldSaveUserWalletsSync(),
saveAccessCodes = settingsRepository.shouldSaveAccessCodes(),

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.ModelScoped
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
@ModelScoped
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.ModelScoped
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
@ModelScoped
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

@ -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.ModelScoped
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
@ModelScoped
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.ModelScoped
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
@ModelScoped
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

@ -0,0 +1,70 @@
package com.tangem.tap.features.details.ui.walletconnect
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import com.arkivanov.essenty.lifecycle.subscribe
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent
import com.tangem.tap.store
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import org.rekotlin.StoreSubscriber
@Suppress("UnusedPrivateMember")
internal class DefaultWalletConnectComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
) : WalletConnectComponent, AppComponentContext by appComponentContext, StoreSubscriber<WalletConnectState> {
private val model: WalletConnectModel = getOrCreateModel()
private var screenState: MutableState<WalletConnectScreenState> =
mutableStateOf(model.updateState(store.state.walletConnectState))
init {
lifecycle.subscribe(
onCreate = {
Analytics.send(WalletConnect.ScreenOpened())
},
onStart = {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.walletConnectState == newState.walletConnectState
}.select { it.walletConnectState }
}
},
onStop = {
store.unsubscribe(this)
},
)
}
override fun newState(state: WalletConnectState) {
screenState.value = model.updateState(state)
}
@Composable
override fun Content(modifier: Modifier) {
WalletConnectScreen(
modifier = modifier,
state = screenState.value,
onBackClick = {
store.dispatchNavigationAction(AppRouter::pop)
},
)
}
@AssistedFactory
interface Factory : WalletConnectComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultWalletConnectComponent
}
}

View file

@ -1,68 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber<WalletConnectState> {
@Inject
override lateinit var uiDependencies: UiDependencies
private val viewModel: WalletConnectViewModel by viewModels()
private var screenState: MutableState<WalletConnectScreenState>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(WalletConnect.ScreenOpened())
lifecycle.addObserver(viewModel)
screenState = mutableStateOf(viewModel.updateState(store.state.walletConnectState))
}
@Composable
override fun ScreenContent(modifier: Modifier) {
val state = screenState?.value ?: return
WalletConnectScreen(
modifier = modifier,
state = state,
onBackClick = {
store.dispatchNavigationAction(AppRouter::pop)
},
)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.walletConnectState == newState.walletConnectState
}.select { it.walletConnectState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: WalletConnectState) {
if (activity == null || view == null) return
screenState?.value = viewModel.updateState(state)
}
}

View file

@ -1,31 +1,34 @@
package com.tangem.tap.features.details.ui.walletconnect
import androidx.lifecycle.*
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.store
import dagger.hilt.android.lifecycle.HiltViewModel
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
internal class WalletConnectViewModel @Inject constructor(
@Stable
@ModelScoped
internal class WalletConnectModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
private val clipboardManager: ClipboardManager,
) : ViewModel(), DefaultLifecycleObserver {
) : Model() {
override fun onCreate(owner: LifecycleOwner) {
viewModelScope.launch {
init {
modelScope.launch {
listenToQrScanningUseCase(SourceType.WALLET_CONNECT)
.getOrElse { emptyFlow() }
.flowWithLifecycle(owner.lifecycle, minActiveState = Lifecycle.State.CREATED)
.collect { store.dispatch(WalletConnectAction.OpenSession(it)) }
}
}

View file

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

View file

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

View file

@ -0,0 +1,74 @@
package com.tangem.tap.features.home
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.arkivanov.essenty.lifecycle.subscribe
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.SystemBarsIconsDisposable
import com.tangem.core.ui.utils.findActivity
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.api.HomeComponent
import com.tangem.tap.features.home.compose.StoriesScreen
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.store
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import org.rekotlin.StoreSubscriber
@Suppress("UnusedPrivateMember")
internal class DefaultHomeComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
) : HomeComponent, AppComponentContext by appComponentContext, StoreSubscriber<HomeState> {
private val model: HomeModel = getOrCreateModel()
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
init {
lifecycle.subscribe(
onCreate = {
store.dispatch(HomeAction.OnCreate)
},
onStart = {
store.subscribe(subscriber = this) { state ->
state
.skipRepeats { oldState, newState -> oldState.homeState == newState.homeState }
.select(AppState::homeState)
}
},
onStop = {
store.unsubscribe(this)
},
)
}
@Composable
override fun Content(modifier: Modifier) {
val activity = LocalContext.current.findActivity()
BackHandler(onBack = activity::finish)
SystemBarsIconsDisposable(darkIcons = false)
StoriesScreen(
homeState = homeState,
onScanButtonClick = model::onScanClick,
onShopButtonClick = model::onShopClick,
onSearchTokensClick = model::onSearchClick,
)
}
override fun newState(state: HomeState) {
homeState.value = state
}
@AssistedFactory
interface Factory : HomeComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultHomeComponent
}
}

View file

@ -1,75 +0,0 @@
package com.tangem.tap.features.home
import android.os.Bundle
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsIconsDisposable
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.compose.StoriesScreen
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
internal class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
@Inject
override lateinit var uiDependencies: UiDependencies
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
private val viewModel by viewModels<HomeViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
store.dispatch(HomeAction.OnCreate)
}
@Composable
override fun ScreenContent(modifier: Modifier) {
BackHandler(onBack = requireActivity()::finish)
SystemBarsIconsDisposable(darkIcons = false)
ScreenContent()
}
override fun onStart() {
super.onStart()
store.subscribe(subscriber = this) { state ->
state
.skipRepeats { oldState, newState -> oldState.homeState == newState.homeState }
.select(AppState::homeState)
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: HomeState) {
if (activity == null || view == null) return
homeState.value = state
}
@Suppress("TopLevelComposableFunctions")
@Composable
private fun ScreenContent() {
StoriesScreen(
homeState = homeState,
onScanButtonClick = viewModel::onScanClick,
onShopButtonClick = viewModel::onShopClick,
onSearchTokensClick = viewModel::onSearchClick,
)
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.home
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.compose.runtime.Stable
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import com.tangem.common.routing.AppRoute
@ -10,6 +9,8 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardSdkConfigRepository
@ -30,7 +31,7 @@ import com.tangem.tap.features.home.redux.HIDE_PROGRESS_DELAY
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
import com.tangem.tap.store
import dagger.hilt.android.lifecycle.HiltViewModel
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -38,8 +39,10 @@ import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@HiltViewModel
internal class HomeViewModel @Inject constructor(
@Stable
@ModelScoped
internal class HomeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val scanCardProcessor: ScanCardProcessor,
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
private val saveWalletUseCase: SaveWalletUseCase,
@ -47,7 +50,7 @@ internal class HomeViewModel @Inject constructor(
private val settingsRepository: SettingsRepository,
private val urlOpener: UrlOpener,
private val analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel() {
) : Model() {
private val tangemErrorHandler = TangemTangemErrorsHandler(store)
@ -73,7 +76,7 @@ internal class HomeViewModel @Inject constructor(
}
private fun scanCard() {
viewModelScope.launch {
modelScope.launch {
cardSdkConfigRepository.isBiometricsRequestPolicy = settingsRepository.shouldSaveAccessCodes()
scanCardProcessor.scan(

View file

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

View file

@ -0,0 +1,25 @@
package com.tangem.tap.features.home.di
import com.tangem.core.decompose.model.Model
import com.tangem.tap.features.home.DefaultHomeComponent
import com.tangem.tap.features.home.HomeModel
import com.tangem.tap.features.home.api.HomeComponent
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 HomeFeatureModule {
@Binds
fun bindFactory(impl: DefaultHomeComponent.Factory): HomeComponent.Factory
@Binds
@IntoMap
@ClassKey(HomeModel::class)
fun bindModel(model: HomeModel): Model
}

View file

@ -1,6 +1,6 @@
package com.tangem.tap.features.welcome.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.tap.features.welcome.model.WelcomeModel
import dagger.Binds
@ -10,7 +10,7 @@ import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
@InstallIn(ModelComponent::class)
internal interface ModelModule {
@Binds

View file

@ -4,6 +4,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.lib.auth.ExpressAuthProvider
import kotlinx.coroutines.runBlocking
import java.util.UUID
@ -25,6 +26,17 @@ internal class DefaultExpressAuthProvider(
}
override fun getRefCode(): String {
val selectedUserWallet = userWalletsStore.selectedUserWalletOrNull ?: error("Can not get selected user wallet")
return when {
isRing(selectedUserWallet) -> "ring"
isChangeNow(selectedUserWallet) -> "ChangeNow"
isPartner(selectedUserWallet) -> "partner"
else -> ""
}
}
private fun isRing(selectedUserWallet: UserWallet): Boolean {
val addedWalletsWithRings = runBlocking {
appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY,
@ -32,9 +44,19 @@ internal class DefaultExpressAuthProvider(
)
}
val userWalletId = userWalletsStore.selectedUserWalletOrNull?.walletId?.stringValue
?: error("No user id provided")
return addedWalletsWithRings.contains(selectedUserWallet.walletId.stringValue)
}
return if (addedWalletsWithRings.contains(userWalletId)) "ring" else ""
private fun isChangeNow(selectedUserWallet: UserWallet): Boolean {
return selectedUserWallet.scanResponse.card.batchId == BATCH_ID_CHANGENOW
}
private fun isPartner(selectedUserWallet: UserWallet): Boolean {
return selectedUserWallet.scanResponse.card.batchId == BATCH_ID_PARTNER
}
private companion object {
const val BATCH_ID_CHANGENOW = "BB000013"
const val BATCH_ID_PARTNER = "AF990015"
}
}

View file

@ -1,19 +0,0 @@
package com.tangem.tap.network.auth
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.model.getAuthHeader
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import javax.inject.Inject
internal class DefaultVisaAuthProvider @Inject constructor(
private val userWalletsListManager: UserWalletsListManager,
) : TangemVisaAuthProvider {
override suspend fun getAuthHeader(cardId: String): String {
val card = userWalletsListManager.userWalletsSync.firstOrNull { it.cardId == cardId }
val status = card?.scanResponse?.visaCardActivationStatus as? VisaCardActivationStatus.Activated
?: return "Error in the app!"
return status.visaAuthTokens.getAuthHeader()
}
}

View file

@ -1,21 +1,17 @@
package com.tangem.tap.network.auth.di
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.tap.network.auth.*
import com.tangem.tap.network.auth.DefaultAppVersionProvider
import com.tangem.tap.network.auth.DefaultAuthProvider
import com.tangem.tap.network.auth.DefaultExpressAuthProvider
import com.tangem.tap.network.auth.DefaultStakeKitAuthProvider
import com.tangem.tap.network.auth.DefaultVisaAuthProvider
import com.tangem.utils.version.AppVersionProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -55,13 +51,4 @@ internal class AuthModule {
fun provideAppVersionProvider(): AppVersionProvider {
return DefaultAppVersionProvider()
}
}
@Module
@InstallIn(SingletonComponent::class)
internal interface AuthBindModule {
@Binds
@Singleton
fun bindVisaAuthProvider(authStorage: DefaultVisaAuthProvider): TangemVisaAuthProvider
}

View file

@ -153,6 +153,8 @@ internal val Blockchain.mercuryoNetwork: String?
Blockchain.Bitrock, Blockchain.BitrockTestnet -> null
Blockchain.Sonic, Blockchain.SonicTestnet -> null
Blockchain.ApeChain, Blockchain.ApeChainTestnet -> null
Blockchain.Scroll, Blockchain.ScrollTestnet -> null
Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet -> null
Blockchain.KaspaTestnet -> null
}
}

View file

@ -154,5 +154,7 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency?
Bitrock, BitrockTestnet -> null
Sonic, SonicTestnet -> null
ApeChain, ApeChainTestnet -> null
Scroll, ScrollTestnet -> null
ZkLinkNova, ZkLinkNovaTestnet -> null
KaspaTestnet -> null
}

View file

@ -33,7 +33,6 @@ import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
@ -67,7 +66,6 @@ data class DaggerGraphState(
val urlOpener: UrlOpener? = null,
val shareManager: ShareManager? = null,
val appRouter: AppRouter? = null,
val pushNotificationsRouter: PushNotificationsRouter? = null,
val transactionSignerFactory: TransactionSignerFactory? = null,
val getUserCountryUseCase: GetUserCountryUseCase? = null,
val onrampFeatureToggles: OnrampFeatureToggles? = null,

View file

@ -2,8 +2,8 @@ package com.tangem.tap.routing.utils
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.referral.ReferralFragment
import com.tangem.feature.qrscanning.QrScanningComponent
import com.tangem.feature.referral.api.ReferralComponent
import com.tangem.feature.stories.api.StoriesComponent
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.features.details.component.DetailsComponent
@ -13,21 +13,21 @@ import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onramp.component.*
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.send.api.SendComponent
import com.tangem.features.swap.SwapComponent
import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
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.resetcard.ResetCardFragment
import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment
import com.tangem.tap.features.details.ui.walletconnect.WalletConnectFragment
import com.tangem.tap.features.home.HomeFragment
import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent
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.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
import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment
import com.tangem.tap.features.onboarding.products.twins.ui.OnboardingTwinsFragment
@ -58,14 +58,23 @@ internal class ChildFactory @Inject constructor(
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
private val welcomeComponentFactory: WelcomeComponent.Factory,
private val storiesComponentFactory: StoriesComponent.Factory,
private val sendComponentFactory: SendComponent.Factory,
private val stakingComponentFactory: StakingComponent.Factory,
private val swapComponentFactory: SwapComponent.Factory,
private val sendRouter: SendRouter,
private val tokenDetailsRouter: TokenDetailsRouter,
private val homeComponentFactory: HomeComponent.Factory,
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 appCurrencySelectorComponentFactory: AppCurrencySelectorComponent.Factory,
private val appSettingsComponentFactory: AppSettingsComponent.Factory,
private val securityModeComponentFactory: SecurityModeComponent.Factory,
private val resetCardComponentFactory: ResetCardComponent.Factory,
private val referralComponentFactory: ReferralComponent.Factory,
private val pushNotificationsComponentFactory: PushNotificationsComponent.Factory,
private val walletRouter: WalletRouter,
private val qrScanningRouter: QrScanningRouter,
private val testerRouter: TesterRouter,
private val pushNotificationRouter: PushNotificationsRouter,
private val routingFeatureToggles: RoutingFeatureToggles,
) {
@ -213,6 +222,16 @@ internal class ChildFactory @Inject constructor(
componentFactory = storiesComponentFactory,
)
}
is AppRoute.CurrencyDetails -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = TokenDetailsComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
),
componentFactory = tokenDetailsComponentFactory,
)
}
is AppRoute.Staking -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
@ -237,25 +256,113 @@ internal class ChildFactory @Inject constructor(
componentFactory = swapComponentFactory,
)
}
is AppRoute.AccessCodeRecovery,
is AppRoute.AppCurrencySelector,
is AppRoute.Send -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SendComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
transactionId = route.transactionId,
amount = route.amount,
tag = route.tag,
destinationAddress = route.destinationAddress,
),
componentFactory = sendComponentFactory,
)
}
is AppRoute.Home -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = homeComponentFactory,
)
}
is AppRoute.WalletConnectSessions -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = walletConnectComponentFactory,
)
}
is AppRoute.QrScanning -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = QrScanningComponent.Params(
source = route.source,
networkName = route.networkName,
),
componentFactory = qrScanningComponentFactory,
)
}
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 -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = appCurrencySelectorComponentFactory,
)
}
is AppRoute.AppSettings -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
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.ReferralProgram -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = ReferralComponent.Params(route.userWalletId),
componentFactory = referralComponentFactory,
)
}
is AppRoute.PushNotification -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = pushNotificationsComponentFactory,
)
}
is AppRoute.SaveWallet,
is AppRoute.Send,
is AppRoute.AppSettings,
is AppRoute.CardSettings,
is AppRoute.DetailsSecurity,
is AppRoute.Home,
is AppRoute.OnboardingNote,
is AppRoute.OnboardingOther,
is AppRoute.OnboardingTwins,
is AppRoute.OnboardingWallet,
is AppRoute.QrScanning,
is AppRoute.ReferralProgram,
is AppRoute.ResetToFactory,
is AppRoute.Wallet,
is AppRoute.WalletConnectSessions,
is AppRoute.CurrencyDetails,
is AppRoute.PushNotification,
-> error("Unsupported route: $route")
}
// endregion
@ -271,22 +378,49 @@ 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() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = appCurrencySelectorComponentFactory,
)
}
is AppRoute.SaveWallet -> {
route.asFragmentChild(Provider { SaveWalletBottomSheetFragment() })
}
is AppRoute.Send -> {
route.asFragmentChild(Provider { sendRouter.getEntryFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SendComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
transactionId = route.transactionId,
amount = route.amount,
tag = route.tag,
destinationAddress = route.destinationAddress,
),
componentFactory = sendComponentFactory,
)
}
is AppRoute.AppSettings -> {
route.asFragmentChild(Provider { AppSettingsFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = appSettingsComponentFactory,
)
}
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(
@ -296,7 +430,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(
@ -306,7 +446,11 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.Home -> {
route.asFragmentChild(Provider { HomeFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = homeComponentFactory,
)
}
is AppRoute.ManageTokens -> {
val source = when (route.source) {
@ -334,13 +478,33 @@ internal class ChildFactory @Inject constructor(
route.asFragmentChild(Provider { OnboardingWalletFragment() })
}
is AppRoute.QrScanning -> {
route.asFragmentChild(Provider { qrScanningRouter.getEntryFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = QrScanningComponent.Params(
source = route.source,
networkName = route.networkName,
),
componentFactory = qrScanningComponentFactory,
)
}
is AppRoute.ReferralProgram -> {
route.asFragmentChild(Provider { ReferralFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = ReferralComponent.Params(route.userWalletId),
componentFactory = referralComponentFactory,
)
}
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(
@ -359,10 +523,21 @@ internal class ChildFactory @Inject constructor(
route.asFragmentChild(Provider { walletRouter.getEntryFragment() })
}
is AppRoute.WalletConnectSessions -> {
route.asFragmentChild(Provider { WalletConnectFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = walletConnectComponentFactory,
)
}
is AppRoute.CurrencyDetails -> {
route.asFragmentChild(Provider { tokenDetailsRouter.getEntryFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = TokenDetailsComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
),
componentFactory = tokenDetailsComponentFactory,
)
}
is AppRoute.Welcome -> {
route.asFragmentChild(Provider { WelcomeFragment() })
@ -382,7 +557,11 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.PushNotification -> {
route.asFragmentChild(Provider { pushNotificationRouter.entryFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = pushNotificationsComponentFactory,
)
}
is AppRoute.WalletSettings -> {
route.asComponentChild(

View file

@ -93,20 +93,7 @@ sealed class AppRoute(val path: String) : Route {
"&$amount" +
"&$tag" +
"&$destinationAddress",
),
RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val USER_WALLET_ID_KEY = "userWalletId"
const val CRYPTO_CURRENCY_KEY = "currency"
const val TRANSACTION_ID_KEY = "transactionId"
const val AMOUNT_KEY = "amount"
const val TAG_KEY = "tag"
const val DESTINATION_ADDRESS_KEY = "destinationAddress"
}
}
)
@Serializable
data class Details(
@ -116,22 +103,12 @@ 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(
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")
@ -156,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 {
@ -196,15 +162,7 @@ sealed class AppRoute(val path: String) : Route {
data class QrScanning(
val source: SourceType,
val networkName: String? = null,
) : AppRoute(path = "/$source/qr_scanning${if (networkName != null) "/$networkName" else ""}"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val SOURCE_KEY = "source"
const val NETWORK_KEY = "networkName"
}
}
) : AppRoute(path = "/$source/qr_scanning${if (networkName != null) "/$networkName" else ""}")
@Serializable
data class ReferralProgram(
@ -231,18 +189,7 @@ sealed class AppRoute(val path: String) : Route {
"/${currencyTo?.id?.value}" +
"/${userWalletId.stringValue}" +
"/$isInitialReverseOrder",
),
RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
companion object {
const val CURRENCY_FROM_KEY = "currencyFrom"
const val CURRENCY_TO_KEY = "currencyTo"
const val USER_WALLET_ID_KEY = "userWalletId"
const val IS_INITIAL_REVERSE_ORDER = "isInitialReverseOrder"
}
}
)
@Serializable
data object TesterMenu : AppRoute(path = "/tester_menu")

View file

@ -8,7 +8,7 @@ sealed class NavigationButtonsState {
data object Empty : NavigationButtonsState()
data class Data(
val primaryButton: NavigationButton,
val primaryButton: NavigationButton?,
val prevButton: NavigationButton?,
val extraButtons: ImmutableList<NavigationButton>,
val txUrl: String? = null,

View file

@ -26,5 +26,13 @@
{
"name": "alephium",
"version": "5.21.0"
},
{
"name": "scroll",
"version": "undefined"
},
{
"name": "zklink",
"version": "undefined"
}
]

View file

@ -46,5 +46,9 @@
{
"name": "BALANCES_CACHING_ENABLED",
"version": "5.21.0"
},
{
"name": "NFT_ENABLED",
"version": "undefined"
}
]

View file

@ -1,6 +0,0 @@
package com.tangem.datasource.api.common.visa
interface TangemVisaAuthProvider {
suspend fun getAuthHeader(cardId: String): String
}

View file

@ -5,9 +5,7 @@ import com.tangem.datasource.api.utils.ReadTimeout
import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest
import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest
import com.tangem.datasource.api.visa.models.request.SetPinCodeRequest
import com.tangem.datasource.api.visa.models.response.CardActivationRemoteStateResponse
import com.tangem.datasource.api.visa.models.response.CardWalletDataToSignResponse
import com.tangem.datasource.api.visa.models.response.CustomerWalletDataToSignResponse
import com.tangem.datasource.api.visa.models.response.*
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
@ -71,4 +69,19 @@ interface TangemVisaApi {
@Header("Authorization") authHeader: String,
@Body body: SetPinCodeRequest,
): ApiResponse<Unit>
@GET("customer/info")
suspend fun getCustomerInfo(
@Header("Authorization") authHeader: String,
@Query("card_id") cardId: String,
): ApiResponse<VisaCustomerInfo>
@GET("product_instance/transactions")
suspend fun getTxHistory(
@Header("Authorization") authHeader: String,
@Query("customer_id") customerId: String,
@Query("product_instance_id") productInstanceId: String,
@Query("limit") limit: Int,
@Query("offset") offset: Int,
): ApiResponse<VisaTxHistoryResponse>
}

View file

@ -1,5 +1,6 @@
package com.tangem.datasource.api.visa
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.visa.models.response.GenerateNonceResponse
import com.tangem.datasource.api.visa.models.response.JWTResponse
import retrofit2.http.Field
@ -27,5 +28,5 @@ interface TangemVisaAuthApi {
): JWTResponse
@POST("auth/refresh_token")
suspend fun refreshAccessToken(@Field("refresh_token") refreshToken: String): JWTResponse
suspend fun refreshAccessToken(@Field("refresh_token") refreshToken: String): ApiResponse<JWTResponse>
}

View file

@ -0,0 +1,15 @@
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class VisaCustomerInfo(
@Json(name = "payment_accounts") val paymentAccounts: List<PaymentAccount>,
) {
data class PaymentAccount(
@Json(name = "id") val id: String,
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
@Json(name = "payment_account_address") val paymentAccountAddress: String,
)
}

View file

@ -1,4 +1,4 @@
package com.tangem.lib.visa.model
package com.tangem.datasource.api.visa.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

View file

@ -121,6 +121,8 @@ object PreferencesKeys {
val SEED_FIRST_NOTIFICATION_SHOW_TIME by lazy { longPreferencesKey("seedFirstNotificationTime") }
val WALLETS_WITH_NFT_ENABLED_KEY by lazy { stringSetPreferencesKey(name = "walletsWithNftEnabled") }
fun getShouldShowStoriesKey(storyId: String) = booleanPreferencesKey("shouldShowStories_$storyId")
// region Permission

View file

@ -2,7 +2,7 @@ package com.tangem.core.decompose.context
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.essenty.instancekeeper.getOrCreate
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.navigation.AppNavigationProvider
import com.tangem.core.decompose.navigation.DefaultAppNavigationProvider
import com.tangem.core.decompose.navigation.DefaultRouter
@ -15,7 +15,7 @@ import kotlinx.coroutines.CoroutineScope
class DefaultAppComponentContext(
componentContext: ComponentContext,
override val dispatchers: CoroutineDispatcherProvider,
override val hiltComponentBuilder: DecomposeComponent.Builder,
override val hiltComponentBuilder: ModelComponent.Builder,
override val messageSender: UiMessageSender,
private val replaceRouter: Router? = null,
) : AppComponentContext, ComponentContext by componentContext {

View file

@ -1,9 +0,0 @@
package com.tangem.core.decompose.di
/**
* Annotation for marking a dependency as a component scoped.
*
* This means that the lifecycle of the dependency is limited to the lifecycle of the component it is attached to.
*/
@Retention(AnnotationRetention.SOURCE)
annotation class ComponentScoped

View file

@ -8,5 +8,5 @@ interface HiltComponentBuilderOwner {
/**
* Provides access to the Hilt component builder instance.
*/
val hiltComponentBuilder: DecomposeComponent.Builder
val hiltComponentBuilder: ModelComponent.Builder
}

View file

@ -10,11 +10,11 @@ import dagger.hilt.components.SingletonComponent
/**
* Interface for the Decompose component.
*
* It is annotated as [ComponentScoped], meaning it has a lifecycle that is scoped to the component.
* It is annotated as [ModelScoped], meaning it has a lifecycle that is scoped to the component.
*/
@ComponentScoped
@ModelScoped
@DefineComponent(parent = SingletonComponent::class)
interface DecomposeComponent {
interface ModelComponent {
/**
* Builder interface for the component.
@ -51,6 +51,6 @@ interface DecomposeComponent {
*
* @return The built Decompose component.
*/
fun build(): DecomposeComponent
fun build(): ModelComponent
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.core.decompose.di
import javax.inject.Scope
/**
* Annotation for marking a dependency as a model scoped.
*
* This means that the lifecycle of the dependency is limited to the lifecycle of the component's model
*/
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class ModelScoped

View file

@ -6,6 +6,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import javax.annotation.OverridingMethodsMustInvokeSuper
/**
* Abstract class for a component's model.
@ -29,6 +30,7 @@ abstract class Model : InstanceKeeper.Instance {
CoroutineScope(context = dispatchers.mainImmediate + SupervisorJob())
}
@OverridingMethodsMustInvokeSuper
override fun onDestroy() {
runCatching { modelScope.cancel() }
}

View file

@ -3,7 +3,7 @@ package com.tangem.core.decompose.model
import com.arkivanov.essenty.instancekeeper.getOrCreate
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.ModelComponent
import dagger.hilt.EntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
@ -15,7 +15,7 @@ import javax.inject.Provider
* It provides a map of model providers.
*/
@EntryPoint
@InstallIn(DecomposeComponent::class)
@InstallIn(ModelComponent::class)
interface ModelsEntryPoint {
fun models(): Map<Class<*>, Provider<Model>>

View file

@ -3,7 +3,7 @@ package com.tangem.core.decompose.model
/**
* Lazy container for [Model] params.
*
* This contrainer can be accessed by DI because it's provided via [com.tangem.core.decompose.di.DecomposeComponent].
* This contrainer can be accessed by DI because it's provided via [com.tangem.core.decompose.di.ModelComponent].
* */
interface ParamsContainer {

View file

@ -10,6 +10,8 @@ android {
}
dependencies {
/* Core */
implementation(projects.core.decompose)
/* Libs - AndroidX */
implementation(deps.lifecycle.runtime.ktx)

View file

@ -1,8 +1,6 @@
package com.tangem.core.deeplink
import android.content.Intent
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
// TODO: Add tests
/**
@ -21,17 +19,11 @@ interface DeepLinksRegistry {
/**
* Registers the given [deepLink].
*
* @see registerWithLifecycle
* @see registerWithViewModel
*/
fun register(deepLink: DeepLink)
/**
* Registers the given [deepLinks].
*
* @see registerWithLifecycle
* @see registerWithViewModel
*/
fun register(deepLinks: Collection<DeepLink>)
@ -50,17 +42,6 @@ interface DeepLinksRegistry {
* */
fun unregisterByIds(ids: Collection<String>)
/**
* Registers the [deepLinks] when the [owner] is resumed and ensures that they are unregistered when the [owner] is
* stopped.
*/
fun registerWithLifecycle(owner: LifecycleOwner, deepLinks: Collection<DeepLink>)
/**
* Registers the [deepLinks] and ensures that they are unregistered when the [ViewModel] is closed.
*/
fun registerWithViewModel(viewModel: ViewModel, deepLinks: Collection<DeepLink>)
/**
* Triggers run last launched [Intent] with deeplink handlers that can handle delayed deeplink
* after handle [Intent] clear that and second time no intent will be handled

View file

@ -3,11 +3,8 @@ package com.tangem.core.deeplink.impl
import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import com.tangem.core.deeplink.DeepLink
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.deeplink.utils.DeepLinksLifecycleObserver
import timber.log.Timber
internal class DefaultDeepLinksRegistry : DeepLinksRegistry {
@ -103,19 +100,6 @@ internal class DefaultDeepLinksRegistry : DeepLinksRegistry {
)
}
override fun registerWithLifecycle(owner: LifecycleOwner, deepLinks: Collection<DeepLink>) {
val observer = DeepLinksLifecycleObserver(deepLinksRegistry = this, deepLinks)
owner.lifecycle.addObserver(observer)
}
override fun registerWithViewModel(viewModel: ViewModel, deepLinks: Collection<DeepLink>) {
viewModel.addCloseable {
unregister(deepLinks)
}
register(deepLinks)
}
override fun triggerDelayedDeeplink() {
if (lastIntent != null) {
val intent = lastIntent

View file

@ -0,0 +1,21 @@
package com.tangem.core.deeplink.utils
import com.arkivanov.essenty.lifecycle.subscribe
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.deeplink.DeepLink
import com.tangem.core.deeplink.DeepLinksRegistry
fun AppComponentContext.registerDeepLinks(registry: DeepLinksRegistry, vararg deepLinks: DeepLink) {
registerDeepLinks(registry, deepLinks.toList())
}
fun AppComponentContext.registerDeepLinks(registry: DeepLinksRegistry, deepLinks: Collection<DeepLink>) {
lifecycle.subscribe(
onCreate = {
registry.register(deepLinks)
},
onDestroy = {
registry.unregister(deepLinks)
},
)
}

View file

@ -1,20 +0,0 @@
package com.tangem.core.deeplink.utils
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.tangem.core.deeplink.DeepLink
import com.tangem.core.deeplink.DeepLinksRegistry
internal class DeepLinksLifecycleObserver(
private val deepLinksRegistry: DeepLinksRegistry,
private val deepLinks: Collection<DeepLink>,
) : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
deepLinksRegistry.register(deepLinks)
}
override fun onPause(owner: LifecycleOwner) {
deepLinksRegistry.unregister(deepLinks)
}
}

View file

@ -90,6 +90,9 @@ fun getActiveIconRes(blockchainId: String): Int {
"bitrock", "bitrock/test" -> R.drawable.img_bitrock_22
"sonic", "sonic/test" -> R.drawable.img_sonic_22
"apechain", "apechain/test" -> R.drawable.img_apecoin_22
"scroll", "scroll/test" -> R.drawable.ic_alert_24 // FIXME: add icon during full integration
"zklink", "zklink/test" -> R.drawable.img_zklink_22
"vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22
else -> R.drawable.ic_alert_24
}
}
@ -178,6 +181,9 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"bitrock", "bitrock/test" -> R.drawable.img_bitrock_22
"sonic", "sonic/test" -> R.drawable.img_sonic_22
"apechain", "apechain/test" -> R.drawable.img_apecoin_22
"scroll", "scroll/test" -> R.drawable.ic_alert_24 // FIXME: add icon during full integration
"zklink", "zklink/test" -> R.drawable.img_zklink_22
"vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22
else -> R.drawable.ic_alert_24
}
}
@ -269,6 +275,9 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"bitrock", "bitrock/test" -> R.drawable.ic_bitrock_22
"sonic", "sonic/test" -> R.drawable.ic_sonic_22
"apechain", "apechain/test" -> R.drawable.ic_apecoin_22
"scroll", "scroll/test" -> R.drawable.ic_alert_24 // FIXME: add icon during full integration
"zklink", "zklink/test" -> R.drawable.ic_zklink_22
"vanar-chain", "vanar-chain/test" -> R.drawable.ic_vanar_22
else -> R.drawable.ic_alert_24
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M12.748,18L14.498,14.999L9.25,6H5.749L4,9H7.502L12.748,18ZM16.249,11.999L18,9L16.249,6H12.75L11.001,9H14.5L16.249,11.999Z"
android:fillColor="#000000"/>
</vector>

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M11.217,3.624L11.002,3.5L10.788,3.624L7.502,5.526L7.288,5.65V5.897V9.455L4.214,11.235L4,11.358V11.605V15.41V15.657L4.214,15.78L7.5,17.683L7.714,17.807L7.929,17.683L11,15.905L14.071,17.683L14.286,17.807L14.5,17.683L17.786,15.78L18,15.657V15.41V11.605V11.358L17.786,11.235L14.716,9.457V5.897V5.65L14.503,5.526L11.217,3.624ZM8.145,6.144L11.002,4.49L13.86,6.144V9.454L11.621,10.75C11.456,10.61 11.243,10.525 11.009,10.525C10.773,10.525 10.557,10.612 10.391,10.755L8.145,9.454V6.144ZM10.068,11.561L7.714,10.198L4.857,11.852V15.163L7.714,16.817L10.571,15.163V12.308C10.295,12.164 10.099,11.886 10.068,11.561ZM11.428,12.318V15.163L14.286,16.817L17.143,15.163V11.852L14.288,10.199L11.951,11.552C11.922,11.888 11.717,12.175 11.428,12.318Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M11.002,7.31L14.716,9.46V13.758L11.002,15.908L7.288,13.758V9.46L11.002,7.31ZM8.145,9.954V13.264L11.002,14.918L13.86,13.264V9.954L11.002,8.299L8.145,9.954Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"/>
<path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#ffffff"/>
<path
android:pathData="M0,0h22v22h-22z"
android:fillColor="#080A0B"/>
<path
android:pathData="M12.748,18L14.498,14.999L9.25,6H5.749L4,9H7.502L12.748,18ZM16.249,11.999L18,9L16.249,6H12.75L11.001,9H14.5L16.249,11.999Z"
android:fillColor="#03D9AF"/>
</group>
</vector>

View file

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"/>
<path
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z"
android:fillColor="#ffffff"/>
<path
android:pathData="M0,0h22v22h-22z"
android:fillColor="#080A0B"/>
<path
android:pathData="M11.217,3.624L11.002,3.5L10.788,3.624L7.502,5.526L7.288,5.65V5.897V9.455L4.214,11.235L4,11.358V11.605V15.41V15.657L4.214,15.78L7.5,17.683L7.714,17.807L7.929,17.683L11,15.905L14.071,17.683L14.286,17.807L14.5,17.683L17.786,15.78L18,15.657V15.41V11.605V11.358L17.786,11.235L14.716,9.457V5.897V5.65L14.503,5.526L11.217,3.624ZM8.145,6.144L11.002,4.49L13.86,6.144V9.454L11.621,10.75C11.456,10.61 11.243,10.525 11.009,10.525C10.773,10.525 10.557,10.612 10.391,10.755L8.145,9.454V6.144ZM10.068,11.561L7.714,10.198L4.857,11.852V15.163L7.714,16.817L10.571,15.163V12.308C10.295,12.164 10.099,11.886 10.068,11.561ZM11.428,12.318V15.163L14.286,16.817L17.143,15.163V11.852L14.288,10.199L11.951,11.552C11.922,11.888 11.717,12.175 11.428,12.318Z"
android:fillColor="#03D498"
android:fillType="evenOdd"/>
<path
android:pathData="M11.002,7.31L14.716,9.46V13.758L11.002,15.908L7.288,13.758V9.46L11.002,7.31ZM8.145,9.954V13.264L11.002,14.918L13.86,13.264V9.954L11.002,8.299L8.145,9.954Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</group>
</vector>

View file

@ -9,7 +9,7 @@ import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface CoroutineDispatcherProviderModule {
internal interface CoroutineDispatcherProviderModule {
@Binds
fun bindCoroutineDispatcherProvider(

View file

@ -12,6 +12,7 @@ android {
dependencies {
implementation(deps.androidx.datastore)
implementation(deps.androidx.appCompat)
/** DI */
implementation(deps.hilt.android)

View file

@ -8,8 +8,11 @@ import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
import javax.inject.Singleton
internal class DefaultBalanceHidingRepository(
@Singleton
internal class DefaultBalanceHidingRepository @Inject constructor(
private val appPreferencesStore: AppPreferencesStore,
) : BalanceHidingRepository {

View file

@ -3,18 +3,40 @@ package com.tangem.data.balancehiding
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorManager
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.tangem.domain.balancehiding.DeviceFlipDetector
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Singleton
internal class DefaultDeviceFlipDetector(context: Context) : DeviceFlipDetector {
@Singleton
class DefaultDeviceFlipDetector @Inject constructor(
@ApplicationContext context: Context,
) : DeviceFlipDetector, DefaultLifecycleObserver {
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private var gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
private var isResumedState = AtomicBoolean(false)
override fun onPause(owner: LifecycleOwner) {
isResumedState.set(false)
}
override fun onResume(owner: LifecycleOwner) {
isResumedState.set(true)
}
override fun getDeviceFlipFlow(): Flow<Unit> = callbackFlow {
val listener = FlipListener { trySend(Unit) }
val listener = FlipListener {
if (isResumedState.get()) {
trySend(Unit)
}
}
gravitySensor?.let {
sensorManager.registerListener(listener, it, SensorManager.SENSOR_DELAY_NORMAL)

View file

@ -1,31 +1,24 @@
package com.tangem.data.balancehiding.di
import android.content.Context
import com.tangem.data.balancehiding.DefaultBalanceHidingRepository
import com.tangem.data.balancehiding.DefaultDeviceFlipDetector
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.balancehiding.DeviceFlipDetector
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object BalanceHidingModule {
internal interface BalanceHidingModule {
@Provides
@Binds
@Singleton
fun provideBalanceHidingRepository(appPreferencesStore: AppPreferencesStore): BalanceHidingRepository {
return DefaultBalanceHidingRepository(appPreferencesStore = appPreferencesStore)
}
fun provideBalanceHidingRepository(impl: DefaultBalanceHidingRepository): BalanceHidingRepository
@Provides
@Binds
@Singleton
fun provideFlipDetector(@ApplicationContext context: Context): DeviceFlipDetector {
return DefaultDeviceFlipDetector(context = context)
}
fun provideFlipDetector(impl: DefaultDeviceFlipDetector): DeviceFlipDetector
}

View file

@ -299,6 +299,8 @@ private fun Blockchain.getSupportedTransactionExtras(): Network.TransactionExtra
Blockchain.Bitrock, Blockchain.BitrockTestnet,
Blockchain.Sonic, Blockchain.SonicTestnet,
Blockchain.ApeChain, Blockchain.ApeChainTestnet,
Blockchain.Scroll, Blockchain.ScrollTestnet,
Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet,
-> Network.TransactionExtrasType.NONE
// endregion
}

View file

@ -237,7 +237,7 @@ internal class DefaultStakingRepository(
): StakingAction {
return withContext(dispatchers.io) {
val response = when (params.actionCommonType) {
StakingActionCommonType.Enter -> stakeKitApi.createEnterAction(
is StakingActionCommonType.Enter -> stakeKitApi.createEnterAction(
createActionRequestBody(
userWalletId,
network,
@ -267,7 +267,7 @@ internal class DefaultStakingRepository(
): StakingGasEstimate {
return withContext(dispatchers.io) {
val gasEstimateDTO = when (params.actionCommonType) {
StakingActionCommonType.Enter -> stakeKitApi.estimateGasOnEnter(
is StakingActionCommonType.Enter -> stakeKitApi.estimateGasOnEnter(
createActionRequestBody(
userWalletId,
network,
@ -582,13 +582,14 @@ internal class DefaultStakingRepository(
-> TransactionData.Compiled.Data.Bytes(unsignedTransaction.hexToBytes())
Blockchain.BSC,
Blockchain.Ethereum,
Blockchain.TON,
Blockchain.Cardano,
-> TransactionData.Compiled.Data.RawString(unsignedTransaction)
Blockchain.Tron -> {
val tronStakeKitTransaction = tronStakeKitTransactionAdapter.fromJson(unsignedTransaction)
?: error("Failed to parse Tron StakeKit transaction")
TransactionData.Compiled.Data.RawString(tronStakeKitTransaction.rawDataHex)
}
Blockchain.TON -> TransactionData.Compiled.Data.RawString(unsignedTransaction)
else -> error("Unsupported blockchain")
}
}
@ -647,6 +648,7 @@ internal class DefaultStakingRepository(
const val KAVA_INTEGRATION_ID = "kava-kava-native-staking"
const val NEAR_INTEGRATION_ID = "near-near-native-staking"
const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking"
const val CARDANO_INTEGRATION_ID = "cardano-ada-native-staking"
const val ETHEREUM_POLYGON_APPROVE_SPENDER = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908"
@ -669,6 +671,7 @@ internal class DefaultStakingRepository(
// Blockchain.Kava.run { id + toCoinId() } to KAVA_INTEGRATION_ID,
// Blockchain.Near.run { id + toCoinId() } to NEAR_INTEGRATION_ID,
// Blockchain.Tezos.run { id + toCoinId() } to TEZOS_INTEGRATION_ID,
Blockchain.Cardano.run { id + toCoinId() } to CARDANO_INTEGRATION_ID,
)
}
}

View file

@ -29,7 +29,7 @@ dependencies {
implementation(projects.libs.blockchainSdk)
/** Project - Libs */
debugImplementation(projects.libs.visa)
implementation(projects.libs.visa)
/** Libs - Other */
implementation(deps.kotlin.coroutines)

View file

@ -1,25 +0,0 @@
package com.tangem.data.visa.di
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.visa.DefaultVisaRepository
import com.tangem.data.visa.config.VisaLibLoader
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.visa.repository.VisaRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ImplementedVisaDataModule {
@Binds
@Singleton
@ImplementedVisaRepository
fun provideVisaRepository(impl: DefaultVisaRepository): VisaRepository
}

View file

@ -1,5 +1,6 @@
package com.tangem.data.visa
import com.tangem.data.visa.config.VisaLibLoader
import com.tangem.data.visa.converter.AccessCodeDataConverter
import com.tangem.data.visa.converter.VisaActivationStatusConverter
import com.tangem.datasource.api.common.response.ApiResponseError
@ -28,6 +29,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
private val visaAuthTokenStorage: VisaAuthTokenStorage,
private val accessCodeDataConverter: AccessCodeDataConverter,
private val visaAuthRepository: VisaAuthRepository,
private val visaLibLoader: VisaLibLoader,
) : VisaActivationRepository {
override suspend fun getActivationRemoteState(): VisaActivationRemoteState = withContext(dispatcherProvider.io) {
@ -192,6 +194,10 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor(
}
}
override suspend fun getPinCodeRsaEncryptionPublicKey(): String {
return visaLibLoader.getOrCreateConfig().rsaPublicKey
}
private suspend fun <T : Any> request(requestBlock: suspend () -> T): T {
return runCatching {
requestBlock()

View file

@ -12,21 +12,18 @@ import com.tangem.common.extensions.toHexString
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.visa.config.VisaLibLoader
import com.tangem.data.visa.utils.*
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.common.visa.TangemVisaAuthProvider
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.visa.TangemVisaApi
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.visa.exception.RefreshTokenExpiredException
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.domain.visa.model.VisaCurrency
import com.tangem.domain.visa.model.VisaTxDetails
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.domain.visa.repository.VisaRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.lib.visa.api.VisaApi
import com.tangem.lib.visa.model.VisaTxHistoryResponse
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@ -35,8 +32,8 @@ import kotlinx.coroutines.withContext
import java.math.BigDecimal
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.jvm.Throws
@Suppress("LongParameterList")
@Singleton
internal class DefaultVisaRepository @Inject constructor(
private val visaLibLoader: VisaLibLoader,
@ -44,8 +41,8 @@ internal class DefaultVisaRepository @Inject constructor(
private val cacheRegistry: CacheRegistry,
private val userWalletsStore: UserWalletsStore,
private val dispatchers: CoroutineDispatcherProvider,
private val visaAuthProvider: TangemVisaAuthProvider,
private val visaAuthRepository: VisaAuthRepository,
private val visaApiRequestMaker: VisaApiRequestMaker,
private val visaApi: TangemVisaApi,
) : VisaRepository {
private val currencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
@ -65,27 +62,32 @@ internal class DefaultVisaRepository @Inject constructor(
override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency {
val address = makeAddress(userWalletId)
fetchVisaCurrencyIfExpired(address, isRefresh)
fetchVisaCurrencyIfExpired(userWalletId, address, isRefresh)
return requireNotNull(fetchedCurrencies.value[address]) {
"Unable to find VISA currency for $address"
}
}
private suspend fun fetchVisaCurrencyIfExpired(address: String, isRefresh: Boolean) {
private suspend fun fetchVisaCurrencyIfExpired(userWalletId: UserWalletId, address: String, isRefresh: Boolean) {
cacheRegistry.invokeOnExpire(
key = getVisaCurrencyKey(address),
skipCache = isRefresh,
block = { fetchVisaCurrency(address) },
block = { fetchVisaCurrency(userWalletId, address) },
)
}
private suspend fun fetchVisaCurrency(address: String) {
private suspend fun fetchVisaCurrency(userWalletId: UserWalletId, address: String) {
val contractInfoProvider = visaLibLoader.getOrCreateProvider()
parZip(
dispatchers.io,
{ contractInfoProvider.getContractInfo(address) },
{
contractInfoProvider.getContractInfo(
walletAddress = address,
paymentAccountAddress = getPaymentAccountAddress(userWalletId),
)
},
{ getFiatRate() },
{ contractInfo, fiatRate ->
fetchedCurrencies.update { value ->
@ -104,7 +106,7 @@ internal class DefaultVisaRepository @Inject constructor(
): Flow<PagingData<VisaTxHistoryItem>> {
val userWallet = findVisaUserWallet(userWalletId)
val cardPubKey = getCardPubKey(userWallet)
val api = visaLibLoader.getOrCreateApi()
val pager = Pager(
config = PagingConfig(
pageSize = pageSize,
@ -121,7 +123,7 @@ internal class DefaultVisaRepository @Inject constructor(
cacheRegistry = cacheRegistry,
fetchedItems = fetchedHistoryItems,
dispatchers = dispatchers,
requestTxHistory = { offset, pageSize -> getTxHistory(api, userWalletId, offset, pageSize) },
requestTxHistory = { offset, pageSize -> getTxHistory(userWalletId, offset, pageSize) },
)
},
)
@ -145,22 +147,31 @@ internal class DefaultVisaRepository @Inject constructor(
}
}
private suspend fun getTxHistory(
api: VisaApi,
userWalletId: UserWalletId,
offset: Int,
pageSize: Int,
): VisaTxHistoryResponse = withContext(dispatchers.io) {
private suspend fun getPaymentAccountAddress(userWalletId: UserWalletId): String? = runCatching {
val userWallet = findVisaUserWallet(userWalletId)
val cardPubKey = getCardPubKey(userWallet)
request(userWalletId = userWalletId) {
api.getTxHistory(
authorizationHeader = visaAuthProvider.getAuthHeader(userWallet.cardId),
cardPublicKey = cardPubKey,
val customerInfo = visaApiRequestMaker.request(userWalletId) { authHeader, _ ->
visaApi.getCustomerInfo(
authHeader = authHeader,
cardId = userWallet.scanResponse.card.cardId,
)
}
// TODO select correct account when multiple accounts are available (will be implemented when backend is ready)
customerInfo.paymentAccounts.firstOrNull()?.paymentAccountAddress
}.getOrNull()
private suspend fun getTxHistory(userWalletId: UserWalletId, offset: Int, pageSize: Int): VisaTxHistoryResponse {
return visaApiRequestMaker.request(
userWalletId = userWalletId,
) { authHeader, accessCodeData ->
visaApi.getTxHistory(
authHeader = authHeader,
customerId = accessCodeData.customerId,
productInstanceId = accessCodeData.productInstanceId,
limit = pageSize,
offset = offset,
).getOrThrow()
)
}
}
@ -217,56 +228,4 @@ internal class DefaultVisaRepository @Inject constructor(
private fun getVisaCurrencyKey(address: String): String {
return "visa_currency_$address"
}
private suspend fun <T : Any> request(
userWalletId: UserWalletId,
requestBlock: suspend () -> T,
): T {
return runCatching {
requestBlock()
}.getOrElse { responseError ->
if (responseError !is ApiResponseError.HttpException ||
responseError.code != ApiResponseError.HttpException.Code.UNAUTHORIZED
) {
throw responseError
}
val authTokens = getAuthTokens(userWalletId)
val newTokens = runCatching {
visaAuthRepository.refreshAccessTokens(authTokens.refreshToken)
}.getOrElse {
if (it is ApiResponseError.HttpException &&
it.code == ApiResponseError.HttpException.Code.UNAUTHORIZED
) {
userWalletsStore.update(userWalletId) { userWallet ->
userWallet.copy(
scanResponse = userWallet.scanResponse.copy(
visaCardActivationStatus = VisaCardActivationStatus.RefreshTokenExpired
)
)
}
}
throw RefreshTokenExpiredException()
}
userWalletsStore.update(userWalletId) { userWallet ->
userWallet.copy(
scanResponse = userWallet.scanResponse.copy(
visaCardActivationStatus = VisaCardActivationStatus.Activated(
visaAuthTokens = newTokens
)
)
)
}
requestBlock()
}
}
@Throws
private suspend fun getAuthTokens(userWalletId: UserWalletId): VisaAuthTokens {
val userWallet = findVisaUserWallet(userWalletId)
val status = userWallet.scanResponse.visaCardActivationStatus ?: error("Visa card activation status not found")
return (status as? VisaCardActivationStatus.Activated)?.visaAuthTokens ?: error("Visa card is not activated")
}
}

View file

@ -45,6 +45,10 @@ class MockVisaActivationRepository @AssistedInject constructor(
override suspend fun sendPinCode(pinCode: VisaEncryptedPinCode) {}
override suspend fun getPinCodeRsaEncryptionPublicKey(): String {
return CryptoUtils.generateRandomBytes(length = 32).toHexString()
}
@AssistedFactory
interface Factory : VisaActivationRepository.Factory {
override fun create(cardId: VisaCardId): MockVisaActivationRepository

View file

@ -7,8 +7,9 @@ import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.domain.visa.repository.VisaRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
internal class DummyVisaRepository : VisaRepository {
internal class MockVisaRepository @Inject constructor() : VisaRepository {
override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency {
TODO("Not implemented for this build type")

View file

@ -11,6 +11,8 @@ internal data class VisaConfig(
val mainnet: Addresses,
@Json(name = "txHistoryAPIAdditionalHeaders")
val header: Header,
@Json(name = "rsaPublicKey")
val rsaPublicKey: String,
) {
@JsonClass(generateAdapter = true)

Some files were not shown because too many files have changed in this diff Show more