Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-01 13:11:43 +05:00
parent 45db7c879f
commit c4adfe3d89
17 changed files with 650 additions and 249 deletions

View file

@ -0,0 +1,202 @@
package com.tangem.common.ui.navigationButtons
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.navigationButtons.preview.NavigationButtonsPreview
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.TangemButtonsDefaults
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
@Composable
fun NavigationButtonsBlock(buttonState: NavigationButtonsState, modifier: Modifier = Modifier) {
val state = buttonState as? NavigationButtonsState.Data
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.fillMaxWidth(),
) {
ExtraButtons(state?.extraButtons, state?.txUrl)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
PreviousButton(state?.prevButton)
PrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f))
}
SecondaryButton(state?.secondaryButton)
}
}
@Composable
private fun PrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) {
AnimatedContent(
targetState = primaryButton,
transitionSpec = {
val isPrimaryToHide = targetState != null && initialState == null
val isPrimaryWasVisible = targetState == null && initialState != null
if (isPrimaryToHide || isPrimaryWasVisible) {
slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn())
.togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()))
} else {
fadeIn().togetherWith(fadeOut())
}
},
contentAlignment = Alignment.Center,
label = "Animate show primary button",
modifier = modifier.fillMaxWidth(),
) { button ->
if (button != null && button.textReference != TextReference.EMPTY) {
val icon = if (button.iconRes != null && button.isIconVisible) {
TangemButtonIconPosition.End(iconResId = button.iconRes)
} else {
TangemButtonIconPosition.None
}
TangemButton(
text = button.textReference.resolveReference(),
enabled = button.isEnabled,
onClick = button.onClick,
showProgress = button.showProgress,
colors = TangemButtonsDefaults.primaryButtonColors,
icon = icon,
modifier = Modifier.fillMaxWidth(),
)
} else {
Spacer(modifier = Modifier.fillMaxWidth())
}
}
}
@Composable
private fun SecondaryButton(secondaryButton: NavigationButton?) {
AnimatedContent(
targetState = secondaryButton,
transitionSpec = {
val isPrimaryToHide = targetState != null && initialState == null
val isPrimaryWasVisible = targetState == null && initialState != null
if (isPrimaryToHide || isPrimaryWasVisible) {
slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn())
.togetherWith(slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()))
} else {
fadeIn().togetherWith(fadeOut())
}
},
contentAlignment = Alignment.Center,
label = "Animate show secondary button",
modifier = Modifier.fillMaxWidth(),
) { button ->
if (button != null && button.textReference != TextReference.EMPTY) {
val icon = button.iconRes?.let { TangemButtonIconPosition.End(iconResId = it) }
?: TangemButtonIconPosition.None
TangemButton(
text = button.textReference.resolveReference(),
enabled = button.isEnabled,
onClick = button.onClick,
icon = icon,
showProgress = button.showProgress,
colors = TangemButtonsDefaults.secondaryButtonColors,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
)
} else {
Spacer(modifier = Modifier.fillMaxWidth())
}
}
}
@Composable
private fun PreviousButton(prevButton: NavigationButton?) {
AnimatedVisibility(
visible = prevButton != null,
enter = expandHorizontally(expandFrom = Alignment.End),
exit = shrinkHorizontally(shrinkTowards = Alignment.End),
label = "Animate show prev button",
) {
val button = remember(this) { requireNotNull(prevButton) }
if (button.iconRes != null && button.isIconVisible) {
Icon(
painter = rememberVectorPainter(
image = ImageVector.vectorResource(button.iconRes),
),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
modifier = Modifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
.background(TangemTheme.colors.button.secondary)
.clickable(onClick = button.onClick)
.padding(TangemTheme.dimens.spacing12),
)
}
}
}
@Composable
private fun ExtraButtons(extraButtons: ImmutableList<NavigationButton>?, txUrl: String?) {
AnimatedVisibility(
visible = !txUrl.isNullOrBlank() && extraButtons != null,
enter = slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn()),
exit = slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()),
label = "Animate show sent state buttons",
modifier = Modifier.fillMaxWidth(),
) {
val buttons = remember(this) { requireNotNull(extraButtons) }
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
) {
buttons.forEach { button ->
val icon = button.iconRes?.let { TangemButtonIconPosition.Start(iconResId = it) }
?: TangemButtonIconPosition.None
TangemButton(
text = button.textReference.resolveReference(),
icon = icon,
onClick = rememberHapticFeedback(state = button, onAction = button.onClick),
modifier = Modifier.weight(1f),
enabled = button.isEnabled,
showProgress = false,
colors = TangemButtonsDefaults.secondaryButtonColors,
)
}
}
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun NavigationButtonsBlock_Preview(
@PreviewParameter(NavigationButtonsBlockDataProvider::class) navigationButtonsState: NavigationButtonsState,
) {
TangemThemePreview {
NavigationButtonsBlock(navigationButtonsState)
}
}
private class NavigationButtonsBlockDataProvider : PreviewParameterProvider<NavigationButtonsState> {
override val values: Sequence<NavigationButtonsState>
get() = sequenceOf(NavigationButtonsPreview.allButtons)
}
// endregion

View file

@ -0,0 +1,27 @@
package com.tangem.common.ui.navigationButtons
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
sealed class NavigationButtonsState {
data object Empty : NavigationButtonsState()
data class Data(
val primaryButton: NavigationButton,
val prevButton: NavigationButton?,
val secondaryButton: NavigationButton?,
val extraButtons: ImmutableList<NavigationButton>,
val txUrl: String? = null,
) : NavigationButtonsState()
}
data class NavigationButton(
val textReference: TextReference,
@DrawableRes val iconRes: Int? = null,
val isSecondary: Boolean,
val isIconVisible: Boolean,
val showProgress: Boolean,
val isEnabled: Boolean,
val onClick: () -> Unit,
)

View file

@ -0,0 +1,67 @@
package com.tangem.common.ui.navigationButtons.preview
import com.tangem.common.ui.R
import com.tangem.common.ui.navigationButtons.NavigationButton
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import kotlinx.collections.immutable.persistentListOf
internal object NavigationButtonsPreview {
private val extraButtons = persistentListOf(
NavigationButton(
textReference = resourceReference(R.string.common_explore),
iconRes = R.drawable.ic_tangem_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = {},
),
NavigationButton(
textReference = resourceReference(R.string.common_share),
iconRes = R.drawable.ic_tangem_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = {},
),
)
private val next = NavigationButton(
textReference = resourceReference(R.string.common_next),
isSecondary = false,
isIconVisible = false,
showProgress = false,
isEnabled = true,
onClick = {},
)
private val prev = NavigationButton(
textReference = TextReference.EMPTY,
iconRes = R.drawable.ic_back_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = {},
)
private val finished = NavigationButton(
textReference = resourceReference(R.string.common_close),
isSecondary = false,
isIconVisible = false,
showProgress = false,
isEnabled = true,
onClick = {},
)
val allButtons = NavigationButtonsState.Data(
primaryButton = finished,
prevButton = prev,
secondaryButton = next,
extraButtons = extraButtons,
txUrl = "https://tangem.com",
)
}

View file

@ -4,10 +4,13 @@
<string name="add_custom_token_title">Add custom token</string>
<string name="add_tokens_title">Manage tokens</string>
<string name="address_qr_code_message_format">Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.</string>
<string name="alert_app_feedback_sent_message">Thank you for your feedback</string>
<string name="alert_app_feedback_sent_title">Sent successfully</string>
<string name="alert_button_how_to_scan">How to scan</string>
<string name="alert_button_request_support">Request support</string>
<string name="alert_button_try_again">Try again</string>
<string name="alert_demo_feature_disabled">This feature is disabled in Demo mode</string>
<string name="alert_failed_to_send_email_title">Failed to send the email</string>
<string name="alert_failed_to_send_transaction_message">Reason: %s</string>
<string name="alert_failed_to_send_transaction_title">Can\'t send a transaction</string>
<string name="alert_manage_tokens_unsupported_blockchain_by_card_message">The selected does not support the %1$s network</string>
@ -35,6 +38,8 @@
<string name="app_settings_theme_selection_system_short">System</string>
<string name="app_settings_theme_selector_title">Theme</string>
<string name="app_settings_title">App settings</string>
<string name="app_settings_warning_subtitle">Go to settings to enable biometric authentication in the Tangem app</string>
<string name="app_settings_warning_title">Enable biometric authentication</string>
<string name="balance_hidden_description">To hide or show your balances, simply flip your device screen down, or switch it off in Settings</string>
<string name="balance_hidden_do_not_show_button">Don\'t show again</string>
<string name="balance_hidden_got_it_button">Got it</string>
@ -43,6 +48,7 @@
<string name="biometric_lockout_warning_description">Please try again in 30 seconds or scan the card</string>
<string name="biometric_lockout_warning_title">Too many attempts</string>
<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="biometry_touch_id_reason">Touch ID is used to save your cards in the app</string>
<string name="button_start_backup_process">Start backup process</string>
<string name="buy_token_description">With your bank card or bank account</string>
<plurals name="card_label_card_count">
@ -51,6 +57,7 @@
</plurals>
<string name="card_settings_access_code_recovery_disabled_description">Disable this option if you don\'t want this card to be used to reset access codes on other cards in this wallet. Please note that this will also prevent you from resetting the access code on this card.</string>
<string name="card_settings_access_code_recovery_enabled_description">Allows you to use this card to reset access code on other cards in this wallet</string>
<string name="card_settings_access_code_recovery_footer">Disable the ability to reset the access code on this card or other cards in this wallet</string>
<string name="card_settings_access_code_recovery_title">Access code recovery</string>
<string name="card_settings_action_sheet_reset">Reset</string>
<string name="card_settings_action_sheet_title">Are you sure you want to do this?</string>
@ -79,13 +86,16 @@
<string name="common_approval">Approval</string>
<string name="common_approve">Approve</string>
<string name="common_attention">Attention</string>
<string name="common_back">Back</string>
<string name="common_balance">Balance: %s</string>
<string name="common_balance_title">Balance</string>
<string name="common_biometric_authentication">biometric authentication</string>
<string name="common_biometrics">biometrics</string>
<string name="common_buy">Buy</string>
<string name="common_buy_currency">Go to %1$s</string>
<string name="common_camera_alert_button_settings">Settings</string>
<string name="common_camera_denied_alert_message">You have not given access to your camera, please adjust your privacy settings</string>
<string name="common_camera_denied_alert_title">Camera access denied</string>
<string name="common_cancel">Cancel</string>
<string name="common_claim_rewards">Claim rewards</string>
<string name="common_close">Close</string>
@ -119,6 +129,7 @@
<string name="common_go_to_token">Go to token</string>
<string name="common_import">Import</string>
<string name="common_later">Later</string>
<string name="common_learn_and_earn">Learn &amp; Earn</string>
<string name="common_locked">Locked</string>
<string name="common_main_network">Main network</string>
<string name="common_network_fee_title">Network fee</string>
@ -131,6 +142,7 @@
<string name="common_origin_card">Primary Card</string>
<string name="common_passphrase">Passphrase</string>
<string name="common_paste">Paste</string>
<string name="common_push">Push</string>
<string name="common_range">%1$s-%2$s</string>
<string name="common_read_more">Read more</string>
<string name="common_receive">Receive</string>
@ -172,7 +184,9 @@
<string name="currency_subtitle_expanded">Available networks</string>
<string name="custom_token_add_token">Add token</string>
<string name="custom_token_contract_address_input_title">Contract address</string>
<string name="custom_token_creation_error_empty_fields">Please fill in all the fields</string>
<string name="custom_token_creation_error_invalid_contract_address">Contract address is invalid</string>
<string name="custom_token_creation_error_invalid_derivation_path">Derivation path is invalid</string>
<string name="custom_token_creation_error_network_not_selected">Please select the network</string>
<string name="custom_token_creation_error_wrong_decimals">Decimal must be a valid integer, up to %li</string>
<string name="custom_token_custom_derivation">Custom derivation</string>
@ -202,6 +216,7 @@
<string name="details_manage_security_access_code_description">You will have to submit the correct access code before scanning the card</string>
<string name="details_manage_security_long_tap">Long Tap</string>
<string name="details_manage_security_long_tap_description">This mechanism protects against proximity attacks on a card. It will enforce a delay between reception and execution of a command. </string>
<string name="details_manage_security_long_tap_shorter">Long Tap</string>
<string name="details_manage_security_passcode">Passcode</string>
<string name="details_manage_security_passcode_description">Before executing any command entailing a change of the card state, you will have to enter the passcode.</string>
<string name="details_referral_title">Referral program</string>
@ -210,6 +225,7 @@
<string name="details_row_title_cid">Card ID</string>
<string name="details_row_title_contact_to_support">Contact support</string>
<string name="details_row_title_create_backup">Link More Cards</string>
<string name="details_row_title_create_backup_footer">You can synchronize up to three cards into one wallet. It can only be done once.</string>
<string name="details_row_title_currency">App Currency</string>
<string name="details_row_title_flip_to_hide">Flip-to-Hide Balances</string>
<string name="details_row_title_issuer">Issuer</string>
@ -224,6 +240,7 @@
<string name="exchange_tokens_empty_tokens">You haven\'t added any tokens yet. Add tokens via Market to swap</string>
<string name="exchange_tokens_unavailable_tokens_header">Cannot be swapped for %s</string>
<string name="express_by_provider">Provided by</string>
<string name="express_by_provider_placeholder">Provided by %s</string>
<string name="express_cex_status_button_title">Status</string>
<string name="express_choose_providers_subtitle">Tangem offers token swaps via 3rd-party providers according to each provider\'s terms</string>
<string name="express_choose_providers_title">Choose provider</string>
@ -287,6 +304,7 @@
<string name="feedback_subject_support">Feedback</string>
<string name="feedback_subject_support_tangem">Tangem feedback</string>
<string name="feedback_subject_tx_failed">Can\'t send a transaction</string>
<string name="feedback_subject_tx_push_failed">Can\'t push a transaction</string>
<string name="give_permission_current_transaction">Current transaction</string>
<string name="give_permission_fee_footer">The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap.</string>
<string name="give_permission_policy_type_footer">Specify the approve limit for the selected token</string>
@ -301,8 +319,10 @@
<string name="initial_message_change_access_code_body">To change the access code tap the card as shown above and do not remove until the end of the operation</string>
<string name="initial_message_change_passcode_body">To change the passcode tap the card as shown above and do not remove until the end of the operation</string>
<string name="initial_message_create_wallet_body">To create the wallet tap the card as shown above and do not remove until the end of the operation</string>
<string name="initial_message_purge_wallet_body">To reset to factory settings tap the card as shown above and do not remove until the end of the operation</string>
<string name="initial_message_reset_backup_card_header">Tap the card #%s of the wallet</string>
<string name="initial_message_scan_header">Tap to scan</string>
<string name="initial_message_sign_body">To sign tap the card as shown above and do not remove until the end of the operation</string>
<string name="initial_message_sign_header">Tap to sign</string>
<string name="initial_message_tap_header">Tap the card</string>
<string name="key_invalidated_warning_description">You have updated biometrics, scan your card to enter</string>
@ -314,6 +334,8 @@
<string name="koinos_mana_exceeds_koin_balance_title">Mana limit</string>
<string name="koinos_mana_level_description">The Koinos network requires Mana for network fees. Your have %1$s/%2$s Mana</string>
<string name="koinos_mana_level_title">Mana level</string>
<string name="mail_error_no_accounts_body">Please, set up an account to send email</string>
<string name="mail_error_no_accounts_title">No Mail accounts</string>
<string name="main_empty_tokens_list_message">To begin tracking your crypto assets and transactions, add tokens</string>
<string name="main_manage_tokens">Manage tokens</string>
<string name="main_scan_card_warning_view_subtitle">To access all the networks you need to scan the card</string>
@ -336,6 +358,11 @@
<item quantity="one">%1$d of %2$d wallet</item>
<item quantity="other">%1$d of %2$d wallets</item>
</plurals>
<string name="manage_tokens_number_of_wallets_ios">%d of %#@total_wallets@</string>
<plurals name="manage_tokens_number_of_wallets_iostotal_wallets">
<item quantity="one">%d wallet</item>
<item quantity="other">%d wallets</item>
</plurals>
<string name="manage_tokens_remove">Remove</string>
<string name="manage_tokens_search_placeholder">e.g. BTC I trust, hodl I must</string>
<string name="manage_tokens_toast_portfolio_updated">Your portfolio has been updated</string>
@ -427,6 +454,7 @@
<string name="onboarding_activation_error_title">Activation error</string>
<string name="onboarding_add_tokens">Add tokens</string>
<string name="onboarding_alert_message_not_max_backup_cards_added">You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process?</string>
<string name="onboarding_alert_message_old_device">iPhone 7/7+ is not able to create a backup for Tangem Wallet due to some system limitations. Please use another phone to perform this operation. All other functions work stably.</string>
<string name="onboarding_backup_exit_warning">The backup process is partly complete. You can\'t exit it now.</string>
<string name="onboarding_bottom_sheet_passphrase_description">The passphrase is an advanced security feature that crypto wallets use. It adds an extra word or phrase of your own choosing to your already existing recovery phrase to unlock a brand-new set of addresses.</string>
<string name="onboarding_button_add_backup_card">Add a backup card</string>
@ -451,6 +479,7 @@
<string name="onboarding_exit_alert_title">Do you want to exit the activation process?</string>
<string name="onboarding_getting_started">Getting started</string>
<string name="onboarding_linking_error_card_with_wallets">Another wallet has already been created on the card you\'re trying to add. If you have funds in this wallet, please withdraw it and then reset this card and add it as a backup.</string>
<string name="onboarding_navbar_save_wallet">Save your wallet</string>
<string name="onboarding_navbar_title_creating_backup">Creating a backup</string>
<string name="onboarding_seed_button_read_more">Read more about seed phrase</string>
<plurals name="onboarding_seed_generate_message_words_count">
@ -470,6 +499,7 @@
<string name="onboarding_seed_mnemonic_invalid_checksum">Invalid seed phrase. Please check the word order.</string>
<string name="onboarding_seed_mnemonic_wrong_words">Invalid seed phrase. Please check your spelling.</string>
<string name="onboarding_seed_phrase_intro_legacy">Legacy</string>
<string name="onboarding_seed_screenshot_alert">We do not recommend storing the seed phrase as a screenshot due to the high risk of loss or hacking</string>
<string name="onboarding_seed_user_validation_message">To check whether youve written down your seed phrase correctly, please enter the 2nd, 7th and 11th words</string>
<string name="onboarding_seed_user_validation_title">So, lets check</string>
<string name="onboarding_subtitle_no_backup_cards">To start the backup process add up to two backup cards.</string>
@ -505,6 +535,9 @@
<string name="organize_tokens_sort_by_balance">By balance</string>
<string name="organize_tokens_title">Organize tokens</string>
<string name="organize_tokens_ungroup">Ungroup</string>
<string name="push_additional_fee">Additional fee</string>
<string name="push_previous_fee">Previous fee</string>
<string name="push_tx_address_hint">Previous transaction total including fee</string>
<string name="qr_scanner_camera_denied_gallery_button">Select from the gallery</string>
<string name="qr_scanner_camera_denied_settings_button">Settings</string>
<string name="qr_scanner_camera_denied_text">You have not given access to your camera</string>
@ -515,6 +548,7 @@
<string name="referral_button_participate">Participate</string>
<string name="referral_error_failed_to_load_info">Failed to load the information about the referral program. Please try again later.</string>
<string name="referral_error_failed_to_load_info_with_reason">Failed to load the information about the referral program. Error code: %s. Please try again later.</string>
<string name="referral_error_failed_to_participate">Your participation request could not be processed. Error code: %s. Please try again later. If the problem persists — feel free to contact our support.</string>
<string name="referral_expected_awards">Upcoming payments</string>
<string name="referral_friends_bought_title">Your friends bought</string>
<string name="referral_less">Less</string>
@ -550,11 +584,15 @@
<string name="russian_bank_card_warning_title">Russian bank cards are not currently accepted</string>
<string name="save_user_wallet_agreement_access_description">Log into the app and check your balance without scanning the card</string>
<string name="save_user_wallet_agreement_access_title">Access the app</string>
<string name="save_user_wallet_agreement_allow">Allow to use %s</string>
<string name="save_user_wallet_agreement_allow_biometrics">Allow to use biometrics</string>
<string name="save_user_wallet_agreement_code_description">%s will be requested instead of the access code for interactions with your wallet</string>
<string name="save_user_wallet_agreement_code_description_biometrics">Biometrics will be requested instead of the access code for interactions with your wallet</string>
<string name="save_user_wallet_agreement_code_title">Access code</string>
<string name="save_user_wallet_agreement_dont_allow">Don\'t allow</string>
<string name="save_user_wallet_agreement_enroll_biometrics_description">It looks like you have biometric authentication disabled, it is necessary to save wallets</string>
<string name="save_user_wallet_agreement_enroll_biometrics_title">Enable biometric authorization</string>
<string name="save_user_wallet_agreement_header">Would you like to use %s?</string>
<string name="save_user_wallet_agreement_header_biometrics">Would you like to use biometrics?</string>
<string name="save_user_wallet_agreement_notice">Note that making a transaction with your funds will still require your card</string>
<string name="scan_card_settings_button">Scan Card</string>
@ -590,6 +628,7 @@
<string name="send_fee_picker_priority">Priority</string>
<string name="send_fee_unreachable_error_text">Check your network connection</string>
<string name="send_fee_unreachable_error_title">Network fee info unreachable</string>
<string name="send_from_wallet">From **%s**</string>
<string name="send_from_wallet_android">From</string>
<string name="send_gas_limit">Gas limit</string>
<string name="send_gas_limit_footer">This is the maximum amount of gas that will be spent to complete a transaction or contract. A gas limit prevents unexpected or unlimited charges when executing a transaction.</string>
@ -653,6 +692,7 @@
<string name="staking_active">Active</string>
<string name="staking_active_footer">To unstake your assets, click here.</string>
<string name="staking_amount_requirement_error">The amount to stake must be at least %s</string>
<string name="staking_claim_unstaked">Claim unstaked</string>
<string name="staking_details_apr">APR</string>
<string name="staking_details_apy">APY</string>
<string name="staking_details_apy_info">The annual percentage return you can earn from participating in staking.</string>
@ -675,15 +715,26 @@
<string name="staking_details_warmup_period">Warmup period</string>
<string name="staking_details_warmup_period_info">The allocated time for activating participation in staking.</string>
<string name="staking_initial_info_title">Stake %s</string>
<string name="staking_migrate">Migrate</string>
<string name="staking_native">Native staking</string>
<string name="staking_notification_earn_rewards_text" formatted="false">Staking allow you to earn %1s. Your staking rewards arrive every ~%2s days.</string>
<string name="staking_notification_earn_rewards_title">Earn staking rewards</string>
<string name="staking_rebond">Rebond</string>
<string name="staking_restake">Restake</string>
<string name="staking_restake_rewards">Restake rewards</string>
<string name="staking_revoke">Revoke</string>
<string name="staking_revote">Revote</string>
<string name="staking_rewards">Rewards</string>
<string name="staking_stake_locked">Stake locked</string>
<string name="staking_stake_more">Stake more</string>
<string name="staking_unlocked_locked">Unlock locked</string>
<string name="staking_unstaked">Unstaked</string>
<string name="staking_unstaked_footer">Check unstaked to claim your assets</string>
<string name="staking_unstaking">Unstaking</string>
<string name="staking_validator">Validator</string>
<string name="staking_vote">Vote</string>
<string name="staking_vote_locked">Vote locked</string>
<string name="staking_withdraw">Withdraw</string>
<string name="story_awe_description">Store your crypto assets secure while keeping private keys contained in your card</string>
<string name="story_awe_title">Revolutionary Hardware Wallet</string>
<string name="story_backup_description">Up to 3 physical cards to one wallet</string>
@ -692,6 +743,8 @@
<string name="story_currencies_title">Thousands of Currencies</string>
<string name="story_finish_description">Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto.</string>
<string name="story_finish_title">The Wallet for Everyone</string>
<string name="story_learn_description">Take three lessons, get a discount on your Tangem Wallet, and receive 1INCH tokens to your wallet</string>
<string name="story_learn_learn">Learn</string>
<string name="story_meet_title">Meet Tangem</string>
<string name="story_web3_description">Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services</string>
<string name="story_web3_title">Web 3.0 Compatible</string>
@ -707,6 +760,7 @@
<string name="swapping_high_price_impact_description">Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.</string>
<string name="swapping_insufficient_funds">Insufficient funds</string>
<string name="swapping_permission_header">Give Permission</string>
<string name="swapping_permit_and_swap">Permit and Swap</string>
<string name="swapping_success_view_title">In progress</string>
<string name="swapping_swap_action">Swap</string>
<string name="swapping_to_title">You receive</string>
@ -772,6 +826,13 @@
<string name="user_wallet_list_rename_popup_title">Rename Wallet</string>
<string name="user_wallet_list_unlock_all">Unlock all</string>
<string name="user_wallet_list_unlock_all_with">Unlock all with %s</string>
<string name="voice_over_close_network_fee_settings">Close network fee settings</string>
<string name="voice_over_nothing_to_paste">Nothing to paste from clipboard</string>
<string name="voice_over_open_card_details">Open card details</string>
<string name="voice_over_open_network_fee_settings">Open network fee settings</string>
<string name="voice_over_open_new_wallet_connect_session">Scan QR code to open new WalletConnect session</string>
<string name="voice_over_paste_from_clipboard">Paste address from clipboard</string>
<string name="voice_over_scan_qr_with_address">Scan, QR, code with address</string>
<string name="wallet_balance_blockchain_unreachable_try_later">Blockchain is unreachable. Try later</string>
<string name="wallet_balance_missing_derivation">Scan the card</string>
<string name="wallet_connect_alert_sign_message">Requesting to sign a message.\n\n%s</string>
@ -850,6 +911,8 @@
<string name="warning_hedera_missing_token_association_message_brief">This token must be associated with your Hedera account before you can receive it</string>
<string name="warning_hedera_missing_token_association_title">Associate your token</string>
<string name="warning_hedera_token_association_not_enough_hbar_message">Not enough %s. Top up your Hedera account to associate this token</string>
<string name="warning_long_transaction_message">iPhone 7/7+ cannot sign transactions on this network. To complete this operation, please use a different phone.</string>
<string name="warning_long_transaction_title">Transaction signing unavailable</string>
<string name="warning_low_signatures_message">Only %s signatures are left on this card. You must withdraw all of your funds.</string>
<string name="warning_low_signatures_title">Low signature count</string>
<string name="warning_manage_tokens_legacy_derivation_message">Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds.</string>
@ -865,6 +928,10 @@
<string name="warning_no_backup_title">Missing backup</string>
<string name="warning_number_of_signed_hashes_incorrect_message">This card has been previously used for transactions. If received from an untrusted source, consider withdrawing all funds. If it\'s your card, no action is required.</string>
<string name="warning_number_of_signed_hashes_incorrect_title">Card has already signed transactions</string>
<string name="warning_old_card_message">Funds in Tangem cards issued before September 2019 cannot be retrieved on iPhones due to iOS restrictions. Please use an Android phone for retrieval. Cards issued after September 2019 work correctly on both OS.</string>
<string name="warning_old_card_title">iOS restriction for older cards</string>
<string name="warning_old_device_old_card_message">Some iPhone 7/7+ models may have NFC issues during certain operations.</string>
<string name="warning_old_device_old_card_title">Device incompatibility detected</string>
<string name="warning_rate_app_message">Your review keeps us motivated to make Tangem Wallet even better</string>
<string name="warning_rate_app_title">Enjoying Tangem?</string>
<string name="warning_receive_blocked_hedera_token_association_required_message">You must associate your token before receiving tokens</string>
@ -876,6 +943,10 @@
<string name="warning_solana_rent_fee_message">Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free.</string>
<string name="warning_some_networks_unreachable_message">Some networks currently are unreachable. Please try again later.</string>
<string name="warning_some_networks_unreachable_title">Some networks are unreachable</string>
<string name="warning_system_deprecation_title">System update required</string>
<string name="warning_system_deprecation_with_date_message">Support for your version of the operating system will end on %s. To receive future app updates you must update it to the latest version.</string>
<string name="warning_system_update_message">Tangem recommends installing the latest iOS update for stable and safe operation</string>
<string name="warning_system_update_title">System update available</string>
<string name="warning_testnet_card_message">This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes.</string>
<string name="warning_testnet_card_title">For testing purposes only</string>
<string name="welcome_interrupted_backup_alert_discard">Discard</string>

View file

@ -11,7 +11,7 @@ sealed interface TangemButtonIconPosition {
data class End(@DrawableRes override val iconResId: Int) : TangemButtonIconPosition
object None : TangemButtonIconPosition {
data object None : TangemButtonIconPosition {
@DrawableRes
override val iconResId: Int? = null
}

View file

@ -187,7 +187,9 @@ internal class DefaultStakingRepository(
params,
),
)
StakingActionCommonType.PENDING -> stakeKitApi.createPendingAction(
StakingActionCommonType.PENDING_OTHER,
StakingActionCommonType.PENDING_REWARDS,
-> stakeKitApi.createPendingAction(
createPendingActionRequestBody(params),
)
}
@ -217,7 +219,9 @@ internal class DefaultStakingRepository(
params,
),
)
StakingActionCommonType.PENDING -> stakeKitApi.estimateGasOnPending(
StakingActionCommonType.PENDING_REWARDS,
StakingActionCommonType.PENDING_OTHER,
-> stakeKitApi.estimateGasOnPending(
createPendingActionRequestBody(params),
)
}

View file

@ -3,5 +3,6 @@ package com.tangem.domain.staking.model.stakekit.action
enum class StakingActionCommonType {
ENTER,
EXIT,
PENDING,
PENDING_REWARDS,
PENDING_OTHER,
}

View file

@ -1,8 +1,11 @@
package com.tangem.features.staking.impl.presentation.state
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
import com.tangem.features.staking.impl.presentation.state.transformers.SetButtonsStateTransformer
import com.tangem.utils.transformer.Transformer
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -20,16 +23,21 @@ internal class StakingStateController @Inject constructor() {
val uiState: StateFlow<StakingUiState> get() = mutableUiState.asStateFlow()
private val buttonsTransformer = SetButtonsStateTransformer()
fun update(function: (StakingUiState) -> StakingUiState) {
mutableUiState.update(function = function)
mutableUiState.update(function = buttonsTransformer::transform)
}
fun update(transformer: Transformer<StakingUiState>) {
mutableUiState.update(function = transformer::transform)
mutableUiState.update(function = buttonsTransformer::transform)
}
fun clear() {
mutableUiState.update { getInitialState() }
mutableUiState.update(function = buttonsTransformer::transform)
}
private fun getInitialState(): StakingUiState {
@ -44,7 +52,8 @@ internal class StakingStateController @Inject constructor() {
isBalanceHidden = false,
event = consumedEvent(),
bottomSheetConfig = null,
routeType = RouteType.STAKE,
actionType = StakingActionCommonType.ENTER,
buttonsState = NavigationButtonsState.Empty,
)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.staking.impl.presentation.state
import com.tangem.common.routing.AppRouter
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
internal class StakingStateRouter(
private val appRouter: AppRouter,
@ -14,12 +15,12 @@ internal class StakingStateRouter(
fun onNextClick() {
when (stateController.value.currentStep) {
StakingStep.InitialInfo -> when (stateController.value.routeType) {
RouteType.STAKE -> showAmount()
RouteType.OTHER,
RouteType.UNSTAKE,
StakingStep.InitialInfo -> when (stateController.value.actionType) {
StakingActionCommonType.ENTER -> showAmount()
StakingActionCommonType.PENDING_OTHER,
StakingActionCommonType.EXIT,
-> showConfirmation()
RouteType.CLAIM -> showRewardsValidators()
StakingActionCommonType.PENDING_REWARDS -> showRewardsValidators()
}
StakingStep.RewardsValidators,
StakingStep.Validators,
@ -37,7 +38,7 @@ internal class StakingStateRouter(
StakingStep.InitialInfo -> onBackClick()
StakingStep.Amount -> showInitial()
StakingStep.Confirmation -> {
if (uiState.routeType == RouteType.OTHER || uiState.routeType == RouteType.UNSTAKE) {
if (uiState.actionType != StakingActionCommonType.ENTER) {
showInitial()
} else {
showAmount()

View file

@ -2,11 +2,13 @@ package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.transformers.InfoType
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import kotlinx.collections.immutable.ImmutableList
@ -25,7 +27,8 @@ internal data class StakingUiState(
val confirmationState: StakingStates.ConfirmationState,
val isBalanceHidden: Boolean,
val bottomSheetConfig: TangemBottomSheetConfig?,
val routeType: RouteType,
val actionType: StakingActionCommonType,
val buttonsState: NavigationButtonsState,
val event: StateEvent<StakingEvent>,
) {
@ -98,11 +101,4 @@ enum class StakingStep {
Amount,
Confirmation,
Validators,
}
enum class RouteType {
STAKE,
UNSTAKE,
CLAIM,
OTHER,
}

View file

@ -0,0 +1,226 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.common.ui.navigationButtons.NavigationButton
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal class SetButtonsStateTransformer : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
val buttonsState = if (prevState.isButtonsVisible()) {
NavigationButtonsState.Data(
primaryButton = getPrimaryButton(prevState),
prevButton = getPrevButton(prevState),
secondaryButton = getSecondaryButton(prevState),
extraButtons = getExtraButtons(prevState),
txUrl = (confirmState?.transactionDoneState as? TransactionDoneState.Content)?.txUrl,
)
} else {
NavigationButtonsState.Empty
}
return prevState.copy(buttonsState = buttonsState)
}
private fun getPrimaryButton(prevState: StakingUiState): NavigationButton {
val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
val innerConfirmState = confirmState?.innerState
val isPrimaryInProgress =
confirmState?.pendingActions?.getPrimaryAction() == confirmState?.pendingActionInProgress
val isConfirmation = prevState.currentStep == StakingStep.Confirmation
val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS
val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED
val isIconVisible = isConfirmation && !isCompleted
val isShowProgress = isInProgress && isPrimaryInProgress
return NavigationButton(
textReference = prevState.getButtonText(),
iconRes = R.drawable.ic_tangem_24,
isSecondary = false,
isIconVisible = isIconVisible,
showProgress = isShowProgress,
isEnabled = prevState.isButtonEnabled(),
onClick = { prevState.onPrimaryClick() },
)
}
private fun getSecondaryButton(prevState: StakingUiState): NavigationButton? {
val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
val innerConfirmState = confirmState?.innerState
val isConfirmation = prevState.currentStep == StakingStep.Confirmation
val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS
val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED
return confirmState?.pendingActions?.getSecondaryAction()?.let { pendingAction ->
val isSecondaryInProgress = pendingAction == confirmState.pendingActionInProgress
val isShowProgress = isInProgress && isSecondaryInProgress
NavigationButton(
textReference = getPendingActionTitle(pendingAction.type),
iconRes = R.drawable.ic_tangem_24,
isSecondary = true,
isIconVisible = true,
showProgress = isShowProgress,
isEnabled = prevState.isButtonEnabled(),
onClick = { prevState.clickIntents.onActionClick(pendingAction) },
).takeIf { isConfirmation && !isCompleted }
}
}
private fun getPrevButton(prevState: StakingUiState): NavigationButton? {
return NavigationButton(
textReference = TextReference.EMPTY,
iconRes = R.drawable.ic_back_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = prevState.clickIntents::onPrevClick,
).takeIf { prevState.currentStep.isPrevButtonVisible() }
}
private fun getExtraButtons(prevState: StakingUiState): ImmutableList<NavigationButton> {
return persistentListOf(
NavigationButton(
textReference = resourceReference(R.string.common_explore),
iconRes = R.drawable.ic_web_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = prevState.clickIntents::onExploreClick,
),
NavigationButton(
textReference = resourceReference(R.string.common_share),
iconRes = R.drawable.ic_share_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = prevState.clickIntents::onShareClick,
),
)
}
private fun List<PendingAction>.getPrimaryAction(): PendingAction? = getOrNull(0)
private fun List<PendingAction>.getSecondaryAction(): PendingAction? = getOrNull(1)
private fun StakingUiState.isButtonsVisible(): Boolean = when (currentStep) {
StakingStep.InitialInfo -> {
val initialState = initialInfoState as? StakingStates.InitialInfoState.Data
initialState?.isStakeMoreAvailable == true || initialState?.yieldBalance is InnerYieldBalanceState.Empty
}
StakingStep.RewardsValidators -> false
else -> true
}
private fun StakingUiState.getButtonText(): TextReference {
return when (currentStep) {
StakingStep.InitialInfo -> {
val initialState = initialInfoState as? StakingStates.InitialInfoState.Data
if (initialState?.yieldBalance is InnerYieldBalanceState.Data) {
resourceReference(R.string.staking_stake_more)
} else {
resourceReference(R.string.common_next)
}
}
StakingStep.Confirmation -> {
if (confirmationState is StakingStates.ConfirmationState.Data) {
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
resourceReference(R.string.common_close)
} else {
when (actionType) {
StakingActionCommonType.ENTER -> resourceReference(R.string.common_stake)
StakingActionCommonType.EXIT -> resourceReference(R.string.common_unstake)
StakingActionCommonType.PENDING_OTHER,
StakingActionCommonType.PENDING_REWARDS,
-> getPendingActionTitle(confirmationState.pendingActions.firstOrNull()?.type)
}
}
} else {
resourceReference(R.string.common_close)
}
}
StakingStep.Validators -> resourceReference(R.string.common_continue)
StakingStep.Amount,
StakingStep.RewardsValidators,
-> resourceReference(R.string.common_next)
}
}
private fun StakingUiState.onPrimaryClick() {
when (currentStep) {
StakingStep.InitialInfo,
StakingStep.Validators,
StakingStep.Amount,
-> clickIntents.onNextClick()
StakingStep.Confirmation -> {
if (confirmationState is StakingStates.ConfirmationState.Data) {
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
clickIntents.onBackClick()
} else {
clickIntents.onActionClick(confirmationState.pendingActions.firstOrNull())
}
} else {
clickIntents.onBackClick()
}
}
StakingStep.RewardsValidators -> Unit
}
}
private fun StakingStep.isPrevButtonVisible(): Boolean = when (this) {
StakingStep.InitialInfo,
StakingStep.RewardsValidators,
StakingStep.Confirmation,
-> false
StakingStep.Amount,
StakingStep.Validators,
-> true
}
private fun StakingUiState.isButtonEnabled(): Boolean {
return when (currentStep) {
StakingStep.InitialInfo -> initialInfoState.isPrimaryButtonEnabled
StakingStep.Amount -> amountState.isPrimaryButtonEnabled
StakingStep.Confirmation -> confirmationState.isPrimaryButtonEnabled
StakingStep.RewardsValidators -> rewardsValidatorsState.isPrimaryButtonEnabled
StakingStep.Validators -> true
}
}
@Suppress("CyclomaticComplexMethod")
private fun getPendingActionTitle(type: StakingActionType?): TextReference = when (type) {
StakingActionType.CLAIM_REWARDS -> resourceReference(R.string.common_claim_rewards)
StakingActionType.RESTAKE_REWARDS -> resourceReference(R.string.staking_restake_rewards)
StakingActionType.WITHDRAW -> resourceReference(R.string.staking_withdraw)
StakingActionType.RESTAKE -> resourceReference(R.string.staking_restake)
StakingActionType.CLAIM_UNSTAKED -> resourceReference(R.string.staking_claim_unstaked)
StakingActionType.UNLOCK_LOCKED -> resourceReference(R.string.staking_unlocked_locked)
StakingActionType.STAKE_LOCKED -> resourceReference(R.string.staking_stake_locked)
StakingActionType.VOTE -> resourceReference(R.string.staking_vote)
StakingActionType.REVOKE -> resourceReference(R.string.staking_revoke)
StakingActionType.VOTE_LOCKED -> resourceReference(R.string.staking_vote_locked)
StakingActionType.REVOTE -> resourceReference(R.string.staking_revote)
StakingActionType.REBOND -> resourceReference(R.string.staking_rebond)
StakingActionType.MIGRATE -> resourceReference(R.string.staking_migrate)
StakingActionType.STAKE -> resourceReference(R.string.common_stake)
StakingActionType.UNSTAKE -> resourceReference(R.string.common_unstake)
StakingActionType.UNKNOWN -> TextReference.EMPTY
null -> TextReference.EMPTY
}
}

View file

@ -19,7 +19,6 @@ internal class SetConfirmationStateAssentTransformer(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val stakingGasEstimate: StakingGasEstimate,
private val pendingActionList: ImmutableList<PendingAction>,
private val pendingAction: PendingAction?,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
@ -49,7 +48,6 @@ internal class SetConfirmationStateAssentTransformer(
),
validatorState = validatorState.copySealed(isClickable = true),
pendingActions = pendingActionList,
pendingActionInProgress = pendingAction,
isPrimaryButtonEnabled = true,
)
} else {

View file

@ -1,11 +1,14 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
internal class SetConfirmationStateInProgressTransformer : Transformer<StakingUiState> {
internal class SetConfirmationStateInProgressTransformer(
private val pendingAction: PendingAction?,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
@ -19,6 +22,7 @@ internal class SetConfirmationStateInProgressTransformer : Transformer<StakingUi
isPrimaryButtonEnabled = false,
innerState = InnerConfirmationStakingState.IN_PROGRESS,
validatorState = validatorState.copySealed(isClickable = false),
pendingActionInProgress = pendingAction,
)
} else {
this

View file

@ -18,8 +18,8 @@ import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.RouteType
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.TransactionDoneState
import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData
@ -34,7 +34,7 @@ internal fun StakingConfirmationContent(
amountState: AmountState,
state: StakingStates.ConfirmationState,
clickIntents: StakingClickIntents,
type: RouteType,
type: StakingActionCommonType,
) {
if (state !is StakingStates.ConfirmationState.Data) return
@ -62,7 +62,7 @@ internal fun StakingConfirmationContent(
isEditingDisabled = true,
onClick = {},
)
if (type == RouteType.STAKE) {
if (type == StakingActionCommonType.ENTER) {
ValidatorBlock(validatorState = state.validatorState, onClick = clickIntents::openValidators)
}
StakingFeeBlock(feeState = state.feeState)
@ -93,7 +93,7 @@ private fun Preview_StakingConfirmationContent() {
amountState = AmountStatePreviewData.amountState,
state = ConfirmationStatePreviewData.assentStakingState,
clickIntents = StakingClickIntentsStub,
type = RouteType.STAKE,
type = StakingActionCommonType.ENTER,
)
}
}

View file

@ -1,199 +0,0 @@
package com.tangem.features.staking.impl.presentation.ui
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.tangem.common.ui.amountScreen.ui.SendDoneButtons
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerW12
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.TangemButtonsDefaults
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.staking.impl.presentation.state.*
@Composable
internal fun StakingNavigationButtons(uiState: StakingUiState, modifier: Modifier = Modifier) {
val confirmInnerState = (uiState.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState
val isSuccessState = confirmInnerState == InnerConfirmationStakingState.COMPLETED
Column(
modifier = modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
val confirmationDataState = uiState.confirmationState as? StakingStates.ConfirmationState.Data
val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content
SendDoneButtons(
txUrl = transactionDoneState?.txUrl.orEmpty(),
onExploreClick = uiState.clickIntents::onExploreClick,
onShareClick = uiState.clickIntents::onShareClick,
isVisible = isSuccessState,
)
StakingNavigationButton(
uiState = uiState,
modifier = Modifier,
)
}
}
@Composable
private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier = Modifier) {
val hapticFeedback = LocalHapticFeedback.current
val isButtonsVisible = isPrevButtonVisible(uiState.currentStep)
val innerConfirmState = (uiState.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState
val isInProgressInnerState = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS
val isInAssentInnerState = innerConfirmState == InnerConfirmationStakingState.ASSENT
val showTangemIcon = uiState.currentStep == StakingStep.Confirmation &&
(isInProgressInnerState || isInAssentInnerState)
val buttonTextId = getButtonData(currentState = uiState)
val (isButtonEnabled, isButtonDisplayed) = isButtonEnabled(uiState)
val buttonIcon = if (showTangemIcon) {
TangemButtonIconPosition.End(R.drawable.ic_tangem_24)
} else {
TangemButtonIconPosition.None
}
Row(modifier = modifier) {
AnimatedVisibility(
visible = isButtonsVisible,
enter = expandHorizontally(expandFrom = Alignment.End),
exit = shrinkHorizontally(shrinkTowards = Alignment.End),
) {
Row {
Icon(
painter = painterResource(R.drawable.ic_back_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
modifier = Modifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
.background(TangemTheme.colors.button.secondary)
.clickable { uiState.clickIntents.onPrevClick() }
.padding(TangemTheme.dimens.spacing12),
)
SpacerW12()
}
}
AnimatedVisibility(
visible = isButtonDisplayed,
enter = fadeIn(),
exit = fadeOut(),
) {
TangemButton(
text = stringResource(buttonTextId),
icon = buttonIcon,
enabled = isButtonEnabled && isButtonDisplayed,
onClick = {
if (showTangemIcon) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onPrimaryClick(uiState)
},
showProgress = isInProgressInnerState,
modifier = Modifier.fillMaxWidth(),
colors = TangemButtonsDefaults.primaryButtonColors,
)
}
}
}
private fun getButtonData(currentState: StakingUiState): Int {
return when (currentState.currentStep) {
StakingStep.InitialInfo -> {
val initialState = currentState.initialInfoState as? StakingStates.InitialInfoState.Data
if (initialState?.yieldBalance is InnerYieldBalanceState.Data) {
R.string.staking_stake_more
} else {
R.string.common_next
}
}
StakingStep.Confirmation -> {
val confirmationState = currentState.confirmationState
if (confirmationState is StakingStates.ConfirmationState.Data) {
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
R.string.common_close
} else {
when (currentState.routeType) {
RouteType.STAKE -> R.string.common_stake
RouteType.CLAIM -> R.string.common_claim_rewards
RouteType.UNSTAKE -> R.string.common_unstake
RouteType.OTHER -> R.string.common_stake
}
}
} else {
R.string.common_close
}
}
StakingStep.Validators -> R.string.common_continue
StakingStep.Amount,
StakingStep.RewardsValidators,
-> R.string.common_next
}
}
private fun onPrimaryClick(currentState: StakingUiState) {
when (currentState.currentStep) {
StakingStep.InitialInfo,
StakingStep.Validators,
StakingStep.Amount,
-> currentState.clickIntents.onNextClick()
StakingStep.Confirmation -> {
val confirmationState = currentState.confirmationState
if (confirmationState is StakingStates.ConfirmationState.Data) {
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
currentState.clickIntents.onBackClick()
} else {
currentState.clickIntents.onNextClick()
}
} else {
currentState.clickIntents.onBackClick()
}
}
StakingStep.RewardsValidators -> Unit
}
}
private fun isPrevButtonVisible(step: StakingStep): Boolean = when (step) {
StakingStep.InitialInfo,
StakingStep.RewardsValidators,
StakingStep.Confirmation,
-> false
StakingStep.Amount,
StakingStep.Validators,
-> true
}
private fun isButtonEnabled(uiState: StakingUiState): Pair<Boolean, Boolean> {
return when (uiState.currentStep) {
StakingStep.InitialInfo -> {
val initialState = uiState.initialInfoState as? StakingStates.InitialInfoState.Data
val isDisplayed = initialState?.isStakeMoreAvailable == true ||
initialState?.yieldBalance is InnerYieldBalanceState.Empty
uiState.initialInfoState.isPrimaryButtonEnabled to isDisplayed
}
StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled to true
StakingStep.Confirmation -> uiState.confirmationState.isPrimaryButtonEnabled to true
StakingStep.RewardsValidators -> uiState.rewardsValidatorsState.isPrimaryButtonEnabled to false
StakingStep.Validators -> true to true
}
}

View file

@ -13,6 +13,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.tangem.common.ui.amountScreen.AmountScreenContent
import com.tangem.common.ui.navigationButtons.NavigationButtonsBlock
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.res.TangemTheme
@ -45,8 +46,13 @@ internal fun StakingScreen(uiState: StakingUiState) {
uiState = uiState,
modifier = Modifier.weight(1f),
)
StakingNavigationButtons(
uiState = uiState,
NavigationButtonsBlock(
buttonState = uiState.buttonsState,
modifier = Modifier.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
)
StakingBottomSheet(bottomSheetConfig = uiState.bottomSheetConfig)
}
@ -155,7 +161,7 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M
amountState = uiState.amountState,
state = uiState.confirmationState,
clickIntents = uiState.clickIntents,
type = uiState.routeType,
type = uiState.actionType,
)
StakingStep.Validators -> {
val confirmState = uiState.confirmationState

View file

@ -117,7 +117,7 @@ internal class StakingViewModel @Inject constructor(
private fun handleOnNextConfirmationClick(pendingAction: PendingAction?) {
if (isAssentState()) {
viewModelScope.launch {
stateController.update(SetConfirmationStateInProgressTransformer())
stateController.update(SetConfirmationStateInProgressTransformer(pendingAction))
val confirmationState =
value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state")
@ -128,7 +128,7 @@ internal class StakingViewModel @Inject constructor(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
actionCommonType = getStakingCommonType(),
actionCommonType = value.actionType,
integrationId = yield.id,
amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided"),
@ -149,7 +149,6 @@ internal class StakingViewModel @Inject constructor(
gasEstimate = stakingTransaction.gasEstimate ?: error("No gas estimate available"),
txData = TransactionData.Compiled(value = it.hexToBytes()),
pendingActionList = confirmationState.pendingActions,
pendingAction = pendingAction,
)
} ?: error("No unsigned transaction available")
}
@ -170,7 +169,7 @@ internal class StakingViewModel @Inject constructor(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
actionCommonType = getStakingCommonType(),
actionCommonType = value.actionType,
integrationId = yield.id,
amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided"),
@ -189,7 +188,6 @@ internal class StakingViewModel @Inject constructor(
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
stakingGasEstimate = stakingGasEstimate,
pendingActionList = pendingActions,
pendingAction = pendingAction,
),
)
}
@ -230,17 +228,17 @@ internal class StakingViewModel @Inject constructor(
}
override fun openRewardsValidators() {
stateController.update { it.copy(routeType = RouteType.CLAIM) }
stateController.update { it.copy(actionType = StakingActionCommonType.PENDING_REWARDS) }
onNextClick()
}
override fun onActiveStake(activeStake: BalanceState) {
val routeType = if (activeStake.pendingActions.isEmpty()) {
RouteType.UNSTAKE
val actionType = if (activeStake.pendingActions.isEmpty()) {
StakingActionCommonType.EXIT
} else {
RouteType.OTHER
StakingActionCommonType.PENDING_OTHER
}
stateController.update { it.copy(routeType = routeType) }
stateController.update { it.copy(actionType = actionType) }
stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, activeStake.cryptoValue))
onNextClick(activeStake.pendingActions)
}
@ -256,7 +254,7 @@ internal class StakingViewModel @Inject constructor(
}
override fun onShareClick() {
// TODO staking analytics event
// TODO add hash to clipboard and send analytics event
}
fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) {
@ -325,7 +323,6 @@ internal class StakingViewModel @Inject constructor(
gasEstimate: StakingGasEstimate,
txData: TransactionData,
pendingActionList: ImmutableList<PendingAction>,
pendingAction: PendingAction?,
) {
sendTransactionUseCase(
txData = txData,
@ -340,7 +337,6 @@ internal class StakingViewModel @Inject constructor(
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
stakingGasEstimate = gasEstimate,
pendingActionList = pendingActionList,
pendingAction = pendingAction,
),
)
// todo add error dialog
@ -398,12 +394,4 @@ internal class StakingViewModel @Inject constructor(
(value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState ==
InnerConfirmationStakingState.ASSENT
}
private fun getStakingCommonType() = when (value.routeType) {
RouteType.STAKE -> StakingActionCommonType.ENTER
RouteType.UNSTAKE -> StakingActionCommonType.EXIT
RouteType.CLAIM,
RouteType.OTHER,
-> StakingActionCommonType.PENDING
}
}