Updated on 2026-08-14
This commit is contained in:
parent
4e05219c25
commit
fd2fcc4735
22 changed files with 1114 additions and 4 deletions
|
|
@ -231,6 +231,8 @@ dependencies {
|
|||
implementation(projects.common.ui)
|
||||
|
||||
/** Features */
|
||||
implementation(projects.features.addressBook.api)
|
||||
implementation(projects.features.addressBook.impl)
|
||||
implementation(projects.features.rating.impl)
|
||||
implementation(projects.features.referral.impl)
|
||||
implementation(projects.features.referral.domain)
|
||||
|
|
|
|||
9
core/ui/src/main/res/drawable/ic_contact_20.xml
Normal file
9
core/ui/src/main/res/drawable/ic_contact_20.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M9.149,16.445C9.427,16.482 9.711,16.5 10,16.5V18C9.646,18 9.297,17.974 8.955,17.93L9.149,16.445ZM10.947,17.188L11.007,17.641L11.044,17.93C10.702,17.974 10.354,18 10,18V16.5C10.289,16.5 10.573,16.482 10.851,16.445L10.947,17.188ZM6.044,15.158C6.492,15.502 6.985,15.789 7.513,16.008L6.938,17.392C6.287,17.122 5.681,16.769 5.131,16.346L6.044,15.158ZM14.868,16.346C14.318,16.769 13.712,17.122 13.061,17.392L12.997,17.234L12.487,16.008C13.015,15.789 13.508,15.502 13.956,15.158L14.868,16.346ZM3.992,12.487C4.211,13.015 4.498,13.508 4.842,13.956L4.247,14.413L3.653,14.868C3.231,14.318 2.877,13.712 2.607,13.061L2.772,12.994L3.992,12.487ZM17.228,12.994L17.392,13.061C17.122,13.712 16.769,14.318 16.346,14.868L15.158,13.956C15.502,13.508 15.789,13.015 16.008,12.487L17.228,12.994ZM2,10C2,9.646 2.025,9.297 2.069,8.955L2.812,9.052L3.555,9.149C3.518,9.427 3.5,9.711 3.5,10C3.5,10.289 3.518,10.573 3.555,10.851L2.069,11.044C2.025,10.702 2,10.354 2,10ZM18,10C18,10.354 17.974,10.702 17.93,11.044L17.188,10.948L16.445,10.851C16.482,10.573 16.5,10.289 16.5,10C16.5,9.711 16.482,9.427 16.445,9.149L17.93,8.955C17.974,9.297 18,9.646 18,10ZM4.842,6.044C4.498,6.492 4.211,6.985 3.992,7.513L2.607,6.938C2.877,6.287 3.231,5.681 3.653,5.131L4.842,6.044ZM16.346,5.131C16.769,5.681 17.122,6.287 17.392,6.938L16.008,7.513C15.789,6.985 15.502,6.492 15.158,6.044L16.346,5.131ZM7.513,3.992C6.985,4.211 6.492,4.498 6.044,4.842L5.131,3.653C5.681,3.231 6.287,2.877 6.938,2.607L7.513,3.992ZM13.061,2.607C13.712,2.877 14.318,3.231 14.868,3.653L13.956,4.842C13.508,4.498 13.015,4.211 12.487,3.992L13.061,2.607ZM10,2C10.354,2 10.702,2.025 11.044,2.069L10.948,2.812L10.947,2.811L10.851,3.555C10.573,3.518 10.289,3.5 10,3.5C9.711,3.5 9.427,3.518 9.149,3.555L8.955,2.069C9.297,2.025 9.646,2 10,2Z"
|
||||
android:fillColor="#0090F9"/>
|
||||
</vector>
|
||||
1
features/address-book/api/.gitignore
vendored
Normal file
1
features/address-book/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
build/
|
||||
22
features/address-book/api/build.gradle.kts
Normal file
22
features/address-book/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.addressbook.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.models)
|
||||
|
||||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/* Compose */
|
||||
implementation(deps.compose.runtime)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.addressbook
|
||||
|
||||
interface AddressBookFeatureToggles {
|
||||
val isAddressBookEnabled: Boolean
|
||||
}
|
||||
1
features/address-book/impl/.gitignore
vendored
Normal file
1
features/address-book/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
build/
|
||||
28
features/address-book/impl/build.gradle.kts
Normal file
28
features/address-book/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.addressbook.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Api */
|
||||
implementation(projects.features.addressBook.api)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.runtime)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.addressbook
|
||||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
|
||||
internal class DefaultAddressBookFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : AddressBookFeatureToggles {
|
||||
override val isAddressBookEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_83_ADDRESS_BOOK_ENABLED)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.features.addressbook.di
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.addressbook.AddressBookFeatureToggles
|
||||
import com.tangem.features.addressbook.DefaultAddressBookFeatureToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object AddressBookModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAddressBookFeatureToggles(featureTogglesManager: FeatureTogglesManager): AddressBookFeatureToggles {
|
||||
return DefaultAddressBookFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ dependencies {
|
|||
implementation(projects.features.tester.api)
|
||||
implementation(projects.features.createWalletSelection.api)
|
||||
implementation(projects.features.onboardingV2.api)
|
||||
implementation(projects.features.addressBook.api)
|
||||
|
||||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ internal class PreviewDetailsComponent : DetailsComponent {
|
|||
},
|
||||
).buildAll(
|
||||
isWalletConnectAvailable = true,
|
||||
isAddressBookAvailable = true,
|
||||
isSupportChatAvailable = true,
|
||||
hasAnyMobileWallet = true,
|
||||
userWalletId = UserWalletId(""),
|
||||
|
|
|
|||
|
|
@ -25,6 +25,15 @@ internal sealed class DetailsItemUM {
|
|||
override val id: String = "wallet_connect"
|
||||
}
|
||||
|
||||
data class WalletConnectAddressBookBlock(val items: List<Item>) : DetailsItemUM() {
|
||||
override val id: String = "wallet_connect_address_book"
|
||||
|
||||
sealed class Item(open val onClick: () -> Unit) {
|
||||
data class WalletConnect(override val onClick: () -> Unit) : Item(onClick)
|
||||
data class AddressBook(override val onClick: () -> Unit) : Item(onClick)
|
||||
}
|
||||
}
|
||||
|
||||
data object UserWalletList : DetailsItemUM() {
|
||||
override val id: String = "user_wallet_list"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
|
|||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.addressbook.AddressBookFeatureToggles
|
||||
import com.tangem.features.details.component.DetailsComponent
|
||||
import com.tangem.features.details.entity.DetailsFooterUM
|
||||
import com.tangem.features.details.entity.DetailsItemUM
|
||||
|
|
@ -51,6 +52,7 @@ internal class DetailsModel @Inject constructor(
|
|||
socialsBuilder: SocialsBuilder,
|
||||
paramsContainer: ParamsContainer,
|
||||
feedbackFeatureToggles: FeedbackFeatureToggles,
|
||||
addressBookFeatureToggles: AddressBookFeatureToggles,
|
||||
private val itemsBuilder: ItemsBuilder,
|
||||
private val appInfoProvider: AppInfoProvider,
|
||||
private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase,
|
||||
|
|
@ -86,6 +88,7 @@ internal class DetailsModel @Inject constructor(
|
|||
items = MutableStateFlow(
|
||||
itemsBuilder.buildAll(
|
||||
isWalletConnectAvailable = isWalletConnectAvailable,
|
||||
isAddressBookAvailable = addressBookFeatureToggles.isAddressBookEnabled,
|
||||
isSupportChatAvailable = feedbackFeatureToggles.isUsedeskEnabled,
|
||||
hasAnyMobileWallet = getWalletsUseCase.invokeSync().any { it is UserWallet.Hot },
|
||||
userWalletId = params.userWalletId,
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ package com.tangem.features.details.ui
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
|
|
@ -13,17 +13,21 @@ import androidx.compose.material3.Icon
|
|||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.BlockItem
|
||||
import com.tangem.core.ui.components.inputrow.InputRowImageBase
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
|
|
@ -155,10 +159,44 @@ private fun Block(
|
|||
onClick = model.onClick,
|
||||
)
|
||||
}
|
||||
is DetailsItemUM.WalletConnectAddressBookBlock -> {
|
||||
BlockCard {
|
||||
WalletConnectAddressBookBlockItems(
|
||||
items = model.items,
|
||||
modifier = itemModifier.padding(12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
is DetailsItemUM.UserWalletList -> {
|
||||
userWalletListBlockContent.Content(modifier = itemModifier)
|
||||
}
|
||||
is DetailsItemUM.UnderSectionText -> { /* Handled above */ }
|
||||
is DetailsItemUM.UnderSectionText -> { /* Handled above */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WalletConnectAddressBookBlockItems(
|
||||
items: List<DetailsItemUM.WalletConnectAddressBookBlock.Item>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
items.fastForEach { item ->
|
||||
when (item) {
|
||||
is DetailsItemUM.WalletConnectAddressBookBlock.Item.WalletConnect -> InputRowImageBase(
|
||||
modifier = modifier.clickable(onClick = item.onClick),
|
||||
iconResVector = R.drawable.ic_wallet_connect_24,
|
||||
iconTint = TangemTheme.colors.icon.primary1,
|
||||
subtitle = TextReference.Res(R.string.wallet_connect_title),
|
||||
caption = TextReference.Res(R.string.wallet_connect_subtitle),
|
||||
)
|
||||
is DetailsItemUM.WalletConnectAddressBookBlock.Item.AddressBook -> InputRowImageBase(
|
||||
modifier = modifier.clickable(onClick = item.onClick),
|
||||
iconResVector = R.drawable.ic_contact_20,
|
||||
iconTint = TangemTheme.colors.icon.accent,
|
||||
subtitle = TextReference.Res(R.string.address_book_title),
|
||||
caption = TextReference.Res(R.string.address_book_description),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ internal class ItemsBuilder @Inject constructor(
|
|||
@Suppress("LongParameterList")
|
||||
fun buildAll(
|
||||
isWalletConnectAvailable: Boolean,
|
||||
isAddressBookAvailable: Boolean,
|
||||
isSupportChatAvailable: Boolean,
|
||||
hasAnyMobileWallet: Boolean,
|
||||
userWalletId: UserWalletId,
|
||||
|
|
@ -33,7 +34,11 @@ internal class ItemsBuilder @Inject constructor(
|
|||
onSupportChatClick: () -> Unit,
|
||||
onBuyClick: () -> Unit,
|
||||
): ImmutableList<DetailsItemUM> = buildList {
|
||||
buildWalletConnectBlock(isWalletConnectAvailable, userWalletId)?.let(::add)
|
||||
if (isAddressBookAvailable) {
|
||||
buildWalletConnectAddressBookBlock(isWalletConnectAvailable, userWalletId)
|
||||
} else {
|
||||
buildWalletConnectBlock(isWalletConnectAvailable, userWalletId)?.let(::add)
|
||||
}
|
||||
buildUserWalletListBlock().let(::add)
|
||||
|
||||
if (hotWalletRestrictionManager.isCreationEnabledSync() && hasAnyMobileWallet) {
|
||||
|
|
@ -86,6 +91,33 @@ internal class ItemsBuilder @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<DetailsItemUM>.buildWalletConnectAddressBookBlock(
|
||||
isWalletConnectAvailable: Boolean,
|
||||
userWalletId: UserWalletId,
|
||||
) {
|
||||
val walletConnectAddressBookItems = buildList {
|
||||
if (isWalletConnectAvailable) add(buildWalletConnectButton(userWalletId))
|
||||
add(buildAddressBookButton())
|
||||
}
|
||||
if (walletConnectAddressBookItems.isNotEmpty()) {
|
||||
add(DetailsItemUM.WalletConnectAddressBookBlock(walletConnectAddressBookItems))
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildWalletConnectButton(
|
||||
userWalletId: UserWalletId,
|
||||
): DetailsItemUM.WalletConnectAddressBookBlock.Item.WalletConnect {
|
||||
return DetailsItemUM.WalletConnectAddressBookBlock.Item.WalletConnect(
|
||||
onClick = { router.push(AppRoute.WalletConnectSessions(userWalletId)) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildAddressBookButton(): DetailsItemUM.WalletConnectAddressBookBlock.Item.AddressBook {
|
||||
return DetailsItemUM.WalletConnectAddressBookBlock.Item.AddressBook(
|
||||
onClick = { },
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildUserWalletListBlock(): DetailsItemUM = DetailsItemUM.UserWalletList
|
||||
|
||||
private fun buildShopBlock(onBuyClick: () -> Unit): DetailsItemUM = DetailsItemUM.Basic(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,191 @@
|
|||
package com.tangem.features.details.model
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.feedback.models.WalletMetaInfo
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.features.details.entity.SelectEmailFeedbackTypeBS
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class DetailsModelFeedbackTest : DetailsModelTestBase() {
|
||||
|
||||
@Test
|
||||
fun `GIVEN all wallets hot WHEN support email clicked THEN DirectUserRequest sent`() = runTest {
|
||||
// Arrange
|
||||
val wallet = hotWallet(wallet1)
|
||||
val meta = metaInfo(wallet1, isVisa = false)
|
||||
every { getWalletsUseCase.invokeSync() } returns listOf(wallet)
|
||||
every { getSelectedWalletSyncUseCase() } returns wallet.right()
|
||||
coEvery { getWalletMetaInfoUseCase(wallet1) } returns meta.right()
|
||||
every { getTangemPayCustomerIdUseCase(wallet1) } returns "".right()
|
||||
|
||||
// Act
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onEmailSlot.captured.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify { analyticsEventHandler.send(any<Basic.ButtonSupport>()) }
|
||||
coVerify { sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(meta)) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN all wallets cold visa with customerId WHEN support email clicked THEN Visa request sent`() = runTest {
|
||||
val wallet = coldWallet(wallet1, isVisa = true)
|
||||
val meta = metaInfo(wallet1, isVisa = true)
|
||||
every { getWalletsUseCase.invokeSync() } returns listOf(wallet)
|
||||
every { getSelectedWalletSyncUseCase() } returns wallet.right()
|
||||
coEvery { getWalletMetaInfoUseCase(wallet1) } returns meta.right()
|
||||
every { getTangemPayCustomerIdUseCase(wallet1) } returns customerId.right()
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onEmailSlot.captured.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { analyticsEventHandler.send(any<Basic.ButtonSupport>()) }
|
||||
coVerify { sendFeedbackEmailUseCase(FeedbackEmailType.Visa.DirectUserRequest(meta, customerId)) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN mixed wallets WHEN support email clicked THEN bottom sheet shown and no email sent`() = runTest {
|
||||
val selected = hotWallet(wallet1)
|
||||
val meta = metaInfo(wallet1, isVisa = false)
|
||||
every { getWalletsUseCase.invokeSync() } returns listOf(selected, coldWallet(wallet2, isVisa = true))
|
||||
every { getSelectedWalletSyncUseCase() } returns selected.right()
|
||||
coEvery { getWalletMetaInfoUseCase(wallet1) } returns meta.right()
|
||||
every { getTangemPayCustomerIdUseCase(wallet1) } returns customerId.right()
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onEmailSlot.captured.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
val bsConfig = model.state.value.selectFeedbackEmailTypeBSConfig
|
||||
assertThat(bsConfig.isShown).isTrue()
|
||||
assertThat(bsConfig.content).isInstanceOf(SelectEmailFeedbackTypeBS::class.java)
|
||||
coVerify(exactly = 0) { sendFeedbackEmailUseCase(any()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN meta info missing WHEN support email clicked THEN no email sent`() = runTest {
|
||||
val wallet = hotWallet(wallet1)
|
||||
every { getWalletsUseCase.invokeSync() } returns listOf(wallet)
|
||||
every { getSelectedWalletSyncUseCase() } returns wallet.right()
|
||||
coEvery { getWalletMetaInfoUseCase(wallet1) } returns Throwable().left()
|
||||
every { getTangemPayCustomerIdUseCase(wallet1) } returns "".right()
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onEmailSlot.captured.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 0) { sendFeedbackEmailUseCase(any()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN General option AND selected meta not visa WHEN selected THEN DirectUserRequest with selected meta`() =
|
||||
runTest {
|
||||
val selectedMeta = metaInfo(wallet1, isVisa = false)
|
||||
val content = openBottomSheet(selectedMeta = selectedMeta)
|
||||
|
||||
content.onOptionClick(SelectEmailFeedbackTypeBS.Option.General)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify { sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(selectedMeta)) }
|
||||
assertThat(currentModel.state.value.selectFeedbackEmailTypeBSConfig.isShown).isFalse()
|
||||
verify { analyticsEventHandler.send(any<Basic.ButtonSupport>()) }
|
||||
currentModel.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN General option AND selected meta visa WHEN selected THEN picks non-visa wallet meta`() = runTest {
|
||||
val selectedMeta = metaInfo(wallet1, isVisa = true)
|
||||
val nonVisaMeta = metaInfo(wallet2, isVisa = false)
|
||||
val content = openBottomSheet(
|
||||
selectedMeta = selectedMeta,
|
||||
wallets = listOf(coldWallet(wallet1, isVisa = true), hotWallet(wallet2)),
|
||||
)
|
||||
coEvery { getWalletMetaInfoUseCase(wallet2) } returns nonVisaMeta.right()
|
||||
|
||||
content.onOptionClick(SelectEmailFeedbackTypeBS.Option.General)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify { sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(nonVisaMeta)) }
|
||||
currentModel.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN Visa option AND selected meta visa with customerId WHEN selected THEN Visa request with selected`() =
|
||||
runTest {
|
||||
val selectedMeta = metaInfo(wallet1, isVisa = true)
|
||||
val content = openBottomSheet(selectedMeta = selectedMeta, customerId = customerId)
|
||||
|
||||
content.onOptionClick(SelectEmailFeedbackTypeBS.Option.Visa)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify {
|
||||
sendFeedbackEmailUseCase(FeedbackEmailType.Visa.DirectUserRequest(selectedMeta, customerId))
|
||||
}
|
||||
currentModel.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN Visa option AND selected meta not visa WHEN selected THEN picks cold visa wallet meta`() = runTest {
|
||||
val selectedMeta = metaInfo(wallet1, isVisa = false)
|
||||
val visaMeta = metaInfo(wallet2, isVisa = true)
|
||||
val content = openBottomSheet(
|
||||
selectedMeta = selectedMeta,
|
||||
wallets = listOf(hotWallet(wallet1), coldWallet(wallet2, isVisa = true)),
|
||||
)
|
||||
coEvery { getWalletMetaInfoUseCase(wallet2) } returns visaMeta.right()
|
||||
every { getTangemPayCustomerIdUseCase(wallet2) } returns customerId.right()
|
||||
|
||||
content.onOptionClick(SelectEmailFeedbackTypeBS.Option.Visa)
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify { sendFeedbackEmailUseCase(FeedbackEmailType.Visa.DirectUserRequest(visaMeta, customerId)) }
|
||||
currentModel.onDestroy()
|
||||
}
|
||||
|
||||
private lateinit var currentModel: DetailsModel
|
||||
|
||||
/**
|
||||
* Drives the model into the "mixed wallets" state so the bottom sheet is shown, then returns its content.
|
||||
* [selectedMeta] is what [getWalletMetaInfoUseCase] returns for the selected wallet ([wallet1]).
|
||||
*/
|
||||
private fun TestScope.openBottomSheet(
|
||||
selectedMeta: WalletMetaInfo,
|
||||
wallets: List<UserWallet> = listOf(hotWallet(wallet1), coldWallet(wallet2, isVisa = true)),
|
||||
customerId: String = "",
|
||||
): SelectEmailFeedbackTypeBS {
|
||||
every { getWalletsUseCase.invokeSync() } returns wallets
|
||||
every { getSelectedWalletSyncUseCase() } returns wallets.first().right()
|
||||
coEvery { getWalletMetaInfoUseCase(wallet1) } returns selectedMeta.right()
|
||||
every { getTangemPayCustomerIdUseCase(wallet1) } returns customerId.right()
|
||||
|
||||
currentModel = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onEmailSlot.captured.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
return currentModel.state.value.selectFeedbackEmailTypeBSConfig.content as SelectEmailFeedbackTypeBS
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
package com.tangem.features.details.model
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.features.details.entity.DetailsFooterUM
|
||||
import com.tangem.features.details.entity.DetailsItemUM
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.verify
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class DetailsModelInitTest : DetailsModelTestBase() {
|
||||
|
||||
@Test
|
||||
fun `GIVEN walletConnect available WHEN init THEN buildAll receives isWalletConnectAvailable true`() = runTest {
|
||||
// Arrange
|
||||
coEvery { checkIsWalletConnectAvailableUseCase(userWalletId) } returns true.right()
|
||||
|
||||
// Act
|
||||
createModel(this).also { advanceUntilIdle() }.onDestroy()
|
||||
|
||||
// Assert
|
||||
assertThat(wcSlot.captured).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN walletConnect unavailable WHEN init THEN buildAll receives isWalletConnectAvailable false`() = runTest {
|
||||
coEvery { checkIsWalletConnectAvailableUseCase(userWalletId) } returns false.right()
|
||||
|
||||
createModel(this).also { advanceUntilIdle() }.onDestroy()
|
||||
|
||||
assertThat(wcSlot.captured).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN walletConnect check fails WHEN init THEN isWalletConnectAvailable falls back to false`() = runTest {
|
||||
coEvery { checkIsWalletConnectAvailableUseCase(userWalletId) } returns Throwable("boom").left()
|
||||
|
||||
createModel(this).also { advanceUntilIdle() }.onDestroy()
|
||||
|
||||
assertThat(wcSlot.captured).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN addressBook enabled WHEN init THEN buildAll receives isAddressBookAvailable true`() = runTest {
|
||||
every { addressBookFeatureToggles.isAddressBookEnabled } returns true
|
||||
|
||||
createModel(this).also { advanceUntilIdle() }.onDestroy()
|
||||
|
||||
assertThat(abSlot.captured).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN addressBook disabled WHEN init THEN buildAll receives isAddressBookAvailable false`() = runTest {
|
||||
every { addressBookFeatureToggles.isAddressBookEnabled } returns false
|
||||
|
||||
createModel(this).also { advanceUntilIdle() }.onDestroy()
|
||||
|
||||
assertThat(abSlot.captured).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN usedesk enabled WHEN init THEN buildAll receives isSupportChatAvailable true`() = runTest {
|
||||
every { feedbackFeatureToggles.isUsedeskEnabled } returns true
|
||||
|
||||
createModel(this).also { advanceUntilIdle() }.onDestroy()
|
||||
|
||||
assertThat(chatSlot.captured).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN usedesk disabled WHEN init THEN buildAll receives isSupportChatAvailable false`() = runTest {
|
||||
every { feedbackFeatureToggles.isUsedeskEnabled } returns false
|
||||
|
||||
createModel(this).also { advanceUntilIdle() }.onDestroy()
|
||||
|
||||
assertThat(chatSlot.captured).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a hot wallet present WHEN init THEN buildAll receives hasAnyMobileWallet true`() = runTest {
|
||||
every { getWalletsUseCase.invokeSync() } returns listOf(hotWallet(wallet1))
|
||||
|
||||
createModel(this).also { advanceUntilIdle() }.onDestroy()
|
||||
|
||||
assertThat(mobileSlot.captured).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN only cold wallets WHEN init THEN buildAll receives hasAnyMobileWallet false`() = runTest {
|
||||
every { getWalletsUseCase.invokeSync() } returns listOf(coldWallet(wallet1, isVisa = false))
|
||||
|
||||
createModel(this).also { advanceUntilIdle() }.onDestroy()
|
||||
|
||||
assertThat(mobileSlot.captured).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN params userWalletId WHEN init THEN buildAll receives it`() = runTest {
|
||||
createModel(this).also { advanceUntilIdle() }.onDestroy()
|
||||
|
||||
assertThat(walletIdSlot.captured).isEqualTo(userWalletId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN app info WHEN init THEN footer appVersion combines version and code`() = runTest {
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(model.state.value.footer.appVersion).isEqualTo("1.2.3 (456)")
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN socials WHEN init THEN footer socials come from SocialsBuilder`() = runTest {
|
||||
val socials = persistentListOf(DetailsFooterUM.Social(id = "tw", iconResId = 0, onClick = {}))
|
||||
every { socialsBuilder.buildAll() } returns socials
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(model.state.value.footer.socials).isEqualTo(socials)
|
||||
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN popBack WHEN invoked THEN router pop is called`() = runTest {
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.state.value.popBack()
|
||||
|
||||
verify { router.pop(onComplete = any()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN buildAll result WHEN not eligible for tangem pay THEN state items equal buildAll result`() = runTest {
|
||||
val items = persistentListOf<DetailsItemUM>(DetailsItemUM.UserWalletList)
|
||||
stubBuildAllReturns(items)
|
||||
coEvery { tangemPayEligibilityManager.getEligibleWallets(any(), any()) } returns emptyList()
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(model.state.value.items).isEqualTo(items)
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN items flow updates WHEN tangem pay item added THEN new items propagate to state`() = runTest {
|
||||
val initial = persistentListOf<DetailsItemUM>()
|
||||
val withTangemPay = persistentListOf<DetailsItemUM>(DetailsItemUM.UserWalletList)
|
||||
stubBuildAllReturns(initial)
|
||||
every { itemsBuilder.addTangemPayItem(any(), any()) } returns withTangemPay
|
||||
coEvery { tangemPayEligibilityManager.getEligibleWallets(any(), any()) } returns listOf(hotWallet(wallet1))
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
assertThat(model.state.value.items).isEqualTo(withTangemPay)
|
||||
model.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.features.details.model
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class DetailsModelNavigationTest : DetailsModelTestBase() {
|
||||
|
||||
@Test
|
||||
fun `GIVEN selected wallet and meta WHEN support chat clicked THEN router pushes Usedesk`() = runTest {
|
||||
// Arrange
|
||||
val wallet = hotWallet(wallet1)
|
||||
val meta = metaInfo(wallet1)
|
||||
every { getSelectedWalletSyncUseCase() } returns wallet.right()
|
||||
coEvery { getWalletMetaInfoUseCase(wallet1) } returns meta.right()
|
||||
|
||||
// Act
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onChatSlot.captured.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify { router.push(route = AppRoute.Usedesk(meta), onComplete = any()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN meta info missing WHEN support chat clicked THEN no navigation`() = runTest {
|
||||
val wallet = hotWallet(wallet1)
|
||||
every { getSelectedWalletSyncUseCase() } returns wallet.right()
|
||||
coEvery { getWalletMetaInfoUseCase(wallet1) } returns Throwable().left()
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onChatSlot.captured.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(exactly = 0) { router.push(route = any(), onComplete = any()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN buy link WHEN buy clicked THEN opens url and sends analytics`() = runTest {
|
||||
coEvery {
|
||||
generateBuyTangemCardLinkUseCase(GenerateBuyTangemCardLinkUseCase.Source.Settings)
|
||||
} returns buyUrl
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onBuySlot.captured.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { analyticsEventHandler.send(any<Basic.ButtonBuy>()) }
|
||||
verify { urlOpener.openUrl(buyUrl) }
|
||||
model.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.tangem.features.details.model
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.domain.pay.model.TangemPayEntryPoint
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal class DetailsModelTangemPayTest : DetailsModelTestBase() {
|
||||
|
||||
@Test
|
||||
fun `GIVEN eligible wallets WHEN init THEN tangem pay item added and analytics sent`() = runTest {
|
||||
// Arrange
|
||||
coEvery {
|
||||
tangemPayEligibilityManager.getEligibleWallets(
|
||||
shouldExcludePaeraCustomers = true,
|
||||
entryPoint = TangemPayEntryPoint.DETAILS,
|
||||
)
|
||||
} returns listOf(hotWallet(wallet1))
|
||||
|
||||
// Act
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
verify { analyticsEventHandler.send(any<TangemPayAnalyticsEvents.PermanentButtonShowed>()) }
|
||||
verify { itemsBuilder.addTangemPayItem(any(), any()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no eligible wallets WHEN init THEN tangem pay item not added`() = runTest {
|
||||
coEvery { tangemPayEligibilityManager.getEligibleWallets(any(), any()) } returns emptyList()
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
|
||||
verify(exactly = 0) { itemsBuilder.addTangemPayItem(any(), any()) }
|
||||
verify(exactly = 0) { analyticsEventHandler.send(any<TangemPayAnalyticsEvents.PermanentButtonShowed>()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tangem pay available WHEN item clicked THEN navigates to onboarding`() = runTest {
|
||||
coEvery { tangemPayEligibilityManager.getEligibleWallets(any(), any()) } returns listOf(hotWallet(wallet1))
|
||||
coEvery { tangemPayEligibilityManager.getTangemPayAvailability(TangemPayEntryPoint.DETAILS) } returns true
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onTangemPaySlot.captured.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { analyticsEventHandler.send(any<TangemPayAnalyticsEvents.DetailsVisaPermanentButtonClicked>()) }
|
||||
verify {
|
||||
router.push(
|
||||
route = AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings),
|
||||
onComplete = any(),
|
||||
)
|
||||
}
|
||||
model.onDestroy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tangem pay unavailable WHEN item clicked THEN item removed and no navigation`() = runTest {
|
||||
coEvery { tangemPayEligibilityManager.getEligibleWallets(any(), any()) } returns listOf(hotWallet(wallet1))
|
||||
coEvery { tangemPayEligibilityManager.getTangemPayAvailability(TangemPayEntryPoint.DETAILS) } returns false
|
||||
|
||||
val model = createModel(this)
|
||||
advanceUntilIdle()
|
||||
onTangemPaySlot.captured.invoke()
|
||||
advanceUntilIdle()
|
||||
|
||||
verify { itemsBuilder.removeTangemPayItem(any()) }
|
||||
verify(exactly = 0) { router.push(route = any(), onComplete = any()) }
|
||||
model.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
package com.tangem.features.details.model
|
||||
|
||||
import arrow.core.right
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.WalletMetaInfo
|
||||
import com.tangem.domain.feedback.repository.FeedbackFeatureToggles
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
|
||||
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
|
||||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.addressbook.AddressBookFeatureToggles
|
||||
import com.tangem.features.details.component.DetailsComponent
|
||||
import com.tangem.features.details.entity.DetailsItemUM
|
||||
import com.tangem.features.details.utils.ItemsBuilder
|
||||
import com.tangem.features.details.utils.SocialsBuilder
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import com.tangem.utils.info.AppInfoProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkObject
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
internal abstract class DetailsModelTestBase {
|
||||
|
||||
protected val userWalletId = UserWalletId("011")
|
||||
protected val wallet1 = UserWalletId("01")
|
||||
protected val wallet2 = UserWalletId("02")
|
||||
protected val customerId = "customer-1"
|
||||
protected val buyUrl = "https://tangem.com/buy"
|
||||
|
||||
protected val socialsBuilder: SocialsBuilder = mockk()
|
||||
protected val feedbackFeatureToggles: FeedbackFeatureToggles = mockk()
|
||||
protected val addressBookFeatureToggles: AddressBookFeatureToggles = mockk()
|
||||
protected val itemsBuilder: ItemsBuilder = mockk()
|
||||
protected val appInfoProvider: AppInfoProvider = mockk()
|
||||
protected val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase = mockk()
|
||||
protected val router: Router = mockk(relaxUnitFun = true)
|
||||
protected val urlOpener: UrlOpener = mockk(relaxUnitFun = true)
|
||||
protected val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase = mockk()
|
||||
protected val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase = mockk()
|
||||
protected val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase = mockk()
|
||||
protected val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase = mockk(relaxUnitFun = true)
|
||||
protected val getWalletsUseCase: GetWalletsUseCase = mockk()
|
||||
protected val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase = mockk()
|
||||
protected val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxUnitFun = true)
|
||||
protected val tangemPayEligibilityManager: TangemPayEligibilityManager = mockk()
|
||||
|
||||
// Captured from itemsBuilder.buildAll(...) so the feature buttons can be driven.
|
||||
protected val wcSlot = slot<Boolean>()
|
||||
protected val abSlot = slot<Boolean>()
|
||||
protected val chatSlot = slot<Boolean>()
|
||||
protected val mobileSlot = slot<Boolean>()
|
||||
protected val walletIdSlot = slot<UserWalletId>()
|
||||
protected val onEmailSlot = slot<() -> Unit>()
|
||||
protected val onChatSlot = slot<() -> Unit>()
|
||||
protected val onBuySlot = slot<() -> Unit>()
|
||||
protected val onTangemPaySlot = slot<() -> Unit>()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
mockkObject(VisaUtilities)
|
||||
|
||||
// Defaults sufficient for model construction (init block).
|
||||
coEvery { checkIsWalletConnectAvailableUseCase(any()) } returns false.right()
|
||||
every { addressBookFeatureToggles.isAddressBookEnabled } returns false
|
||||
every { feedbackFeatureToggles.isUsedeskEnabled } returns false
|
||||
every { getWalletsUseCase.invokeSync() } returns emptyList()
|
||||
every { socialsBuilder.buildAll() } returns persistentListOf()
|
||||
every { appInfoProvider.appVersion } returns "1.2.3"
|
||||
every { appInfoProvider.appVersionCode } returns 456
|
||||
coEvery { tangemPayEligibilityManager.getEligibleWallets(any(), any()) } returns emptyList()
|
||||
|
||||
every {
|
||||
itemsBuilder.buildAll(
|
||||
isWalletConnectAvailable = capture(wcSlot),
|
||||
isAddressBookAvailable = capture(abSlot),
|
||||
isSupportChatAvailable = capture(chatSlot),
|
||||
hasAnyMobileWallet = capture(mobileSlot),
|
||||
userWalletId = capture(walletIdSlot),
|
||||
onSupportEmailClick = capture(onEmailSlot),
|
||||
onSupportChatClick = capture(onChatSlot),
|
||||
onBuyClick = capture(onBuySlot),
|
||||
)
|
||||
} returns persistentListOf()
|
||||
every { itemsBuilder.addTangemPayItem(any(), capture(onTangemPaySlot)) } answers { firstArg() }
|
||||
every { itemsBuilder.removeTangemPayItem(any()) } answers { firstArg() }
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkObject(VisaUtilities)
|
||||
}
|
||||
|
||||
protected fun createModel(testScope: TestScope): DetailsModel = DetailsModel(
|
||||
socialsBuilder = socialsBuilder,
|
||||
paramsContainer = MutableParamsContainer(DetailsComponent.Params(userWalletId)),
|
||||
feedbackFeatureToggles = feedbackFeatureToggles,
|
||||
addressBookFeatureToggles = addressBookFeatureToggles,
|
||||
itemsBuilder = itemsBuilder,
|
||||
appInfoProvider = appInfoProvider,
|
||||
checkIsWalletConnectAvailableUseCase = checkIsWalletConnectAvailableUseCase,
|
||||
router = router,
|
||||
urlOpener = urlOpener,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
getWalletMetaInfoUseCase = getWalletMetaInfoUseCase,
|
||||
getTangemPayCustomerIdUseCase = getTangemPayCustomerIdUseCase,
|
||||
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
|
||||
getWalletsUseCase = getWalletsUseCase,
|
||||
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
|
||||
generateBuyTangemCardLinkUseCase = generateBuyTangemCardLinkUseCase,
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
tangemPayEligibilityManager = tangemPayEligibilityManager,
|
||||
)
|
||||
|
||||
protected fun stubBuildAllReturns(list: ImmutableList<DetailsItemUM>) {
|
||||
every {
|
||||
itemsBuilder.buildAll(any(), any(), any(), any(), any(), any(), any(), any())
|
||||
} returns list
|
||||
}
|
||||
|
||||
protected fun metaInfo(id: UserWalletId = userWalletId, isVisa: Boolean? = null): WalletMetaInfo =
|
||||
WalletMetaInfo(userWalletId = id, isVisa = isVisa)
|
||||
|
||||
protected fun hotWallet(id: UserWalletId): UserWallet.Hot = mockk {
|
||||
every { walletId } returns id
|
||||
}
|
||||
|
||||
protected fun coldWallet(id: UserWalletId, isVisa: Boolean): UserWallet.Cold {
|
||||
val cardMock = mockk<CardDTO>()
|
||||
every { VisaUtilities.isVisaCard(cardMock) } returns isVisa
|
||||
val scan = mockk<ScanResponse> { every { card } returns cardMock }
|
||||
return mockk {
|
||||
every { walletId } returns id
|
||||
every { scanResponse } returns scan
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
|
||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||
return TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
package com.tangem.features.details.utils
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.components.block.model.BlockUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.settings.HotWalletRestrictionManager
|
||||
import com.tangem.features.details.entity.DetailsItemUM
|
||||
import com.tangem.features.details.impl.R
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class ItemsBuilderTest {
|
||||
|
||||
private val router: Router = mockk(relaxUnitFun = true)
|
||||
private val hotWalletRestrictionManager: HotWalletRestrictionManager = mockk()
|
||||
|
||||
private val itemsBuilder = ItemsBuilder(
|
||||
router = router,
|
||||
hotWalletRestrictionManager = hotWalletRestrictionManager,
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
clearMocks(router, hotWalletRestrictionManager)
|
||||
every { hotWalletRestrictionManager.isCreationEnabledSync() } returns false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN walletConnect available AND addressBook unavailable WHEN buildAll THEN standalone WalletConnect block`() {
|
||||
// Act
|
||||
val result = buildAll(isWalletConnectAvailable = true, isAddressBookAvailable = false)
|
||||
|
||||
// Assert
|
||||
assertThat(result.map { it.id }).containsExactly(
|
||||
"wallet_connect",
|
||||
"user_wallet_list",
|
||||
"shop",
|
||||
"settings",
|
||||
"support",
|
||||
).inOrder()
|
||||
assertThat(result.first()).isInstanceOf(DetailsItemUM.WalletConnect::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN walletConnect AND addressBook available WHEN buildAll THEN combined block with both items`() {
|
||||
// Act
|
||||
val result = buildAll(isWalletConnectAvailable = true, isAddressBookAvailable = true)
|
||||
|
||||
// Assert
|
||||
assertThat(result.map { it.id }).containsExactly(
|
||||
"wallet_connect_address_book",
|
||||
"user_wallet_list",
|
||||
"shop",
|
||||
"settings",
|
||||
"support",
|
||||
).inOrder()
|
||||
|
||||
val block = result.first() as DetailsItemUM.WalletConnectAddressBookBlock
|
||||
assertThat(block.items.map { it::class.java }).containsExactly(
|
||||
DetailsItemUM.WalletConnectAddressBookBlock.Item.WalletConnect::class.java,
|
||||
DetailsItemUM.WalletConnectAddressBookBlock.Item.AddressBook::class.java,
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN walletConnect unavailable AND addressBook available WHEN buildAll THEN combined block with addressBook only`() {
|
||||
// Act
|
||||
val result = buildAll(isWalletConnectAvailable = false, isAddressBookAvailable = true)
|
||||
|
||||
// Assert
|
||||
assertThat(result.map { it.id }).containsExactly(
|
||||
"wallet_connect_address_book",
|
||||
"user_wallet_list",
|
||||
"shop",
|
||||
"settings",
|
||||
"support",
|
||||
).inOrder()
|
||||
|
||||
val block = result.first() as DetailsItemUM.WalletConnectAddressBookBlock
|
||||
assertThat(block.items.map { it::class.java }).containsExactly(
|
||||
DetailsItemUM.WalletConnectAddressBookBlock.Item.AddressBook::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN walletConnect AND addressBook unavailable WHEN buildAll THEN no walletConnect block`() {
|
||||
// Act
|
||||
val result = buildAll(isWalletConnectAvailable = false, isAddressBookAvailable = false)
|
||||
|
||||
// Assert
|
||||
assertThat(result.map { it.id }).containsExactly(
|
||||
"user_wallet_list",
|
||||
"shop",
|
||||
"settings",
|
||||
"support",
|
||||
).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN creation enabled AND has mobile wallet WHEN buildAll THEN under section text is added`() {
|
||||
// Arrange
|
||||
every { hotWalletRestrictionManager.isCreationEnabledSync() } returns true
|
||||
|
||||
// Act
|
||||
val result = buildAll(hasAnyMobileWallet = true)
|
||||
|
||||
// Assert
|
||||
val underSectionText = result.filterIsInstance<DetailsItemUM.UnderSectionText>().single()
|
||||
assertThat(underSectionText).isEqualTo(
|
||||
DetailsItemUM.UnderSectionText(
|
||||
id = "only_one_mobile_wallet_explanation",
|
||||
text = resourceReference(R.string.only_one_mobile_wallet_explanation),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN creation disabled OR no mobile wallet WHEN buildAll THEN no under section text`() {
|
||||
// Arrange — creation enabled but no mobile wallet
|
||||
every { hotWalletRestrictionManager.isCreationEnabledSync() } returns true
|
||||
|
||||
// Act
|
||||
val noMobileWallet = buildAll(hasAnyMobileWallet = false)
|
||||
|
||||
// Arrange — has mobile wallet but creation disabled
|
||||
every { hotWalletRestrictionManager.isCreationEnabledSync() } returns false
|
||||
val creationDisabled = buildAll(hasAnyMobileWallet = true)
|
||||
|
||||
// Assert
|
||||
assertThat(noMobileWallet.filterIsInstance<DetailsItemUM.UnderSectionText>()).isEmpty()
|
||||
assertThat(creationDisabled.filterIsInstance<DetailsItemUM.UnderSectionText>()).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN any flags WHEN buildAll THEN unconditional blocks are always present`() {
|
||||
// Act — most restrictive combination: nothing optional is added
|
||||
val result = buildAll(
|
||||
isWalletConnectAvailable = false,
|
||||
isAddressBookAvailable = false,
|
||||
isSupportChatAvailable = false,
|
||||
hasAnyMobileWallet = false,
|
||||
)
|
||||
|
||||
// Assert
|
||||
assertThat(result.map { it.id }).containsAtLeast(
|
||||
"user_wallet_list",
|
||||
"shop",
|
||||
"settings",
|
||||
"support",
|
||||
)
|
||||
assertThat(result.filterIsInstance<DetailsItemUM.UserWalletList>()).hasSize(1)
|
||||
|
||||
val shop = result.single { it.id == "shop" } as DetailsItemUM.Basic
|
||||
assertThat(shop.items.map { it.id }).containsExactly("buy_tangem_wallet")
|
||||
|
||||
val support = result.single { it.id == "support" } as DetailsItemUM.Basic
|
||||
assertThat(support.items.map { it.id }).containsExactly("support_email", "disclaimer").inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN shop block WHEN addTangemPayItem THEN tangem pay item appended to shop only`() {
|
||||
// Arrange
|
||||
val onClick: () -> Unit = {}
|
||||
val items: ImmutableList<DetailsItemUM> = persistentListOf(
|
||||
basicBlock(id = "shop", itemIds = listOf("buy_tangem_wallet")),
|
||||
basicBlock(id = "settings", itemIds = listOf("app_settings")),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = itemsBuilder.addTangemPayItem(items = items, onClick = onClick)
|
||||
|
||||
// Assert
|
||||
val shop = result.single { it.id == "shop" } as DetailsItemUM.Basic
|
||||
assertThat(shop.items.map { it.id }).containsExactly("buy_tangem_wallet", "get_tangem_pay").inOrder()
|
||||
|
||||
val settings = result.single { it.id == "settings" } as DetailsItemUM.Basic
|
||||
assertThat(settings.items.map { it.id }).containsExactly("app_settings")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN shop block with tangem pay item WHEN removeTangemPayItem THEN item removed from shop only`() {
|
||||
// Arrange
|
||||
val items: ImmutableList<DetailsItemUM> = persistentListOf(
|
||||
basicBlock(id = "shop", itemIds = listOf("buy_tangem_wallet", "get_tangem_pay")),
|
||||
basicBlock(id = "settings", itemIds = listOf("app_settings")),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = itemsBuilder.removeTangemPayItem(items = items)
|
||||
|
||||
// Assert
|
||||
val shop = result.single { it.id == "shop" } as DetailsItemUM.Basic
|
||||
assertThat(shop.items.map { it.id }).containsExactly("buy_tangem_wallet")
|
||||
|
||||
val settings = result.single { it.id == "settings" } as DetailsItemUM.Basic
|
||||
assertThat(settings.items.map { it.id }).containsExactly("app_settings")
|
||||
}
|
||||
|
||||
private fun buildAll(
|
||||
isWalletConnectAvailable: Boolean = false,
|
||||
isAddressBookAvailable: Boolean = false,
|
||||
isSupportChatAvailable: Boolean = false,
|
||||
hasAnyMobileWallet: Boolean = false,
|
||||
): ImmutableList<DetailsItemUM> = itemsBuilder.buildAll(
|
||||
isWalletConnectAvailable = isWalletConnectAvailable,
|
||||
isAddressBookAvailable = isAddressBookAvailable,
|
||||
isSupportChatAvailable = isSupportChatAvailable,
|
||||
hasAnyMobileWallet = hasAnyMobileWallet,
|
||||
userWalletId = USER_WALLET_ID,
|
||||
onSupportEmailClick = {},
|
||||
onSupportChatClick = {},
|
||||
onBuyClick = {},
|
||||
)
|
||||
|
||||
private fun basicBlock(id: String, itemIds: List<String>): DetailsItemUM.Basic = DetailsItemUM.Basic(
|
||||
id = id,
|
||||
items = itemIds.map { itemId ->
|
||||
DetailsItemUM.Basic.Item(
|
||||
id = itemId,
|
||||
block = BlockUM(
|
||||
text = resourceReference(R.string.common_unknown_error),
|
||||
iconRes = R.drawable.ic_tangem_24,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
}.toImmutableList(),
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val USER_WALLET_ID = UserWalletId("011")
|
||||
}
|
||||
}
|
||||
|
|
@ -188,6 +188,9 @@ include(":libs:tangem-sdk-api")
|
|||
// endregion Libs modules
|
||||
|
||||
// region Feature modules
|
||||
include(":features:address-book:api")
|
||||
include(":features:address-book:impl")
|
||||
|
||||
include(":features:onboarding-v2:api")
|
||||
include(":features:onboarding-v2:impl")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue