Updated on 2026-08-14
This commit is contained in:
parent
3b1df52676
commit
e8a478c8b4
13 changed files with 475 additions and 38 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>?>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue