Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-21 14:27:12 +03:00
commit 8a962212d5
159 changed files with 4492 additions and 3415 deletions

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

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

@ -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.cardsettings.api.CardSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.api.AccessCodeRecoveryComponent
import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment
import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment
import com.tangem.tap.features.details.ui.walletconnect.WalletConnectFragment
import com.tangem.tap.features.home.HomeFragment
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,16 @@ 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 walletRouter: WalletRouter,
private val qrScanningRouter: QrScanningRouter,
private val testerRouter: TesterRouter,
private val pushNotificationRouter: PushNotificationsRouter,
private val routingFeatureToggles: RoutingFeatureToggles,
@ -213,6 +217,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 +251,69 @@ internal class ChildFactory @Inject constructor(
componentFactory = swapComponentFactory,
)
}
is AppRoute.AccessCodeRecovery,
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,
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,7 +330,11 @@ internal class ChildFactory @Inject constructor(
Child.Initial
}
is AppRoute.AccessCodeRecovery -> {
route.asFragmentChild(Provider { AccessCodeRecoveryFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = accessCodeRecoveryComponentFactory,
)
}
is AppRoute.AppCurrencySelector -> {
route.asFragmentChild(Provider { AppCurrencySelectorFragment() })
@ -280,13 +343,28 @@ internal class ChildFactory @Inject constructor(
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() })
}
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(
@ -306,7 +384,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,7 +416,14 @@ 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() })
@ -359,10 +448,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() })