Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-28 15:57:21 +03:00
commit 31be70ecf3
529 changed files with 15730 additions and 4170 deletions

View file

@ -17,6 +17,7 @@ import com.tangem.features.account.PortfolioSelectorComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.serialization.builtins.serializer
class DefaultReferralComponent @AssistedInject constructor(
private val portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory,
@ -27,7 +28,7 @@ class DefaultReferralComponent @AssistedInject constructor(
private val model: ReferralModel = getOrCreateModel(params)
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = null,
serializer = Unit.serializer(),
handleBackButton = false,
childFactory = { configuration, context -> bottomSheetChild(context) },
)

View file

@ -1,5 +1,6 @@
package com.tangem.feature.referral.model
import com.tangem.common.ui.account.PortfolioSelectUM
import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedCryptoAmount
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedFiatAmount
@ -14,8 +15,10 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.feature.referral.models.ReferralStateHolder.AccountAward
import com.tangem.utils.converter.Converter
@Suppress("LongParameterList")
internal class AccountAwardConverter(
private val isBalanceHidden: Boolean,
private val isSingleAccount: Boolean,
private val appCurrency: AppCurrency,
private val awardCryptoCurrency: CryptoCurrency,
private val accountAwardToken: CryptoCurrencyStatus?,
@ -56,11 +59,15 @@ internal class AccountAwardConverter(
}
return AccountAward(
accountName = cryptoPortfolio.account.accountName.toUM().value,
accountIcon = cryptoPortfolio.account.icon.toUM(),
onAccountClick = onAccountClick,
isBalanceHidden = isBalanceHidden,
tokenState = tokenState,
accountSelectUM = PortfolioSelectUM(
icon = cryptoPortfolio.account.icon.toUM(),
name = cryptoPortfolio.account.accountName.toUM().value,
isAccountMode = true,
onClick = onAccountClick,
isMultiChoice = !isSingleAccount,
),
)
}
}

View file

@ -4,7 +4,6 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import arrow.core.getOrElse
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
@ -23,7 +22,6 @@ import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.PortfolioId
@ -87,8 +85,8 @@ internal class ReferralModel @Inject constructor(
private set
val portfolioSelectorCallback = object : PortfolioSelectorComponent.BottomSheetCallback {
override val onDismiss: () -> Unit = { bottomSheetNavigation::dismiss }
override val onBack: () -> Unit = { bottomSheetNavigation::dismiss }
override val onDismiss: () -> Unit = { bottomSheetNavigation.dismiss() }
override val onBack: () -> Unit = { bottomSheetNavigation.dismiss() }
}
init {
@ -117,39 +115,45 @@ internal class ReferralModel @Inject constructor(
}
private fun combineAccountUI(referralData: ReferralData): Flow<AccountAward?> = combine(
flow = portfolioSelectorController.selectedAccountWithData(portfolioFetcher),
flow2 = getBalanceHidingSettingsFlow(),
flow3 = appCurrencyFlow(),
) { a, b, c -> Triple(a, b, c) }.mapLatest { triple ->
val (_, selectedAccount) = triple.first ?: return@mapLatest null
val isBalanceHidden = triple.second
val appCurrency: AppCurrency = triple.third
val awardCryptoCurrency = referralInteractor.getCryptoCurrency(
userWalletId = params.userWalletId,
tokenData = referralData.getToken(),
) ?: return@mapLatest null
flow = portfolioSelectorController.selectedAccountWithData(portfolioFetcher)
.onEach { bottomSheetNavigation.dismiss() },
flow2 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
flow3 = getSelectedAppCurrencyUseCase.invokeOrDefault(),
flow4 = portfolioFetcher.data,
) { pair, isBalanceHidden, appCurrency, portfolios ->
val selectedAccount = pair?.second ?: return@combine null
val cryptoPortfolio = when (selectedAccount) {
is AccountStatus.CryptoPortfolio -> selectedAccount
}
val awardCryptoCurrency = referralInteractor.getCryptoCurrency(
userWalletId = params.userWalletId,
tokenData = referralData.getToken(),
accountIndex = cryptoPortfolio.account.derivationIndex,
) ?: return@combine null
val accountAwardToken = cryptoPortfolio.tokenList.flattenCurrencies()
.find { it.currency.id == awardCryptoCurrency.id }
return@mapLatest AccountAwardConverter(
return@combine AccountAwardConverter(
isBalanceHidden = isBalanceHidden,
awardCryptoCurrency = awardCryptoCurrency,
accountAwardToken = accountAwardToken,
cryptoPortfolio = cryptoPortfolio,
appCurrency = appCurrency,
isSingleAccount = portfolios.isSingleChoice,
onAccountClick = { bottomSheetNavigation.activate(Unit) },
).convert(Unit)
}
private fun createInitiallyUiState() = ReferralStateHolder(
headerState = ReferralStateHolder.HeaderState(
headerState = HeaderState(
onBackClicked = appRouter::pop,
),
referralInfoState = ReferralInfoState.Loading,
errorSnackbar = null,
analytics = ReferralStateHolder.Analytics(
analytics = Analytics(
onAgreementClicked = ::onAgreementClicked,
onCopyClicked = ::onCopyClicked,
onShareClicked = ::onShareClicked,
@ -277,18 +281,6 @@ internal class ReferralModel @Inject constructor(
}
}
private fun getBalanceHidingSettingsFlow(): Flow<Boolean> {
return getBalanceHidingSettingsUseCase()
.map { it.isBalanceHidden }
.distinctUntilChanged()
}
private fun appCurrencyFlow(): Flow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map { it.getOrElse { AppCurrency.Default } }
.distinctUntilChanged()
}
@Suppress("MagicNumber")
private fun ReferralInfo.getAddressValue(): String {
check(address.length > 5) { "Invalid address" }

View file

@ -1,9 +1,8 @@
package com.tangem.feature.referral.models
import androidx.annotation.DrawableRes
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.common.ui.account.PortfolioSelectUM
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.referral.domain.models.ExpectedAwards
internal data class ReferralStateHolder(
@ -64,8 +63,6 @@ internal data class ReferralStateHolder(
data class AccountAward(
val tokenState: TokenItemState,
val isBalanceHidden: Boolean,
val accountName: TextReference,
val accountIcon: CryptoPortfolioIconUM,
val onAccountClick: () -> Unit,
val accountSelectUM: PortfolioSelectUM,
)
}

View file

@ -10,25 +10,33 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountIcon
import com.tangem.core.res.getStringSafe
import com.tangem.core.ui.components.PrimaryButtonIconStart
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerW12
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.rows.RoundableCornersRow
import com.tangem.core.ui.extensions.pluralStringResourceSafe
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.referral.domain.models.ExpectedAward
import com.tangem.feature.referral.domain.models.ExpectedAwards
import com.tangem.feature.referral.models.ReferralStateHolder
import com.tangem.feature.referral.presentation.R
import kotlinx.coroutines.launch
@ -43,6 +51,7 @@ internal fun ParticipateBottomBlock(
onAgreementClick: () -> Unit,
onCopyClick: () -> Unit,
onShareClick: (String) -> Unit,
accountAward: ReferralStateHolder.AccountAward?,
) {
Column(
modifier = Modifier
@ -53,7 +62,7 @@ internal fun ParticipateBottomBlock(
.padding(horizontal = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
PersonalCodeCard(code = code)
PersonalCodeCard(code = code, accountAward = accountAward)
AdditionalButtons(
code = code,
shareLink = shareLink,
@ -242,19 +251,13 @@ private fun ExtraItems(extraItems: List<ExpectedAward>, overallItemsLastIndex: I
}
@Composable
private fun PersonalCodeCard(code: String) {
private fun PersonalCodeCard(code: String, accountAward: ReferralStateHolder.AccountAward?) {
Column(
modifier = Modifier
.shadow(
elevation = TangemTheme.dimens.elevation2,
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
)
.background(
color = TangemTheme.colors.background.secondary,
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
)
.clip(RoundedCornerShape(TangemTheme.dimens.radius12))
.background(color = TangemTheme.colors.background.primary)
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens.spacing12),
.padding(top = TangemTheme.dimens.spacing12),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
@ -270,6 +273,53 @@ private fun PersonalCodeCard(code: String) {
maxLines = 1,
style = TangemTheme.typography.h2,
)
if (accountAward != null) {
AwardAccount(accountAward)
} else {
SpacerH12()
}
}
}
@Composable
private fun AwardAccount(accountAward: ReferralStateHolder.AccountAward) {
Column {
HorizontalDivider(
thickness = 0.5.dp,
color = TangemTheme.colors.stroke.primary,
modifier = Modifier.padding(horizontal = 12.dp),
)
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f),
text = stringResourceSafe(R.string.account_for_rewards),
style = TangemTheme.typography.body1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = TangemTheme.colors.text.secondary,
)
SpacerW12()
val icon = accountAward.accountSelectUM.icon
if (icon != null) {
AccountIcon(
name = accountAward.accountSelectUM.name,
icon = icon,
size = AccountIconSize.Small,
)
}
Text(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 4.dp),
text = accountAward.accountSelectUM.name.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
}
}
}
@ -351,10 +401,11 @@ private fun ParticipateBottomBlockPreview(
code = data.code,
shareLink = data.shareLink,
expectedAwards = data.expectedAwards,
onAgreementClick = data.onAgreementClick,
snackbarHostState = SnackbarHostState(),
onAgreementClick = data.onAgreementClick,
onCopyClick = data.onCopyClick,
onShareClick = data.onShareClick,
accountAward = data.accountAward,
)
}
}
@ -410,12 +461,18 @@ private class ParticipateBottomBlockDataProvider : CollectionPreviewParameterPro
purchasedWalletCount = 0,
expectedAwards = null,
),
ParticipateBottomBlockData(
purchasedWalletCount = 0,
expectedAwards = null,
accountAward = accountReward,
),
),
)
private data class ParticipateBottomBlockData(
val purchasedWalletCount: Int,
val expectedAwards: ExpectedAwards?,
val accountAward: ReferralStateHolder.AccountAward? = null,
val code: String = "x4JDK",
val shareLink: String = "",
val onAgreementClick: () -> Unit = {},

View file

@ -15,6 +15,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
@ -26,14 +27,15 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountIcon
import com.tangem.common.ui.account.AccountIconPreviewData
import com.tangem.common.ui.account.PortfolioSelectRow
import com.tangem.common.ui.account.PortfolioSelectUM
import com.tangem.common.ui.account.toUM
import com.tangem.core.res.getStringSafe
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar
@ -41,13 +43,13 @@ import com.tangem.core.ui.components.snackbar.TangemSnackbar
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.ReferralProgramScreenTestTags
import com.tangem.domain.models.account.AccountName
import com.tangem.feature.referral.domain.models.ExpectedAward
import com.tangem.feature.referral.domain.models.ExpectedAwards
import com.tangem.feature.referral.models.DemoModeException
@ -183,6 +185,7 @@ private fun ReferralInfo(
code = state.code,
shareLink = state.shareLink,
expectedAwards = state.expectedAwards,
accountAward = state.accountAward,
snackbarHostState = snackbarHostState,
onAgreementClick = onAgreementClick,
onCopyClick = stateHolder.analytics.onCopyClicked,
@ -390,10 +393,8 @@ private fun NonParticipateAccountAwardBlock(accountAward: AccountAward) {
Column(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.background(
color = TangemTheme.colors.background.primary,
shape = RoundedCornerShape(TangemTheme.dimens.spacing16),
),
.clip(RoundedCornerShape(TangemTheme.dimens.spacing16))
.background(color = TangemTheme.colors.background.primary),
) {
Text(
modifier = Modifier.padding(
@ -418,44 +419,7 @@ private fun NonParticipateAccountAwardBlock(accountAward: AccountAward) {
color = TangemTheme.colors.stroke.primary,
modifier = Modifier.padding(horizontal = 12.dp),
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f),
text = stringResourceSafe(R.string.account_details_title),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
AccountIcon(
name = accountAward.accountName,
icon = accountAward.accountIcon,
size = AccountIconSize.Small,
)
Text(
modifier = Modifier.padding(horizontal = 4.dp),
text = accountAward.accountName.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Icon(
modifier = Modifier.size(width = 18.dp, height = 24.dp),
painter = painterResource(id = R.drawable.ic_select_18_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
PortfolioSelectRow(accountAward.accountSelectUM)
}
}
@ -516,6 +480,7 @@ private fun Preview_ReferralScreen_Participant_With_Referrals() {
code = "x4JdK",
shareLink = "",
url = "",
accountAward = accountReward,
expectedAwards = ExpectedAwards(
numberOfWallets = 5,
expectedAwards = listOf(
@ -572,6 +537,41 @@ private fun Preview_ReferralScreen_NonParticipant() {
}
}
internal val account = PortfolioSelectUM(
icon = AccountIconPreviewData.randomAccountIcon(),
name = AccountName.DefaultMain.toUM().value,
isAccountMode = true,
isMultiChoice = true,
onClick = {},
)
internal val accountReward
get() = AccountAward(
accountSelectUM = account,
isBalanceHidden = false,
tokenState = TokenItemState.Content(
id = UUID.randomUUID().toString(),
iconState = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = R.drawable.img_tron_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
shouldShowCustomBadge = false,
),
titleState = TokenItemState.TitleState.Content(
text = stringReference(value = "Tether"),
),
fiatAmountState = FiatAmountState.Content(
text = "129,65 $",
),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "129,65 USDT"),
subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference("USDT")),
onItemClick = null,
onItemLongClick = null,
),
)
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
@ -587,33 +587,7 @@ private fun Preview_ReferralScreen_NonParticipantAccount() {
url = "",
onParticipateClicked = {},
participateButtonIcon = R.drawable.ic_tangem_24,
accountAward = AccountAward(
onAccountClick = {},
accountIcon = AccountIconPreviewData.randomAccountIcon(),
accountName = stringReference("Main Account"),
isBalanceHidden = false,
tokenState = TokenItemState.Content(
id = UUID.randomUUID().toString(),
iconState = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = R.drawable.img_tron_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
shouldShowCustomBadge = false,
),
titleState = TokenItemState.TitleState.Content(
text = stringReference(value = "Tether"),
),
fiatAmountState = FiatAmountState.Content(
text = "129,65 $",
),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "129,65 USDT"),
subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference("USDT")),
onItemClick = {},
onItemLongClick = {},
),
),
accountAward = accountReward,
),
errorSnackbar = null,
analytics = Analytics(