Updated on 2026-08-14
This commit is contained in:
parent
f899b889e6
commit
c0092847b7
8 changed files with 653 additions and 11 deletions
|
|
@ -56,7 +56,6 @@ dependencies {
|
||||||
kapt(deps.hilt.kapt)
|
kapt(deps.hilt.kapt)
|
||||||
|
|
||||||
/** Tests */
|
/** Tests */
|
||||||
testImplementation(deps.test.junit5)
|
testImplementation(projects.test.core)
|
||||||
testImplementation(deps.test.truth)
|
testImplementation(projects.common.test)
|
||||||
testImplementation(deps.kotlin.serialization)
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.tangem.features.promobanners.impl.campaigns.analytics
|
||||||
|
|
||||||
|
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||||
|
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
|
||||||
|
|
||||||
|
internal sealed class PromoCampaignsAnalyticsEvent(
|
||||||
|
event: String,
|
||||||
|
params: Map<String, String> = emptyMap(),
|
||||||
|
) : AnalyticsEvent(category = "Promotion", event = event, params = params) {
|
||||||
|
|
||||||
|
class PromotionScreenOpened(campaignType: CampaignType) : PromoCampaignsAnalyticsEvent(
|
||||||
|
event = "Promotion Screen Opened",
|
||||||
|
params = mapOf("Screen" to campaignType.analyticsName),
|
||||||
|
)
|
||||||
|
|
||||||
|
class EnrollButtonClicked(
|
||||||
|
campaignType: CampaignType,
|
||||||
|
token: String,
|
||||||
|
blockchain: String,
|
||||||
|
) : PromoCampaignsAnalyticsEvent(
|
||||||
|
event = "Enroll Button Clicked",
|
||||||
|
params = mapOf(
|
||||||
|
"Campaign" to campaignType.analyticsName,
|
||||||
|
"Token" to token,
|
||||||
|
"Blockchain" to blockchain,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class AlreadyEnrolledScreenOpened : PromoCampaignsAnalyticsEvent(event = "Already Enrolled Screen Opened")
|
||||||
|
}
|
||||||
|
|
||||||
|
private val CampaignType.analyticsName: String
|
||||||
|
get() = when (this) {
|
||||||
|
is CampaignType.WhaleSwapCashback -> "Cashback"
|
||||||
|
is CampaignType.ReactivationCashback -> "Reactivation"
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.promobanners.impl.campaigns.model
|
||||||
import com.tangem.common.ui.account.AccountIconItemStateConverter
|
import com.tangem.common.ui.account.AccountIconItemStateConverter
|
||||||
import com.tangem.common.ui.account.toUM
|
import com.tangem.common.ui.account.toUM
|
||||||
import com.tangem.common.ui.tokens.TokenItemStateConverter
|
import com.tangem.common.ui.tokens.TokenItemStateConverter
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||||
import com.tangem.core.decompose.di.ModelScoped
|
import com.tangem.core.decompose.di.ModelScoped
|
||||||
import com.tangem.core.decompose.model.Model
|
import com.tangem.core.decompose.model.Model
|
||||||
|
|
@ -13,13 +14,12 @@ import com.tangem.core.ui.components.account.AccountIconSize
|
||||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.extensions.wrappedList
|
import com.tangem.core.ui.extensions.wrappedList
|
||||||
import com.tangem.core.ui.message.SnackbarMessage
|
import com.tangem.core.ui.message.ToastMessage
|
||||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.models.account.Account
|
import com.tangem.domain.models.account.Account
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.promo.models.EnrollResult
|
import com.tangem.domain.promo.models.EnrollResult
|
||||||
import com.tangem.domain.promo.models.PromoCampaignId
|
import com.tangem.domain.promo.models.PromoCampaignId
|
||||||
|
|
@ -28,6 +28,7 @@ import com.tangem.domain.promo.usecase.EnrollPromoCampaignUseCase
|
||||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
|
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge
|
||||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
|
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult
|
||||||
import com.tangem.features.promobanners.impl.R
|
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
|
import com.tangem.features.promobanners.impl.campaigns.component.ActivateCampaignBottomSheetComponent
|
||||||
import com.tangem.features.promobanners.impl.campaigns.entity.ActivateCampaignUM
|
import com.tangem.features.promobanners.impl.campaigns.entity.ActivateCampaignUM
|
||||||
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignTypeToContentConverter
|
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignTypeToContentConverter
|
||||||
|
|
@ -57,6 +58,7 @@ internal class ActivateCampaignsModel @Inject constructor(
|
||||||
private val enrollPromoCampaignUseCase: EnrollPromoCampaignUseCase,
|
private val enrollPromoCampaignUseCase: EnrollPromoCampaignUseCase,
|
||||||
private val urlOpener: UrlOpener,
|
private val urlOpener: UrlOpener,
|
||||||
@GlobalUiMessageSender private val messageSender: UiMessageSender,
|
@GlobalUiMessageSender private val messageSender: UiMessageSender,
|
||||||
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
) : Model() {
|
) : Model() {
|
||||||
|
|
||||||
private val params = paramsContainer.require<ActivateCampaignBottomSheetComponent.Params>()
|
private val params = paramsContainer.require<ActivateCampaignBottomSheetComponent.Params>()
|
||||||
|
|
@ -80,6 +82,8 @@ internal class ActivateCampaignsModel @Inject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
analyticsEventHandler.send(PromoCampaignsAnalyticsEvent.PromotionScreenOpened(campaignType))
|
||||||
|
|
||||||
getSelectedAppCurrencyUseCase.invokeOrDefault()
|
getSelectedAppCurrencyUseCase.invokeOrDefault()
|
||||||
.onEach { appCurrency = it }
|
.onEach { appCurrency = it }
|
||||||
.launchIn(modelScope)
|
.launchIn(modelScope)
|
||||||
|
|
@ -120,20 +124,26 @@ internal class ActivateCampaignsModel @Inject constructor(
|
||||||
uiState.update { it.copy(isChoosingToken = false) }
|
uiState.update { it.copy(isChoosingToken = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onEnrollClick(selectedWalletId: UserWalletId, selectedCurrencyStatus: CryptoCurrencyStatus) {
|
private fun onEnrollClick(selectedWalletId: UserWalletId, selectedToken: CryptoCurrency.Token) {
|
||||||
val token = selectedCurrencyStatus.currency as? CryptoCurrency.Token ?: return
|
analyticsEventHandler.send(
|
||||||
|
PromoCampaignsAnalyticsEvent.EnrollButtonClicked(
|
||||||
|
campaignType = campaignType,
|
||||||
|
token = selectedToken.symbol,
|
||||||
|
blockchain = selectedToken.network.name,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
enrollPromoCampaignUseCase.invoke(
|
enrollPromoCampaignUseCase.invoke(
|
||||||
campaign = campaignId,
|
campaign = campaignId,
|
||||||
tokenReward = TokenReward(
|
tokenReward = TokenReward(
|
||||||
tokenAddress = token.contractAddress,
|
tokenAddress = selectedToken.contractAddress,
|
||||||
networkId = token.network.rawId,
|
networkId = selectedToken.network.rawId,
|
||||||
),
|
),
|
||||||
walletIds = listOf(selectedWalletId),
|
walletIds = listOf(selectedWalletId),
|
||||||
).onLeft { error ->
|
).onLeft { error ->
|
||||||
TangemLogger.e("Error enrolling campaign ${campaignType.campaignId}", error)
|
TangemLogger.e("Error enrolling campaign ${campaignType.campaignId}", error)
|
||||||
messageSender.send(SnackbarMessage(message = resourceReference(R.string.common_unknown_error)))
|
messageSender.send(ToastMessage(message = resourceReference(R.string.common_unknown_error)))
|
||||||
}.onRight {
|
}.onRight {
|
||||||
handleEnrollResponse(it)
|
handleEnrollResponse(it)
|
||||||
}
|
}
|
||||||
|
|
@ -156,6 +166,8 @@ internal class ActivateCampaignsModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onTokenChosen(result: ChooseTokenResult) {
|
private fun onTokenChosen(result: ChooseTokenResult) {
|
||||||
|
val selectedToken = result.currency.currency as? CryptoCurrency.Token ?: return
|
||||||
|
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
val selectedAccountUM = if (isAccountsModeEnabledUseCase.invokeSync()) {
|
val selectedAccountUM = if (isAccountsModeEnabledUseCase.invokeSync()) {
|
||||||
when (val account = result.account.account) {
|
when (val account = result.account.account) {
|
||||||
|
|
@ -185,7 +197,7 @@ internal class ActivateCampaignsModel @Inject constructor(
|
||||||
onPrimaryButtonClick = {
|
onPrimaryButtonClick = {
|
||||||
onEnrollClick(
|
onEnrollClick(
|
||||||
selectedWalletId = result.walletId,
|
selectedWalletId = result.walletId,
|
||||||
selectedCurrencyStatus = result.currency,
|
selectedToken = selectedToken,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
terms = TermsUM(
|
terms = TermsUM(
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.compose.ui.unit.dp
|
||||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||||
import com.arkivanov.decompose.router.slot.activate
|
import com.arkivanov.decompose.router.slot.activate
|
||||||
import com.arkivanov.decompose.router.slot.dismiss
|
import com.arkivanov.decompose.router.slot.dismiss
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||||
import com.tangem.core.decompose.di.ModelScoped
|
import com.tangem.core.decompose.di.ModelScoped
|
||||||
import com.tangem.core.decompose.model.Model
|
import com.tangem.core.decompose.model.Model
|
||||||
|
|
@ -15,6 +16,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.promo.models.PromoCampaignState
|
import com.tangem.domain.promo.models.PromoCampaignState
|
||||||
import com.tangem.domain.promo.usecase.GetPromoCampaignStateUseCase
|
import com.tangem.domain.promo.usecase.GetPromoCampaignStateUseCase
|
||||||
import com.tangem.features.promobanners.impl.R
|
import com.tangem.features.promobanners.impl.R
|
||||||
|
import com.tangem.features.promobanners.impl.campaigns.analytics.PromoCampaignsAnalyticsEvent
|
||||||
import com.tangem.features.promobanners.impl.campaigns.converters.CampaignIdConverter
|
import com.tangem.features.promobanners.impl.campaigns.converters.CampaignIdConverter
|
||||||
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignsBottomSheetConfig
|
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignsBottomSheetConfig
|
||||||
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
|
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
|
||||||
|
|
@ -36,6 +38,7 @@ internal class CampaignsModel @Inject constructor(
|
||||||
campaignsService: CampaignsService,
|
campaignsService: CampaignsService,
|
||||||
private val getPromoCampaignStateUseCase: GetPromoCampaignStateUseCase,
|
private val getPromoCampaignStateUseCase: GetPromoCampaignStateUseCase,
|
||||||
@GlobalUiMessageSender private val messageSender: UiMessageSender,
|
@GlobalUiMessageSender private val messageSender: UiMessageSender,
|
||||||
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
) : Model() {
|
) : Model() {
|
||||||
|
|
||||||
val bottomSheetNavigation: SlotNavigation<CampaignsBottomSheetConfig> = SlotNavigation()
|
val bottomSheetNavigation: SlotNavigation<CampaignsBottomSheetConfig> = SlotNavigation()
|
||||||
|
|
@ -102,6 +105,7 @@ internal class CampaignsModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onAlreadyActivated(campaignType: CampaignType) {
|
fun onAlreadyActivated(campaignType: CampaignType) {
|
||||||
|
analyticsEventHandler.send(PromoCampaignsAnalyticsEvent.AlreadyEnrolledScreenOpened())
|
||||||
footerExtraHeightState.value = 0.dp
|
footerExtraHeightState.value = 0.dp
|
||||||
bottomSheetNavigation.activate(CampaignsBottomSheetConfig.AlreadyActivated(campaignType = campaignType))
|
bottomSheetNavigation.activate(CampaignsBottomSheetConfig.AlreadyActivated(campaignType = campaignType))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
package com.tangem.features.promobanners.impl.campaigns.analytics
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
|
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
internal class PromoCampaignsAnalyticsEventTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN any event WHEN created THEN category is Promotion`() {
|
||||||
|
// Arrange
|
||||||
|
val events = listOf(
|
||||||
|
PromoCampaignsAnalyticsEvent.PromotionScreenOpened(campaignType = whaleSwap),
|
||||||
|
PromoCampaignsAnalyticsEvent.EnrollButtonClicked(
|
||||||
|
campaignType = whaleSwap,
|
||||||
|
token = "USDT",
|
||||||
|
blockchain = "Ethereum",
|
||||||
|
),
|
||||||
|
PromoCampaignsAnalyticsEvent.AlreadyEnrolledScreenOpened(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(events.map { it.category }).containsExactly("Promotion", "Promotion", "Promotion")
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideCampaignTypes")
|
||||||
|
fun `GIVEN campaign type WHEN PromotionScreenOpened THEN event name and Screen param are correct`(
|
||||||
|
model: CampaignNameModel,
|
||||||
|
) {
|
||||||
|
// Act
|
||||||
|
val event = PromoCampaignsAnalyticsEvent.PromotionScreenOpened(campaignType = model.campaignType)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(event.event).isEqualTo("Promotion Screen Opened")
|
||||||
|
assertThat(event.params).containsExactly("Screen", model.expectedAnalyticsName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideCampaignTypes")
|
||||||
|
fun `GIVEN campaign type WHEN EnrollButtonClicked THEN event name and params are correct`(
|
||||||
|
model: CampaignNameModel,
|
||||||
|
) {
|
||||||
|
// Act
|
||||||
|
val event = PromoCampaignsAnalyticsEvent.EnrollButtonClicked(
|
||||||
|
campaignType = model.campaignType,
|
||||||
|
token = "USDT",
|
||||||
|
blockchain = "Ethereum",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(event.event).isEqualTo("Enroll Button Clicked")
|
||||||
|
assertThat(event.params).containsExactly(
|
||||||
|
"Campaign", model.expectedAnalyticsName,
|
||||||
|
"Token", "USDT",
|
||||||
|
"Blockchain", "Ethereum",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN AlreadyEnrolledScreenOpened WHEN created THEN event name is correct and no params`() {
|
||||||
|
// Act
|
||||||
|
val event = PromoCampaignsAnalyticsEvent.AlreadyEnrolledScreenOpened()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(event.event).isEqualTo("Already Enrolled Screen Opened")
|
||||||
|
assertThat(event.params).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun provideCampaignTypes() = listOf(
|
||||||
|
CampaignNameModel(campaignType = whaleSwap, expectedAnalyticsName = "Cashback"),
|
||||||
|
CampaignNameModel(campaignType = reactivation, expectedAnalyticsName = "Reactivation"),
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class CampaignNameModel(
|
||||||
|
val campaignType: CampaignType,
|
||||||
|
val expectedAnalyticsName: String,
|
||||||
|
) {
|
||||||
|
override fun toString(): String = "${campaignType::class.simpleName} -> $expectedAnalyticsName"
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val whaleSwap = CampaignType.WhaleSwapCashback(campaignId = "whale")
|
||||||
|
val reactivation = CampaignType.ReactivationCashback(campaignId = "reactivation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.tangem.features.promobanners.impl.campaigns.converters
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
|
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
internal class CampaignIdConverterTest {
|
||||||
|
|
||||||
|
private val converter = CampaignIdConverter()
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideConvertModels")
|
||||||
|
fun `GIVEN campaign id WHEN convert THEN correct campaign type is returned`(model: ConvertModel) {
|
||||||
|
// Act
|
||||||
|
val actual = converter.convert(model.id)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(actual).isEqualTo(model.expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun provideConvertModels() = listOf(
|
||||||
|
ConvertModel(id = "1", expected = CampaignType.WhaleSwapCashback(campaignId = "1")),
|
||||||
|
ConvertModel(id = "2", expected = CampaignType.ReactivationCashback(campaignId = "2")),
|
||||||
|
ConvertModel(id = "0", expected = null),
|
||||||
|
ConvertModel(id = "unknown", expected = null),
|
||||||
|
ConvertModel(id = "", expected = null),
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class ConvertModel(
|
||||||
|
val id: String,
|
||||||
|
val expected: CampaignType?,
|
||||||
|
) {
|
||||||
|
override fun toString(): String = "\"$id\" -> ${expected?.let { it::class.simpleName } ?: "null"}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
package com.tangem.features.promobanners.impl.campaigns.model
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.blockchain.common.Blockchain
|
||||||
|
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
|
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||||
|
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||||
|
import com.tangem.core.decompose.ui.UiMessageSender
|
||||||
|
import com.tangem.core.navigation.url.UrlOpener
|
||||||
|
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||||
|
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||||
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
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.TokenReward
|
||||||
|
import com.tangem.domain.promo.usecase.EnrollPromoCampaignUseCase
|
||||||
|
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
|
||||||
|
import com.tangem.features.promobanners.impl.campaigns.component.ActivateCampaignBottomSheetComponent
|
||||||
|
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
|
||||||
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
|
import io.mockk.clearMocks
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
|
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
internal class ActivateCampaignsModelTest {
|
||||||
|
|
||||||
|
private val chooseTokenBridgeFactory: ChooseTokenBridge.Factory = mockk(relaxed = true)
|
||||||
|
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase = mockk()
|
||||||
|
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase = mockk()
|
||||||
|
private val enrollPromoCampaignUseCase: EnrollPromoCampaignUseCase = mockk()
|
||||||
|
private val urlOpener: UrlOpener = mockk(relaxed = true)
|
||||||
|
private val messageSender: UiMessageSender = mockk(relaxed = true)
|
||||||
|
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||||
|
private val modelCallbacks: ActivateCampaignBottomSheetComponent.ActivateCampaignModelCallbacks =
|
||||||
|
mockk(relaxed = true)
|
||||||
|
|
||||||
|
private val cryptoCurrencyFactory = MockCryptoCurrencyFactory()
|
||||||
|
|
||||||
|
private lateinit var onCurrencyChosen: Channel<ChooseTokenResult>
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
clearMocks(
|
||||||
|
getSelectedAppCurrencyUseCase,
|
||||||
|
isAccountsModeEnabledUseCase,
|
||||||
|
enrollPromoCampaignUseCase,
|
||||||
|
messageSender,
|
||||||
|
analyticsEventHandler,
|
||||||
|
modelCallbacks,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideCampaignTypes")
|
||||||
|
fun `GIVEN campaign type WHEN model created THEN PromotionScreenOpened is sent`(campaignType: CampaignType) =
|
||||||
|
runTest {
|
||||||
|
// Act
|
||||||
|
val model = createModel(campaignType)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
verify(exactly = 1) {
|
||||||
|
analyticsEventHandler.send(PromoCampaignsAnalyticsEvent.PromotionScreenOpened(campaignType))
|
||||||
|
}
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `WHEN choose token clicked and dismissed THEN isChoosingToken toggles`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val model = createModel(CampaignType.WhaleSwapCashback(campaignId = "1"))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
model.uiState.value.onChooseTokenClick()
|
||||||
|
assertThat(model.uiState.value.isChoosingToken).isTrue()
|
||||||
|
|
||||||
|
model.uiState.value.onChooseTokenDismiss()
|
||||||
|
assertThat(model.uiState.value.isChoosingToken).isFalse()
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN a coin is chosen WHEN currency chosen THEN it is ignored`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val model = createModel(CampaignType.WhaleSwapCashback(campaignId = "1"))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
onCurrencyChosen.send(chooseTokenResult(currency = coin()))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(model.uiState.value.selectedToken).isNull()
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN a token chosen and enroll succeeds WHEN enroll clicked THEN event carries symbol and blockchain`() =
|
||||||
|
runTest {
|
||||||
|
// Arrange
|
||||||
|
val token = token()
|
||||||
|
val campaignType = CampaignType.WhaleSwapCashback(campaignId = "1")
|
||||||
|
coEvery { enrollPromoCampaignUseCase.invoke(any(), any(), any()) } returns
|
||||||
|
Either.Right(EnrollResult.Success(TokenReward(tokenAddress = "a", networkId = "b")))
|
||||||
|
val model = createModel(campaignType)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
onCurrencyChosen.send(chooseTokenResult(currency = token))
|
||||||
|
advanceUntilIdle()
|
||||||
|
model.uiState.value.footerUM.onPrimaryButtonClick()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Assert — Token param is the symbol (TTK), Blockchain param is the network name (Ethereum)
|
||||||
|
val events = mutableListOf<AnalyticsEvent>()
|
||||||
|
verify { analyticsEventHandler.send(capture(events)) }
|
||||||
|
val enrollEvent = events.filterIsInstance<PromoCampaignsAnalyticsEvent.EnrollButtonClicked>().single()
|
||||||
|
assertThat(enrollEvent.event).isEqualTo("Enroll Button Clicked")
|
||||||
|
assertThat(enrollEvent.params).containsExactly(
|
||||||
|
"Campaign", "Cashback",
|
||||||
|
"Token", "TTK",
|
||||||
|
"Blockchain", "Ethereum",
|
||||||
|
)
|
||||||
|
|
||||||
|
coVerify(exactly = 1) {
|
||||||
|
enrollPromoCampaignUseCase.invoke(
|
||||||
|
campaign = PromoCampaignId.WhaleSwapCashback,
|
||||||
|
tokenReward = TokenReward(tokenAddress = token.contractAddress, networkId = token.network.rawId),
|
||||||
|
walletIds = listOf(userWalletId),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
verify(exactly = 1) { modelCallbacks.onActivated(campaignType) }
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN enroll returns AlreadyEnrolled WHEN enroll clicked THEN onAlreadyActivated called`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val campaignType = CampaignType.WhaleSwapCashback(campaignId = "1")
|
||||||
|
coEvery { enrollPromoCampaignUseCase.invoke(any(), any(), any()) } returns
|
||||||
|
Either.Right(EnrollResult.AlreadyEnrolled(TokenReward(tokenAddress = "a", networkId = "b")))
|
||||||
|
val model = createModel(campaignType)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
onCurrencyChosen.send(chooseTokenResult(currency = token()))
|
||||||
|
advanceUntilIdle()
|
||||||
|
model.uiState.value.footerUM.onPrimaryButtonClick()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
verify(exactly = 1) { modelCallbacks.onAlreadyActivated(campaignType) }
|
||||||
|
verify(exactly = 0) { modelCallbacks.onActivated(any()) }
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN enroll fails WHEN enroll clicked THEN error message is sent and no callback`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
coEvery { enrollPromoCampaignUseCase.invoke(any(), any(), any()) } returns
|
||||||
|
Either.Left(RuntimeException("network"))
|
||||||
|
val model = createModel(CampaignType.WhaleSwapCashback(campaignId = "1"))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
onCurrencyChosen.send(chooseTokenResult(currency = token()))
|
||||||
|
advanceUntilIdle()
|
||||||
|
model.uiState.value.footerUM.onPrimaryButtonClick()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
verify(exactly = 1) { messageSender.send(any()) }
|
||||||
|
verify(exactly = 0) { modelCallbacks.onActivated(any()) }
|
||||||
|
verify(exactly = 0) { modelCallbacks.onAlreadyActivated(any()) }
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun chooseTokenResult(currency: CryptoCurrency): ChooseTokenResult {
|
||||||
|
val status = CryptoCurrencyStatus(
|
||||||
|
currency = currency,
|
||||||
|
value = CryptoCurrencyStatus.MissedDerivation(priceChange = null, fiatRate = null),
|
||||||
|
)
|
||||||
|
val wallet: UserWallet = mockk {
|
||||||
|
every { walletId } returns userWalletId
|
||||||
|
}
|
||||||
|
return ChooseTokenResult(currency = status, account = mockk(relaxed = true), wallet = wallet)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createModel(campaignType: CampaignType): ActivateCampaignsModel {
|
||||||
|
onCurrencyChosen = Channel(capacity = Channel.UNLIMITED)
|
||||||
|
val bridge = mockk<ChooseTokenBridge>(relaxed = true) {
|
||||||
|
every { onCurrencyChosen } returns this@ActivateCampaignsModelTest.onCurrencyChosen
|
||||||
|
every { onClose } returns Channel()
|
||||||
|
}
|
||||||
|
every { chooseTokenBridgeFactory.create(any(), any(), any()) } returns bridge
|
||||||
|
every { getSelectedAppCurrencyUseCase.invokeOrDefault() } returns flowOf(AppCurrency.Default)
|
||||||
|
coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns false
|
||||||
|
return ActivateCampaignsModel(
|
||||||
|
paramsContainer = MutableParamsContainer(
|
||||||
|
ActivateCampaignBottomSheetComponent.Params(
|
||||||
|
campaignType = campaignType,
|
||||||
|
modelCallbacks = modelCallbacks,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dispatchers = createTestingCoroutineDispatcherProvider(),
|
||||||
|
chooseTokenBridgeFactory = chooseTokenBridgeFactory,
|
||||||
|
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
|
||||||
|
isAccountsModeEnabledUseCase = isAccountsModeEnabledUseCase,
|
||||||
|
enrollPromoCampaignUseCase = enrollPromoCampaignUseCase,
|
||||||
|
urlOpener = urlOpener,
|
||||||
|
messageSender = messageSender,
|
||||||
|
analyticsEventHandler = analyticsEventHandler,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
|
||||||
|
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||||
|
return TestingCoroutineDispatcherProvider(
|
||||||
|
main = testDispatcher,
|
||||||
|
mainImmediate = testDispatcher,
|
||||||
|
io = testDispatcher,
|
||||||
|
default = testDispatcher,
|
||||||
|
single = testDispatcher,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override name/symbol so analytics assertions pin the symbol ("TTK"), not the name ("TEST_TOKEN").
|
||||||
|
private fun token(): CryptoCurrency.Token = cryptoCurrencyFactory
|
||||||
|
.createToken(blockchain = Blockchain.Ethereum, contractAddress = "0xToken")
|
||||||
|
.copy(name = "TEST_TOKEN", symbol = "TTK")
|
||||||
|
|
||||||
|
private fun coin(): CryptoCurrency.Coin = cryptoCurrencyFactory.ethereum
|
||||||
|
|
||||||
|
private fun provideCampaignTypes() = listOf(
|
||||||
|
CampaignType.WhaleSwapCashback(campaignId = "1"),
|
||||||
|
CampaignType.ReactivationCashback(campaignId = "2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val userWalletId = UserWalletId("0011223344556677")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
package com.tangem.features.promobanners.impl.campaigns.model
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import arrow.core.Either
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
|
import com.tangem.core.decompose.ui.UiMessageSender
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.promo.models.PromoCampaignId
|
||||||
|
import com.tangem.domain.promo.models.PromoCampaignState
|
||||||
|
import com.tangem.domain.promo.usecase.GetPromoCampaignStateUseCase
|
||||||
|
import com.tangem.features.promobanners.impl.campaigns.analytics.PromoCampaignsAnalyticsEvent
|
||||||
|
import com.tangem.features.promobanners.impl.campaigns.converters.CampaignIdConverter
|
||||||
|
import com.tangem.features.promobanners.impl.campaigns.entity.CampaignType
|
||||||
|
import com.tangem.features.promobanners.impl.campaigns.service.CampaignRequest
|
||||||
|
import com.tangem.features.promobanners.impl.campaigns.service.CampaignsService
|
||||||
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
|
import io.mockk.Called
|
||||||
|
import io.mockk.clearMocks
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
|
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
internal class CampaignsModelTest {
|
||||||
|
|
||||||
|
private val getPromoCampaignStateUseCase: GetPromoCampaignStateUseCase = mockk()
|
||||||
|
private val messageSender: UiMessageSender = mockk(relaxed = true)
|
||||||
|
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||||
|
|
||||||
|
// Real converter: "1" -> WhaleSwapCashback, "2" -> ReactivationCashback, else null.
|
||||||
|
private val campaignIdConverter = CampaignIdConverter()
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
clearMocks(getPromoCampaignStateUseCase, messageSender, analyticsEventHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideKnownCampaignModels")
|
||||||
|
fun `GIVEN known campaign request WHEN emitted THEN campaign state is checked with mapped id`(
|
||||||
|
model: KnownCampaignModel,
|
||||||
|
) = runTest {
|
||||||
|
// Arrange
|
||||||
|
coEvery { getPromoCampaignStateUseCase.invoke(any(), any(), any()) } returns
|
||||||
|
Either.Right(PromoCampaignState.NotActive(model.expectedPromoId))
|
||||||
|
val campaignsModel = createModel(
|
||||||
|
campaignFlow = flowOf(CampaignRequest(campaignId = model.campaignId, userWalletId = userWalletId)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
coVerify(exactly = 1) {
|
||||||
|
getPromoCampaignStateUseCase.invoke(
|
||||||
|
campaign = model.expectedPromoId,
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
forceRefresh = any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
verify { messageSender wasNot Called }
|
||||||
|
campaignsModel.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN campaign state fails WHEN emitted THEN error message is sent`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
coEvery { getPromoCampaignStateUseCase.invoke(any(), any(), any()) } returns
|
||||||
|
Either.Left(RuntimeException("network"))
|
||||||
|
val model = createModel(campaignFlow = flowOf(CampaignRequest(campaignId = "1", userWalletId = userWalletId)))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
verify(exactly = 1) { messageSender.send(any()) }
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN unknown campaign id WHEN emitted THEN campaign state is not checked`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val model = createModel(campaignFlow = flowOf(CampaignRequest(campaignId = "unknown", userWalletId = userWalletId)))
|
||||||
|
|
||||||
|
// Act
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
coVerify(exactly = 0) { getPromoCampaignStateUseCase.invoke(any(), any(), any()) }
|
||||||
|
verify { messageSender wasNot Called }
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN footer height set WHEN onAlreadyActivated THEN analytics sent and height reset`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val model = createModel(campaignFlow = emptyFlow())
|
||||||
|
advanceUntilIdle()
|
||||||
|
model.onFooterExtraHeightReady(100.dp)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
model.onAlreadyActivated(CampaignType.WhaleSwapCashback(campaignId = "1"))
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
verify(exactly = 1) {
|
||||||
|
analyticsEventHandler.send(PromoCampaignsAnalyticsEvent.AlreadyEnrolledScreenOpened())
|
||||||
|
}
|
||||||
|
assertThat(model.footerExtraHeightState.value).isEqualTo(0.dp)
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN footer height set WHEN onActivated THEN no analytics and height reset`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val model = createModel(campaignFlow = emptyFlow())
|
||||||
|
advanceUntilIdle()
|
||||||
|
model.onFooterExtraHeightReady(100.dp)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
model.onActivated(CampaignType.WhaleSwapCashback(campaignId = "1"))
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
verify { analyticsEventHandler wasNot Called }
|
||||||
|
assertThat(model.footerExtraHeightState.value).isEqualTo(0.dp)
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `WHEN onFooterExtraHeightReady THEN height state is updated`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val model = createModel(campaignFlow = emptyFlow())
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
model.onFooterExtraHeightReady(42.dp)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(model.footerExtraHeightState.value).isEqualTo(42.dp)
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createModel(campaignFlow: Flow<CampaignRequest>): CampaignsModel {
|
||||||
|
val campaignsService: CampaignsService = mockk {
|
||||||
|
every { this@mockk.campaignFlow } returns campaignFlow
|
||||||
|
}
|
||||||
|
return CampaignsModel(
|
||||||
|
dispatchers = createTestingCoroutineDispatcherProvider(),
|
||||||
|
campaignIdConverter = campaignIdConverter,
|
||||||
|
campaignsService = campaignsService,
|
||||||
|
getPromoCampaignStateUseCase = getPromoCampaignStateUseCase,
|
||||||
|
messageSender = messageSender,
|
||||||
|
analyticsEventHandler = analyticsEventHandler,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
|
||||||
|
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||||
|
return TestingCoroutineDispatcherProvider(
|
||||||
|
main = testDispatcher,
|
||||||
|
mainImmediate = testDispatcher,
|
||||||
|
io = testDispatcher,
|
||||||
|
default = testDispatcher,
|
||||||
|
single = testDispatcher,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun provideKnownCampaignModels() = listOf(
|
||||||
|
KnownCampaignModel(campaignId = "1", expectedPromoId = PromoCampaignId.WhaleSwapCashback),
|
||||||
|
KnownCampaignModel(campaignId = "2", expectedPromoId = PromoCampaignId.ReactivationCashback),
|
||||||
|
)
|
||||||
|
|
||||||
|
internal data class KnownCampaignModel(
|
||||||
|
val campaignId: String,
|
||||||
|
val expectedPromoId: PromoCampaignId,
|
||||||
|
) {
|
||||||
|
override fun toString(): String = "\"$campaignId\" -> $expectedPromoId"
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val userWalletId = UserWalletId("0011223344556677")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue