Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-13 13:12:16 +03:00
commit 5b832e2591
26 changed files with 584 additions and 85 deletions

@ -1 +1 @@
Subproject commit 9f34ca0ea3969b5ba6b82e1aea2fef0a44463fc8
Subproject commit a8cf062a6bb58332458c0f5b43026e062378e5c7

View file

@ -134,7 +134,7 @@
<string name="biometric_unavailable_warning">お使いの携帯電話で生体認証が無効になっているため、アプリにウォレットを保存できません。ウォレットを保存するには、携帯電話の設定で生体認証機能を有効にしてください。</string>
<string name="bitcoin_promo_activation_error">プロモーションコードの処理中にエラーが発生しました。しばらくしてからもう一度お試しください。</string>
<string name="bitcoin_promo_activation_error_title">アクティベーションエラー</string>
<string name="bitcoin_promo_activation_success">プロモコードが正常に有効化されました。リワードは14日以内にあなたのアカウントに付与されます。</string>
<string name="bitcoin_promo_activation_success">プロモーションコードが正常に有効化されました。報酬は14日以内にビットコインアカウントに入金されます。</string>
<string name="bitcoin_promo_activation_success_title">プロモーションコードが有効になりました</string>
<string name="bitcoin_promo_already_activated">このプロモーションコードは既に使用されているため、再度使うことはできません。</string>
<string name="bitcoin_promo_already_activated_title">コードが利用できません</string>
@ -180,6 +180,7 @@
<string name="common_add">追加</string>
<string name="common_add_to_portfolio">ポートフォリオに追加</string>
<string name="common_add_token">トークンを追加</string>
<string name="common_added">追加済み</string>
<string name="common_address">アドレス</string>
<string name="common_all">すべて</string>
<string name="common_allow">許可する</string>
@ -1365,7 +1366,7 @@
<string name="user_push_notification_agreement_argument_two_subtitle">新しい機能や限定オファーへの早期アクセス。</string>
<string name="user_push_notification_agreement_argument_two_title">機能およびニュースのアップデート</string>
<string name="user_push_notification_agreement_header">プッシュ通知を使用しますか?</string>
<string name="user_push_notification_banner_subtitle">プッシュ通知を有効にすると、資金が到着するとすぐに通知されます。</string>
<string name="user_push_notification_banner_subtitle">プッシュ通知を有効にすると、ウォレットに着金したときにアラートを受信できます。</string>
<string name="user_push_notification_banner_title">取引を見逃さない</string>
<string name="user_wallet_list_add_button">新しいウォレットを追加</string>
<string name="user_wallet_list_delete_hw_prompt">バックアップせずにこのウォレットを削除すると、資金に永久にアクセスできなくなります。</string>
@ -1444,6 +1445,7 @@
<string name="wallet_connect_subtitle">dAppsに接続する</string>
<string name="wallet_connect_title">WalletConnect</string>
<string name="wallet_connect_toast_awaiting_session_proposal">接続には数秒かかる場合があります</string>
<string name="wallet_create_common_title">Tangemウォレットを作成する</string>
<string name="wallet_create_hardware_badge">%sから</string>
<string name="wallet_create_hardware_description">Tangemウォレットを購入ーこれは、あなたの秘密鍵をオフラインで安全に保管する物理デバイスです。</string>
<string name="wallet_create_hardware_title">ハードウェアウォレット</string>
@ -1759,7 +1761,8 @@
<string name="yield_module_supply_apr">供給APR</string>
<string name="yield_module_token_details_earn_notification_apy">APY</string>
<string name="yield_module_token_details_earn_notification_description">あなたの資産を眠らせない — 残高を運用して利息を得ましょう。</string>
<string name="yield_module_token_details_earn_notification_earning_on_your_balance_title">保有資産から報酬を得る</string>
<string name="yield_module_token_details_earn_notification_earning_on_your_balance_subtitle">利息は自動的に発生します</string>
<string name="yield_module_token_details_earn_notification_earning_on_your_balance_title">Aaveレンディング</string>
<string name="yield_module_token_details_earn_notification_processing">入金の処理中</string>
<string name="yield_module_token_details_earn_notification_title">年間%1$s%%の収益</string>
<string name="yield_module_transfer_mode_automatic">自動</string>

View file

@ -80,7 +80,7 @@
<string name="biometric_unavailable_warning">Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона.</string>
<string name="bitcoin_promo_activation_error">При обработке промокода произошла ошибка. Пожалуйста, попробуйте позже.</string>
<string name="bitcoin_promo_activation_error_title">Ошибка активации</string>
<string name="bitcoin_promo_activation_success">Ваш промокод был успешно активирован. Награда будет зачислена на ваш счёт в течение 14 дней.</string>
<string name="bitcoin_promo_activation_success">Ваш промокод был успешно активирован. Награда будет зачислена на ваш Bitcoin адрес в течение 14 дней.</string>
<string name="bitcoin_promo_activation_success_title">Промокод активирован</string>
<string name="bitcoin_promo_already_activated">Этот промокод уже был использован и не может быть активирован повторно.</string>
<string name="bitcoin_promo_already_activated_title">Код недоступен</string>

View file

@ -135,7 +135,7 @@
<string name="biometric_unavailable_warning">You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings.</string>
<string name="bitcoin_promo_activation_error">An error occurred while processing your promo code. Please try again later.</string>
<string name="bitcoin_promo_activation_error_title">Activation error</string>
<string name="bitcoin_promo_activation_success">Your promo code was successfully activated. A reward will be credited to your account within 14 days.</string>
<string name="bitcoin_promo_activation_success">Your promo code was successfully activated. The reward will be credited to your Bitcoin account within 14 days.</string>
<string name="bitcoin_promo_activation_success_title">Promo Code Activated</string>
<string name="bitcoin_promo_already_activated">This promo code has already been used and cannot be activated again.</string>
<string name="bitcoin_promo_already_activated_title">Code unavailable</string>
@ -1797,7 +1797,10 @@
<string name="yield_module_fee_policy_sheet_description">All future %s top-ups will be supplied to Aave automatically, with the transaction fee deducted.</string>
<string name="yield_module_fee_policy_sheet_max_fee_note">If network fees rise above maximum fee, the transaction wont go through until they decrease. You can change this limit later.</string>
<string name="yield_module_fee_policy_sheet_max_fee_title">Maximum fee</string>
<string name="yield_module_fee_policy_sheet_min_amount_note">The transaction fee must stay below 4% of your deposit. Tangem will transfer funds to Aave only once your balance is large enough to meet this condition. </string>
<string name="yield_module_fee_policy_sheet_min_amount_title">Minimal amount</string>
<string name="yield_module_fee_policy_sheet_title">Fee policy</string>
<string name="yield_module_fee_policy_tangem_service_fee_title">Tangem also takes a 3% service fee on the yield earned.</string>
<string name="yield_module_high_fee_error">Network fee is too high right now. Waiting until it falls below your limit.</string>
<string name="yield_module_historical_returns">Historical returns</string>
<string name="yield_module_main_view_approve_notification_description">Write description here. In one, two or three lines will be awesome. [PLACEHOLDER]</string>

View file

@ -85,6 +85,7 @@ fun Notification(
subtitle = config.subtitle,
subtitleColor = subtitleColor,
showArrowIcon = isEnabled && config.shouldShowArrowIcon,
hasCloseButton = config.onCloseClick != null,
)
}
}
@ -148,6 +149,7 @@ private fun MainContent(
titleColor: Color,
subtitleColor: Color,
showArrowIcon: Boolean,
hasCloseButton: Boolean,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
@ -161,10 +163,24 @@ private fun MainContent(
SpacerW(width = TangemTheme.dimens.spacing10)
TextsBlock(title = title, titleColor = titleColor, subtitle = subtitle, subtitleColor = subtitleColor)
TextsBlock(
title = title,
titleColor = titleColor,
subtitle = subtitle,
subtitleColor = subtitleColor,
modifier = Modifier
.weight(1f, fill = false)
.then(
if (hasCloseButton && !showArrowIcon) {
Modifier.padding(end = TangemTheme.dimens.size32)
} else {
Modifier
},
),
)
if (showArrowIcon) {
SpacerWMax()
SpacerW(width = TangemTheme.dimens.spacing8)
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),

View file

@ -41,6 +41,11 @@ internal class DefaultCustomTokensRepository(
private val networkFactory: NetworkFactory,
) : CustomTokensRepository {
private val excludedBlockchainsForCustom = setOf(
Blockchain.Quai,
Blockchain.QuaiTestnet,
)
private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains)
private val userTokensResponseFactory = UserTokensResponseFactory()
private val tokenAddressConverter = TokenAddressesConverter()
@ -268,6 +273,7 @@ internal class DefaultCustomTokensRepository(
val scanResponse = userWallet.scanResponse
Blockchain.entries
.filter { it !in excludedBlockchainsForCustom }
.mapNotNull { blockchain ->
val canHandleBlockchain = scanResponse.card.canHandleBlockchain(
blockchain,

View file

@ -45,19 +45,33 @@ fun CardDTO.supportedBlockchains(
excludedBlockchains: ExcludedBlockchains,
): List<Blockchain> {
val supportedBlockchains = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
Blockchain.fromCurve(EllipticCurve.Secp256k1).toMutableList()
Blockchain.fromCurve(EllipticCurve.Secp256k1)
} else if (!cardTypesResolver.isWallet2() && !cardTypesResolver.isTangemWallet()) {
// need for old multiwallet that supports only secp256k1
wallets.flatMap { Blockchain.fromCurve(it.curve) }.distinct().toMutableList()
wallets.flatMap { Blockchain.fromCurve(it.curve) }.distinct()
} else {
// multiwallet supports all blockchains, move this logic to config
Blockchain.entries.toMutableList()
Blockchain.entries
}
return supportedBlockchains
.filter { isTestCard == it.isTestnet() }
.filterNot(::isBlockchainUnsupported)
.filter { it !in excludedBlockchains }
}
/**
* Filters out blockchains that are not supported by the specific card firmware.
* @return `false` if blockchain is not supported, `true` otherwise.
*/
private fun CardDTO.isBlockchainUnsupported(blockchain: Blockchain): Boolean {
return when (blockchain) {
Blockchain.Quai, Blockchain.QuaiTestnet -> {
firmwareVersion <= FirmwareVersion.HDWalletAvailable
}
else -> false
}
}
fun CardDTO.supportedTokens(
cardTypesResolver: CardTypesResolver,
excludedBlockchains: ExcludedBlockchains,

View file

@ -2,7 +2,6 @@ package com.tangem.feature.qrscanning.presentation.transformers
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.qrscanning.models.QrResultSource
import com.tangem.domain.qrscanning.models.SourceType
@ -22,7 +21,7 @@ internal class InitializeQrScanningStateTransformer(
override fun transform(prevState: QrScanningState): QrScanningState {
val message = when (source) {
SourceType.SEND -> network?.let { resourceReference(R.string.send_qrcode_scan_info, wrappedList(it)) }
SourceType.WALLET_CONNECT -> stringReference("Open Web3 app and chose WalletConnect option")
SourceType.WALLET_CONNECT -> resourceReference(R.string.wc_qr_scan_hint)
}
return QrScanningState(

View file

@ -69,4 +69,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun showPrimaryClickAlert()
fun onOpenLearnMoreAboutApproveClick()
fun onActivateTonAccountNotificationClick()
fun onActivateTonAccountClick()
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.staking.impl.presentation.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
@ -48,8 +49,11 @@ import com.tangem.domain.staking.utils.getValidatorsCount
import com.tangem.domain.tokens.*
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase
import com.tangem.domain.transaction.usecase.GetAllowanceUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.staking.impl.analytics.StakingParamsInterceptor
@ -69,10 +73,15 @@ import com.tangem.features.staking.impl.presentation.state.transformers.approval
import com.tangem.features.staking.impl.presentation.state.transformers.confirmation.SetUpdatedAllowanceTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.notifications.AddStakingNotificationsTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.notifications.DismissStakingNotificationsStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.ton.CompleteInitializeBottomSheetTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.ton.SetFeeToTonInitializeBottomSheetTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.ton.ShowTonInitializeBottomSheetTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.ton.SetFeeErrorToTonInitializeBottomSheetTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer
import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount
import com.tangem.features.staking.impl.presentation.state.utils.isSingleAction
import com.tangem.features.staking.impl.presentation.state.utils.withStubUnstakeAction
import com.tangem.lib.crypto.BlockchainUtils.isTon
import com.tangem.utils.Provider
import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP
import com.tangem.utils.coroutines.*
@ -122,6 +131,9 @@ internal class StakingModel @Inject constructor(
private val getActionsUseCase: GetActionsUseCase,
private val getYieldUseCase: GetYieldUseCase,
private val checkAccountInitializedUseCase: CheckAccountInitializedUseCase,
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase,
private val getActionRequirementAmountUseCase: GetActionRequirementAmountUseCase,
private val paramsInterceptorHolder: ParamsInterceptorHolder,
private val shareManager: ShareManager,
@ -150,11 +162,14 @@ internal class StakingModel @Inject constructor(
}
}
private var isAccountInitialized: Boolean = true
private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
private var stakingActions: List<StakingAction> = emptyList()
private var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null
private var minimumTransactionAmount: EnterAmountBoundary? = null
private var tonAccountInitializeTransaction: TransactionData.Uncompiled? = null
private val userWallet by lazy {
requireNotNull(
getUserWalletUseCase(userWalletId).getOrNull(),
@ -250,26 +265,12 @@ internal class StakingModel @Inject constructor(
val isInitialInfoStep = value.currentStep == StakingStep.InitialInfo
val noBalanceState = balanceState == null
val noYieldBalanceData = cryptoCurrencyStatus.value.yieldBalance !is YieldBalance.Data
val isAccountInitialized = checkAccountInitializedUseCase.invoke(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
).getOrElse {
Timber.e(it)
false
}
when {
isInitialInfoStep && noBalanceState && yield.allValidatorsFull && noYieldBalanceData -> {
stakingEventFactory.createStakingValidatorsUnavailableAlert()
return@launch
}
isInitialInfoStep && noBalanceState && !isAccountInitialized -> {
analyticsEventHandler.send(StakingAnalyticsEvent.UnitializedAddress(
token = cryptoCurrencyStatus.currency.symbol,
))
stakingEventFactory.createInitializeAccountAlert()
return@launch
}
isInitialInfoStep && noBalanceState -> {
val list = buildList {
SetConfirmationStateInitTransformer(
@ -763,6 +764,7 @@ internal class StakingModel @Inject constructor(
AddStakingNotificationsTransformer(
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
appCurrencyProvider = Provider { appCurrency },
isAccountInitializedProvider = Provider { isAccountInitialized },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
currencyWarning = currencyWarning,
currencyCheck = currencyStatus,
@ -934,6 +936,85 @@ internal class StakingModel @Inject constructor(
urlOpener.openUrl(RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP)
}
override fun onActivateTonAccountNotificationClick() {
modelScope.launch {
stateController.update(
ShowTonInitializeBottomSheetTransformer(
onDismiss = { stateController.update(DismissBottomSheetStateTransformer) },
),
)
val tonAmount = BigDecimal(1)
val destination = getNetworkAddressesUseCase.invokeSync(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
).getOrNull(0)?.address ?: return@launch
val transaction = createTransferTransactionUseCase(
amount = tonAmount.convertToSdkAmount(cryptoCurrencyStatus),
destination = destination,
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
memo = null,
)
tonAccountInitializeTransaction = transaction.getOrElse {
stateController.update(
SetFeeErrorToTonInitializeBottomSheetTransformer(),
)
return@launch
}
val transactionFee = getFeeUseCase(
userWallet = userWallet,
network = cryptoCurrencyStatus.currency.network,
transactionData = tonAccountInitializeTransaction!!,
)
transactionFee.fold(
ifLeft = {
stateController.update(
SetFeeErrorToTonInitializeBottomSheetTransformer(),
)
},
ifRight = {
stateController.update(
SetFeeToTonInitializeBottomSheetTransformer(
appCurrencyProvider = Provider { appCurrency },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
fee = it.normal,
isFeeApproximate = false,
),
)
},
)
}
}
override fun onActivateTonAccountClick() {
modelScope.launch {
tonAccountInitializeTransaction?.let { transaction ->
sendTransactionUseCase.invoke(
txData = transaction,
userWallet = userWallet,
network = cryptoCurrencyStatus.currency.network,
).fold(
ifLeft = {
stateController.update(DismissBottomSheetStateTransformer)
},
ifRight = {
stateController.update(CompleteInitializeBottomSheetTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount,
))
balanceUpdater.partialUpdateWithDelay()
stateController.update(DismissBottomSheetStateTransformer)
},
)
}
}
}
private suspend fun setupApprovalNeeded() {
val approval = StakingIntegrationID.create(currencyId = cryptoCurrencyStatus.currency.id)?.approval
?: StakingApproval.Empty
@ -961,7 +1042,9 @@ internal class StakingModel @Inject constructor(
)
.conflate()
.distinctUntilChanged()
.filter { value.currentStep == StakingStep.InitialInfo }
.filter {
value.currentStep == StakingStep.InitialInfo || isCaseWithUnitializedTonAccount()
}
.onEach { maybeStatus ->
maybeStatus.fold(
ifRight = { status ->
@ -989,6 +1072,19 @@ internal class StakingModel @Inject constructor(
}
cryptoCurrencyStatus = status
val isAccountInitializedNew = checkAccountInitializedUseCase.invoke(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
).getOrElse {
Timber.e(it)
false
}
if (isAccountInitializedNew && !isAccountInitialized) {
isAccountInitialized = true
updateNotifications()
}
isAccountInitialized = isAccountInitializedNew
setupApprovalNeeded()
setupIsAnyTokenStaked()
checkIfSubtractAvailable()
@ -1150,6 +1246,12 @@ internal class StakingModel @Inject constructor(
.getOrElse { false }
}
private fun isCaseWithUnitializedTonAccount(): Boolean {
return value.currentStep == StakingStep.Confirmation &&
isTon(cryptoCurrencyStatus.currency.network.rawId) &&
!isAccountInitialized
}
private companion object {
const val WHAT_IS_STAKING_ARTICLE_URL = "https://tangem.com/en/blog/post/how-to-stake-cryptocurrency/"
const val ALLOWANCE_UPDATE_DELAY = 10_000L

View file

@ -60,6 +60,15 @@ internal object StakingNotification {
title = resourceReference(R.string.staking_notification_low_staked_balance_title),
subtitle = resourceReference(R.string.staking_notification_low_staked_balance_text),
)
data class InitializeTonAccount(private val onInitializeClick: () -> Unit) : Info(
title = resourceReference(R.string.staking_notification_ton_account_initialization_title),
subtitle = resourceReference(R.string.staking_notification_ton_account_initialization_message),
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = resourceReference(R.string.common_activate),
onClick = onInitializeClick,
),
)
}
sealed class Info(

View file

@ -0,0 +1,15 @@
package com.tangem.features.staking.impl.presentation.state.bottomsheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.staking.impl.presentation.state.FeeState
internal data class TonInitializeAccountBottomSheetConfig(
val title: TextReference,
val message: TextReference,
val footer: TextReference,
val onButtonClick: () -> Unit,
val isButtonEnabled: Boolean,
val isButtonLoading: Boolean,
val feeState: FeeState,
) : TangemBottomSheetConfigContent

View file

@ -62,13 +62,6 @@ internal sealed class StakingAlertUM : AlertUM {
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
}
data object InitializeAccount : StakingAlertUM() {
override val onConfirmClick: (() -> Unit)? = null
override val title: TextReference = resourceReference(id = R.string.staking_error_no_validators_title)
override val message: TextReference = resourceReference(id = R.string.staking_notification_ton_activate_account)
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
}
data class RewardsMinimumRequirementsError(
val cryptoCurrencyName: String,
val cryptoAmountValue: String,

View file

@ -47,11 +47,6 @@ internal class StakingEventFactory(
stateController.updateEvent(alert)
}
fun createInitializeAccountAlert() {
val alert = StakingEvent.ShowAlert(alert = StakingAlertUM.InitializeAccount)
stateController.updateEvent(alert)
}
fun createStakingRewardsMinimumRequirementsErrorAlert(cryptoCurrencyName: String, cryptoAmountValue: String) {
stateController.updateEvent(
StakingEvent.ShowAlert(

View file

@ -76,6 +76,11 @@ internal class StakingBalanceUpdater @AssistedInject constructor(
}
}
suspend fun partialUpdateWithDelay() {
delay(BALANCE_UPDATE_DELAY)
partialUpdate()
}
private suspend fun fetchCurrencyStatus(delayMillis: Long = 0L) {
delay(delayMillis)
fetchCurrencyStatusUseCase(

View file

@ -78,4 +78,8 @@ internal object StakingClickIntentsStub : StakingClickIntents {
override fun showPrimaryClickAlert() {}
override fun onOpenLearnMoreAboutApproveClick() {}
override fun onActivateTonAccountNotificationClick() {}
override fun onActivateTonAccountClick() {}
}

View file

@ -40,6 +40,7 @@ import java.math.BigDecimal
internal class AddStakingNotificationsTransformer(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val isAccountInitializedProvider: Provider<Boolean>,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
private val currencyWarning: CryptoCurrencyWarning?,
private val feeError: GetFeeError?,
@ -90,34 +91,40 @@ internal class AddStakingNotificationsTransformer(
BigDecimal.ZERO
}
val notifications = buildList {
// errors
addErrorNotifications(
prevState = prevState,
feeError = feeError,
sendingAmount = sendingAmount,
onReload = prevState.clickIntents::getFee,
feeValue = feeValue,
)
addStakingErrorNotifications(stakingError = stakingError, onReload = prevState.clickIntents::getFee)
// warnings
addWarningNotifications(
prevState = prevState,
amountState = amountState,
feeState = feeState,
sendingAmount = sendingAmount,
isFeeCoverage = isFeeCoverage && isEnterAction && !sendingAmount.equals(minimumRequirement),
)
val notifications = if (isAccountInitializedProvider.invoke()) {
buildList {
// errors
addErrorNotifications(
prevState = prevState,
feeError = feeError,
sendingAmount = sendingAmount,
onReload = prevState.clickIntents::getFee,
feeValue = feeValue,
)
addStakingErrorNotifications(stakingError = stakingError, onReload = prevState.clickIntents::getFee)
// warnings
addWarningNotifications(
prevState = prevState,
amountState = amountState,
feeState = feeState,
sendingAmount = sendingAmount,
isFeeCoverage = isFeeCoverage && isEnterAction && !sendingAmount.equals(minimumRequirement),
)
stakingInfoNotificationsFactory.addInfoNotifications(
notifications = this,
prevState = prevState,
sendingAmount = sendingAmount,
actionAmount = amountValue,
feeValue = feeValue,
tonBalanceExtraFeeThreshold = TON_BALANCE_EXTRA_FEE_THRESHOLD,
)
}.toImmutableList()
stakingInfoNotificationsFactory.addInfoNotifications(
notifications = this,
prevState = prevState,
sendingAmount = sendingAmount,
actionAmount = amountValue,
feeValue = feeValue,
tonBalanceExtraFeeThreshold = TON_BALANCE_EXTRA_FEE_THRESHOLD,
)
}.toImmutableList()
} else {
buildList {
addTonInitializeAccountNotification(prevState)
}.toImmutableList()
}
val isActualSources = with(cryptoCurrencyStatus.value) {
sources.yieldBalanceSource.isActual() && sources.networkSource.isActual()
@ -130,7 +137,8 @@ internal class AddStakingNotificationsTransformer(
it is StakingNotification.Error ||
it is NotificationUM.Error ||
it is NotificationUM.Warning.NetworkFeeUnreachable ||
it is StakingNotification.Warning.TransactionInProgress
it is StakingNotification.Warning.TransactionInProgress ||
it is StakingNotification.Warning.InitializeTonAccount
} && isActualSources,
),
)
@ -219,7 +227,6 @@ internal class AddStakingNotificationsTransformer(
) {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val appCurrency = appCurrencyProvider()
cryptoCurrencyStatus.currency
addRentExemptionNotification(
rentWarning = currencyCheck.rentWarning,
@ -282,6 +289,19 @@ internal class AddStakingNotificationsTransformer(
}
}
private fun MutableList<NotificationUM>.addTonInitializeAccountNotification(prevState: StakingUiState) {
val isAccountInitialized = isAccountInitializedProvider.invoke()
val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.rawId
if (isTon(cryptoCurrencyNetworkIdValue) && !isAccountInitialized) {
add(
StakingNotification.Warning.InitializeTonAccount(
onInitializeClick = prevState.clickIntents::onActivateTonAccountNotificationClick,
),
)
}
}
private companion object {
val TON_BALANCE_EXTRA_FEE_THRESHOLD = BigDecimal(0.2)
}

View file

@ -0,0 +1,53 @@
package com.tangem.features.staking.impl.presentation.state.transformers.ton
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.FeeState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.TonInitializeAccountBottomSheetConfig
import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal
internal class CompleteInitializeBottomSheetTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val minimumTransactionAmount: EnterAmountBoundary?,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val prevBottomSheetConfig = prevState.bottomSheetConfig ?: return prevState
val prevBottomSheetConfigContent =
prevBottomSheetConfig.content as? TonInitializeAccountBottomSheetConfig ?: return prevState
val feeAmount = (prevBottomSheetConfigContent.feeState as? FeeState.Content)
?.fee?.amount?.value?.multiply(BigDecimal(FEE_MULTIPLIER)) ?: return prevState
val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState
return prevState.copy(
amountState = AmountReduceByTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
minimumTransactionAmount = minimumTransactionAmount,
value = ReduceByData(
feeAmount,
feeAmount,
),
).transform(prevState.amountState),
confirmationState = confirmationState.copy(
reduceAmountBy = feeAmount,
),
bottomSheetConfig = prevBottomSheetConfig.copy(
content = prevBottomSheetConfigContent.copy(
isButtonLoading = true,
isButtonEnabled = false,
),
),
)
}
private companion object {
const val FEE_MULTIPLIER = 1.1
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.features.staking.impl.presentation.state.transformers.ton
import com.tangem.features.staking.impl.presentation.state.FeeState
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.TonInitializeAccountBottomSheetConfig
import com.tangem.utils.transformer.Transformer
internal class SetFeeErrorToTonInitializeBottomSheetTransformer : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val prevBottomSheetConfig = prevState.bottomSheetConfig ?: return prevState
val prevBottomSheetConfigContent =
prevBottomSheetConfig.content as? TonInitializeAccountBottomSheetConfig ?: return prevState
return prevState.copy(
bottomSheetConfig = prevBottomSheetConfig.copy(
content = prevBottomSheetConfigContent.copy(
isButtonEnabled = false,
feeState = FeeState.Error,
isButtonLoading = false,
),
),
)
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.features.staking.impl.presentation.state.transformers.ton
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.FeeState
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.TonInitializeAccountBottomSheetConfig
import com.tangem.utils.Provider
import com.tangem.utils.transformer.Transformer
internal class SetFeeToTonInitializeBottomSheetTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
private val fee: Fee,
private val isFeeApproximate: Boolean,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val prevBottomSheetConfig = prevState.bottomSheetConfig ?: return prevState
val prevBottomSheetConfigContent =
prevBottomSheetConfig.content as? TonInitializeAccountBottomSheetConfig ?: return prevState
val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true
return prevState.copy(
bottomSheetConfig = prevBottomSheetConfig.copy(
content = prevBottomSheetConfigContent.copy(
isButtonEnabled = true,
isButtonLoading = false,
feeState = FeeState.Content(
fee = fee,
rate = feeCryptoCurrencyStatus?.value?.fiatRate,
isFeeConvertibleToFiat = isFeeConvertibleToFiat,
appCurrency = appCurrencyProvider(),
isFeeApproximate = isFeeApproximate,
),
),
),
)
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.features.staking.impl.presentation.state.transformers.ton
import com.tangem.common.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.staking.impl.presentation.state.FeeState
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.TonInitializeAccountBottomSheetConfig
import com.tangem.utils.transformer.Transformer
internal class ShowTonInitializeBottomSheetTransformer(
private val onDismiss: () -> Unit,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = onDismiss,
content = TonInitializeAccountBottomSheetConfig(
title = resourceReference(R.string.staking_account_initialization_title),
message = resourceReference(R.string.staking_account_initialization_message),
footer = resourceReference(R.string.staking_account_initialization_footer),
onButtonClick = prevState.clickIntents::onActivateTonAccountClick,
isButtonEnabled = false,
feeState = FeeState.Loading,
isButtonLoading = false,
),
),
)
}
}

View file

@ -28,8 +28,10 @@ import com.tangem.features.staking.impl.presentation.state.StakingStep
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingActionSelectionBottomSheetConfig
import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig
import com.tangem.features.staking.impl.presentation.state.bottomsheet.TonInitializeAccountBottomSheetConfig
import com.tangem.features.staking.impl.presentation.ui.bottomsheet.StakingActionSelectorBottomSheet
import com.tangem.features.staking.impl.presentation.ui.bottomsheet.StakingInfoBottomSheet
import com.tangem.features.staking.impl.presentation.ui.bottomsheet.TonInitializeAccountBottomSheet
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@ -83,6 +85,7 @@ fun StakingBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) {
is StakingInfoBottomSheetConfig -> StakingInfoBottomSheet(bottomSheetConfig)
is GiveTxPermissionBottomSheetConfig -> GiveTxPermissionBottomSheet(bottomSheetConfig)
is StakingActionSelectionBottomSheetConfig -> StakingActionSelectorBottomSheet(bottomSheetConfig)
is TonInitializeAccountBottomSheetConfig -> TonInitializeAccountBottomSheet(bottomSheetConfig)
}
}

View file

@ -0,0 +1,158 @@
package com.tangem.features.staking.impl.presentation.ui.bottomsheet
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.staking.impl.presentation.state.FeeState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.TonInitializeAccountBottomSheetConfig
import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock
import java.math.BigDecimal
@Composable
internal fun TonInitializeAccountBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<TonInitializeAccountBottomSheetConfig>(
config = config,
containerColor = TangemTheme.colors.background.tertiary,
) { content ->
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpacerH(height = 16.dp)
Image(
painter = painterResource(id = R.drawable.img_ton_22),
modifier = Modifier.size(56.dp),
contentDescription = null,
)
SpacerH(height = 24.dp)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
text = content.title.resolveReference(),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH(height = 16.dp)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
text = content.message.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH(height = 24.dp)
StakingFeeBlock(content.feeState)
SpacerH(height = 12.dp)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
text = content.footer.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
SpacerH(height = 20.dp)
TangemButton(
text = stringResourceSafe(R.string.common_activate),
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
onClick = content.onButtonClick,
colors = TangemButtonsDefaults.primaryButtonColors,
showProgress = content.feeState is FeeState.Loading || content.isButtonLoading,
enabled = content.isButtonEnabled,
size = TangemButtonSize.WideAction,
textStyle = TangemTheme.typography.subtitle1,
modifier = Modifier.fillMaxWidth(),
)
SpacerH(height = 8.dp)
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_TonInitializeAccountBottomSheetContent() {
TangemThemePreview {
TonInitializeAccountBottomSheet(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = TonInitializeAccountBottomSheetConfig(
title = resourceReference(R.string.staking_account_initialization_title),
message = resourceReference(R.string.staking_account_initialization_message),
footer = resourceReference(R.string.staking_account_initialization_footer),
onButtonClick = {},
isButtonEnabled = true,
isButtonLoading = false,
feeState = FeeState.Content(
fee = Fee.Common(
amount = Amount(
currencySymbol = "MATIC",
value = BigDecimal(0.159806),
decimals = 18,
type = AmountType.Coin,
),
),
rate = BigDecimal.ONE,
appCurrency = AppCurrency.Default,
isFeeApproximate = false,
isFeeConvertibleToFiat = true,
),
),
),
)
}
}
// endregion Preview

View file

@ -241,8 +241,8 @@ internal class SwapNotificationsFactory(
) {
add(
SwapNotificationUM.Info.PermissionNeeded(
providerName = fromToken.symbol,
fromTokenSymbol = providerName,
providerName = providerName,
fromTokenSymbol = fromToken.symbol,
onApproveClick = actions.openPermissionBottomSheet,
),
)

View file

@ -208,7 +208,7 @@ internal object SwapNotificationUM {
),
iconResId = R.drawable.ic_locked_24,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = TextReference.Res(R.string.common_approve),
text = TextReference.Res(R.string.give_permission_title),
onClick = onApproveClick,
),
)

View file

@ -2,16 +2,15 @@
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs = -Xmx6144m -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
org.gradle.parallel = true
org.gradle.daemon = true
android.useAndroidX = true
org.gradle.unsafe.configuration-cache = true
android.nonTransitiveRClass = false
org.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.workers.max=8
org.gradle.daemon=true
org.gradle.caching=true
org.gradle.configuration-cache=true
android.useAndroidX=true
android.nonTransitiveRClass=false
kotlin.incremental=true