Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-02 18:20:50 +07:00
parent 3b1df52676
commit e8a478c8b4
13 changed files with 475 additions and 38 deletions

View file

@ -261,6 +261,7 @@
<string name="common_left">残り%1$s</string>
<string name="common_legacy_bitcoin_address">レガシービットコイン</string>
<string name="common_locked">ロックされています</string>
<string name="common_locked_wallets">ロックされたウォレット</string>
<string name="common_main_network">メインネットワーク</string>
<string name="common_month"></string>
<string name="common_network_fee_title">ネットワーク手数料</string>
@ -310,6 +311,7 @@
<string name="common_sign">署名</string>
<string name="common_sign_and_send">署名して送信</string>
<string name="common_skip">スキップ</string>
<string name="common_something_went_wrong">問題が発生しました</string>
<string name="common_stake">ステーキング</string>
<string name="common_staking">ステーキング</string>
<string name="common_start">始める</string>
@ -1146,7 +1148,7 @@
<string name="staking_notification_restake_rewards_text">獲得した報酬をステーキングに再投資し、潜在的な収益を増やします。</string>
<string name="staking_notification_restake_text">再ステーキングを使うと、ステーキングを解除することなく、あるバリデータから別のバリデータに資金を移動できます。</string>
<string name="staking_notification_stake_entire_balance_text">残高のすべてをステーキングしようとしています。ステーキング解除や報酬請求にかかるネットワーク手数料をカバーするために、少額を残しておくことをお勧めします。</string>
<string name="staking_notification_ton_activate_account">TONでステーキングを開始するには、まず任意の金額の送金を行ってください。これにより、ウォレットがアクティブになります。</string>
<string name="staking_notification_ton_activate_account">TONのステーキングを開始するには、まず自分のアドレスに少額の取引を送信します。これによりウォレットが有効になります。</string>
<string name="staking_notification_ton_extra_reserve_info">取引を完了するには、ネットワーク手数料に加えて最大0.2TONが必要になる場合があります。未使用分は返金されます。</string>
<string name="staking_notification_ton_extra_reserve_is_required">この操作を続行するには、ネットワーク手数料に加えて0.2 TONが必要です。残高を補充してください。</string>
<string name="staking_notification_ton_extra_reserve_title">TONの準備金が必要です</string>

View file

@ -4,6 +4,10 @@ import com.tangem.domain.account.status.producer.MultiAccountStatusListProducer
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCase
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -35,4 +39,18 @@ internal object AccountStatusListSupplierModule {
keyCreator = { "multi_account_status_list" },
) {}
}
@Provides
@Singleton
fun provideGetAccountCurrencyByAddressUseCase(
userWalletsListRepository: UserWalletsListRepository,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
singleAccountListSupplier: SingleAccountListSupplier,
): GetAccountCurrencyByAddressUseCase {
return GetAccountCurrencyByAddressUseCase(
userWalletsListRepository = userWalletsListRepository,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
singleAccountListSupplier = singleAccountListSupplier,
)
}
}

View file

@ -1,11 +1,11 @@
package com.tangem.features.account
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWallet
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@ -20,9 +20,7 @@ interface PortfolioSelectorComponent : ComposableBottomSheetComponent, Composabl
val controller: PortfolioSelectorController,
)
interface Factory {
fun create(appComponentContext: AppComponentContext, params: Params): PortfolioSelectorComponent
}
interface Factory : ComponentFactory<Params, PortfolioSelectorComponent>
}
/**
@ -30,8 +28,8 @@ interface PortfolioSelectorComponent : ComposableBottomSheetComponent, Composabl
*/
interface PortfolioSelectorController {
val isAccountMode: Flow<Boolean>
val selectedAccount: Flow<AccountId?>
val selectedAccount: StateFlow<AccountId?>
fun selectAccount(accountId: AccountId?)
fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow<Pair<UserWallet, Account>?>
fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow<Pair<UserWallet, AccountStatus>?>
}

View file

@ -0,0 +1,28 @@
package com.tangem.features.account.di
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.account.PortfolioSelectorController
import com.tangem.features.account.fetcher.DefaultPortfolioFetcher
import com.tangem.features.account.selector.DefaultPortfolioSelectorComponent
import com.tangem.features.account.selector.DefaultPortfolioSelectorController
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal interface AccountFeatureModule {
@Binds
fun bindPortfolioFetcherFactory(impl: DefaultPortfolioFetcher.Factory): PortfolioFetcher.Factory
@Binds
fun bindPortfolioSelectorController(impl: DefaultPortfolioSelectorController): PortfolioSelectorController
@Binds
fun bindPortfolioSelectorComponentFactory(
impl: DefaultPortfolioSelectorComponent.Factory,
): PortfolioSelectorComponent.Factory
}

View file

@ -1,8 +1,8 @@
package com.tangem.features.account.selector
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.account.PortfolioSelectorController
import com.tangem.features.account.PortfolioFetcher
@ -23,18 +23,18 @@ internal class DefaultPortfolioSelectorController @Inject constructor(
_selectedAccount.update { accountId }
}
override fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow<Pair<UserWallet, Account>?> =
override fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow<Pair<UserWallet, AccountStatus>?> =
combine(
flow = _selectedAccount,
flow2 = portfolioFetcher.data,
transform = { accountId, data ->
accountId ?: return@combine null
var result: Pair<UserWallet, Account>? = null
var result: Pair<UserWallet, AccountStatus>? = null
data.balances.forEach { wallet, balance ->
val accountStatuses = balance.accountsBalance.accountStatuses
.find { accountId == it.account.accountId }
if (accountStatuses != null) result = wallet to accountStatuses.account
if (accountStatuses != null) result = wallet to accountStatuses
}
return@combine result

View file

@ -1,11 +1,16 @@
package com.tangem.feature.referral.domain
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.referral.domain.models.ReferralData
import com.tangem.feature.referral.domain.models.TokenData
interface ReferralInteractor {
suspend fun getReferralStatus(userWalletId: UserWalletId): ReferralData
suspend fun startReferral(userWalletId: UserWalletId): ReferralData
suspend fun startReferral(portfolioId: PortfolioId): ReferralData
suspend fun getCryptoCurrency(userWalletId: UserWalletId, tokenData: TokenData): CryptoCurrency?
}

View file

@ -2,9 +2,11 @@ package com.tangem.feature.referral.domain
import arrow.core.getOrElse
import com.tangem.common.core.TangemSdkError
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.models.PortfolioId
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.referral.domain.errors.ReferralError
import com.tangem.feature.referral.domain.models.ReferralData
@ -31,10 +33,11 @@ internal class ReferralInteractorImpl(
return referralData
}
override suspend fun startReferral(userWalletId: UserWalletId): ReferralData {
override suspend fun startReferral(portfolioId: PortfolioId): ReferralData {
if (tokensForReferral.isEmpty()) error("Tokens for ref is empty")
val tokenData = tokensForReferral.first()
val userWalletId = portfolioId.userWalletId
val userWallet = getUserWalletUseCase(userWalletId).getOrElse {
error("Failed to get user wallet $userWalletId: $it")
}
@ -42,20 +45,27 @@ internal class ReferralInteractorImpl(
val cryptoCurrency = repository.getCryptoCurrency(userWalletId = userWallet.walletId, tokenData = tokenData)
?: error("Failed to create crypto currency")
// todo account different for account?
derivePublicKeysUseCase(userWallet.walletId, listOfNotNull(cryptoCurrency)).getOrElse {
Timber.e("Failed to derive public keys: $it")
throw it.mapToDomainError()
}
addCryptoCurrenciesUseCase(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
)
when (portfolioId) {
is PortfolioId.Account -> TODO("account")
is PortfolioId.Wallet -> addCryptoCurrenciesUseCase(
userWalletId = userWallet.walletId,
currency = cryptoCurrency,
)
}
val publicAddress = userWalletManager.getWalletAddress(
networkId = tokenData.networkId,
derivationPath = cryptoCurrency.network.derivationPath.value,
)
val publicAddress = when (portfolioId) {
is PortfolioId.Account -> TODO("account")
is PortfolioId.Wallet -> userWalletManager.getWalletAddress(
networkId = tokenData.networkId,
derivationPath = cryptoCurrency.network.derivationPath.value,
)
}
return repository.startReferral(
walletId = userWalletManager.getWalletId(),
@ -65,6 +75,9 @@ internal class ReferralInteractorImpl(
)
}
override suspend fun getCryptoCurrency(userWalletId: UserWalletId, tokenData: TokenData): CryptoCurrency? =
repository.getCryptoCurrency(userWalletId = userWalletId, tokenData = tokenData)
private fun saveReferralTokens(tokens: List<TokenData>) {
tokensForReferral.clear()
tokensForReferral.addAll(tokens)

View file

@ -13,6 +13,7 @@ android {
dependencies {
/** Api */
api(projects.features.referral.api)
api(projects.features.account.api)
/** Core modules */
implementation(projects.core.analytics)
@ -44,10 +45,18 @@ dependencies {
implementation(projects.domain.card)
implementation(projects.domain.wallets.models)
implementation(projects.domain.notifications.models)
implementation(projects.domain.account.status)
implementation(projects.domain.account)
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
implementation(projects.features.referral.domain)
/** Other libraries */
implementation(deps.compose.shimmer)
implementation(deps.kotlin.immutable.collections)
implementation(deps.decompose.ext.compose)
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.timber)

View file

@ -1,28 +1,55 @@
package com.tangem.feature.referral
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.feature.referral.api.ReferralComponent
import com.tangem.feature.referral.model.ReferralModel
import com.tangem.feature.referral.ui.ReferralScreen
import com.tangem.features.account.PortfolioSelectorComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
class DefaultReferralComponent @AssistedInject constructor(
private val portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory,
@Assisted appComponentContext: AppComponentContext,
@Assisted params: ReferralComponent.Params,
) : ReferralComponent, AppComponentContext by appComponentContext {
private val model: ReferralModel = getOrCreateModel(params)
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = null,
handleBackButton = false,
childFactory = { configuration, context -> bottomSheetChild(context) },
)
@Composable
override fun Content(modifier: Modifier) {
val bottomSheet by bottomSheetSlot.subscribeAsState()
ReferralScreen(stateHolder = model.uiState)
bottomSheet.child?.instance?.BottomSheet()
}
private fun bottomSheetChild(componentContext: ComponentContext): ComposableBottomSheetComponent =
portfolioSelectorComponentFactory.create(
context = childByContext(componentContext),
params = PortfolioSelectorComponent.Params(
portfolioFetcher = model.portfolioFetcher,
controller = model.portfolioSelectorController,
onDismiss = model.bottomSheetNavigation::dismiss,
),
)
@AssistedFactory
interface Factory : ReferralComponent.Factory {
override fun create(context: AppComponentContext, params: ReferralComponent.Params): DefaultReferralComponent

View file

@ -0,0 +1,66 @@
package com.tangem.feature.referral.model
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
import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.feature.referral.models.ReferralStateHolder.AccountAward
import com.tangem.utils.converter.Converter
internal class AccountAwardConverter(
private val isBalanceHidden: Boolean,
private val appCurrency: AppCurrency,
private val awardCryptoCurrency: CryptoCurrency,
private val accountAwardToken: CryptoCurrencyStatus?,
private val cryptoPortfolio: AccountStatus.CryptoPortfolio,
private val onAccountClick: () -> Unit,
) : Converter<Unit, AccountAward> {
override fun convert(value: Unit): AccountAward {
val tokenState = if (accountAwardToken != null) {
val currency = accountAwardToken.currency
TokenItemState.Content(
id = currency.id.value,
iconState = CryptoCurrencyToIconStateConverter().convert(currency),
titleState = TokenItemState.TitleState.Content(stringReference(currency.name)),
fiatAmountState = TokenItemState.FiatAmountState.Content(
text = accountAwardToken.getFormattedFiatAmount(appCurrency = appCurrency),
),
subtitle2State = TokenItemState.Subtitle2State.TextContent(
text = accountAwardToken.getFormattedCryptoAmount(),
isFlickering = accountAwardToken.value.isFlickering(),
),
subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(currency.symbol)),
onItemClick = null,
onItemLongClick = null,
)
} else {
val currency = awardCryptoCurrency
TokenItemState.Content(
id = currency.id.value,
iconState = CryptoCurrencyToIconStateConverter().convert(currency),
titleState = TokenItemState.TitleState.Content(stringReference(currency.name)),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = ""),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = ""),
subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(currency.symbol)),
onItemClick = null,
onItemLongClick = null,
)
}
return AccountAward(
accountName = cryptoPortfolio.account.accountName.toUM().value,
accountIcon = cryptoPortfolio.account.icon.toUM(),
onAccountClick = onAccountClick,
isBalanceHidden = isBalanceHidden,
tokenState = tokenState,
)
}
}

View file

@ -4,6 +4,9 @@ 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.tangem.common.routing.AppRouter
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -12,8 +15,20 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.model.AccountCryptoCurrency
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
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.referral.analytics.ReferralEvents
import com.tangem.feature.referral.api.ReferralComponent
@ -24,9 +39,11 @@ import com.tangem.feature.referral.domain.models.ReferralData
import com.tangem.feature.referral.domain.models.ReferralInfo
import com.tangem.feature.referral.models.DemoModeException
import com.tangem.feature.referral.models.ReferralStateHolder
import com.tangem.feature.referral.models.ReferralStateHolder.ErrorSnackbar
import com.tangem.feature.referral.models.ReferralStateHolder.ReferralInfoState
import com.tangem.feature.referral.models.ReferralStateHolder.*
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioSelectorController
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -42,12 +59,27 @@ internal class ReferralModel @Inject constructor(
private val urlOpener: UrlOpener,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
private val portfolioFetcherFactory: PortfolioFetcher.Factory,
val portfolioSelectorController: PortfolioSelectorController,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getAccountCurrencyByAddressUseCase: GetAccountCurrencyByAddressUseCase,
private val appRouter: AppRouter,
) : Model() {
private val params = paramsContainer.require<ReferralComponent.Params>()
val bottomSheetNavigation: SlotNavigation<Unit> = SlotNavigation()
val portfolioFetcher: PortfolioFetcher by lazy {
portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.Wallet(params.userWalletId),
scope = modelScope,
)
}
private var lastReferralData: ReferralData? = null
private var referralData: MutableStateFlow<ReferralData?> = MutableStateFlow(null)
private val lastReferralData get() = referralData.value
var uiState: ReferralStateHolder by mutableStateOf(createInitiallyUiState())
private set
@ -55,6 +87,47 @@ internal class ReferralModel @Inject constructor(
init {
analyticsEventHandler.send(ReferralEvents.ReferralScreenOpened)
loadReferralData()
combine(
flow = referralData.filterNotNull().onEach(::selectAccount),
flow2 = portfolioSelectorController.isAccountMode,
transform = { referralData, isAccountMode -> referralData to isAccountMode },
).transformLatest { (referralData, isAccountMode) ->
when (isAccountMode) {
false -> showContent(referralData)
true -> combineAccountUI(referralData)
.map { referralData to it }
.collect(::emit)
}
}
.onEach { (referralData, accountAward) -> showContent(referralData, accountAward) }
.launchIn(modelScope)
}
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
val cryptoPortfolio = when (selectedAccount) {
is AccountStatus.CryptoPortfolio -> selectedAccount
}
val accountAwardToken = cryptoPortfolio.tokenList.flattenCurrencies()
.find { it.currency.id == awardCryptoCurrency.id }
return@mapLatest AccountAwardConverter(
isBalanceHidden = isBalanceHidden,
awardCryptoCurrency = awardCryptoCurrency,
accountAwardToken = accountAwardToken,
cryptoPortfolio = cryptoPortfolio,
appCurrency = appCurrency,
onAccountClick = { bottomSheetNavigation.activate(Unit) },
).convert(Unit)
}
private fun createInitiallyUiState() = ReferralStateHolder(
@ -75,10 +148,9 @@ internal class ReferralModel @Inject constructor(
modelScope.launch {
runCatching {
referralInteractor.getReferralStatus(params.userWalletId).apply {
lastReferralData = this
referralData.value = this
}
}
.onSuccess(::showContent)
.onFailure(::showErrorSnackbar)
}
}
@ -92,16 +164,19 @@ internal class ReferralModel @Inject constructor(
analyticsEventHandler.send(ReferralEvents.ClickParticipate)
uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading)
modelScope.launch {
runCatching { referralInteractor.startReferral(params.userWalletId) }
val lastInfoState = uiState.referralInfoState
val portfolioId = when (accountsFeatureToggles.isFeatureEnabled) {
true -> PortfolioId(requireNotNull(portfolioSelectorController.selectedAccount.value))
false -> PortfolioId(params.userWalletId)
}
runCatching { referralInteractor.startReferral(portfolioId) }
.onSuccess {
analyticsEventHandler.send(ReferralEvents.ParticipateSuccessful)
showContent(it)
referralData.value = it
}
.onFailure { throwable ->
if (throwable is ReferralError.UserCancelledException) {
lastReferralData?.let { referralData ->
showContent(referralData)
}
uiState = uiState.copy(referralInfoState = lastInfoState)
} else {
showErrorSnackbar(throwable)
}
@ -110,8 +185,8 @@ internal class ReferralModel @Inject constructor(
}
}
private fun showContent(referralData: ReferralData) {
uiState = uiState.copy(referralInfoState = referralData.convertToReferralInfoState())
private fun showContent(referralData: ReferralData, accountAward: AccountAward? = null) {
uiState = uiState.copy(referralInfoState = referralData.convertToReferralInfoState(accountAward))
}
private fun showErrorSnackbar(throwable: Throwable) {
@ -136,7 +211,7 @@ internal class ReferralModel @Inject constructor(
shareManager.shareText(text = text)
}
private fun ReferralData.convertToReferralInfoState(): ReferralInfoState = when (this) {
private fun ReferralData.convertToReferralInfoState(accountAward: AccountAward?): ReferralInfoState = when (this) {
is ReferralData.ParticipantData -> ReferralInfoState.ParticipantContent(
award = getAwardValue(),
networkName = getNetworkName(),
@ -147,6 +222,7 @@ internal class ReferralModel @Inject constructor(
shareLink = referral.shareLink,
url = tosLink,
expectedAwards = expectedAwards,
accountAward = accountAward,
)
is ReferralData.NonParticipantData -> {
val userWallet = getUserWalletUseCase(params.userWalletId).getOrNull() ?: error("User wallet not found")
@ -158,6 +234,7 @@ internal class ReferralModel @Inject constructor(
url = tosLink,
onParticipateClicked = ::participate,
participateButtonIcon = walletInterationIcon(userWallet),
accountAward = accountAward,
)
}
}
@ -166,6 +243,39 @@ internal class ReferralModel @Inject constructor(
private fun ReferralData.getNetworkName(): String = getToken().networkId.replaceFirstChar(Char::uppercase)
private fun walletAccounts(walletId: UserWalletId): Flow<AccountStatusList> = singleAccountStatusListSupplier(
SingleAccountStatusListProducer.Params(walletId),
)
private suspend fun findAccountByAddress(address: String): AccountCryptoCurrency? {
return getAccountCurrencyByAddressUseCase(address).getOrNull()
}
private suspend fun selectAccount(referralData: ReferralData) {
when (referralData) {
is ReferralData.NonParticipantData -> if (portfolioSelectorController.selectedAccount.value == null) {
portfolioSelectorController.selectAccount(
accountId = walletAccounts(params.userWalletId).first().mainAccount.account.accountId,
)
}
is ReferralData.ParticipantData -> portfolioSelectorController.selectAccount(
accountId = findAccountByAddress(referralData.referral.address)?.account?.accountId,
)
}
}
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,6 +1,9 @@
package com.tangem.feature.referral.models
import androidx.annotation.DrawableRes
import com.tangem.common.ui.account.CryptoPortfolioIconUM
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(
@ -17,6 +20,7 @@ internal data class ReferralStateHolder(
val networkName: String
val discount: String
val url: String
val accountAward: AccountAward?
}
sealed interface ReferralInfoState {
@ -29,6 +33,7 @@ internal data class ReferralStateHolder(
val code: String,
val shareLink: String,
override val url: String,
override val accountAward: AccountAward? = null,
val expectedAwards: ExpectedAwards?,
) : ReferralInfoState, ReferralInfoContentState
@ -37,6 +42,7 @@ internal data class ReferralStateHolder(
override val networkName: String,
override val discount: String,
override val url: String,
override val accountAward: AccountAward? = null,
@DrawableRes val participateButtonIcon: Int?,
val onParticipateClicked: () -> Unit,
) : ReferralInfoState, ReferralInfoContentState
@ -54,4 +60,12 @@ internal data class ReferralStateHolder(
val onCopyClicked: () -> Unit,
val onShareClicked: (String) -> Unit,
)
data class AccountAward(
val tokenState: TokenItemState,
val isBalanceHidden: Boolean,
val accountName: TextReference,
val accountIcon: CryptoPortfolioIconUM,
val onAccountClick: () -> Unit,
)
}

View file

@ -22,17 +22,29 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
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.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
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
@ -43,6 +55,7 @@ import com.tangem.feature.referral.models.ReferralStateHolder
import com.tangem.feature.referral.models.ReferralStateHolder.*
import com.tangem.feature.referral.presentation.R
import kotlinx.coroutines.launch
import java.util.UUID
/**
* Referral program screen for participant and non-participant
@ -179,7 +192,13 @@ private fun ReferralInfo(
is ReferralInfoState.NonParticipantContent -> {
Conditions(state = state)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing44))
if (state.accountAward != null) {
SpacerH16()
NonParticipateAccountAwardBlock(state.accountAward)
SpacerH24()
} else {
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing44))
}
NonParticipateBottomBlock(
onAgreementClick = onAgreementClick,
onParticipateClick = state.onParticipateClicked,
@ -366,6 +385,80 @@ private fun ShimmerInfo() {
}
}
@Composable
private fun NonParticipateAccountAwardBlock(accountAward: AccountAward) {
Column(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.background(
color = TangemTheme.colors.background.primary,
shape = RoundedCornerShape(TangemTheme.dimens.spacing16),
),
) {
Text(
modifier = Modifier.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
top = TangemTheme.dimens.spacing12,
),
text = stringResourceSafe(R.string.referral_address_for_rewards),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
TokenItem(
state = accountAward.tokenState,
isBalanceHidden = accountAward.isBalanceHidden,
)
HorizontalDivider(
thickness = 0.5.dp,
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,
)
}
}
}
private fun Resources.getMessageForErrorSnackbar(throwable: Throwable): String {
return when {
throwable is DemoModeException -> getStringSafe(R.string.alert_demo_feature_disabled)
@ -479,6 +572,60 @@ private fun Preview_ReferralScreen_NonParticipant() {
}
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_ReferralScreen_NonParticipantAccount() {
TangemThemePreview {
ReferralScreen(
stateHolder = ReferralStateHolder(
headerState = HeaderState(onBackClicked = {}),
referralInfoState = ReferralInfoState.NonParticipantContent(
award = "10 USDT",
networkName = "Tron",
discount = "10%",
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 = {},
),
),
),
errorSnackbar = null,
analytics = Analytics(
onAgreementClicked = {},
onCopyClicked = {},
onShareClicked = {},
),
),
)
}
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable