From e8a478c8b42963560f1c4fb368c54abd6d5ecba6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Oct 2025 18:20:50 +0700 Subject: [PATCH] Updated on 2026-08-14 --- core/res/src/main/res/values-ja/strings.xml | 4 +- .../di/AccountStatusListSupplierModule.kt | 18 +++ .../account/PortfolioSelectorComponent.kt | 12 +- .../account/di/AccountFeatureModule.kt | 28 ++++ .../DefaultPortfolioSelectorController.kt | 8 +- .../referral/domain/ReferralInteractor.kt | 7 +- .../referral/domain/ReferralInteractorImpl.kt | 35 ++-- features/referral/impl/build.gradle.kts | 9 ++ .../referral/DefaultReferralComponent.kt | 27 ++++ .../referral/model/AccountAwardConverter.kt | 66 ++++++++ .../feature/referral/model/ReferralModel.kt | 136 ++++++++++++++-- .../referral/models/ReferralStateHolder.kt | 14 ++ .../feature/referral/ui/ReferralScreen.kt | 149 +++++++++++++++++- 13 files changed, 475 insertions(+), 38 deletions(-) create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/di/AccountFeatureModule.kt create mode 100644 features/referral/impl/src/main/java/com/tangem/feature/referral/model/AccountAwardConverter.kt diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index a0dde9c0c3..bd9bd6be0d 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -261,6 +261,7 @@ 残り%1$s レガシービットコイン ロックされています + ロックされたウォレット メインネットワーク ネットワーク手数料 @@ -310,6 +311,7 @@ 署名 署名して送信 スキップ + 問題が発生しました ステーキング ステーキング 始める @@ -1146,7 +1148,7 @@ 獲得した報酬をステーキングに再投資し、潜在的な収益を増やします。 再ステーキングを使うと、ステーキングを解除することなく、あるバリデータから別のバリデータに資金を移動できます。 残高のすべてをステーキングしようとしています。ステーキング解除や報酬請求にかかるネットワーク手数料をカバーするために、少額を残しておくことをお勧めします。 - TONでステーキングを開始するには、まず任意の金額の送金を行ってください。これにより、ウォレットがアクティブになります。 + TONのステーキングを開始するには、まず自分のアドレスに少額の取引を送信します。これによりウォレットが有効になります。 取引を完了するには、ネットワーク手数料に加えて最大0.2TONが必要になる場合があります。未使用分は返金されます。 この操作を続行するには、ネットワーク手数料に加えて0.2 TONが必要です。残高を補充してください。 TONの準備金が必要です diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusListSupplierModule.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusListSupplierModule.kt index 51b10dc806..f71bbe7191 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusListSupplierModule.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusListSupplierModule.kt @@ -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, + ) + } } \ No newline at end of file diff --git a/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt index 825d41cf48..7216cd8ad7 100644 --- a/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt +++ b/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt @@ -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 } /** @@ -30,8 +28,8 @@ interface PortfolioSelectorComponent : ComposableBottomSheetComponent, Composabl */ interface PortfolioSelectorController { val isAccountMode: Flow - val selectedAccount: Flow + val selectedAccount: StateFlow fun selectAccount(accountId: AccountId?) - fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow?> + fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow?> } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/di/AccountFeatureModule.kt b/features/account/impl/src/main/java/com/tangem/features/account/di/AccountFeatureModule.kt new file mode 100644 index 0000000000..67ce3bbd1a --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/di/AccountFeatureModule.kt @@ -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 +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt b/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt index 1c55c39755..f607006e40 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt @@ -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?> = + override fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow?> = combine( flow = _selectedAccount, flow2 = portfolioFetcher.data, transform = { accountId, data -> accountId ?: return@combine null - var result: Pair? = null + var result: Pair? = 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 diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt index 2b752a2c1d..a673964cf8 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt @@ -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? } \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 9520d1847e..0e24966a47 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -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) { tokensForReferral.clear() tokensForReferral.addAll(tokens) diff --git a/features/referral/impl/build.gradle.kts b/features/referral/impl/build.gradle.kts index 13e30dd409..1ced33e61d 100644 --- a/features/referral/impl/build.gradle.kts +++ b/features/referral/impl/build.gradle.kts @@ -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) diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/DefaultReferralComponent.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/DefaultReferralComponent.kt index f30cefaab8..bb045bb975 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/DefaultReferralComponent.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/DefaultReferralComponent.kt @@ -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 diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/AccountAwardConverter.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/AccountAwardConverter.kt new file mode 100644 index 0000000000..1bcaa44999 --- /dev/null +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/AccountAwardConverter.kt @@ -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 { + + 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, + ) + } +} \ No newline at end of file diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt index 91384054bc..fbf7aa2332 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt @@ -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() + val bottomSheetNavigation: SlotNavigation = 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 = 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 = 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 = 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 { + return getBalanceHidingSettingsUseCase() + .map { it.isBalanceHidden } + .distinctUntilChanged() + } + + private fun appCurrencyFlow(): Flow { + return getSelectedAppCurrencyUseCase() + .map { it.getOrElse { AppCurrency.Default } } + .distinctUntilChanged() + } + @Suppress("MagicNumber") private fun ReferralInfo.getAddressValue(): String { check(address.length > 5) { "Invalid address" } diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt index 3631e0c996..616e7715ab 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt @@ -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, + ) } \ No newline at end of file diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index fcda66f563..3c1f14d3fc 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -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