Updated on 2026-08-14
This commit is contained in:
parent
2b943f239f
commit
49f24cf911
14 changed files with 132 additions and 117 deletions
|
|
@ -14,5 +14,7 @@ data class CreatePromotionRegistrationBody(
|
||||||
data class TokenRewardDto(
|
data class TokenRewardDto(
|
||||||
@Json(name = "tokenAddress") val tokenAddress: String,
|
@Json(name = "tokenAddress") val tokenAddress: String,
|
||||||
@Json(name = "networkId") val networkId: String,
|
@Json(name = "networkId") val networkId: String,
|
||||||
|
@Json(name = "userAddress") val userAddress: String,
|
||||||
|
@Json(name = "tokenId") val tokenId: String,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -14,6 +14,13 @@ data class PromotionRegistrationResponse(
|
||||||
data class RegistrationData(
|
data class RegistrationData(
|
||||||
@Json(name = "campaignId") val campaignId: String,
|
@Json(name = "campaignId") val campaignId: String,
|
||||||
@Json(name = "registeredAt") val registeredAt: String?,
|
@Json(name = "registeredAt") val registeredAt: String?,
|
||||||
@Json(name = "tokenReward") val tokenReward: CreatePromotionRegistrationBody.TokenRewardDto,
|
@Json(name = "tokenReward") val tokenReward: RegisteredTokenRewardDto,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class RegisteredTokenRewardDto(
|
||||||
|
@Json(name = "tokenAddress") val tokenAddress: String,
|
||||||
|
@Json(name = "networkId") val networkId: String,
|
||||||
|
@Json(name = "tokenId") val tokenId: String,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -129,8 +129,6 @@ object PreferencesKeys {
|
||||||
|
|
||||||
val PENDING_ASSETS_DISCOVERY_KEY by lazy { stringPreferencesKey(name = "pendingAssetsDiscovery") }
|
val PENDING_ASSETS_DISCOVERY_KEY by lazy { stringPreferencesKey(name = "pendingAssetsDiscovery") }
|
||||||
|
|
||||||
val PROMO_ENROLLMENTS_KEY by lazy { stringPreferencesKey(name = "promoEnrollments") }
|
|
||||||
|
|
||||||
// region Notifications
|
// region Notifications
|
||||||
val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") }
|
val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package com.tangem.data.promo
|
||||||
|
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.tangem.data.promo.converter.PromoCampaignConverter
|
import com.tangem.data.promo.converter.PromoCampaignConverter
|
||||||
import com.tangem.data.promo.store.PromoEnrollmentStore
|
|
||||||
import com.tangem.datasource.api.common.response.ApiResponse
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||||
import com.tangem.datasource.api.promotion.models.CreatePromotionRegistrationBody
|
import com.tangem.datasource.api.promotion.models.CreatePromotionRegistrationBody
|
||||||
|
|
@ -12,6 +11,7 @@ import com.tangem.datasource.local.promotion.PromotionsSupplier
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.promo.PromoRepository
|
import com.tangem.domain.promo.PromoRepository
|
||||||
import com.tangem.domain.promo.models.EnrollResult
|
import com.tangem.domain.promo.models.EnrollResult
|
||||||
|
import com.tangem.domain.promo.models.EnrolledTokenReward
|
||||||
import com.tangem.domain.promo.models.PromoCampaignId
|
import com.tangem.domain.promo.models.PromoCampaignId
|
||||||
import com.tangem.domain.promo.models.PromoCampaignState
|
import com.tangem.domain.promo.models.PromoCampaignState
|
||||||
import com.tangem.domain.promo.models.TokenReward
|
import com.tangem.domain.promo.models.TokenReward
|
||||||
|
|
@ -21,7 +21,6 @@ import kotlinx.coroutines.withContext
|
||||||
internal class DefaultPromoRepository(
|
internal class DefaultPromoRepository(
|
||||||
private val promotionsSupplier: PromotionsSupplier,
|
private val promotionsSupplier: PromotionsSupplier,
|
||||||
private val tangemApi: TangemTechApi,
|
private val tangemApi: TangemTechApi,
|
||||||
private val enrollmentStore: PromoEnrollmentStore,
|
|
||||||
private val moshi: Moshi,
|
private val moshi: Moshi,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
) : PromoRepository {
|
) : PromoRepository {
|
||||||
|
|
@ -31,9 +30,6 @@ internal class DefaultPromoRepository(
|
||||||
userWalletId: UserWalletId,
|
userWalletId: UserWalletId,
|
||||||
forceRefresh: Boolean,
|
forceRefresh: Boolean,
|
||||||
): PromoCampaignState = withContext(dispatchers.io) {
|
): PromoCampaignState = withContext(dispatchers.io) {
|
||||||
enrollmentStore.getSyncOrNull(campaign)?.let {
|
|
||||||
return@withContext PromoCampaignState.Enrolled(campaign, it)
|
|
||||||
}
|
|
||||||
val all = promotionsSupplier.getPromotions(userWalletId, forceRefresh)
|
val all = promotionsSupplier.getPromotions(userWalletId, forceRefresh)
|
||||||
.promotions.firstOrNull { it.name == campaign.slug }?.all
|
.promotions.firstOrNull { it.name == campaign.slug }?.all
|
||||||
when {
|
when {
|
||||||
|
|
@ -56,7 +52,6 @@ internal class DefaultPromoRepository(
|
||||||
when (val response = tangemApi.createPromotionRegistration(body)) {
|
when (val response = tangemApi.createPromotionRegistration(body)) {
|
||||||
is ApiResponse.Success -> {
|
is ApiResponse.Success -> {
|
||||||
val saved = response.data.data.tokenReward.toDomain()
|
val saved = response.data.data.tokenReward.toDomain()
|
||||||
enrollmentStore.store(campaign, saved)
|
|
||||||
EnrollResult.Success(saved)
|
EnrollResult.Success(saved)
|
||||||
}
|
}
|
||||||
is ApiResponse.Error -> {
|
is ApiResponse.Error -> {
|
||||||
|
|
@ -64,8 +59,10 @@ internal class DefaultPromoRepository(
|
||||||
val conflict = (cause as? ApiResponseError.HttpException)
|
val conflict = (cause as? ApiResponseError.HttpException)
|
||||||
?.takeIf { it.code == ApiResponseError.HttpException.Code.CONFLICT }
|
?.takeIf { it.code == ApiResponseError.HttpException.Code.CONFLICT }
|
||||||
if (conflict != null) {
|
if (conflict != null) {
|
||||||
val existing = parseConflict(conflict.errorBody)?.data?.tokenReward?.toDomain() ?: tokenReward
|
val existing = parseConflict(conflict.errorBody)?.data
|
||||||
enrollmentStore.store(campaign, existing)
|
?.tokenReward
|
||||||
|
?.toDomain()
|
||||||
|
?: tokenReward.toEnrolledTokenReward()
|
||||||
EnrollResult.AlreadyEnrolled(existing)
|
EnrollResult.AlreadyEnrolled(existing)
|
||||||
} else {
|
} else {
|
||||||
throw cause
|
throw cause
|
||||||
|
|
@ -84,11 +81,20 @@ internal class DefaultPromoRepository(
|
||||||
private fun TokenReward.toDto() = CreatePromotionRegistrationBody.TokenRewardDto(
|
private fun TokenReward.toDto() = CreatePromotionRegistrationBody.TokenRewardDto(
|
||||||
tokenAddress = tokenAddress,
|
tokenAddress = tokenAddress,
|
||||||
networkId = networkId,
|
networkId = networkId,
|
||||||
|
userAddress = userAddress,
|
||||||
|
tokenId = tokenId,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun CreatePromotionRegistrationBody.TokenRewardDto.toDomain() = TokenReward(
|
private fun TokenReward.toEnrolledTokenReward() = EnrolledTokenReward(
|
||||||
tokenAddress = tokenAddress,
|
tokenAddress = tokenAddress,
|
||||||
networkId = networkId,
|
networkId = networkId,
|
||||||
|
tokenId = tokenId,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun PromotionRegistrationResponse.RegisteredTokenRewardDto.toDomain() = EnrolledTokenReward(
|
||||||
|
tokenAddress = tokenAddress,
|
||||||
|
networkId = networkId,
|
||||||
|
tokenId = tokenId,
|
||||||
)
|
)
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,8 @@ package com.tangem.data.promo.di
|
||||||
|
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.tangem.data.promo.DefaultPromoRepository
|
import com.tangem.data.promo.DefaultPromoRepository
|
||||||
import com.tangem.data.promo.store.DefaultPromoEnrollmentStore
|
|
||||||
import com.tangem.data.promo.store.PromoEnrollmentStore
|
|
||||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
import com.tangem.datasource.di.NetworkMoshi
|
import com.tangem.datasource.di.NetworkMoshi
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
|
||||||
import com.tangem.datasource.local.promotion.PromotionsSupplier
|
import com.tangem.datasource.local.promotion.PromotionsSupplier
|
||||||
import com.tangem.domain.promo.PromoRepository
|
import com.tangem.domain.promo.PromoRepository
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
|
@ -20,25 +17,17 @@ import javax.inject.Singleton
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object PromoDataModule {
|
object PromoDataModule {
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun providePromoEnrollmentStore(appPreferencesStore: AppPreferencesStore): PromoEnrollmentStore {
|
|
||||||
return DefaultPromoEnrollmentStore(appPreferencesStore)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providePromoRepository(
|
fun providePromoRepository(
|
||||||
promotionsSupplier: PromotionsSupplier,
|
promotionsSupplier: PromotionsSupplier,
|
||||||
tangemApi: TangemTechApi,
|
tangemApi: TangemTechApi,
|
||||||
enrollmentStore: PromoEnrollmentStore,
|
|
||||||
@NetworkMoshi moshi: Moshi,
|
@NetworkMoshi moshi: Moshi,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
): PromoRepository {
|
): PromoRepository {
|
||||||
return DefaultPromoRepository(
|
return DefaultPromoRepository(
|
||||||
promotionsSupplier = promotionsSupplier,
|
promotionsSupplier = promotionsSupplier,
|
||||||
tangemApi = tangemApi,
|
tangemApi = tangemApi,
|
||||||
enrollmentStore = enrollmentStore,
|
|
||||||
moshi = moshi,
|
moshi = moshi,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
package com.tangem.data.promo.store
|
|
||||||
|
|
||||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
|
||||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
|
||||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
|
||||||
import com.tangem.domain.promo.models.PromoCampaignId
|
|
||||||
import com.tangem.domain.promo.models.TokenReward
|
|
||||||
|
|
||||||
internal class DefaultPromoEnrollmentStore(
|
|
||||||
private val appPreferencesStore: AppPreferencesStore,
|
|
||||||
) : PromoEnrollmentStore {
|
|
||||||
|
|
||||||
override suspend fun getSyncOrNull(campaign: PromoCampaignId): TokenReward? {
|
|
||||||
return appPreferencesStore
|
|
||||||
.getObjectMapSync<TokenReward>(PreferencesKeys.PROMO_ENROLLMENTS_KEY)[campaign.slug]
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun store(campaign: PromoCampaignId, tokenReward: TokenReward) {
|
|
||||||
appPreferencesStore.editData { mutablePreferences ->
|
|
||||||
val current = mutablePreferences.getObjectMap<TokenReward>(PreferencesKeys.PROMO_ENROLLMENTS_KEY)
|
|
||||||
mutablePreferences.setObjectMap(
|
|
||||||
key = PreferencesKeys.PROMO_ENROLLMENTS_KEY,
|
|
||||||
value = current + (campaign.slug to tokenReward),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
package com.tangem.data.promo.store
|
|
||||||
|
|
||||||
import com.tangem.domain.promo.models.PromoCampaignId
|
|
||||||
import com.tangem.domain.promo.models.TokenReward
|
|
||||||
|
|
||||||
interface PromoEnrollmentStore {
|
|
||||||
|
|
||||||
suspend fun getSyncOrNull(campaign: PromoCampaignId): TokenReward?
|
|
||||||
|
|
||||||
suspend fun store(campaign: PromoCampaignId, tokenReward: TokenReward)
|
|
||||||
}
|
|
||||||
|
|
@ -3,10 +3,8 @@ package com.tangem.data.promo
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||||
import com.tangem.data.promo.store.PromoEnrollmentStore
|
|
||||||
import com.tangem.datasource.api.common.response.ApiResponse
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||||
import com.tangem.datasource.api.promotion.models.CreatePromotionRegistrationBody
|
|
||||||
import com.tangem.datasource.api.promotion.models.PromotionRegistrationResponse
|
import com.tangem.datasource.api.promotion.models.PromotionRegistrationResponse
|
||||||
import com.tangem.datasource.api.promotion.models.PromotionsResponse
|
import com.tangem.datasource.api.promotion.models.PromotionsResponse
|
||||||
import com.tangem.datasource.api.promotion.models.PromotionsResponse.PromotionDto
|
import com.tangem.datasource.api.promotion.models.PromotionsResponse.PromotionDto
|
||||||
|
|
@ -17,13 +15,13 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||||
import com.tangem.datasource.local.promotion.PromotionsSupplier
|
import com.tangem.datasource.local.promotion.PromotionsSupplier
|
||||||
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.EnrolledTokenReward
|
||||||
import com.tangem.domain.promo.models.PromoCampaignId
|
import com.tangem.domain.promo.models.PromoCampaignId
|
||||||
import com.tangem.domain.promo.models.PromoCampaignState
|
import com.tangem.domain.promo.models.PromoCampaignState
|
||||||
import com.tangem.domain.promo.models.TokenReward
|
import com.tangem.domain.promo.models.TokenReward
|
||||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
import io.mockk.clearMocks
|
import io.mockk.clearMocks
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
|
@ -35,20 +33,21 @@ internal class DefaultPromoRepositoryTest {
|
||||||
|
|
||||||
private val promotionsSupplier: PromotionsSupplier = mockk()
|
private val promotionsSupplier: PromotionsSupplier = mockk()
|
||||||
private val tangemApi: TangemTechApi = mockk()
|
private val tangemApi: TangemTechApi = mockk()
|
||||||
private val enrollmentStore: PromoEnrollmentStore = mockk(relaxed = true)
|
|
||||||
private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
private val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
||||||
|
|
||||||
private val repository = DefaultPromoRepository(
|
private val repository = DefaultPromoRepository(
|
||||||
promotionsSupplier = promotionsSupplier,
|
promotionsSupplier = promotionsSupplier,
|
||||||
tangemApi = tangemApi,
|
tangemApi = tangemApi,
|
||||||
enrollmentStore = enrollmentStore,
|
|
||||||
moshi = moshi,
|
moshi = moshi,
|
||||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val campaign = PromoCampaignId.WhaleSwapCashback
|
private val campaign = PromoCampaignId.WhaleSwapCashback
|
||||||
private val userWalletId = UserWalletId("abcdef012345")
|
private val userWalletId = UserWalletId("abcdef012345")
|
||||||
private val tokenReward = TokenReward("0xToken", "ethereum")
|
private val tokenReward = TokenReward("0xToken", "ethereum", "0xUser", "tether")
|
||||||
|
|
||||||
|
// The enroll result drops userAddress — this is what the submitted tokenReward collapses to.
|
||||||
|
private val resultTokenReward = EnrolledTokenReward("0xToken", "ethereum", "tether")
|
||||||
|
|
||||||
private fun activeDto() = PromotionDto(
|
private fun activeDto() = PromotionDto(
|
||||||
name = campaign.slug,
|
name = campaign.slug,
|
||||||
|
|
@ -70,25 +69,11 @@ internal class DefaultPromoRepositoryTest {
|
||||||
)
|
)
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setUp() = clearMocks(promotionsSupplier, tangemApi, enrollmentStore)
|
fun setUp() = clearMocks(promotionsSupplier, tangemApi)
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN locally enrolled WHEN getCampaignState THEN Enrolled without api`() = runTest {
|
|
||||||
// Arrange
|
|
||||||
coEvery { enrollmentStore.getSyncOrNull(campaign) } returns tokenReward
|
|
||||||
|
|
||||||
// Act
|
|
||||||
val result = repository.getCampaignState(campaign, userWalletId)
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertThat(result).isEqualTo(PromoCampaignState.Enrolled(campaign, tokenReward))
|
|
||||||
coVerify(exactly = 0) { promotionsSupplier.getPromotions(any(), any()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN active campaign present and not enrolled WHEN getCampaignState THEN Available`() = runTest {
|
fun `GIVEN active campaign present and not enrolled WHEN getCampaignState THEN Available`() = runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
coEvery { enrollmentStore.getSyncOrNull(campaign) } returns null
|
|
||||||
coEvery { promotionsSupplier.getPromotions(userWalletId, any()) } returns
|
coEvery { promotionsSupplier.getPromotions(userWalletId, any()) } returns
|
||||||
PromotionsResponse(promotions = listOf(activeDto()))
|
PromotionsResponse(promotions = listOf(activeDto()))
|
||||||
|
|
||||||
|
|
@ -102,7 +87,6 @@ internal class DefaultPromoRepositoryTest {
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN campaign absent WHEN getCampaignState THEN NotActive`() = runTest {
|
fun `GIVEN campaign absent WHEN getCampaignState THEN NotActive`() = runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
coEvery { enrollmentStore.getSyncOrNull(campaign) } returns null
|
|
||||||
coEvery { promotionsSupplier.getPromotions(userWalletId, any()) } returns
|
coEvery { promotionsSupplier.getPromotions(userWalletId, any()) } returns
|
||||||
PromotionsResponse(promotions = emptyList())
|
PromotionsResponse(promotions = emptyList())
|
||||||
|
|
||||||
|
|
@ -116,7 +100,6 @@ internal class DefaultPromoRepositoryTest {
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN campaign present but finished WHEN getCampaignState THEN NotActive`() = runTest {
|
fun `GIVEN campaign present but finished WHEN getCampaignState THEN NotActive`() = runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
coEvery { enrollmentStore.getSyncOrNull(campaign) } returns null
|
|
||||||
val finished = activeDto().copy(all = activeDto().all!!.copy(status = "finished"))
|
val finished = activeDto().copy(all = activeDto().all!!.copy(status = "finished"))
|
||||||
coEvery { promotionsSupplier.getPromotions(userWalletId, any()) } returns
|
coEvery { promotionsSupplier.getPromotions(userWalletId, any()) } returns
|
||||||
PromotionsResponse(promotions = listOf(finished))
|
PromotionsResponse(promotions = listOf(finished))
|
||||||
|
|
@ -129,12 +112,16 @@ internal class DefaultPromoRepositoryTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN api returns 201 with canonical token WHEN enroll THEN Success and persists backend token`() = runTest {
|
fun `GIVEN api returns 201 with canonical token WHEN enroll THEN Success with backend token`() = runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
val data = PromotionRegistrationResponse.RegistrationData(
|
val data = PromotionRegistrationResponse.RegistrationData(
|
||||||
campaignId = campaign.slug,
|
campaignId = campaign.slug,
|
||||||
registeredAt = "2026-07-06T09:27:13.363Z",
|
registeredAt = "2026-07-06T09:27:13.363Z",
|
||||||
tokenReward = CreatePromotionRegistrationBody.TokenRewardDto("0xCanonical", "ethereum"),
|
tokenReward = PromotionRegistrationResponse.RegisteredTokenRewardDto(
|
||||||
|
tokenAddress = "0xCanonical",
|
||||||
|
networkId = "ethereum",
|
||||||
|
tokenId = "tether",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
coEvery { tangemApi.createPromotionRegistration(any()) } returns ApiResponse.Success(
|
coEvery { tangemApi.createPromotionRegistration(any()) } returns ApiResponse.Success(
|
||||||
PromotionRegistrationResponse(status = "saved", message = null, data = data),
|
PromotionRegistrationResponse(status = "saved", message = null, data = data),
|
||||||
|
|
@ -144,9 +131,8 @@ internal class DefaultPromoRepositoryTest {
|
||||||
val result = repository.enroll(campaign, tokenReward, listOf(userWalletId))
|
val result = repository.enroll(campaign, tokenReward, listOf(userWalletId))
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
val backendToken = TokenReward("0xCanonical", "ethereum")
|
val backendToken = EnrolledTokenReward("0xCanonical", "ethereum", "tether")
|
||||||
assertThat(result).isEqualTo(EnrollResult.Success(backendToken))
|
assertThat(result).isEqualTo(EnrollResult.Success(backendToken))
|
||||||
coVerify(exactly = 1) { enrollmentStore.store(campaign, backendToken) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -155,7 +141,7 @@ internal class DefaultPromoRepositoryTest {
|
||||||
val existing = """
|
val existing = """
|
||||||
{"status":"already_exists","message":"exists","data":{"campaignId":"${campaign.slug}",
|
{"status":"already_exists","message":"exists","data":{"campaignId":"${campaign.slug}",
|
||||||
"registeredAt":"2026-07-01T10:00:00.000Z","tokenReward":{"tokenAddress":"0xOther",
|
"registeredAt":"2026-07-01T10:00:00.000Z","tokenReward":{"tokenAddress":"0xOther",
|
||||||
"networkId":"base","userAddress":"0xExisting"}}}
|
"networkId":"base","userAddress":"0xExisting","tokenId":"usd-coin"}}}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
coEvery { tangemApi.createPromotionRegistration(any()) } returns ApiResponse.Error(
|
coEvery { tangemApi.createPromotionRegistration(any()) } returns ApiResponse.Error(
|
||||||
|
|
@ -170,9 +156,8 @@ internal class DefaultPromoRepositoryTest {
|
||||||
val result = repository.enroll(campaign, tokenReward, listOf(userWalletId))
|
val result = repository.enroll(campaign, tokenReward, listOf(userWalletId))
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
val expectedToken = TokenReward("0xOther", "base")
|
val expectedToken = EnrolledTokenReward("0xOther", "base", "usd-coin")
|
||||||
assertThat(result).isEqualTo(EnrollResult.AlreadyEnrolled(expectedToken))
|
assertThat(result).isEqualTo(EnrollResult.AlreadyEnrolled(expectedToken))
|
||||||
coVerify(exactly = 1) { enrollmentStore.store(campaign, expectedToken) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -191,8 +176,7 @@ internal class DefaultPromoRepositoryTest {
|
||||||
val result = repository.enroll(campaign, tokenReward, listOf(userWalletId))
|
val result = repository.enroll(campaign, tokenReward, listOf(userWalletId))
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertThat(result).isEqualTo(EnrollResult.AlreadyEnrolled(tokenReward))
|
assertThat(result).isEqualTo(EnrollResult.AlreadyEnrolled(resultTokenReward))
|
||||||
coVerify(exactly = 1) { enrollmentStore.store(campaign, tokenReward) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -212,6 +196,5 @@ internal class DefaultPromoRepositoryTest {
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertThat(error).isNotNull()
|
assertThat(error).isNotNull()
|
||||||
coVerify(exactly = 0) { enrollmentStore.store(any(), any()) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -10,11 +10,6 @@ sealed interface PromoCampaignState {
|
||||||
val timeline: PromoTimeline,
|
val timeline: PromoTimeline,
|
||||||
) : PromoCampaignState
|
) : PromoCampaignState
|
||||||
|
|
||||||
data class Enrolled(
|
|
||||||
override val campaign: PromoCampaignId,
|
|
||||||
val tokenReward: TokenReward,
|
|
||||||
) : PromoCampaignState
|
|
||||||
|
|
||||||
data class NotActive(
|
data class NotActive(
|
||||||
override val campaign: PromoCampaignId,
|
override val campaign: PromoCampaignId,
|
||||||
) : PromoCampaignState
|
) : PromoCampaignState
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,19 @@ data class PromoTimeline(
|
||||||
data class TokenReward(
|
data class TokenReward(
|
||||||
val tokenAddress: String,
|
val tokenAddress: String,
|
||||||
val networkId: String,
|
val networkId: String,
|
||||||
|
val userAddress: String,
|
||||||
|
val tokenId: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class EnrolledTokenReward(
|
||||||
|
val tokenAddress: String,
|
||||||
|
val networkId: String,
|
||||||
|
val tokenId: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed interface EnrollResult {
|
sealed interface EnrollResult {
|
||||||
val tokenReward: TokenReward
|
val tokenReward: EnrolledTokenReward
|
||||||
|
|
||||||
data class Success(override val tokenReward: TokenReward) : EnrollResult
|
data class Success(override val tokenReward: EnrolledTokenReward) : EnrollResult
|
||||||
data class AlreadyEnrolled(override val tokenReward: TokenReward) : EnrollResult
|
data class AlreadyEnrolled(override val tokenReward: EnrolledTokenReward) : EnrollResult
|
||||||
}
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.promo.PromoRepository
|
import com.tangem.domain.promo.PromoRepository
|
||||||
import com.tangem.domain.promo.models.EnrollResult
|
import com.tangem.domain.promo.models.EnrollResult
|
||||||
|
import com.tangem.domain.promo.models.EnrolledTokenReward
|
||||||
import com.tangem.domain.promo.models.PromoCampaignId
|
import com.tangem.domain.promo.models.PromoCampaignId
|
||||||
import com.tangem.domain.promo.models.TokenReward
|
import com.tangem.domain.promo.models.TokenReward
|
||||||
import com.tangem.test.core.assertEitherLeft
|
import com.tangem.test.core.assertEitherLeft
|
||||||
|
|
@ -24,7 +25,8 @@ internal class EnrollPromoCampaignUseCaseTest {
|
||||||
|
|
||||||
private val campaign = PromoCampaignId.WhaleSwapCashback
|
private val campaign = PromoCampaignId.WhaleSwapCashback
|
||||||
private val walletIds = listOf(UserWalletId("abcdef012345"))
|
private val walletIds = listOf(UserWalletId("abcdef012345"))
|
||||||
private val tokenReward = TokenReward("0xToken", "ethereum")
|
private val tokenReward = TokenReward("0xToken", "ethereum", "0xUser", "tether")
|
||||||
|
private val resultTokenReward = EnrolledTokenReward("0xToken", "ethereum", "tether")
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setUp() = clearMocks(repository)
|
fun setUp() = clearMocks(repository)
|
||||||
|
|
@ -32,7 +34,7 @@ internal class EnrollPromoCampaignUseCaseTest {
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN repo returns Success WHEN invoke THEN Right Success`() = runTest {
|
fun `GIVEN repo returns Success WHEN invoke THEN Right Success`() = runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
val expected = EnrollResult.Success(tokenReward)
|
val expected = EnrollResult.Success(resultTokenReward)
|
||||||
coEvery { repository.enroll(campaign, tokenReward, walletIds) } returns expected
|
coEvery { repository.enroll(campaign, tokenReward, walletIds) } returns expected
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,14 @@ 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.wallet.UserWalletId
|
import com.tangem.domain.models.network.NetworkAddress
|
||||||
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
|
||||||
import com.tangem.domain.promo.models.PromoCampaignState
|
import com.tangem.domain.promo.models.PromoCampaignState
|
||||||
import com.tangem.domain.promo.models.TokenReward
|
import com.tangem.domain.promo.models.TokenReward
|
||||||
import com.tangem.domain.promo.usecase.EnrollPromoCampaignUseCase
|
import com.tangem.domain.promo.usecase.EnrollPromoCampaignUseCase
|
||||||
import com.tangem.domain.promo.usecase.GetPromoCampaignStateUseCase
|
import com.tangem.domain.promo.usecase.GetPromoCampaignStateUseCase
|
||||||
|
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||||
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.commonfeatures.api.choosetoken.ChooserBlock
|
import com.tangem.features.commonfeatures.api.choosetoken.ChooserBlock
|
||||||
|
|
@ -42,6 +43,7 @@ import com.tangem.features.promobanners.impl.campaigns.entity.TermsUM
|
||||||
import com.tangem.features.promobanners.impl.campaigns.entity.toPromoCampaignId
|
import com.tangem.features.promobanners.impl.campaigns.entity.toPromoCampaignId
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import com.tangem.utils.logging.TangemLogger
|
import com.tangem.utils.logging.TangemLogger
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -65,6 +67,7 @@ internal class ActivateCampaignsModel @Inject constructor(
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
private val getPromoCampaignStateUseCase: GetPromoCampaignStateUseCase,
|
private val getPromoCampaignStateUseCase: GetPromoCampaignStateUseCase,
|
||||||
private val predefinedTokenResolver: PredefinedTokenResolver,
|
private val predefinedTokenResolver: PredefinedTokenResolver,
|
||||||
|
private val getWalletsUseCase: GetWalletsUseCase,
|
||||||
) : Model() {
|
) : Model() {
|
||||||
|
|
||||||
private val params = paramsContainer.require<ActivateCampaignBottomSheetComponent.Params>()
|
private val params = paramsContainer.require<ActivateCampaignBottomSheetComponent.Params>()
|
||||||
|
|
@ -76,6 +79,8 @@ internal class ActivateCampaignsModel @Inject constructor(
|
||||||
|
|
||||||
private val predefinedTokensFlow = MutableStateFlow<List<PredefinedTokenToAdd>>(emptyList())
|
private val predefinedTokensFlow = MutableStateFlow<List<PredefinedTokenToAdd>>(emptyList())
|
||||||
|
|
||||||
|
private var enrollJob: Job? = null
|
||||||
|
|
||||||
val uiState: StateFlow<ActivateCampaignUM>
|
val uiState: StateFlow<ActivateCampaignUM>
|
||||||
field = MutableStateFlow(buildInitialModel())
|
field = MutableStateFlow(buildInitialModel())
|
||||||
|
|
||||||
|
|
@ -144,7 +149,9 @@ internal class ActivateCampaignsModel @Inject constructor(
|
||||||
uiState.update { it.copy(isChoosingToken = false) }
|
uiState.update { it.copy(isChoosingToken = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onEnrollClick(selectedWalletId: UserWalletId, selectedToken: CryptoCurrency.Token) {
|
private fun onEnrollClick(selectedToken: CryptoCurrency.Token, networkAddress: NetworkAddress) {
|
||||||
|
if (enrollJob?.isActive == true) return
|
||||||
|
|
||||||
analyticsEventHandler.send(
|
analyticsEventHandler.send(
|
||||||
PromoCampaignsAnalyticsEvent.EnrollButtonClicked(
|
PromoCampaignsAnalyticsEvent.EnrollButtonClicked(
|
||||||
campaignType = campaignType,
|
campaignType = campaignType,
|
||||||
|
|
@ -153,14 +160,16 @@ internal class ActivateCampaignsModel @Inject constructor(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
modelScope.launch {
|
enrollJob = modelScope.launch {
|
||||||
enrollPromoCampaignUseCase.invoke(
|
enrollPromoCampaignUseCase.invoke(
|
||||||
campaign = campaignId,
|
campaign = campaignId,
|
||||||
tokenReward = TokenReward(
|
tokenReward = TokenReward(
|
||||||
tokenAddress = selectedToken.contractAddress,
|
tokenAddress = selectedToken.contractAddress,
|
||||||
networkId = selectedToken.network.rawId,
|
networkId = selectedToken.network.rawId,
|
||||||
|
tokenId = selectedToken.id.rawCurrencyId?.value.orEmpty(),
|
||||||
|
userAddress = networkAddress.defaultAddress.value,
|
||||||
),
|
),
|
||||||
walletIds = listOf(selectedWalletId),
|
walletIds = getAllUserWalletIds(),
|
||||||
).onLeft { error ->
|
).onLeft { error ->
|
||||||
TangemLogger.e("Error enrolling campaign ${campaignType.campaignId}", error)
|
TangemLogger.e("Error enrolling campaign ${campaignType.campaignId}", error)
|
||||||
messageSender.send(ToastMessage(message = resourceReference(R.string.common_unknown_error)))
|
messageSender.send(ToastMessage(message = resourceReference(R.string.common_unknown_error)))
|
||||||
|
|
@ -170,6 +179,10 @@ internal class ActivateCampaignsModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getAllUserWalletIds() = getWalletsUseCase
|
||||||
|
.invokeSync()
|
||||||
|
.map { it.walletId }
|
||||||
|
|
||||||
private fun handleEnrollResponse(enrollResult: EnrollResult) {
|
private fun handleEnrollResponse(enrollResult: EnrollResult) {
|
||||||
when (enrollResult) {
|
when (enrollResult) {
|
||||||
is EnrollResult.AlreadyEnrolled -> params.modelCallbacks.onAlreadyActivated(campaignType)
|
is EnrollResult.AlreadyEnrolled -> params.modelCallbacks.onAlreadyActivated(campaignType)
|
||||||
|
|
@ -187,6 +200,7 @@ internal class ActivateCampaignsModel @Inject constructor(
|
||||||
|
|
||||||
private fun onTokenChosen(result: ChooseTokenResult) {
|
private fun onTokenChosen(result: ChooseTokenResult) {
|
||||||
val selectedToken = result.currency.currency as? CryptoCurrency.Token ?: return
|
val selectedToken = result.currency.currency as? CryptoCurrency.Token ?: return
|
||||||
|
val networkAddress = result.currency.value.networkAddress ?: return
|
||||||
|
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
val selectedAccountUM = if (isAccountsModeEnabledUseCase.invokeSync()) {
|
val selectedAccountUM = if (isAccountsModeEnabledUseCase.invokeSync()) {
|
||||||
|
|
@ -216,8 +230,8 @@ internal class ActivateCampaignsModel @Inject constructor(
|
||||||
label = resourceReference(R.string.promo_campaign_enroll),
|
label = resourceReference(R.string.promo_campaign_enroll),
|
||||||
onPrimaryButtonClick = {
|
onPrimaryButtonClick = {
|
||||||
onEnrollClick(
|
onEnrollClick(
|
||||||
selectedWalletId = result.walletId,
|
|
||||||
selectedToken = selectedToken,
|
selectedToken = selectedToken,
|
||||||
|
networkAddress = networkAddress,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
terms = TermsUM(
|
terms = TermsUM(
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,7 @@ internal class CampaignsModel @Inject constructor(
|
||||||
},
|
},
|
||||||
ifRight = { campaignState ->
|
ifRight = { campaignState ->
|
||||||
when (campaignState) {
|
when (campaignState) {
|
||||||
is PromoCampaignState.Enrolled,
|
is PromoCampaignState.Available -> CampaignsBottomSheetConfig.Activate(campaignType, userWalletId)
|
||||||
is PromoCampaignState.Available,
|
|
||||||
-> CampaignsBottomSheetConfig.Activate(campaignType, userWalletId)
|
|
||||||
is PromoCampaignState.NotActive -> CampaignsBottomSheetConfig.NotActive
|
is PromoCampaignState.NotActive -> CampaignsBottomSheetConfig.NotActive
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,16 @@ 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.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
|
import com.tangem.domain.models.network.NetworkAddress
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
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.EnrolledTokenReward
|
||||||
import com.tangem.domain.promo.models.PromoCampaignId
|
import com.tangem.domain.promo.models.PromoCampaignId
|
||||||
import com.tangem.domain.promo.models.TokenReward
|
import com.tangem.domain.promo.models.TokenReward
|
||||||
import com.tangem.domain.promo.usecase.EnrollPromoCampaignUseCase
|
import com.tangem.domain.promo.usecase.EnrollPromoCampaignUseCase
|
||||||
import com.tangem.domain.promo.usecase.GetPromoCampaignStateUseCase
|
import com.tangem.domain.promo.usecase.GetPromoCampaignStateUseCase
|
||||||
|
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||||
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.campaigns.analytics.PromoCampaignsAnalyticsEvent
|
import com.tangem.features.promobanners.impl.campaigns.analytics.PromoCampaignsAnalyticsEvent
|
||||||
|
|
@ -56,6 +59,7 @@ internal class ActivateCampaignsModelTest {
|
||||||
private val messageSender: UiMessageSender = mockk(relaxed = true)
|
private val messageSender: UiMessageSender = mockk(relaxed = true)
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||||
private val getPromoCampaignStateUseCase: GetPromoCampaignStateUseCase = mockk()
|
private val getPromoCampaignStateUseCase: GetPromoCampaignStateUseCase = mockk()
|
||||||
|
private val getWalletsUseCase: GetWalletsUseCase = mockk()
|
||||||
private val predefinedTokenResolver: PredefinedTokenResolver = mockk(relaxed = true)
|
private val predefinedTokenResolver: PredefinedTokenResolver = mockk(relaxed = true)
|
||||||
private val modelCallbacks: ActivateCampaignBottomSheetComponent.ActivateCampaignModelCallbacks =
|
private val modelCallbacks: ActivateCampaignBottomSheetComponent.ActivateCampaignModelCallbacks =
|
||||||
mockk(relaxed = true)
|
mockk(relaxed = true)
|
||||||
|
|
@ -70,6 +74,7 @@ internal class ActivateCampaignsModelTest {
|
||||||
getSelectedAppCurrencyUseCase,
|
getSelectedAppCurrencyUseCase,
|
||||||
isAccountsModeEnabledUseCase,
|
isAccountsModeEnabledUseCase,
|
||||||
enrollPromoCampaignUseCase,
|
enrollPromoCampaignUseCase,
|
||||||
|
getWalletsUseCase,
|
||||||
messageSender,
|
messageSender,
|
||||||
analyticsEventHandler,
|
analyticsEventHandler,
|
||||||
modelCallbacks,
|
modelCallbacks,
|
||||||
|
|
@ -128,7 +133,7 @@ internal class ActivateCampaignsModelTest {
|
||||||
val token = token()
|
val token = token()
|
||||||
val campaignType = CampaignType.WhaleSwapCashback(campaignId = "1")
|
val campaignType = CampaignType.WhaleSwapCashback(campaignId = "1")
|
||||||
coEvery { enrollPromoCampaignUseCase.invoke(any(), any(), any()) } returns
|
coEvery { enrollPromoCampaignUseCase.invoke(any(), any(), any()) } returns
|
||||||
Either.Right(EnrollResult.Success(TokenReward(tokenAddress = "a", networkId = "b")))
|
Either.Right(EnrollResult.Success(EnrolledTokenReward(tokenAddress = "a", networkId = "b", tokenId = "d")))
|
||||||
val model = createModel(campaignType)
|
val model = createModel(campaignType)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
|
@ -152,20 +157,45 @@ internal class ActivateCampaignsModelTest {
|
||||||
coVerify(exactly = 1) {
|
coVerify(exactly = 1) {
|
||||||
enrollPromoCampaignUseCase.invoke(
|
enrollPromoCampaignUseCase.invoke(
|
||||||
campaign = PromoCampaignId.WhaleSwapCashback,
|
campaign = PromoCampaignId.WhaleSwapCashback,
|
||||||
tokenReward = TokenReward(tokenAddress = token.contractAddress, networkId = token.network.rawId),
|
tokenReward = TokenReward(
|
||||||
walletIds = listOf(userWalletId),
|
tokenAddress = token.contractAddress,
|
||||||
|
networkId = token.network.rawId,
|
||||||
|
userAddress = userAddress,
|
||||||
|
tokenId = token.id.rawCurrencyId?.value.orEmpty(),
|
||||||
|
),
|
||||||
|
walletIds = allWalletIds,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
verify(exactly = 1) { modelCallbacks.onActivated(campaignType) }
|
verify(exactly = 1) { modelCallbacks.onActivated(campaignType) }
|
||||||
model.onDestroy()
|
model.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN enroll in progress WHEN enroll clicked again THEN use case invoked once`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
coEvery { enrollPromoCampaignUseCase.invoke(any(), any(), any()) } returns
|
||||||
|
Either.Right(EnrollResult.Success(EnrolledTokenReward(tokenAddress = "a", networkId = "b", tokenId = "d")))
|
||||||
|
val model = createModel(CampaignType.WhaleSwapCashback(campaignId = "1"))
|
||||||
|
advanceUntilIdle()
|
||||||
|
onCurrencyChosen.send(chooseTokenResult(currency = token()))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Act — click twice before the in-flight enroll coroutine gets a chance to run
|
||||||
|
model.uiState.value.footerUM.onPrimaryButtonClick()
|
||||||
|
model.uiState.value.footerUM.onPrimaryButtonClick()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Assert — the re-entrant click is ignored: enroll is triggered only once
|
||||||
|
coVerify(exactly = 1) { enrollPromoCampaignUseCase.invoke(any(), any(), any()) }
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `GIVEN enroll returns AlreadyEnrolled WHEN enroll clicked THEN onAlreadyActivated called`() = runTest {
|
fun `GIVEN enroll returns AlreadyEnrolled WHEN enroll clicked THEN onAlreadyActivated called`() = runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
val campaignType = CampaignType.WhaleSwapCashback(campaignId = "1")
|
val campaignType = CampaignType.WhaleSwapCashback(campaignId = "1")
|
||||||
coEvery { enrollPromoCampaignUseCase.invoke(any(), any(), any()) } returns
|
coEvery { enrollPromoCampaignUseCase.invoke(any(), any(), any()) } returns
|
||||||
Either.Right(EnrollResult.AlreadyEnrolled(TokenReward(tokenAddress = "a", networkId = "b")))
|
Either.Right(EnrollResult.AlreadyEnrolled(EnrolledTokenReward(tokenAddress = "a", networkId = "b", tokenId = "d")))
|
||||||
val model = createModel(campaignType)
|
val model = createModel(campaignType)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
|
@ -205,7 +235,14 @@ internal class ActivateCampaignsModelTest {
|
||||||
private fun chooseTokenResult(currency: CryptoCurrency): ChooseTokenResult {
|
private fun chooseTokenResult(currency: CryptoCurrency): ChooseTokenResult {
|
||||||
val status = CryptoCurrencyStatus(
|
val status = CryptoCurrencyStatus(
|
||||||
currency = currency,
|
currency = currency,
|
||||||
value = CryptoCurrencyStatus.MissedDerivation(priceChange = null, fiatRate = null),
|
// Must carry a networkAddress: the model resolves userAddress from it and otherwise drops the token.
|
||||||
|
value = CryptoCurrencyStatus.Unreachable(
|
||||||
|
priceChange = null,
|
||||||
|
fiatRate = null,
|
||||||
|
networkAddress = NetworkAddress.Single(
|
||||||
|
NetworkAddress.Address(value = userAddress, type = NetworkAddress.Address.Type.Primary),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
val wallet: UserWallet = mockk {
|
val wallet: UserWallet = mockk {
|
||||||
every { walletId } returns userWalletId
|
every { walletId } returns userWalletId
|
||||||
|
|
@ -223,6 +260,9 @@ internal class ActivateCampaignsModelTest {
|
||||||
every { getSelectedAppCurrencyUseCase.invokeOrDefault() } returns flowOf(AppCurrency.Default)
|
every { getSelectedAppCurrencyUseCase.invokeOrDefault() } returns flowOf(AppCurrency.Default)
|
||||||
coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns false
|
coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns false
|
||||||
coEvery { getPromoCampaignStateUseCase(any(), any(), any()) } returns Either.Left(Throwable())
|
coEvery { getPromoCampaignStateUseCase(any(), any(), any()) } returns Either.Left(Throwable())
|
||||||
|
every { getWalletsUseCase.invokeSync() } returns allWalletIds.map { walletId ->
|
||||||
|
mockk<UserWallet> { every { this@mockk.walletId } returns walletId }
|
||||||
|
}
|
||||||
return ActivateCampaignsModel(
|
return ActivateCampaignsModel(
|
||||||
paramsContainer = MutableParamsContainer(
|
paramsContainer = MutableParamsContainer(
|
||||||
ActivateCampaignBottomSheetComponent.Params(
|
ActivateCampaignBottomSheetComponent.Params(
|
||||||
|
|
@ -241,6 +281,7 @@ internal class ActivateCampaignsModelTest {
|
||||||
analyticsEventHandler = analyticsEventHandler,
|
analyticsEventHandler = analyticsEventHandler,
|
||||||
getPromoCampaignStateUseCase = getPromoCampaignStateUseCase,
|
getPromoCampaignStateUseCase = getPromoCampaignStateUseCase,
|
||||||
predefinedTokenResolver = predefinedTokenResolver,
|
predefinedTokenResolver = predefinedTokenResolver,
|
||||||
|
getWalletsUseCase = getWalletsUseCase,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,5 +310,15 @@ internal class ActivateCampaignsModelTest {
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
val userWalletId = UserWalletId("0011223344556677")
|
val userWalletId = UserWalletId("0011223344556677")
|
||||||
|
|
||||||
|
// The user's payout address the model resolves from the chosen token's networkAddress.
|
||||||
|
const val userAddress = "0xUserPayoutAddress"
|
||||||
|
|
||||||
|
// Enrollment must target ALL user wallets ([REDACTED_TASK_KEY]), not only the currently selected one.
|
||||||
|
val allWalletIds = listOf(
|
||||||
|
userWalletId,
|
||||||
|
UserWalletId("8899aabbccddeeff"),
|
||||||
|
UserWalletId("a1b2c3d4e5f60718"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue