Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-24 11:42:34 +03:00
commit c8024b166d
656 changed files with 18270 additions and 4656 deletions

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.navigation.DummyRouter
import com.tangem.core.navigation.url.DummyUrlOpener
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.details.entity.DetailsFooterUM
import com.tangem.features.details.entity.DetailsUM
@ -28,6 +29,7 @@ internal class PreviewDetailsComponent : DetailsComponent {
val previewState = DetailsUM(
items = previewBlocks,
footer = previewFooter,
selectFeedbackEmailTypeBSConfig = TangemBottomSheetConfig.Empty,
popBack = { /* no-op */ },
)

View file

@ -29,7 +29,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
name = stringReference("My Wallet"),
information = getInformation(cardCount = 1),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = true,
onClick = {},
),
@ -38,7 +37,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
name = stringReference("Old wallet"),
information = getInformation(cardCount = 2),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = true,
onClick = {},
),
@ -47,7 +45,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
name = stringReference("Multi Card"),
information = getInformation(cardCount = 3),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = false,
onClick = {},
),

View file

@ -1,9 +1,11 @@
package com.tangem.features.details.entity
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import kotlinx.collections.immutable.ImmutableList
internal data class DetailsUM(
val items: ImmutableList<DetailsItemUM>,
val footer: DetailsFooterUM,
val selectFeedbackEmailTypeBSConfig: TangemBottomSheetConfig,
val popBack: () -> Unit,
)

View file

@ -0,0 +1,16 @@
package com.tangem.features.details.entity
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.details.impl.R
internal data class SelectEmailFeedbackTypeBS(
val onOptionClick: (Option) -> Unit,
) : TangemBottomSheetConfigContent {
enum class Option(val text: TextReference) {
General(resourceReference(R.string.common_contact_tangem_support)),
Visa(resourceReference(R.string.common_contact_visa_support)),
}
}

View file

@ -7,17 +7,22 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.common.TapWorkarounds.isVisa
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.details.entity.DetailsFooterUM
import com.tangem.features.details.entity.DetailsItemUM
import com.tangem.features.details.entity.DetailsUM
import com.tangem.features.details.entity.SelectEmailFeedbackTypeBS
import com.tangem.features.details.utils.ItemsBuilder
import com.tangem.features.details.utils.SocialsBuilder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -47,6 +52,7 @@ internal class DetailsModel @Inject constructor(
private val appStateHolder: ReduxStateHolder,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
@ -84,6 +90,7 @@ internal class DetailsModel @Inject constructor(
socials = socialsBuilder.buildAll(),
appVersion = getAppVersion(),
),
selectFeedbackEmailTypeBSConfig = TangemBottomSheetConfig.Empty,
popBack = router::pop,
),
)
@ -99,12 +106,88 @@ internal class DetailsModel @Inject constructor(
private fun sendFeedback() {
modelScope.launch {
val userWallets = getWalletsUseCase.invokeSync()
val scanResponse = getSelectedWalletSyncUseCase().getOrNull()?.scanResponse
?: error("Selected wallet is null")
val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(type = FeedbackEmailType.DirectUserRequest(cardInfo = cardInfo))
val feedbackType = when {
userWallets.all { it.scanResponse.card.isVisa } -> FeedbackEmailType.Visa.DirectUserRequest(cardInfo)
userWallets.all { it.scanResponse.card.isVisa.not() } -> FeedbackEmailType.DirectUserRequest(cardInfo)
else -> {
showFeedbackEmailTypeOptionBS(cardInfo)
return@launch
}
}
sendFeedbackEmailUseCase(feedbackType)
}
}
private fun showFeedbackEmailTypeOptionBS(selectedCardInfo: CardInfo) {
state.update {
it.copy(
selectFeedbackEmailTypeBSConfig = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {
state.update {
it.copy(
selectFeedbackEmailTypeBSConfig =
it.selectFeedbackEmailTypeBSConfig.copy(isShown = false),
)
}
},
content = SelectEmailFeedbackTypeBS(
onOptionClick = { option ->
onEmailFeedbackTypeOptionSelected(
selectedCardInfo = selectedCardInfo,
option = option,
)
state.update {
it.copy(
selectFeedbackEmailTypeBSConfig =
it.selectFeedbackEmailTypeBSConfig.copy(isShown = false),
)
}
},
),
),
)
}
}
private fun onEmailFeedbackTypeOptionSelected(
selectedCardInfo: CardInfo,
option: SelectEmailFeedbackTypeBS.Option,
) {
modelScope.launch {
val feedbackType = when (option) {
SelectEmailFeedbackTypeBS.Option.General -> {
if (selectedCardInfo.isVisa.not()) {
FeedbackEmailType.DirectUserRequest(selectedCardInfo)
} else {
val scanResponse = getWalletsUseCase.invokeSync()
.firstOrNull { it.scanResponse.card.isVisa.not() }?.scanResponse ?: return@launch
val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch
FeedbackEmailType.DirectUserRequest(cardInfo)
}
}
SelectEmailFeedbackTypeBS.Option.Visa -> {
if (selectedCardInfo.isVisa) {
FeedbackEmailType.Visa.DirectUserRequest(selectedCardInfo)
} else {
val scanResponse = getWalletsUseCase.invokeSync()
.firstOrNull { it.scanResponse.card.isVisa }?.scanResponse ?: return@launch
val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch
FeedbackEmailType.Visa.DirectUserRequest(cardInfo)
}
}
}
sendFeedbackEmailUseCase(feedbackType)
}
}

View file

@ -59,6 +59,8 @@ internal fun DetailsScreen(
userWalletListBlockContent = userWalletListBlockContent,
)
}
SelectFeedbackEmailTypeBottomSheet(state.selectFeedbackEmailTypeBSConfig)
}
@Composable

View file

@ -0,0 +1,58 @@
package com.tangem.features.details.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.inputrow.InputRowChecked
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.details.entity.SelectEmailFeedbackTypeBS
import com.tangem.features.details.impl.R
@Composable
internal fun SelectFeedbackEmailTypeBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<SelectEmailFeedbackTypeBS>(
config = config,
titleText = resourceReference(R.string.common_choose_action),
containerColor = TangemTheme.colors.background.tertiary,
content = { Content(it) },
)
}
@Composable
private fun Content(content: SelectEmailFeedbackTypeBS) {
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
SelectEmailFeedbackTypeBS.Option.entries.forEachIndexed { index, type ->
DividerContainer(
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = SelectEmailFeedbackTypeBS.Option.entries.lastIndex,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action)
.clickable { content.onOptionClick(type) },
showDivider = index != SelectEmailFeedbackTypeBS.Option.entries.lastIndex,
) {
InputRowChecked(
text = type.text,
checked = false,
)
}
}
}
}

View file

@ -18,19 +18,23 @@ import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.lce
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.core.utils.toLce
import com.tangem.domain.models.ArtworkModel
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.details.impl.R
import com.tangem.operations.attestation.ArtworkSize
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class UserWalletsFetcher @Inject constructor(
getWalletsUseCase: GetWalletsUseCase,
@ -39,8 +43,11 @@ internal class UserWalletsFetcher @Inject constructor(
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val router: Router,
private val messageSender: UiMessageSender,
private val getCardImageUseCase: GetCardImageUseCase,
) {
private var loadedArtworks: HashMap<UserWalletId, ArtworkModel> = hashMapOf()
@OptIn(ExperimentalCoroutinesApi::class)
val userWallets: Flow<ImmutableList<UserWalletItemUM>> = getWalletsUseCase().transformLatest { wallets ->
val uiModels = UserWalletItemUMConverter(onClick = ::navigateToWalletSettings).convertList(wallets)
@ -52,12 +59,14 @@ internal class UserWalletsFetcher @Inject constructor(
flow = getSelectedAppCurrencyUseCase().distinctUntilChanged(),
flow2 = getBalanceHidingSettingsUseCase().distinctUntilChanged(),
flow3 = getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(),
) { maybeAppCurrency, balanceHidingSettings, maybeBalances ->
flow4 = loadArtworks(wallets),
) { maybeAppCurrency, balanceHidingSettings, maybeBalances, artworks ->
val models = createUiModels(
wallets = wallets,
maybeAppCurrency = maybeAppCurrency,
maybeBalances = maybeBalances,
balanceHidingSettings = balanceHidingSettings,
artworks = artworks,
).getOrElse(
ifLoading = { return@combine },
ifError = {
@ -72,11 +81,27 @@ internal class UserWalletsFetcher @Inject constructor(
}.collect()
}
private fun loadArtworks(wallets: List<UserWallet>): Flow<HashMap<UserWalletId, ArtworkModel>> {
return flow {
emit(hashMapOf()) // emits right away so the transform doesn't wait for the images' loading to finish
wallets.forEach { wallet ->
val artwork = getCardImageUseCase(
cardId = wallet.cardId,
cardPublicKey = wallet.scanResponse.card.cardPublicKey,
size = ArtworkSize.SMALL,
)
loadedArtworks[wallet.walletId] = artwork
emit(loadedArtworks)
}
}
}
private fun createUiModels(
wallets: List<UserWallet>,
maybeAppCurrency: Either<SelectedAppCurrencyError, AppCurrency>,
maybeBalances: Lce<TokenListError, Map<UserWalletId, TotalFiatBalance>>,
balanceHidingSettings: BalanceHidingSettings,
artworks: HashMap<UserWalletId, ArtworkModel>,
): Lce<Error, ImmutableList<UserWalletItemUM>> = lce {
val balances = withError(
transform = { Error.UnableToGetBalances },
@ -99,6 +124,7 @@ internal class UserWalletsFetcher @Inject constructor(
appCurrency = appCurrency,
balance = balance,
isBalanceHidden = balanceHidingSettings.isBalanceHidden,
artwork = artworks[userWallet.walletId],
)
.convert(userWallet)
}