Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-25 09:46:39 +03:00
commit ec15925bce
225 changed files with 5402 additions and 3982 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

@ -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,10 @@ 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 +139,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
@ -217,6 +206,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 +275,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
lifecycle.addObserver(WindowObscurationObserver)
lifecycle.addObserver(defaultDeviceFlipDetector)
}
private fun installEventMessageEffect() {

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.ComponentScoped
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
@ComponentScoped
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.ComponentScoped
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
@ComponentScoped
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.ComponentScoped
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
@ComponentScoped
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.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryScreenState
import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor
import com.tangem.tap.features.details.ui.common.utils.isAccessCodeRecoveryEnabled
import com.tangem.tap.store
import dagger.hilt.android.lifecycle.HiltViewModel
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
internal class AccessCodeRecoveryViewModel @Inject constructor(
@Stable
@ComponentScoped
internal class AccessCodeRecoveryModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val tangemSdkManager: TangemSdkManager,
private val cardSettingsInteractor: CardSettingsInteractor,
) : ViewModel() {
) : Model() {
private val scannedScanResponse = cardSettingsInteractor.scannedScanResponse.value
?: error("Scan response is null")
@ -47,7 +51,7 @@ internal class AccessCodeRecoveryViewModel @Inject constructor(
)
}
private fun saveChanges() = viewModelScope.launch {
private fun saveChanges() = modelScope.launch {
val isEnabled = screenState.value.enabledSelection
tangemSdkManager

View file

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

View file

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

View file

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

View file

@ -1,34 +0,0 @@
package com.tangem.tap.features.details.ui.resetcard
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class ResetCardFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
private val viewModel: ResetCardViewModel by viewModels()
@Composable
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.screenState.collectAsStateWithLifecycle()
ResetCardScreen(
state = state,
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
modifier = modifier,
)
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,34 +0,0 @@
package com.tangem.tap.features.details.ui.securitymode
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class SecurityModeFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
private val viewModel: SecurityModeViewModel by viewModels()
@Composable
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.screenState.collectAsStateWithLifecycle()
SecurityModeScreen(
modifier = modifier,
state = state,
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
)
}
}

View file

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

View file

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

View file

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

View file

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

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

@ -2,7 +2,7 @@ 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.qrscanning.QrScanningComponent
import com.tangem.feature.referral.ReferralFragment
import com.tangem.feature.stories.api.StoriesComponent
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
@ -14,20 +14,20 @@ 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.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,12 +58,20 @@ 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 walletRouter: WalletRouter,
private val qrScanningRouter: QrScanningRouter,
private val testerRouter: TesterRouter,
private val pushNotificationRouter: PushNotificationsRouter,
private val routingFeatureToggles: RoutingFeatureToggles,
@ -213,6 +221,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,24 +255,100 @@ 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.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")
}
@ -271,22 +365,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 +417,13 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.DetailsSecurity -> {
route.asFragmentChild(Provider { SecurityModeFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SecurityModeComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = securityModeComponentFactory,
)
}
is AppRoute.Disclaimer -> {
route.asComponentChild(
@ -306,7 +433,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 +465,29 @@ 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() })
}
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 +506,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() })

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

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

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

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

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

@ -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)

View file

@ -1,13 +1,9 @@
package com.tangem.data.visa.config
import com.squareup.moshi.Moshi
import com.tangem.data.visa.BuildConfig
import com.tangem.data.visa.utils.VisaConstants
import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.lib.visa.VisaContractInfoProvider
import com.tangem.lib.visa.api.VisaApi
import com.tangem.lib.visa.api.VisaApiBuilder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -15,22 +11,24 @@ import javax.inject.Inject
internal class VisaLibLoader @Inject constructor(
private val assetLoader: AssetLoader,
@NetworkMoshi private val moshi: Moshi,
private val dispatchers: CoroutineDispatcherProvider,
) {
private val createMutex = Mutex()
private val createMutex2 = Mutex()
@Volatile
private var config: VisaConfig? = null
@Volatile
private var provider: VisaContractInfoProvider? = null
private var api: VisaApi? = null
suspend fun getOrCreateConfig(): VisaConfig = config ?: getOrLoadConfig()
suspend fun getOrCreateProvider(): VisaContractInfoProvider = provider ?: createProvider()
suspend fun getOrCreateApi(): VisaApi = api ?: createApi()
private suspend fun createProvider(): VisaContractInfoProvider = createMutex.withLock {
if (provider != null) return@withLock requireNotNull(provider)
val config = getOrLoadConfig()
provider = VisaContractInfoProvider.Builder(
@ -54,33 +52,17 @@ internal class VisaLibLoader @Inject constructor(
}
}
private suspend fun createApi(): VisaApi = createMutex.withLock {
val config = getOrLoadConfig()
private suspend fun getOrLoadConfig(): VisaConfig = createMutex2.withLock {
if (config != null) return@withLock requireNotNull(config)
api = VisaApiBuilder(
useDevApi = VisaConstants.USE_TEST_ENV,
isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED,
moshi = moshi,
headers = mapOf(
X_ASN_HEADER_NAME to config.header.xAsn,
),
).build()
return requireNotNull(api) {
"Visa API is not created"
}
}
private suspend fun getOrLoadConfig(): VisaConfig {
config = assetLoader.load<VisaConfig>(VISA_CONFIG_FILE_NAME)
return requireNotNull(config) {
"Visa config is not found"
return@withLock requireNotNull(config) {
"Visa config is not loaded"
}
}
companion object {
private const val VISA_CONFIG_FILE_NAME = "tangem-app-config/visa_config"
private const val X_ASN_HEADER_NAME = "x-asn"
}
}

View file

@ -1,6 +0,0 @@
package com.tangem.data.visa.di
import javax.inject.Qualifier
@Qualifier
internal annotation class ImplementedVisaRepository

View file

@ -1,16 +0,0 @@
package com.tangem.data.visa.di
import com.tangem.domain.visa.repository.VisaRepository
import dagger.BindsOptionalOf
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal interface ImplementedVisaRepositoryModule {
@BindsOptionalOf
@ImplementedVisaRepository
fun bindImplementedVisaRepository(): VisaRepository
}

View file

@ -1,36 +1,20 @@
package com.tangem.data.visa.di
import com.tangem.data.visa.DefaultVisaAuthRepository
import com.tangem.data.visa.DummyVisaRepository
import com.tangem.data.visa.MockVisaRepository
import com.tangem.data.visa.MockVisaActivationRepository
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.domain.visa.repository.VisaRepository
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import java.util.Optional
import javax.inject.Singleton
import kotlin.jvm.optionals.getOrNull
@Module
@InstallIn(SingletonComponent::class)
internal object VisaDataModule {
@Provides
@Singleton
fun provideVisaRepository(
@ImplementedVisaRepository implementedVisaRepository: Optional<VisaRepository>,
): VisaRepository {
return implementedVisaRepository.getOrNull() ?: DummyVisaRepository()
}
}
@Module
@InstallIn(SingletonComponent::class)
internal interface VisaDataBindsModule {
internal interface VisaDataModule {
@Binds
@Singleton
@ -48,4 +32,11 @@ internal interface VisaDataBindsModule {
fun bindVisaActivationRepositoryFactory(
repository: MockVisaActivationRepository.Factory,
): VisaActivationRepository.Factory
// @Binds
// fun bindVisaRepository(repository: DefaultVisaRepository): VisaRepository
// Mocked
@Binds
fun bindVisaRepository(repository: MockVisaRepository): VisaRepository
}

View file

@ -0,0 +1,107 @@
package com.tangem.data.visa.utils
import com.tangem.data.visa.converter.AccessCodeDataConverter
import com.tangem.data.visa.model.AccessCodeData
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.visa.TangemVisaAuthApi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.visa.exception.RefreshTokenExpiredException
import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.model.getAuthHeader
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import javax.inject.Inject
import kotlin.jvm.Throws
typealias VisaAuthorizationHeader = String
internal class VisaApiRequestMaker @Inject constructor(
private val userWalletsStore: UserWalletsStore,
private val visaAuthApi: TangemVisaAuthApi,
private val accessCodeDataConverter: AccessCodeDataConverter,
private val dispatcherProvider: CoroutineDispatcherProvider,
) {
suspend fun <T : Any> request(
userWalletId: UserWalletId,
requestBlock: suspend (header: VisaAuthorizationHeader, accessCodeData: AccessCodeData) -> ApiResponse<T>,
): T = withContext(dispatcherProvider.io) {
val authTokens = getAuthTokens(userWalletId)
val authHeader = authTokens.getAuthHeader()
val accessCodeData = accessCodeDataConverter.convert(authTokens)
runCatching {
requestBlock(authHeader, accessCodeData).getOrThrow()
}.getOrElse { responseError ->
if (responseError !is ApiResponseError.HttpException ||
responseError.code != ApiResponseError.HttpException.Code.UNAUTHORIZED
) {
throw responseError
}
val newTokens = runCatching {
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,
),
),
)
}
val newAuthHeader = newTokens.getAuthHeader()
val newAccessCodeData = accessCodeDataConverter.convert(newTokens)
requestBlock(newAuthHeader, newAccessCodeData).getOrThrow()
}
}
private suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): VisaAuthTokens {
val result = visaAuthApi.refreshAccessToken(refreshToken.value).getOrThrow()
return VisaAuthTokens(
accessToken = result.accessToken,
refreshToken = VisaAuthTokens.RefreshToken(result.refreshToken),
)
}
@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")
}
private suspend fun findVisaUserWallet(userWalletId: UserWalletId): UserWallet {
val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"No user wallet found: $userWalletId"
}
if (!userWallet.scanResponse.cardTypesResolver.isVisaWallet()) {
error("VISA wallet required: $userWalletId")
}
return userWallet
}
}

View file

@ -15,9 +15,9 @@ internal object VisaConstants {
)
/*
* Must be `false` in production
* Don't forget to change CardTypesResolver.isVisaWallet
* */
* Must be `false` in production
* Don't forget to change CardTypesResolver.isVisaWallet
* */
const val IS_DEMO_MODE_ENABLED = false
const val USE_TEST_ENV = true
@ -40,5 +40,7 @@ internal fun getDemoAddress(): String {
internal fun getDemoPublicKey(): String {
return if (VisaConstants.USE_TEST_ENV) {
VisaConstants.DEMO_TESTNET_PUBLIC_KEY
} else VisaConstants.DEMO_MAINNET_PUBLIC_KEY
} else {
VisaConstants.DEMO_MAINNET_PUBLIC_KEY
}
}

View file

@ -32,7 +32,6 @@ internal class VisaCurrencyFactory {
available = balances.available.forPayment,
blocked = balances.blocked,
debt = balances.debt,
pendingRefund = balances.pendingRefund,
)
},
limits = VisaCurrency.Limits(

View file

@ -2,8 +2,8 @@ package com.tangem.data.visa.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.externallinkprovider.TxExploreState
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
import com.tangem.domain.visa.model.VisaTxDetails
import com.tangem.lib.visa.model.VisaTxHistoryResponse
internal class VisaTxDetailsFactory {

View file

@ -1,7 +1,7 @@
package com.tangem.data.visa.utils
import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.lib.visa.model.VisaTxHistoryResponse
internal class VisaTxHistoryItemFactory {

View file

@ -3,13 +3,9 @@ package com.tangem.data.visa.utils
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.tangem.data.common.cache.CacheRegistry
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.visa.models.response.VisaTxHistoryResponse
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.lib.visa.api.VisaApi
import com.tangem.lib.visa.model.VisaTxHistoryResponse
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update

View file

@ -202,4 +202,24 @@ internal class DefaultWalletsRepository(
}
}
}
override fun nftEnabledStatus(userWalletId: UserWalletId): Flow<Boolean> {
return appPreferencesStore
.get(key = PreferencesKeys.WALLETS_WITH_NFT_ENABLED_KEY, default = emptySet())
.map { it.contains(userWalletId.stringValue) }
}
override suspend fun enableNFT(userWalletId: UserWalletId) {
appPreferencesStore.editData {
val added = it[PreferencesKeys.WALLETS_WITH_NFT_ENABLED_KEY].orEmpty()
it[PreferencesKeys.WALLETS_WITH_NFT_ENABLED_KEY] = added + userWalletId.stringValue
}
}
override suspend fun disableNFT(userWalletId: UserWalletId) {
appPreferencesStore.editData {
val added = it[PreferencesKeys.WALLETS_WITH_NFT_ENABLED_KEY].orEmpty()
it[PreferencesKeys.WALLETS_WITH_NFT_ENABLED_KEY] = added - userWalletId.stringValue
}
}
}

View file

@ -201,6 +201,10 @@ data object Wallet2CardConfig : CardConfig {
Blockchain.KaspaTestnet -> EllipticCurve.Secp256k1
Blockchain.Alephium -> EllipticCurve.Secp256k1
Blockchain.AlephiumTestnet -> EllipticCurve.Secp256k1
Blockchain.Scroll -> EllipticCurve.Secp256k1
Blockchain.ScrollTestnet -> EllipticCurve.Secp256k1
Blockchain.ZkLinkNova -> EllipticCurve.Secp256k1
Blockchain.ZkLinkNovaTestnet -> EllipticCurve.Secp256k1
}
}
}

View file

@ -158,6 +158,10 @@ class Wallet2CardConfigTest {
Blockchain.KaspaTestnet to EllipticCurve.Secp256k1,
Blockchain.Alephium to EllipticCurve.Secp256k1,
Blockchain.AlephiumTestnet to EllipticCurve.Secp256k1,
Blockchain.Scroll to EllipticCurve.Secp256k1,
Blockchain.ScrollTestnet to EllipticCurve.Secp256k1,
Blockchain.ZkLinkNova to EllipticCurve.Secp256k1,
Blockchain.ZkLinkNovaTestnet to EllipticCurve.Secp256k1,
)
@Test

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