Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-25 10:31:38 +03:00
commit 74f71080ea
23 changed files with 143 additions and 95 deletions

View file

@ -146,7 +146,7 @@ dependencies {
/** Features */
implementation(projects.features.onboarding)
implementation(projects.features.referral.presentation)
implementation(projects.features.referral.impl)
implementation(projects.features.referral.domain)
implementation(projects.features.referral.data)
implementation(projects.features.swap.api)

View file

@ -3,7 +3,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.QrScanningComponent
import com.tangem.feature.referral.ReferralFragment
import com.tangem.feature.referral.api.ReferralComponent
import com.tangem.feature.stories.api.StoriesComponent
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.features.details.component.DetailsComponent
@ -71,6 +71,7 @@ internal class ChildFactory @Inject constructor(
private val appSettingsComponentFactory: AppSettingsComponent.Factory,
private val securityModeComponentFactory: SecurityModeComponent.Factory,
private val resetCardComponentFactory: ResetCardComponent.Factory,
private val referralComponentFactory: ReferralComponent.Factory,
private val walletRouter: WalletRouter,
private val testerRouter: TesterRouter,
private val pushNotificationRouter: PushNotificationsRouter,
@ -342,12 +343,18 @@ internal class ChildFactory @Inject constructor(
componentFactory = resetCardComponentFactory,
)
}
is AppRoute.ReferralProgram -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = ReferralComponent.Params(route.userWalletId),
componentFactory = referralComponentFactory,
)
}
is AppRoute.SaveWallet,
is AppRoute.OnboardingNote,
is AppRoute.OnboardingOther,
is AppRoute.OnboardingTwins,
is AppRoute.OnboardingWallet,
is AppRoute.ReferralProgram,
is AppRoute.Wallet,
is AppRoute.PushNotification,
-> error("Unsupported route: $route")
@ -475,7 +482,11 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.ReferralProgram -> {
route.asFragmentChild(Provider { ReferralFragment() })
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = ReferralComponent.Params(route.userWalletId),
componentFactory = referralComponentFactory,
)
}
is AppRoute.ResetToFactory -> {
route.asComponentChild(

View file

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

1
features/referral/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,20 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.serialization)
id("kotlin-parcelize")
id("configuration")
}
android {
namespace = "com.tangem.features.referral.api"
}
dependencies {
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/* Project - Domain */
implementation(projects.domain.wallets.models)
}

View file

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

View file

@ -13,6 +13,7 @@ dependencies {
/** Libs */
implementation(projects.core.utils)
implementation(projects.core.decompose)
/** Core modules */
implementation(projects.libs.crypto)

View file

@ -1,5 +1,7 @@
package com.tangem.feature.referral.domain.di
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
@ -10,15 +12,13 @@ import com.tangem.lib.crypto.UserWalletManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
@InstallIn(DecomposeComponent::class)
class ReferralDomainModule {
@Provides
@ViewModelScoped
@ComponentScoped
fun provideReferralInteractor(
referralRepository: ReferralRepository,
userWalletManager: UserWalletManager,

View file

@ -11,6 +11,9 @@ android {
}
dependencies {
/** Api */
api(projects.features.referral.api)
/** Core modules */
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
@ -18,6 +21,7 @@ dependencies {
implementation(projects.core.res)
implementation(projects.core.utils)
implementation(projects.core.ui)
implementation(projects.core.decompose)
implementation(projects.libs.crypto)
implementation(projects.common.routing)

View file

@ -0,0 +1,30 @@
package com.tangem.feature.referral
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.feature.referral.api.ReferralComponent
import com.tangem.feature.referral.model.ReferralModel
import com.tangem.feature.referral.ui.ReferralScreen
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
class DefaultReferralComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: ReferralComponent.Params,
) : ReferralComponent, AppComponentContext by appComponentContext {
private val model: ReferralModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
ReferralScreen(stateHolder = model.uiState)
}
@AssistedFactory
interface Factory : ReferralComponent.Factory {
override fun create(context: AppComponentContext, params: ReferralComponent.Params): DefaultReferralComponent
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.feature.referral.di
import com.tangem.core.decompose.model.Model
import com.tangem.feature.referral.DefaultReferralComponent
import com.tangem.feature.referral.api.ReferralComponent
import com.tangem.feature.referral.model.ReferralModel
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 ComponentModule {
@Binds
fun provideReferralComponentFactory(impl: DefaultReferralComponent.Factory): ReferralComponent.Factory
@Binds
@IntoMap
@ClassKey(ReferralModel::class)
fun bindModel(model: ReferralModel): Model
}

View file

@ -1,21 +1,20 @@
package com.tangem.feature.referral.viewmodels
package com.tangem.feature.referral.model
import android.os.Bundle
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
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.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.referral.analytics.ReferralEvents
import com.tangem.feature.referral.api.ReferralComponent
import com.tangem.feature.referral.domain.ReferralInteractor
import com.tangem.feature.referral.domain.errors.ReferralError
import com.tangem.feature.referral.domain.models.DiscountType
@ -25,29 +24,26 @@ import com.tangem.feature.referral.models.DemoModeException
import com.tangem.feature.referral.models.ReferralStateHolder
import com.tangem.feature.referral.models.ReferralStateHolder.ErrorSnackbar
import com.tangem.feature.referral.models.ReferralStateHolder.ReferralInfoState
import com.tangem.feature.referral.router.ReferralRouter
import dagger.hilt.android.lifecycle.HiltViewModel
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.properties.Delegates
@Suppress("LongParameterList")
@HiltViewModel
internal class ReferralViewModel @Inject constructor(
@Stable
@ComponentScoped
internal class ReferralModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val referralInteractor: ReferralInteractor,
private val analyticsEventHandler: AnalyticsEventHandler,
private val shareManager: ShareManager,
private val urlOpener: UrlOpener,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val isDemoCardUseCase: IsDemoCardUseCase,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val appRouter: AppRouter,
) : Model() {
private val userWalletId = savedStateHandle.get<Bundle>(AppRoute.ReferralProgram.USER_WALLET_ID_KEY)
?.unbundle(UserWalletId.serializer())
?: error("User wallet ID is required for Referral screen")
private var referralRouter: ReferralRouter by Delegates.notNull()
private val params = paramsContainer.require<ReferralComponent.Params>()
private var lastReferralData: ReferralData? = null
@ -55,20 +51,14 @@ internal class ReferralViewModel @Inject constructor(
private set
init {
analyticsEventHandler.send(ReferralEvents.ReferralScreenOpened)
loadReferralData()
}
fun setRouter(router: ReferralRouter) {
referralRouter = router
uiState = uiState.copy(headerState = ReferralStateHolder.HeaderState(onBackClicked = router::back))
}
fun onScreenOpened() {
analyticsEventHandler.send(ReferralEvents.ReferralScreenOpened)
}
private fun createInitiallyUiState() = ReferralStateHolder(
headerState = ReferralStateHolder.HeaderState(onBackClicked = { }),
headerState = ReferralStateHolder.HeaderState(
onBackClicked = appRouter::pop,
),
referralInfoState = ReferralInfoState.Loading,
errorSnackbar = null,
analytics = ReferralStateHolder.Analytics(
@ -80,9 +70,9 @@ internal class ReferralViewModel @Inject constructor(
private fun loadReferralData() {
uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading)
viewModelScope.launch {
modelScope.launch {
runCatching {
referralInteractor.getReferralStatus(userWalletId).apply {
referralInteractor.getReferralStatus(params.userWalletId).apply {
lastReferralData = this
}
}
@ -92,15 +82,15 @@ internal class ReferralViewModel @Inject constructor(
}
private fun participate() {
val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: error("User wallet not found")
val userWallet = getUserWalletUseCase(params.userWalletId).getOrNull() ?: error("User wallet not found")
if (isDemoCardUseCase(cardId = userWallet.cardId)) {
showErrorSnackbar(DemoModeException())
} else {
analyticsEventHandler.send(ReferralEvents.ClickParticipate)
uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading)
viewModelScope.launch {
runCatching { referralInteractor.startReferral(userWalletId) }
modelScope.launch {
runCatching { referralInteractor.startReferral(params.userWalletId) }
.onSuccess(::showContent)
.onFailure { throwable ->
if (throwable is ReferralError.UserCancelledException) {
@ -121,7 +111,7 @@ internal class ReferralViewModel @Inject constructor(
private fun showErrorSnackbar(throwable: Throwable) {
uiState = uiState.copy(
errorSnackbar = ErrorSnackbar(throwable = throwable, onOkClicked = referralRouter::back),
errorSnackbar = ErrorSnackbar(throwable = throwable, onOkClicked = appRouter::pop),
)
}

View file

@ -1,37 +0,0 @@
package com.tangem.feature.referral
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.feature.referral.router.ReferralRouter
import com.tangem.feature.referral.ui.ReferralScreen
import com.tangem.feature.referral.viewmodels.ReferralViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class ReferralFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
@Inject
internal lateinit var appRouter: AppRouter
private val viewModel by viewModels<ReferralViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.onScreenOpened()
viewModel.setRouter(ReferralRouter(appRouter))
}
@Composable
override fun ScreenContent(modifier: Modifier) {
ReferralScreen(stateHolder = viewModel.uiState)
}
}

View file

@ -1,12 +0,0 @@
package com.tangem.feature.referral.router
import com.tangem.common.routing.AppRouter
internal class ReferralRouter(
private val appRouter: AppRouter,
) {
fun back() {
appRouter.pop()
}
}

View file

@ -171,9 +171,10 @@ include(":features:onboarding")
include(":features:onboarding-v2:api")
include(":features:onboarding-v2:impl")
include(":features:referral:api")
include(":features:referral:data")
include(":features:referral:domain")
include(":features:referral:presentation")
include(":features:referral:impl")
include(":features:swap:api")
include(":features:swap:data")