Updated on 2026-08-14
This commit is contained in:
parent
5ca696d677
commit
579b3cba32
27 changed files with 887 additions and 58 deletions
|
|
@ -24,6 +24,10 @@ dependencies {
|
|||
implementation(projects.domain.appCurrency)
|
||||
implementation(projects.domain.account.status)
|
||||
implementation(projects.domain.promo)
|
||||
implementation(projects.domain.promo.models)
|
||||
|
||||
/** Data */
|
||||
implementation(projects.data.common)
|
||||
|
||||
/** Core */
|
||||
api(projects.core.configToggles)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.tangem.core.ui.ds2.button.Close
|
|||
import com.tangem.core.ui.ds2.button.TangemButton
|
||||
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
|
||||
import com.tangem.features.promobanners.impl.campaigns.model.ActivateCampaignsModel
|
||||
import com.tangem.features.promobanners.impl.campaigns.ui.ActivateCampaignContent
|
||||
|
|
@ -83,6 +84,7 @@ internal class ActivateCampaignBottomSheetComponent(
|
|||
|
||||
data class Params(
|
||||
val campaignType: CampaignType,
|
||||
val userWalletId: UserWalletId,
|
||||
val modelCallbacks: ActivateCampaignModelCallbacks,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ internal class DefaultCampaignsComponent @AssistedInject constructor(
|
|||
onFooterExtraHeightReady = model::onFooterExtraHeightReady,
|
||||
params = ActivateCampaignBottomSheetComponent.Params(
|
||||
campaignType = config.campaignType,
|
||||
userWalletId = config.userWalletId,
|
||||
modelCallbacks = object : ActivateCampaignModelCallbacks {
|
||||
override val onActivated: (CampaignType) -> Unit = model::onActivated
|
||||
override val onAlreadyActivated: (CampaignType) -> Unit = model::onAlreadyActivated
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.promobanners.impl.campaigns.entity
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
|
@ -16,6 +17,7 @@ internal sealed class CampaignsBottomSheetConfig {
|
|||
@Serializable
|
||||
data class Activate(
|
||||
val campaignType: CampaignType,
|
||||
val userWalletId: UserWalletId,
|
||||
) : CampaignsBottomSheetConfig()
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -23,10 +23,14 @@ import com.tangem.domain.models.currency.CryptoCurrency
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.promo.models.EnrollResult
|
||||
import com.tangem.domain.promo.models.PromoCampaignId
|
||||
import com.tangem.domain.promo.models.PromoCampaignState
|
||||
import com.tangem.domain.promo.models.TokenReward
|
||||
import com.tangem.domain.promo.usecase.EnrollPromoCampaignUseCase
|
||||
import com.tangem.domain.promo.usecase.GetPromoCampaignStateUseCase
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooserBlock
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.PredefinedTokenToAdd
|
||||
import com.tangem.features.promobanners.impl.R
|
||||
import com.tangem.features.promobanners.impl.campaigns.analytics.PromoCampaignsAnalyticsEvent
|
||||
import com.tangem.features.promobanners.impl.campaigns.component.ActivateCampaignBottomSheetComponent
|
||||
|
|
@ -59,6 +63,8 @@ internal class ActivateCampaignsModel @Inject constructor(
|
|||
private val urlOpener: UrlOpener,
|
||||
@GlobalUiMessageSender private val messageSender: UiMessageSender,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val getPromoCampaignStateUseCase: GetPromoCampaignStateUseCase,
|
||||
private val predefinedTokenResolver: PredefinedTokenResolver,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<ActivateCampaignBottomSheetComponent.Params>()
|
||||
|
|
@ -68,6 +74,8 @@ internal class ActivateCampaignsModel @Inject constructor(
|
|||
private val campaignId: PromoCampaignId = params.campaignType.toPromoCampaignId()
|
||||
private val campaignContent = CampaignTypeToContentConverter().convert(campaignType)
|
||||
|
||||
private val predefinedTokensFlow = MutableStateFlow<List<PredefinedTokenToAdd>>(emptyList())
|
||||
|
||||
val uiState: StateFlow<ActivateCampaignUM>
|
||||
field = MutableStateFlow(buildInitialModel())
|
||||
|
||||
|
|
@ -75,7 +83,7 @@ internal class ActivateCampaignsModel @Inject constructor(
|
|||
modelScope = modelScope,
|
||||
settings = ChooseTokenBridge.Settings(
|
||||
title = resourceReference(R.string.common_choose_token),
|
||||
isShowMarketBlock = false,
|
||||
chooserBlock = ChooserBlock.Predefined(predefinedTokensFlow),
|
||||
isShowPaymentAccount = false,
|
||||
isShowSingleCurrencyWallets = true,
|
||||
),
|
||||
|
|
@ -95,6 +103,18 @@ internal class ActivateCampaignsModel @Inject constructor(
|
|||
bridge.onClose.receiveAsFlow()
|
||||
.onEach { onChooseTokenDismiss() }
|
||||
.launchIn(modelScope)
|
||||
|
||||
modelScope.launch { loadPredefinedTokens() }
|
||||
}
|
||||
|
||||
private suspend fun loadPredefinedTokens() {
|
||||
getPromoCampaignStateUseCase(campaignId, params.userWalletId)
|
||||
.onLeft { error -> TangemLogger.e("Error loading campaign ${campaignType.campaignId} state", error) }
|
||||
.onRight { state ->
|
||||
if (state is PromoCampaignState.Available) {
|
||||
predefinedTokensFlow.value = predefinedTokenResolver.resolve(state.payoutTokens)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildInitialModel(): ActivateCampaignUM = ActivateCampaignUM(
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ internal class CampaignsModel @Inject constructor(
|
|||
when (campaignState) {
|
||||
is PromoCampaignState.Enrolled,
|
||||
is PromoCampaignState.Available,
|
||||
-> CampaignsBottomSheetConfig.Activate(campaignType)
|
||||
-> CampaignsBottomSheetConfig.Activate(campaignType, userWalletId)
|
||||
is PromoCampaignState.NotActive -> CampaignsBottomSheetConfig.NotActive
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.features.promobanners.impl.campaigns.model
|
||||
|
||||
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||
import com.tangem.domain.markets.RawMarketToken
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.promo.models.PromoPayoutToken
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.PredefinedTokenToAdd
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Maps backend promo payout tokens into [PredefinedTokenToAdd] for the token chooser.
|
||||
*
|
||||
* The backend payload already carries everything needed — `tokenId` (the market raw id), decimals,
|
||||
* symbol, name, contract address and network id — so no catalog lookup is required. The icon url is
|
||||
* derived from the raw id via the canonical host helper, matching what the add-to-portfolio flow uses.
|
||||
*/
|
||||
internal class PredefinedTokenResolver @Inject constructor() {
|
||||
|
||||
fun resolve(payoutTokens: List<PromoPayoutToken>): List<PredefinedTokenToAdd> = payoutTokens
|
||||
.map { payoutToken -> payoutToken.toPredefinedToken() }
|
||||
.distinctBy { it.token.id.value to it.network.networkId }
|
||||
|
||||
private fun PromoPayoutToken.toPredefinedToken(): PredefinedTokenToAdd {
|
||||
val rawId = CryptoCurrency.RawID(tokenId)
|
||||
return PredefinedTokenToAdd(
|
||||
token = RawMarketToken(
|
||||
id = rawId,
|
||||
name = tokenName,
|
||||
symbol = tokenSymbol,
|
||||
),
|
||||
network = TokenMarketInfo.Network(
|
||||
networkId = networkId,
|
||||
isExchangeable = false,
|
||||
contractAddress = tokenAddress,
|
||||
decimalCount = decimals,
|
||||
),
|
||||
iconUrl = getTokenIconUrlFromDefaultHost(rawId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import com.tangem.domain.promo.models.EnrollResult
|
|||
import com.tangem.domain.promo.models.PromoCampaignId
|
||||
import com.tangem.domain.promo.models.TokenReward
|
||||
import com.tangem.domain.promo.usecase.EnrollPromoCampaignUseCase
|
||||
import com.tangem.domain.promo.usecase.GetPromoCampaignStateUseCase
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
|
||||
import com.tangem.features.promobanners.impl.campaigns.analytics.PromoCampaignsAnalyticsEvent
|
||||
|
|
@ -54,6 +55,8 @@ internal class ActivateCampaignsModelTest {
|
|||
private val urlOpener: UrlOpener = mockk(relaxed = true)
|
||||
private val messageSender: UiMessageSender = mockk(relaxed = true)
|
||||
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
private val getPromoCampaignStateUseCase: GetPromoCampaignStateUseCase = mockk()
|
||||
private val predefinedTokenResolver: PredefinedTokenResolver = mockk(relaxed = true)
|
||||
private val modelCallbacks: ActivateCampaignBottomSheetComponent.ActivateCampaignModelCallbacks =
|
||||
mockk(relaxed = true)
|
||||
|
||||
|
|
@ -219,10 +222,12 @@ internal class ActivateCampaignsModelTest {
|
|||
every { chooseTokenBridgeFactory.create(any(), any(), any()) } returns bridge
|
||||
every { getSelectedAppCurrencyUseCase.invokeOrDefault() } returns flowOf(AppCurrency.Default)
|
||||
coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns false
|
||||
coEvery { getPromoCampaignStateUseCase(any(), any(), any()) } returns Either.Left(Throwable())
|
||||
return ActivateCampaignsModel(
|
||||
paramsContainer = MutableParamsContainer(
|
||||
ActivateCampaignBottomSheetComponent.Params(
|
||||
campaignType = campaignType,
|
||||
userWalletId = userWalletId,
|
||||
modelCallbacks = modelCallbacks,
|
||||
),
|
||||
),
|
||||
|
|
@ -234,6 +239,8 @@ internal class ActivateCampaignsModelTest {
|
|||
urlOpener = urlOpener,
|
||||
messageSender = messageSender,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
getPromoCampaignStateUseCase = getPromoCampaignStateUseCase,
|
||||
predefinedTokenResolver = predefinedTokenResolver,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
package com.tangem.features.promobanners.impl.campaigns.model
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||
import com.tangem.domain.markets.RawMarketToken
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.promo.models.PromoPayoutToken
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.PredefinedTokenToAdd
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class PredefinedTokenResolverTest {
|
||||
|
||||
private val resolver = PredefinedTokenResolver()
|
||||
|
||||
@Test
|
||||
fun `GIVEN payout token WHEN resolve THEN mapped from payload`() {
|
||||
// Arrange
|
||||
val payoutToken = createPayoutToken(
|
||||
tokenId = "cat-token",
|
||||
tokenAddress = "0xContract",
|
||||
tokenSymbol = "CAT",
|
||||
tokenName = "Cat Token",
|
||||
networkId = "ethereum",
|
||||
decimals = 6,
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = resolver.resolve(listOf(payoutToken))
|
||||
|
||||
// Assert
|
||||
val rawId = CryptoCurrency.RawID("cat-token")
|
||||
val expected = PredefinedTokenToAdd(
|
||||
token = RawMarketToken(id = rawId, name = "Cat Token", symbol = "CAT"),
|
||||
network = TokenMarketInfo.Network(
|
||||
networkId = "ethereum",
|
||||
isExchangeable = false,
|
||||
contractAddress = "0xContract",
|
||||
decimalCount = 6,
|
||||
),
|
||||
iconUrl = getTokenIconUrlFromDefaultHost(rawId),
|
||||
)
|
||||
assertThat(actual).containsExactly(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN multiple payout tokens WHEN resolve THEN input order preserved`() {
|
||||
// Arrange
|
||||
val first = createPayoutToken(tokenId = "first-token", tokenSymbol = "AAA", networkId = "ethereum")
|
||||
val second = createPayoutToken(tokenId = "second-token", tokenSymbol = "BBB", networkId = "polygon")
|
||||
|
||||
// Act
|
||||
val actual = resolver.resolve(listOf(first, second)).map { it.token.id.value }
|
||||
|
||||
// Assert
|
||||
assertThat(actual).containsExactly("first-token", "second-token").inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN two payouts with same id on same network WHEN resolve THEN duplicate removed`() {
|
||||
// Arrange
|
||||
val first = createPayoutToken(tokenId = "usd-coin", tokenAddress = "0xFirst", networkId = "ethereum")
|
||||
val second = createPayoutToken(tokenId = "usd-coin", tokenAddress = "0xSecond", networkId = "ethereum")
|
||||
|
||||
// Act
|
||||
val actual = resolver.resolve(listOf(first, second))
|
||||
|
||||
// Assert
|
||||
assertThat(actual).hasSize(1)
|
||||
assertThat(actual.single().network.contractAddress).isEqualTo("0xFirst")
|
||||
}
|
||||
|
||||
private fun createPayoutToken(
|
||||
tokenId: String = "cat-token",
|
||||
tokenAddress: String = "0xContract",
|
||||
tokenSymbol: String = "CAT",
|
||||
tokenName: String = "Cat Token",
|
||||
networkId: String = "ethereum",
|
||||
decimals: Int = 6,
|
||||
) = PromoPayoutToken(
|
||||
tokenId = tokenId,
|
||||
tokenAddress = tokenAddress,
|
||||
tokenSymbol = tokenSymbol,
|
||||
tokenName = tokenName,
|
||||
networkId = networkId,
|
||||
decimals = decimals,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue