Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-02 20:46:41 +07:00
parent 82aae53d53
commit 38e82b9423
20 changed files with 198 additions and 142 deletions

View file

@ -979,6 +979,7 @@
<string name="staking_details_annual_percentage_rate">Porcentaje de rendimiento anual</string>
<string name="staking_details_annual_percentage_rate_info">El porcentaje de rendimiento anual que podrá ganar como participante en el staking.</string>
<string name="staking_details_apr">APR</string>
<string name="staking_details_apy">APY</string>
<string name="staking_details_auto_claiming_rewards_daily_text">Las recompensas se acumulan automáticamente en su saldo de staking diariamente.</string>
<string name="staking_details_available">Disponible</string>
<string name="staking_details_average_reward_rate">Tasa de recompensa promedio</string>

View file

@ -160,7 +160,7 @@
<string name="common_enable">Activer</string>
<string name="common_enabled">Activé</string>
<string name="common_error">Erreur</string>
<string name="common_exchange">Échangez</string>
<string name="common_exchange">Echanger</string>
<string name="common_explore">Explorez</string>
<string name="common_explore_transaction_history">Explorez l\'historique des transactions</string>
<string name="common_explorer">Explorateur</string>
@ -953,6 +953,7 @@
<string name="staking_details_annual_percentage_rate">Pourcentage de rendement annuel</string>
<string name="staking_details_annual_percentage_rate_info">Le pourcentage de rendement annuel que vous pouvez gagner en participant au staking.</string>
<string name="staking_details_apr">APR</string>
<string name="staking_details_apy">APY</string>
<string name="staking_details_auto_claiming_rewards_daily_text">Les récompenses s\'accumulent automatiquement sur votre solde de staking quotidiennement.</string>
<string name="staking_details_available">Disponible</string>
<string name="staking_details_average_reward_rate">Taux de récompense moyen</string>

View file

@ -911,6 +911,7 @@
<string name="receive_bottom_sheet_warning_message_full">このアドレスには%sのみを送金してください。他のトークンを送信すると、取り返しのつかない損失が発生します。</string>
<string name="receive_bottom_sheet_warning_title">%2$sネットワークの%1$sのみを送信してください</string>
<string name="receive_token_description">ウォレットや取引所から資金を送金する</string>
<string name="referral_address_for_rewards">報酬の送付先アドレス</string>
<string name="referral_button_participate">参加する</string>
<string name="referral_error_failed_to_load_info">紹介プログラムに関する情報を読み込めませんでした。しばらくしてからもう一度お試しください。</string>
<string name="referral_error_failed_to_load_info_with_reason">紹介プログラムに関する情報を読み込めませんでした。エラー コード: %s 。しばらくしてからもう一度お試しください。</string>
@ -1094,6 +1095,7 @@
<string name="staking_details_annual_percentage_rate">年率</string>
<string name="staking_details_annual_percentage_rate_info">ステーキングに参加することで得られる年間収益率。</string>
<string name="staking_details_apr">APR</string>
<string name="staking_details_apy">APY</string>
<string name="staking_details_auto_claiming_rewards_daily_text">報酬は毎日自動的にステーキング残高に蓄積されます。</string>
<string name="staking_details_available">利用可能</string>
<string name="staking_details_average_reward_rate">平均報酬率</string>
@ -1391,8 +1393,8 @@
<string name="wallet_connect_error_wrong_card_selected">Tangemアプリで誤ったカードまたはリングが選択されました</string>
<string name="wallet_connect_failed_to_build_tx">Dappデータから取引を作成できませんでした。コード: %s</string>
<string name="wallet_connect_generic_error_with_code">不明なエラーが発生しました。エラーコード: %d 。問題が解決しない場合は、お気軽にサポートにお問い合わせください。</string>
<string name="wallet_connect_multiple_transactions">複数の取引</string>
<string name="wallet_connect_multiple_transactions_description">このプロセスを完了するには、Tangemデバイスを数回タップする必要があります。</string>
<string name="wallet_connect_multiple_transactions">マルチパート取引</string>
<string name="wallet_connect_multiple_transactions_description">正常に処理するため、取引は複数に分割されます。完了するにはカードを数回タップする必要があります。</string>
<string name="wallet_connect_no_sessions_message">WalletConnectセッションが開かれていません</string>
<string name="wallet_connect_no_sessions_title">おっと。セッションがありません。</string>
<string name="wallet_connect_pairing_error">WalletConnectセッションのペアリングに失敗しました: %1$s</string>
@ -1404,8 +1406,8 @@
<string name="wallet_connect_scanner_error_not_valid_card">このカードは、WalletConnectセッションの確立には使用できません。</string>
<string name="wallet_connect_scanner_error_unsupported_network">このネットワークはサポートされていません。別のネットワークを選択してください。</string>
<string name="wallet_connect_select_network">ネットワークを選択</string>
<string name="wallet_connect_sending_multiple_explanation">取引を処理中です</string>
<string name="wallet_connect_sending_multiple_tx">送金しています...</string>
<string name="wallet_connect_sending_multiple_explanation">取引を処理中です。完了するにはカードを複数回タップしてください。</string>
<string name="wallet_connect_sending_multiple_tx">取引進行中</string>
<string name="wallet_connect_sessions_title">WalletConnectセッション</string>
<string name="wallet_connect_subtitle">dAppsに接続する</string>
<string name="wallet_connect_title">WalletConnect</string>
@ -1659,6 +1661,7 @@
<string name="yield_module_approve_sheet_title">承認を確定する</string>
<string name="yield_module_balance_info_sheet_subtitle">資産残高に関するテキスト [プレースホルダー]</string>
<string name="yield_module_balance_info_sheet_title">あなたの%sはAaveに預けられています</string>
<string name="yield_module_deposit_error_notification_title">受け取った金額%1$s %2$sはAaveに入金されませんでした。</string>
<string name="yield_module_earn_badge">%1$s%% を獲得</string>
<string name="yield_module_earn_sheet_available_title">利用可能</string>
<string name="yield_module_earn_sheet_current_apy_title">現在のAPY</string>
@ -1674,6 +1677,7 @@
<string name="yield_module_fee_policy_sheet_max_fee_note">ネットワーク手数料が上限手数料を超えた場合、手数料が下がるまで取引は成立しません。この制限は後で変更できます。</string>
<string name="yield_module_fee_policy_sheet_max_fee_title">最大手数料</string>
<string name="yield_module_fee_policy_sheet_title">手数料ポリシー</string>
<string name="yield_module_high_fee_error">ネットワーク手数料が現在高すぎます。設定した上限を下回るまで待機しています。</string>
<string name="yield_module_main_view_approve_notification_description">ここに説明を入力してください。1〜3行が理想的です。[プレースホルダー]</string>
<string name="yield_module_main_view_approve_notification_title">トークン承認が必要</string>
<string name="yield_module_network_fee_unreachable_notification_description">ネットワーク接続を確認してください</string>

View file

@ -1043,6 +1043,7 @@
<string name="staking_details_annual_percentage_rate">Процентная ставка</string>
<string name="staking_details_annual_percentage_rate_info">Годовой процентный доход, который вы можете получить от участия в стейкинге.</string>
<string name="staking_details_apr">APR</string>
<string name="staking_details_apy">APY</string>
<string name="staking_details_auto_claiming_rewards_daily_text">Награда автоматически аккумулируется на вашем стейкинг балансе.</string>
<string name="staking_details_available">Доступно</string>
<string name="staking_details_average_reward_rate">Средння ставка вознаграждения</string>

View file

@ -959,6 +959,7 @@
<string name="staking_details_annual_percentage_rate">Процентна ставка</string>
<string name="staking_details_annual_percentage_rate_info">Річний відсоток, який ви можете отримати, беручи участь у стейкінгу.</string>
<string name="staking_details_apr">APR</string>
<string name="staking_details_apy">APY</string>
<string name="staking_details_auto_claiming_rewards_daily_text">Винагороди автоматично накопичуються на вашому балансі щодня.</string>
<string name="staking_details_available">Доступно</string>
<string name="staking_details_average_reward_rate">Середня ставка винагороди</string>

View file

@ -33,6 +33,7 @@
<string name="account_form_create_button">Add account</string>
<string name="account_form_edit_button">Save</string>
<string name="account_form_name">Account name</string>
<string name="account_form_name_already_exist_error_description">Account name already exists</string>
<string name="account_form_placeholder_edit_account">Account</string>
<string name="account_form_placeholder_new_account">New account</string>
<string name="account_form_title_create">Add account</string>
@ -905,6 +906,7 @@
</plurals>
<string name="onramp_timing_minutes">%s min</string>
<string name="onramp_title_you_get">You get</string>
<string name="onramp_tos_external_providers">Service is provided by an external provider. \nTangem is not responsible.</string>
<string name="onramp_transaction_status_footer_text">You can close this screen and check the transaction status on the token details screen.</string>
<string name="onramp_up_to_rate">Up to</string>
<string name="onramp_via">Via</string>
@ -1116,7 +1118,10 @@
<string name="staking_details_account_fee_info">A staking account is a special account where staked SOL tokens are stored. It is created when you delegate your tokens to a validator to participate in transaction validation and receive rewards. A small fee is charged for creating the staking account, which is returned after the staking is completed.</string>
<string name="staking_details_annual_percentage_rate">Annual percentage rate</string>
<string name="staking_details_annual_percentage_rate_info">The annual percentage return you can earn from participating in staking.</string>
<string name="staking_details_annual_percentage_yield">Annual percentage yield</string>
<string name="staking_details_annual_percentage_yield_info">APY shows the validators annual percentage yield based on its performance</string>
<string name="staking_details_apr">APR</string>
<string name="staking_details_apy">APY</string>
<string name="staking_details_auto_claiming_rewards_daily_text">Rewards automatically accumulate in your staking balance daily.</string>
<string name="staking_details_available">Available</string>
<string name="staking_details_average_reward_rate">Average Reward Rate</string>
@ -1314,6 +1319,7 @@
<string name="token_details_token_type_subtitle_no_standard">Token in %%image%% %1$s network</string>
<string name="token_details_unable_hide_alert_message">The %1$s (%2$s) token is the main currency on the %3$s network and cannot be hidden as long as you have other tokens on this network in the list</string>
<string name="token_details_unable_hide_alert_title">Unable to hide %s</string>
<string name="token_receive_show_qr_code_title">Show QR code</string>
<string name="token_swap_changelly_promotion_message">Exchange this token for another at %1$s service fees from February %2$s-%3$s.</string>
<string name="token_swap_changelly_promotion_title">Swap with Changelly, %s fees</string>
<string name="token_swap_promotion_button">Swap now</string>
@ -1788,4 +1794,5 @@
<string name="yield_module_unable_to_cover_fee_title">Unable to cover %s fee</string>
<string name="yield_module_unavailable_subtitle">The interest service isnt available at the moment. Please try again later.</string>
<string name="yield_module_unavailable_title">Earnings unavailable</string>
<string name="yield_supply_chart_loading_error">Unable to load chart...</string>
</resources>

View file

@ -127,7 +127,7 @@ data class AccountList private constructor(
companion object {
private const val MAX_ACCOUNTS_COUNT = 20
const val MAX_ACCOUNTS_COUNT = 20
private const val MAX_MAIN_ACCOUNTS_COUNT = 1
/**

View file

@ -1,6 +1,7 @@
package com.tangem.domain.appcurrency
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.left
import arrow.core.right
import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError
@ -19,6 +20,10 @@ class GetSelectedAppCurrencyUseCase(
.onEmpty { emit(SelectedAppCurrencyError.NoAppCurrencySelected.left()) }
}
fun invokeOrDefault(): Flow<AppCurrency> = invoke()
.map { it.getOrElse { AppCurrency.Default } }
.distinctUntilChanged()
suspend fun invokeSync(): Either<SelectedAppCurrencyError, AppCurrency> {
return invoke().firstOrNull() ?: SelectedAppCurrencyError.NoAppCurrencySelected.left()
}

View file

@ -2,6 +2,8 @@ package com.tangem.domain.balancehiding
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
class GetBalanceHidingSettingsUseCase(
private val balanceHidingRepository: BalanceHidingRepository,
@ -10,4 +12,8 @@ class GetBalanceHidingSettingsUseCase(
operator fun invoke(): Flow<BalanceHidingSettings> {
return balanceHidingRepository.getBalanceHidingSettingsFlow()
}
fun isBalanceHidden(): Flow<Boolean> = invoke()
.map { it.isBalanceHidden }
.distinctUntilChanged()
}

View file

@ -78,10 +78,14 @@ internal class AccountCreateEditModel @Inject constructor(
isWarning = true,
onClick = { router.pop() },
)
val messageRes = when (params) {
is AccountCreateEditComponent.Params.Create -> R.string.account_unsaved_dialog_message_create
is AccountCreateEditComponent.Params.Edit -> R.string.account_unsaved_dialog_message_edit
}
messageSender.send(
DialogMessage(
title = resourceReference(R.string.account_unsaved_dialog_title),
message = resourceReference(R.string.account_unsaved_dialog_message_create),
message = resourceReference(messageRes),
firstActionBuilder = { firstAction },
secondActionBuilder = { secondAction },
),
@ -120,17 +124,15 @@ internal class AccountCreateEditModel @Inject constructor(
}
private fun handleAddAccountError(error: AddCryptoPortfolioUseCase.Error) {
if (error is AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet &&
error.cause is AccountList.Error.DuplicateAccountNames
) {
// TODO("account") show alert that the account name already exists
return
val duplicateAccountNames = (error as? AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet)
?.cause is AccountList.Error.DuplicateAccountNames
when {
duplicateAccountNames -> showAccountNameExist()
else -> {
showSomethingWrong()
logError(error = AccountFeatureError.CreateAccount.FailedToCreateAccount(cause = error))
}
}
// TODO: show alert https://www.figma.com/design/09KKG4ZVuFDZhj8WLv5rGJ/%F0%9F%9A%A7-App-experience?node-id=38882-113775&t=vk6TCy4MkYol1cPb-4
logError(
error = AccountFeatureError.CreateAccount.FailedToCreateAccount(cause = error),
)
}
private suspend fun editCryptoPortfolio(params: AccountCreateEditComponent.Params.Edit) {
@ -157,17 +159,15 @@ internal class AccountCreateEditModel @Inject constructor(
}
private fun handleEditAccountError(error: UpdateCryptoPortfolioUseCase.Error) {
if (error is UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet &&
error.cause is AccountList.Error.DuplicateAccountNames
) {
// TODO("account") show alert that the account name already exists
return
val duplicateAccountNames = (error as? UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet)
?.cause is AccountList.Error.DuplicateAccountNames
when {
duplicateAccountNames -> showAccountNameExist()
else -> {
showSomethingWrong()
logError(error = AccountFeatureError.EditAccount.FailedToEditAccount(cause = error))
}
}
// TODO: show alert like in create account flow
logError(
error = AccountFeatureError.EditAccount.FailedToEditAccount(cause = error),
)
}
private fun showMessage(@StringRes id: Int) {
@ -175,7 +175,10 @@ internal class AccountCreateEditModel @Inject constructor(
messageSender.send(ToastMessage(message = message))
}
private fun onCloseClick() = unsaveChangeDialog()
private fun onCloseClick() {
val showConfirmDialog = uiState.value.buttonState.isButtonEnabled
if (showConfirmDialog) unsaveChangeDialog() else router.pop()
}
private fun onIconSelect(icon: CryptoPortfolioIcon.Icon) {
uiState.value = uiState.value
@ -256,4 +259,20 @@ internal class AccountCreateEditModel @Inject constructor(
event = ExceptionAnalyticsEvent(exception = exception, params = params),
)
}
private fun showSomethingWrong() {
val dialogMessage = DialogMessage(
title = resourceReference(R.string.common_something_went_wrong),
message = resourceReference(R.string.account_could_not_create),
)
messageSender.send(dialogMessage)
}
private fun showAccountNameExist() {
val dialogMessage = DialogMessage(
title = resourceReference(R.string.common_something_went_wrong),
message = resourceReference(R.string.account_form_name_already_exist_error_description),
)
messageSender.send(dialogMessage)
}
}

View file

@ -19,6 +19,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -30,8 +32,8 @@ import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.fields.AutoSizeTextField
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
@ -43,9 +45,11 @@ import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modifier = Modifier) {
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
Column(
modifier = modifier
.background(color = TangemTheme.colors.background.tertiary)
.background(color = TangemTheme.colors.background.secondary)
.fillMaxSize()
.imePadding()
.systemBarsPadding(),
@ -76,6 +80,10 @@ internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modi
color = TangemTheme.colors.text.tertiary,
)
}
if (state.buttonState.showProgress) {
focusManager.clearFocus()
keyboardController?.hide()
}
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
@ -161,6 +169,7 @@ private fun AccountColor(colorsState: AccountCreateEditUM.Colors) {
contentAlignment = Alignment.Center,
modifier = Modifier
.wrapContentSize()
.clip(CircleShape)
.clickable(onClick = { colorsState.onColorSelect(color) })
.size(48.dp),
) {
@ -208,6 +217,7 @@ private fun AccountIcons(iconsState: AccountCreateEditUM.Icons) {
contentAlignment = Alignment.Center,
modifier = Modifier
.wrapContentSize()
.clip(CircleShape)
.clickable(onClick = { iconsState.onIconSelect(icon) })
.size(52.dp),
) {

View file

@ -1,11 +1,9 @@
package com.tangem.features.account.fetcher
import arrow.core.getOrElse
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
@ -60,8 +58,8 @@ internal class DefaultPortfolioFetcher @AssistedInject constructor(
.map { it.filterWallets(mode) }
.distinctUntilChanged()
.flatMapLatest { wallets -> balancesForWallets(wallets) },
flow2 = appCurrencyFlow(),
flow3 = balanceHidingFlow(),
flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(),
flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
) { balances, appCurrency, isBalanceHiding ->
Data(
appCurrency = appCurrency,
@ -94,18 +92,6 @@ internal class DefaultPortfolioFetcher @AssistedInject constructor(
private fun accountStatusListFlow(wallet: UserWallet): Flow<AccountStatusList> =
singleAccountStatusListSupplier(SingleAccountStatusListProducer.Params(wallet.walletId))
private fun appCurrencyFlow(): Flow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map { it.getOrElse { AppCurrency.Default } }
.distinctUntilChanged()
}
private fun balanceHidingFlow(): Flow<Boolean> {
return getBalanceHidingSettingsUseCase()
.map { it.isBalanceHidden }
.distinctUntilChanged()
}
@AssistedFactory
interface Factory : PortfolioFetcher.Factory {
override fun create(mode: Mode, scope: CoroutineScope): DefaultPortfolioFetcher

View file

@ -1,8 +1,6 @@
package com.tangem.features.markets.portfolio.impl.loader
import arrow.core.getOrElse
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.TotalFiatBalance
@ -15,7 +13,6 @@ import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.error.TokenListError
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import timber.log.Timber
import javax.inject.Inject
/**
@ -41,8 +38,8 @@ internal class PortfolioDataLoader @Inject constructor(
fun load(currencyRawId: CryptoCurrency.RawID): Flow<PortfolioData> {
return combine(
flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId),
flow2 = getSelectedAppCurrencyFlow(),
flow3 = getBalanceHidingSettingsFlow(),
flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(),
flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
) { walletsWithCurrencies, appCurrency, isBalanceHidden ->
PortfolioData(
walletsWithCurrencies = walletsWithCurrencies,
@ -113,23 +110,6 @@ internal class PortfolioDataLoader @Inject constructor(
.distinctUntilChanged()
}
private fun getSelectedAppCurrencyFlow(): Flow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map {
it.getOrElse { e ->
Timber.e("Failed to load app currency: $e")
AppCurrency.Default
}
}
.distinctUntilChanged()
}
private fun getBalanceHidingSettingsFlow(): Flow<Boolean> {
return getBalanceHidingSettingsUseCase()
.map { it.isBalanceHidden }
.distinctUntilChanged()
}
private fun getWalletsWithTotalBalanceFlow(
ids: List<UserWalletId>,
): Flow<Map<UserWalletId, Lce<TokenListError, TotalFiatBalance>>> {

View file

@ -241,8 +241,8 @@ internal class AvailableSwapPairsModel @Inject constructor(
private fun getAppCurrencyAndBalanceHidingFlow(): Flow<Pair<AppCurrency, Boolean>> {
return combine(
flow = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(),
flow2 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(),
flow = getSelectedAppCurrencyUseCase.invokeOrDefault(),
flow2 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
transform = ::Pair,
)
}

View file

@ -33,6 +33,11 @@ dependencies {
implementation(projects.common.ui)
/* Project - Domain */
implementation(projects.domain.account.status)
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.legacy)
implementation(projects.domain.card)
implementation(projects.domain.models)

View file

@ -3,6 +3,7 @@ package com.tangem.feature.walletsettings.component.preview
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.common.ui.account.AccountIconPreviewData
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.common.ui.userwallet.state.UserWalletItemUM.ImageState
import com.tangem.core.analytics.DummyAnalyticsEventHandler
import com.tangem.core.decompose.navigation.DummyRouter
@ -20,9 +21,27 @@ import com.tangem.feature.walletsettings.impl.R
import com.tangem.feature.walletsettings.ui.WalletSettingsScreen
import com.tangem.feature.walletsettings.utils.ItemsBuilder
import com.tangem.hot.sdk.model.HotWalletId
import java.util.UUID
internal class PreviewWalletSettingsComponent : WalletSettingsComponent {
private val accountName get() = stringReference(value = "Main account")
private val accountItem: UserWalletItemUM
get() = UserWalletItemUM(
id = UserWalletId(UUID.randomUUID().toString().encodeToByteArray()),
name = accountName,
information = UserWalletItemUM.Information.Loaded(stringReference("12 tokens")),
balance = UserWalletItemUM.Balance.Loaded("$726.04", false),
isEnabled = true,
onClick = { },
imageState = ImageState.Account(
name = accountName,
icon = AccountIconPreviewData.randomAccountIcon(),
),
label = null,
)
private val previewState = WalletSettingsUM(
popBack = {},
items = ItemsBuilder(
@ -66,14 +85,7 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent {
id = "accounts_header",
text = resourceReference(R.string.common_accounts),
).let(::add)
WalletSettingsAccountsUM.Account(
id = "accountId",
accountName = stringReference("Main account"),
accountIconUM = AccountIconPreviewData.randomAccountIcon(),
tokensInfo = stringReference("10 tokens"),
networksInfo = stringReference("2 networks"),
onClick = {},
).let(::add)
add(WalletSettingsAccountsUM.Account(accountItem))
WalletSettingsAccountsUM.Footer(
id = "accounts_footer",
addAccount = AddAccountUM(
@ -86,6 +98,7 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent {
iconRes = R.drawable.ic_archive_24,
onClick = {},
),
showDescription = true,
description = resourceReference(R.string.account_reorder_description),
).let(::add)
}

View file

@ -1,7 +1,7 @@
package com.tangem.feature.walletsettings.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.common.ui.userwallet.state.UserWalletItemUM.ImageState
import com.tangem.core.ui.components.block.model.BlockUM
import com.tangem.core.ui.extensions.TextReference
@ -65,18 +65,16 @@ internal sealed class WalletSettingsAccountsUM : WalletSettingsItemUM() {
) : WalletSettingsAccountsUM()
data class Account(
override val id: String,
val accountName: TextReference,
val accountIconUM: CryptoPortfolioIconUM,
val tokensInfo: TextReference,
val networksInfo: TextReference,
val onClick: () -> Unit,
) : WalletSettingsAccountsUM()
val state: UserWalletItemUM,
) : WalletSettingsAccountsUM() {
override val id: String get() = state.id.stringValue
}
data class Footer(
override val id: String,
val addAccount: AddAccountUM,
val archivedAccounts: BlockUM,
val showDescription: Boolean,
val description: TextReference,
) : WalletSettingsAccountsUM() {

View file

@ -38,6 +38,7 @@ import com.tangem.feature.walletsettings.analytics.WalletSettingsAnalyticEvents
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.feature.walletsettings.entity.DialogConfig
import com.tangem.feature.walletsettings.entity.NetworksAvailableForNotificationBSConfig
import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM
import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM
import com.tangem.feature.walletsettings.entity.WalletSettingsUM
import com.tangem.feature.walletsettings.impl.R
@ -123,7 +124,8 @@ internal class WalletSettingsModel @Inject constructor(
getWalletNotificationsEnabledUseCase(params.userWalletId),
isUpgradeWalletNotificationEnabledUseCase(params.userWalletId),
walletCardItemDelegate.cardItemFlow(wallet),
) { nftEnabled, notificationsEnabled, isUpgradeNotificationEnabled, cardItem ->
accountItemsDelegate.loadAccount(),
) { nftEnabled, notificationsEnabled, isUpgradeNotificationEnabled, cardItem, accountList ->
val isWalletBackedUp = when (wallet) {
is UserWallet.Hot -> wallet.backedUp
is UserWallet.Cold -> true
@ -138,6 +140,7 @@ internal class WalletSettingsModel @Inject constructor(
isNotificationsFeatureEnabled = true,
isNotificationsPermissionGranted = isNotificationsPermissionGranted(),
isUpgradeNotificationEnabled = isUpgradeNotificationEnabled,
accountList = accountList,
),
isWalletBackedUp = isWalletBackedUp,
)
@ -168,6 +171,7 @@ internal class WalletSettingsModel @Inject constructor(
isNotificationsEnabled: Boolean,
isNotificationsPermissionGranted: Boolean,
isUpgradeNotificationEnabled: Boolean,
accountList: List<WalletSettingsAccountsUM>,
): PersistentList<WalletSettingsItemUM> {
val isMultiCurrency = when (userWallet) {
is UserWallet.Cold -> userWallet.isMultiCurrency
@ -219,7 +223,7 @@ internal class WalletSettingsModel @Inject constructor(
walletUpgradeDismissed = isUpgradeNotificationEnabled,
onUpgradeWalletClick = ::onUpgradeWalletClick,
onDismissUpgradeWalletClick = ::onDismissUpgradeWalletClick,
accountsUM = with(accountItemsDelegate) { listOf() }, // todo account
accountsUM = accountList,
)
}

View file

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
@ -22,9 +23,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountRow
import com.tangem.common.ui.userwallet.CardImage
import com.tangem.core.ui.components.SpacerH
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.TangemSwitch
import com.tangem.core.ui.components.appbar.TangemTopAppBar
@ -292,7 +292,7 @@ private fun AccountsHeader(model: WalletSettingsAccountsUM.Header, modifier: Mod
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing8,
top = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing4,
),
text = model.text.resolveReference(),
@ -305,21 +305,10 @@ private fun AccountsHeader(model: WalletSettingsAccountsUM.Header, modifier: Mod
@Composable
private fun AccountItem(model: WalletSettingsAccountsUM.Account, modifier: Modifier = Modifier) {
val subtitle = stringResourceSafe(
id = R.string.account_label_tokens_info,
formatArgs = arrayOf(
model.tokensInfo.resolveReference(),
model.networksInfo.resolveReference(),
),
)
AccountRow(
UserWalletItem(
state = model.state,
modifier = modifier
.background(color = TangemTheme.colors.background.primary)
.clickable(onClick = model.onClick)
.padding(12.dp),
title = model.accountName,
subtitle = stringReference(subtitle),
icon = model.accountIconUM,
.background(color = TangemTheme.colors.background.primary),
)
}
@ -336,18 +325,19 @@ private fun AccountsFooter(model: WalletSettingsAccountsUM.Footer, modifier: Mod
),
) {
AddAccountRow(model.addAccount)
SpacerH(
height = TangemTheme.dimens.size0_5,
HorizontalDivider(
thickness = TangemTheme.dimens.size0_5,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing12)
.background(TangemTheme.colors.stroke.primary),
.padding(horizontal = TangemTheme.dimens.spacing12),
color = TangemTheme.colors.stroke.primary,
)
BlockItem(
modifier = Modifier.fillMaxWidth(),
model = model.archivedAccounts,
)
}
if (!model.showDescription) return
SpacerH8()
Text(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12),

View file

@ -1,30 +1,81 @@
package com.tangem.feature.walletsettings.utils
import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.account.AccountPortfolioItemUMConverter
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.components.block.model.BlockUM
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM
import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM.Footer.AddAccountUM
import com.tangem.feature.walletsettings.impl.R
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class AccountItemsDelegate @Inject constructor(
paramsContainer: ParamsContainer,
private val router: Router,
private val messageSender: UiMessageSender,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val accountsFeatureToggles: AccountsFeatureToggles,
) {
val userWalletId = paramsContainer.require<WalletSettingsComponent.Params>().userWalletId
fun buildUiList(userWalletId: UserWalletId, accounts: List<Account>): List<WalletSettingsAccountsUM> = buildList {
fun loadAccount(): Flow<List<WalletSettingsAccountsUM>> {
if (!accountsFeatureToggles.isFeatureEnabled) return flowOf(emptyList())
val params = SingleAccountStatusListProducer.Params(userWalletId)
return combine(
flow = singleAccountStatusListSupplier(params),
flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(),
flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(),
) { accountStatusList, appCurrency, isBalanceHidden ->
buildUiList(accountStatusList, appCurrency, isBalanceHidden)
}
}
private fun buildUiList(
accountStatusList: AccountStatusList,
appCurrency: AppCurrency,
isBalanceHidden: Boolean,
): List<WalletSettingsAccountsUM> = buildList {
fun AccountStatus.CryptoPortfolio.mapCryptoPortfolio(): WalletSettingsAccountsUM {
val accountItemUM = AccountPortfolioItemUMConverter(
onClick = { openAccountDetails(this.account) },
appCurrency = appCurrency,
accountBalance = this.tokenList.totalFiatBalance,
isBalanceHidden = isBalanceHidden,
).convert(account)
return WalletSettingsAccountsUM.Account(state = accountItemUM)
}
fun mapAccount(account: AccountStatus): WalletSettingsAccountsUM = when (account) {
is AccountStatus.CryptoPortfolio -> account.mapCryptoPortfolio()
}
val accounts = accountStatusList.accountStatuses
WalletSettingsAccountsUM.Header(
id = "accounts_header",
text = resourceReference(R.string.common_accounts),
@ -32,7 +83,8 @@ internal class AccountItemsDelegate @Inject constructor(
addAll(accounts.map(::mapAccount))
val addAccountEnabled = true // todo account
val addAccountEnabled = accounts.size < AccountList.MAX_ACCOUNTS_COUNT
val showDescription = accounts.size > 1
WalletSettingsAccountsUM.Footer(
id = "accounts_footer",
addAccount = AddAccountUM(
@ -47,33 +99,11 @@ internal class AccountItemsDelegate @Inject constructor(
iconRes = R.drawable.ic_archive_24,
onClick = { openArchivedAccounts(userWalletId) },
),
showDescription = showDescription,
description = resourceReference(R.string.account_reorder_description),
).let(::add)
}
private fun mapAccount(account: Account): WalletSettingsAccountsUM = when (account) {
is Account.CryptoPortfolio -> account.mapCryptoPortfolio()
}
private fun Account.CryptoPortfolio.mapCryptoPortfolio(): WalletSettingsAccountsUM {
return WalletSettingsAccountsUM.Account(
id = accountId.value,
accountName = accountName.toUM().value,
accountIconUM = icon.toUM(),
tokensInfo = pluralReference(
R.plurals.common_tokens_count,
count = tokensCount,
formatArgs = wrappedList(tokensCount),
),
networksInfo = pluralReference(
R.plurals.common_networks_count,
count = networksCount,
formatArgs = wrappedList(networksCount),
),
onClick = { openAccountDetails(this) },
)
}
private fun openAccountDetails(account: Account) {
router.push(AppRoute.AccountDetails(account))
}
@ -96,15 +126,10 @@ internal class AccountItemsDelegate @Inject constructor(
title = resourceReference(R.string.account_add_limit_dialog_title),
message = resourceReference(
id = R.string.account_add_limit_dialog_description,
formatArgs = wrappedList(MAX_ACCOUNT_COUNT.toString()),
formatArgs = wrappedList(AccountList.MAX_ACCOUNTS_COUNT.toString()),
),
firstActionBuilder = { firstAction },
),
)
}
companion object {
// todo account use domain const?
private const val MAX_ACCOUNT_COUNT = 20
}
}