Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-03 17:17:22 +05:00
commit d61801a6d4
18 changed files with 414 additions and 383 deletions

View file

@ -0,0 +1,340 @@
package com.tangem.common.ui.bottomsheets
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.window.PopupProperties
import com.tangem.common.ui.R
import com.tangem.common.ui.bottomsheets.state.*
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons
import com.tangem.core.ui.components.appbar.models.AdditionalButton
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
@Composable
fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(
config = config,
containerColor = TangemTheme.colors.background.secondary,
) { content: GiveTxPermissionBottomSheetConfig ->
GiveTxPermissionBottomSheetContent(content = content)
}
}
@Composable
private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSheetConfig) {
var isPermissionAlertShow by remember { mutableStateOf(false) }
val data = content.data
Column(
modifier = Modifier
.background(color = TangemTheme.colors.background.secondary)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AppBarWithAdditionalButtons(
text = resourceReference(id = R.string.give_permission_title),
iconColor = TangemTheme.colors.text.tertiary,
endButton = AdditionalButton(
iconRes = R.drawable.ic_information_24,
onIconClicked = { isPermissionAlertShow = true },
),
)
Text(
text = stringResource(
id = R.string.give_permission_subtitle,
data.currency,
),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body2,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing24),
)
SpacerH16()
ApprovalBottomSheetInfo(data)
SpacerH(height = TangemTheme.dimens.spacing20)
PrimaryButtonIconEnd(
text = stringResource(id = R.string.common_approve),
iconResId = R.drawable.ic_tangem_24,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
onClick = data.approveButton.onClick,
)
SpacerH12()
SecondaryButton(
text = stringResource(id = R.string.common_cancel),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
onClick = content.onCancel,
)
SpacerH16()
// region dialog
if (isPermissionAlertShow) {
BasicDialog(
message = stringResource(id = R.string.swapping_approve_information_text),
title = stringResource(id = R.string.swapping_approve_information_title),
confirmButton = DialogButton { isPermissionAlertShow = false },
onDismissDialog = {},
)
}
}
}
@Composable
private fun ApprovalBottomSheetInfo(data: GiveTxPermissionState.ReadyForRequest) {
FooterContainer(
footer = stringResource(id = R.string.give_permission_policy_type_footer),
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
AmountItem(
currency = data.currency,
approveType = data.approveType,
onChangeApproveType = data.onChangeApproveType,
approveItems = data.approveItems,
)
}
SpacerH16()
FooterContainer(
footer = stringResource(id = R.string.give_permission_fee_footer),
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
FeeItem(fee = data.fee)
}
}
@Composable
private fun FeeItem(fee: TextReference) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.padding(
top = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing16,
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(R.string.common_network_fee_title),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
maxLines = 1,
)
EllipsisText(
text = fee.resolveReference(),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body1,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing16),
)
}
}
@Composable
private fun AmountItem(
currency: String,
approveType: ApproveType,
approveItems: ImmutableList<ApproveType>,
onChangeApproveType: (ApproveType) -> Unit,
) {
var isExpandSelector by remember { mutableStateOf(false) }
var amountSize by remember { mutableStateOf(IntSize.Zero) }
Box(
modifier = Modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
onClick = { isExpandSelector = true },
),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.onSizeChanged { amountSize = it }
.padding(
top = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing16,
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(id = R.string.give_permission_rows_amount, currency),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
maxLines = 1,
)
when (approveType) {
ApproveType.LIMITED -> {
Text(
text = stringResource(id = R.string.give_permission_current_transaction),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body1,
maxLines = 1,
)
}
ApproveType.UNLIMITED -> {
Icon(
painter = rememberVectorPainter(
image = ImageVector.vectorResource(id = R.drawable.ic_infinity_24),
),
tint = TangemTheme.colors.text.tertiary,
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens.size20),
)
}
}
}
DropdownSelector(
isExpanded = isExpandSelector,
onDismiss = { isExpandSelector = false },
onItemClick = { approveType ->
isExpandSelector = false
onChangeApproveType.invoke(approveType)
},
items = approveItems,
selectedType = approveType,
amountSize = amountSize,
)
}
}
@Suppress("LongParameterList")
@Composable
private fun DropdownSelector(
isExpanded: Boolean,
onDismiss: () -> Unit,
onItemClick: (ApproveType) -> Unit,
items: ImmutableList<ApproveType>,
selectedType: ApproveType,
amountSize: IntSize,
) {
var dropDownWidth by remember { mutableStateOf(IntSize.Zero) }
val offsetY = amountSize.height.times(-1)
val offsetX = amountSize.width - dropDownWidth.width
// Workaround to set color and shape of dropdown menu
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(surface = TangemTheme.colors.background.action),
shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(TangemTheme.dimens.radius16)),
) {
DropdownMenu(
expanded = isExpanded,
onDismissRequest = onDismiss,
properties = PopupProperties(clippingEnabled = false),
offset = with(LocalDensity.current) {
DpOffset(x = offsetX.toDp(), y = offsetY.toDp())
},
modifier = Modifier
.wrapContentSize()
.background(TangemTheme.colors.background.action)
.onSizeChanged { dropDownWidth = it },
) {
items.forEach { item ->
val color = if (item == selectedType) TangemTheme.colors.icon.accent else Color.Transparent
DropdownMenuItem(
modifier = Modifier.fillMaxWidth(),
text = {
Row {
Text(
text = when (item) {
ApproveType.LIMITED -> stringResource(
id = R.string.give_permission_current_transaction,
)
ApproveType.UNLIMITED -> stringResource(id = R.string.give_permission_unlimited)
},
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body1,
maxLines = 1,
)
SpacerWMax()
Icon(
painter = rememberVectorPainter(
image = ImageVector.vectorResource(id = R.drawable.ic_check_24),
),
tint = color,
contentDescription = null,
modifier = Modifier.padding(start = TangemTheme.dimens.size20),
)
}
},
onClick = {
onItemClick.invoke(item)
},
)
}
}
}
}
// region preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_AgreementBottomSheet() {
TangemThemePreview {
GiveTxPermissionBottomSheetContent(content = previewData)
}
}
private val previewData = GiveTxPermissionBottomSheetConfig(
data = GiveTxPermissionState.ReadyForRequest(
currency = "DAI",
amount = "1",
walletAddress = "",
spenderAddress = "",
fee = TextReference.Str("2,14$"),
approveType = ApproveType.UNLIMITED,
approveButton = ApprovePermissionButton(true) {},
cancelButton = CancelPermissionButton(true),
onChangeApproveType = { ApproveType.UNLIMITED },
),
onCancel = {},
)

View file

@ -0,0 +1,8 @@
package com.tangem.common.ui.bottomsheets.state
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
data class GiveTxPermissionBottomSheetConfig(
val data: GiveTxPermissionState.ReadyForRequest,
val onCancel: () -> Unit,
) : TangemBottomSheetConfigContent

View file

@ -1,15 +1,14 @@
package com.tangem.feature.swap.models
package com.tangem.common.ui.bottomsheets.state
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.swap.domain.models.domain.SwapApproveType
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
sealed class SwapPermissionState {
sealed class GiveTxPermissionState {
object InProgress : SwapPermissionState()
data object InProgress : GiveTxPermissionState()
object Empty : SwapPermissionState()
data object Empty : GiveTxPermissionState()
data class ReadyForRequest(
val currency: String,
@ -18,24 +17,17 @@ sealed class SwapPermissionState {
val spenderAddress: String,
val fee: TextReference,
val approveType: ApproveType,
val approveItems: ImmutableList<ApproveType> = ApproveType.values().toList().toImmutableList(),
val approveItems: ImmutableList<ApproveType> = ApproveType.entries.toImmutableList(),
val approveButton: ApprovePermissionButton,
val cancelButton: CancelPermissionButton,
val onChangeApproveType: (ApproveType) -> Unit,
) : SwapPermissionState()
) : GiveTxPermissionState()
}
enum class ApproveType {
LIMITED, UNLIMITED
}
fun ApproveType.toDomainApproveType(): SwapApproveType {
return when (this) {
ApproveType.LIMITED -> SwapApproveType.LIMITED
ApproveType.UNLIMITED -> SwapApproveType.UNLIMITED
}
}
data class ApprovePermissionButton(
val enabled: Boolean,
val loading: Boolean = false,

View file

@ -70,6 +70,7 @@
<string name="common_access_denied">Доступ запрещен</string>
<string name="common_apply">Применить</string>
<string name="common_approval">Одобрение</string>
<string name="common_approve">Подтвердить</string>
<string name="common_attention">Внимание</string>
<string name="common_balance">Баланс: %s</string>
<string name="common_balance_title">Баланс</string>
@ -261,6 +262,13 @@
<string name="feedback_subject_support">Обращение в поддержку</string>
<string name="feedback_subject_support_tangem">Обращение в поддержку Tangem</string>
<string name="feedback_subject_tx_failed">Не могу отправить транзакцию</string>
<string name="give_permission_current_transaction">Текущая транзакция</string>
<string name="give_permission_fee_footer">Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена.</string>
<string name="give_permission_policy_type_footer">Укажите лимит доступа к выбранному токену</string>
<string name="give_permission_rows_amount">Количество %s</string>
<string name="give_permission_subtitle">Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s</string>
<string name="give_permission_title">Дать разрешение</string>
<string name="give_permission_unlimited">Безлимитно</string>
<string name="home_button_order">Купить</string>
<string name="home_button_scan">Сканировать</string>
<string name="initial_message_change_access_code_body">Чтобы изменить код доступа, приложите карту как показано выше и не убирайте до окончания операции</string>
@ -388,6 +396,7 @@
<string name="onboarding_title">Активация карты</string>
<string name="onboarding_title_backup_card_format">Резервная карта #%d</string>
<string name="onboarding_title_no_backup_cards">Нет резервных карт</string>
<string name="onboarding_title_notifications">Уведомления</string>
<string name="onboarding_title_one_backup_card">Добавлена ​​одна резервная карта</string>
<string name="onboarding_title_scan_origin_card">Подготовьте свою карту</string>
<string name="onboarding_title_two_backup_cards">Добавлены две резервные карты</string>
@ -581,17 +590,9 @@
<string name="swapping_approve_information_text">Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен.</string>
<string name="swapping_approve_information_title">Подтвердить</string>
<string name="swapping_from_title">Вы отправляете</string>
<string name="swapping_give_permission">Дать разрешение</string>
<string name="swapping_high_price_impact_description">Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму.</string>
<string name="swapping_insufficient_funds">Недостаточно средств</string>
<string name="swapping_permission_buttons_approve">Подтвердить</string>
<string name="swapping_permission_current_transaction">Текущая транзакция</string>
<string name="swapping_permission_fee_footer">Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена.</string>
<string name="swapping_permission_header">Дать разрешение</string>
<string name="swapping_permission_policy_type_footer">Укажите лимит доступа к выбранному токену</string>
<string name="swapping_permission_rows_amount">Количество %s</string>
<string name="swapping_permission_subheader">Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s</string>
<string name="swapping_permission_unlimited">Безлимитно</string>
<string name="swapping_success_view_title">В процессе</string>
<string name="swapping_swap_action">Обменять</string>
<string name="swapping_to_title">Вы получите</string>

View file

@ -277,13 +277,9 @@
<string name="story_web3_title">兼容Web3.0</string>
<string name="swapping_approve_information_text">批准被視為所有去中心化交易所的行業標準,並保護您的錢包在未經您許可的情況下不被智能合約訪問。按照設計,智能合約無法訪問您的代幣,除非您從您的終端批准訪問。通過“解鎖”您的代幣,您將獲得 1inch 智能合約使用您的資產的權限。網絡的礦工將獲得Gas Fee由您支付作為補償以在區塊鏈上記錄此操作。一旦獲得許可您就可以交易您的代幣。</string>
<string name="swapping_approve_information_title">批准</string>
<string name="swapping_give_permission">賦予權限</string>
<string name="swapping_high_price_impact_description">在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量</string>
<string name="swapping_insufficient_funds">餘額不足</string>
<string name="swapping_permission_buttons_approve">允許</string>
<string name="swapping_permission_header">賦予權限</string>
<string name="swapping_permission_rows_amount">數量 %s</string>
<string name="swapping_permission_subheader">要繼續,您需要允許 1inch 智能合約使用您的 %s</string>
<string name="swapping_success_view_title">進行中</string>
<string name="swapping_swap_action">交易</string>
<string name="swapping_token_list_title">選擇代幣</string>

View file

@ -71,6 +71,7 @@
<string name="common_allow">Allow</string>
<string name="common_apply">Apply</string>
<string name="common_approval">Approval</string>
<string name="common_approve">Approve</string>
<string name="common_attention">Attention</string>
<string name="common_balance">Balance: %s</string>
<string name="common_balance_title">Balance</string>
@ -264,6 +265,13 @@
<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="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>
<string name="give_permission_rows_amount">Amount %s</string>
<string name="give_permission_subtitle">To continue, grant 1inch smart contracts permission to use your %s</string>
<string name="give_permission_title">Give Permission</string>
<string name="give_permission_unlimited">Unlimited</string>
<string name="home_button_order">Order card</string>
<string name="home_button_scan">Scan card</string>
<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>
@ -397,6 +405,7 @@
<string name="onboarding_title">Activating card</string>
<string name="onboarding_title_backup_card_format">Backup card #%d</string>
<string name="onboarding_title_no_backup_cards">No backup cards</string>
<string name="onboarding_title_notifications">Notifications</string>
<string name="onboarding_title_one_backup_card">One backup card added</string>
<string name="onboarding_title_scan_origin_card">Prepare your card</string>
<string name="onboarding_title_two_backup_cards">Two backup cards added</string>
@ -607,17 +616,9 @@
<string name="swapping_approve_information_text">All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval.</string>
<string name="swapping_approve_information_title">Approve</string>
<string name="swapping_from_title">You swap</string>
<string name="swapping_give_permission">Give Permission</string>
<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_buttons_approve">Approve</string>
<string name="swapping_permission_current_transaction">Current transaction</string>
<string name="swapping_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="swapping_permission_header">Give Permission</string>
<string name="swapping_permission_policy_type_footer">Specify the approve limit for the selected token</string>
<string name="swapping_permission_rows_amount">Amount %s</string>
<string name="swapping_permission_subheader">To continue, grant 1inch smart contracts permission to use your %s</string>
<string name="swapping_permission_unlimited">Unlimited</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>
@ -671,7 +672,7 @@
<string name="unlock_wallet_description_full">Use %s or scan a card to have an access to your wallet</string>
<string name="user_push_notification_agreement_argument_one">Stay up to date with the latest features and news</string>
<string name="user_push_notification_agreement_argument_two">Be able to get important market notifications</string>
<string name="user_push_notification_agreement_header">Would you like to usePush-notifications?</string>
<string name="user_push_notification_agreement_header">Would you like to use\nPush-notifications?</string>
<string name="user_wallet_list_add_button">Add new wallet</string>
<string name="user_wallet_list_delete_prompt">Are you sure you want to delete this wallet?</string>
<string name="user_wallet_list_error_unable_to_unlock">An error has occurred, please scan your card to log in</string>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#1E1E1E" android:pathData="M5.5,17.5C3.967,17.5 2.667,16.967 1.6,15.9C0.533,14.833 0,13.533 0,12C0,10.467 0.533,9.167 1.6,8.1C2.667,7.033 3.967,6.5 5.5,6.5C6.117,6.5 6.708,6.608 7.275,6.825C7.842,7.042 8.35,7.35 8.8,7.75L10.5,9.3L9,10.65L7.45,9.25C7.183,9.017 6.883,8.833 6.55,8.7C6.217,8.567 5.867,8.5 5.5,8.5C4.533,8.5 3.708,8.842 3.025,9.525C2.342,10.208 2,11.033 2,12C2,12.967 2.342,13.792 3.025,14.475C3.708,15.158 4.533,15.5 5.5,15.5C5.867,15.5 6.217,15.433 6.55,15.3C6.883,15.167 7.183,14.983 7.45,14.75L15.2,7.75C15.65,7.35 16.158,7.042 16.725,6.825C17.292,6.608 17.883,6.5 18.5,6.5C20.033,6.5 21.333,7.033 22.4,8.1C23.467,9.167 24,10.467 24,12C24,13.533 23.467,14.833 22.4,15.9C21.333,16.967 20.033,17.5 18.5,17.5C17.883,17.5 17.292,17.392 16.725,17.175C16.158,16.958 15.65,16.65 15.2,16.25L13.5,14.7L15,13.35L16.55,14.75C16.817,14.983 17.117,15.167 17.45,15.3C17.783,15.433 18.133,15.5 18.5,15.5C19.467,15.5 20.292,15.158 20.975,14.475C21.658,13.792 22,12.967 22,12C22,11.033 21.658,10.208 20.975,9.525C20.292,8.842 19.467,8.5 18.5,8.5C18.133,8.5 17.783,8.567 17.45,8.7C17.117,8.833 16.817,9.017 16.55,9.25L8.8,16.25C8.35,16.65 7.842,16.958 7.275,17.175C6.708,17.392 6.117,17.5 5.5,17.5Z"/>
</vector>

View file

@ -20,6 +20,7 @@ dependencies {
implementation(projects.core.utils)
implementation(projects.core.ui)
implementation(projects.common.routing)
implementation(projects.common.ui)
/** Domain modules **/
implementation(projects.domain.appCurrency)

View file

@ -1,9 +1,9 @@
package com.tangem.feature.swap.analytics
import com.tangem.common.ui.bottomsheets.state.ApproveType
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.ui.FeeType
import com.tangem.feature.swap.models.ApproveType
private const val SWAP_CATEGORY = "Swap"

View file

@ -3,6 +3,7 @@ package com.tangem.feature.swap.models
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.ui.bottomsheets.state.GiveTxPermissionState
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.notifications.NotificationConfig
@ -22,7 +23,7 @@ data class SwapStateHolder(
val providerState: ProviderState,
val fee: FeeItemState = FeeItemState.Empty,
val permissionState: SwapPermissionState = SwapPermissionState.Empty,
val permissionState: GiveTxPermissionState = GiveTxPermissionState.Empty,
val priceImpact: PriceImpact,
val successState: SwapSuccessStateHolder? = null,

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.models
import com.tangem.common.ui.bottomsheets.state.ApproveType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.ui.TxFee

View file

@ -1,9 +0,0 @@
package com.tangem.feature.swap.models.states
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.feature.swap.models.SwapPermissionState
data class GivePermissionBottomSheetConfig(
val data: SwapPermissionState.ReadyForRequest,
val onCancel: () -> Unit,
) : TangemBottomSheetConfigContent

View file

@ -2,6 +2,7 @@ package com.tangem.feature.swap.ui
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.ui.bottomsheets.state.*
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.notifications.NotificationConfig
@ -649,7 +650,7 @@ internal class StateBuilder(
),
receiveCardData = receiveCardData,
warnings = warnings,
permissionState = SwapPermissionState.Empty,
permissionState = GiveTxPermissionState.Empty,
fee = FeeItemState.Empty,
swapButton = SwapButton(
enabled = false,
@ -906,7 +907,7 @@ internal class StateBuilder(
}
fun updateApproveType(uiState: SwapStateHolder, approveType: ApproveType): SwapStateHolder {
val config = uiState.bottomSheetConfig?.content as? GivePermissionBottomSheetConfig
val config = uiState.bottomSheetConfig?.content as? GiveTxPermissionBottomSheetConfig
return if (config != null) {
uiState.copy(
bottomSheetConfig = uiState.bottomSheetConfig.copy(
@ -980,7 +981,7 @@ internal class StateBuilder(
),
)
return uiState.copy(
permissionState = SwapPermissionState.InProgress,
permissionState = GiveTxPermissionState.InProgress,
warnings = warnings,
)
}
@ -1140,27 +1141,27 @@ internal class StateBuilder(
}
private fun convertPermissionState(
lastPermissionState: SwapPermissionState,
lastPermissionState: GiveTxPermissionState,
permissionDataState: PermissionDataState,
onGivePermissionClick: () -> Unit,
onChangeApproveType: (ApproveType) -> Unit,
): SwapPermissionState {
val approveType = if (lastPermissionState is SwapPermissionState.ReadyForRequest) {
): GiveTxPermissionState {
val approveType = if (lastPermissionState is GiveTxPermissionState.ReadyForRequest) {
lastPermissionState.approveType
} else {
ApproveType.UNLIMITED
}
return when (permissionDataState) {
PermissionDataState.Empty -> SwapPermissionState.Empty
PermissionDataState.PermissionFailed -> SwapPermissionState.Empty
PermissionDataState.PermissionLoading -> SwapPermissionState.InProgress
PermissionDataState.Empty -> GiveTxPermissionState.Empty
PermissionDataState.PermissionFailed -> GiveTxPermissionState.Empty
PermissionDataState.PermissionLoading -> GiveTxPermissionState.InProgress
is PermissionDataState.PermissionReadyForRequest -> {
val permissionFee = when (val fee = permissionDataState.requestApproveData.fee) {
TxFeeState.Empty -> error("Fee shouldn't be empty")
is TxFeeState.MultipleFeeState -> fee.priorityFee
is TxFeeState.SingleFeeState -> fee.fee
}
SwapPermissionState.ReadyForRequest(
GiveTxPermissionState.ReadyForRequest(
currency = permissionDataState.currency,
amount = permissionDataState.amount,
approveType = approveType,
@ -1193,8 +1194,8 @@ internal class StateBuilder(
fun showPermissionBottomSheet(uiState: SwapStateHolder, onDismiss: () -> Unit): SwapStateHolder {
val permissionState = uiState.permissionState
if (permissionState is SwapPermissionState.ReadyForRequest) {
val config = GivePermissionBottomSheetConfig(
if (permissionState is GiveTxPermissionState.ReadyForRequest) {
val config = GiveTxPermissionBottomSheetConfig(
data = permissionState,
onCancel = onDismiss,
)
@ -1402,7 +1403,7 @@ internal class StateBuilder(
return NotificationConfig(
title = resourceReference(R.string.express_provider_permission_needed),
subtitle = resourceReference(
id = R.string.swapping_permission_subheader,
id = R.string.give_permission_subtitle,
formatArgs = wrappedList(fromTokenSymbol),
),
iconResId = R.drawable.ic_locked_24,

View file

@ -1,317 +0,0 @@
package com.tangem.feature.swap.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.swap.models.ApprovePermissionButton
import com.tangem.feature.swap.models.ApproveType
import com.tangem.feature.swap.models.CancelPermissionButton
import com.tangem.feature.swap.models.SwapPermissionState
import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig
import com.tangem.feature.swap.presentation.R
import kotlinx.collections.immutable.ImmutableList
@Composable
fun SwapPermissionBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(config) { content: GivePermissionBottomSheetConfig ->
SwapPermissionBottomSheetContent(content = content)
}
}
@Composable
private fun SwapPermissionBottomSheetContent(content: GivePermissionBottomSheetConfig) {
var isPermissionAlertShow by remember { mutableStateOf(false) }
val data = content.data
Column(
modifier = Modifier
.background(color = TangemTheme.colors.background.primary)
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(modifier = Modifier.fillMaxWidth()) {
Text(
modifier = Modifier.align(Alignment.Center),
text = stringResource(id = R.string.swapping_permission_header),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
)
IconButton(
modifier = Modifier.align(Alignment.CenterEnd),
onClick = { isPermissionAlertShow = true },
) {
Icon(
painter = painterResource(id = R.drawable.ic_question_24),
contentDescription = null,
)
}
}
SpacerH10()
Text(
text = stringResource(
id = R.string.swapping_permission_subheader,
data.currency,
),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body2,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing8),
)
SpacerH16()
ApprovalBottomSheetInfo(data)
SpacerH28()
PrimaryButtonIconEnd(
text = stringResource(id = R.string.swapping_permission_buttons_approve),
iconResId = R.drawable.ic_tangem_24,
modifier = Modifier.fillMaxWidth(),
onClick = data.approveButton.onClick,
)
SpacerH12()
SecondaryButton(
text = stringResource(id = R.string.common_cancel),
modifier = Modifier.fillMaxWidth(),
onClick = {
content.onCancel()
},
)
SpacerH16()
// region dialog
if (isPermissionAlertShow) {
BasicDialog(
message = stringResource(id = R.string.swapping_approve_information_text),
title = stringResource(id = R.string.swapping_approve_information_title),
confirmButton = DialogButton { isPermissionAlertShow = false },
onDismissDialog = {},
)
}
}
}
@Composable
private fun ApprovalBottomSheetInfo(data: SwapPermissionState.ReadyForRequest) {
Column(
modifier = Modifier
.background(color = TangemTheme.colors.background.primary)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AmountItem(
currency = data.currency,
approveType = data.approveType,
onChangeApproveType = data.onChangeApproveType,
approveItems = data.approveItems,
)
SubtitleItem(
subtitle = stringResource(id = R.string.swapping_permission_policy_type_footer),
modifier = Modifier.fillMaxWidth(),
)
SpacerH24()
DividerBottomSheet()
FeeItem(fee = data.fee.resolveReference())
SubtitleItem(
subtitle = stringResource(id = R.string.swapping_permission_fee_footer),
modifier = Modifier.fillMaxWidth(),
)
}
}
@Composable
private fun DividerBottomSheet() {
Divider(
color = TangemTheme.colors.stroke.primary,
thickness = TangemTheme.dimens.size0_5,
)
}
@Composable
private fun InformationItem(subtitle: String, value: String) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens.spacing16),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = subtitle,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
maxLines = 1,
)
MiddleEllipsisText(
text = value,
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing16),
)
}
}
@Composable
private fun AmountItem(
currency: String,
approveType: ApproveType,
approveItems: ImmutableList<ApproveType>,
onChangeApproveType: (ApproveType) -> Unit,
) {
var isExpandSelector by remember {
mutableStateOf(false)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens.spacing16),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(id = R.string.swapping_permission_rows_amount, currency),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
maxLines = 1,
)
Box {
SelectorItem(
getTitleForApproveType(approveType = approveType),
) {
isExpandSelector = true
}
DropdownSelector(
isExpanded = isExpandSelector,
onDismiss = { isExpandSelector = false },
onItemClick = { approveType ->
isExpandSelector = false
onChangeApproveType.invoke(approveType)
},
items = approveItems,
)
}
}
}
@Composable
private fun SelectorItem(title: String, onClick: () -> Unit) {
Row(
modifier = Modifier.clickable { onClick() },
) {
Text(
text = title,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body1,
maxLines = 1,
)
Icon(
painter = painterResource(id = R.drawable.ic_chevron_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
}
}
@Composable
private fun DropdownSelector(
isExpanded: Boolean,
onDismiss: () -> Unit,
onItemClick: (ApproveType) -> Unit,
items: ImmutableList<ApproveType>,
) {
DropdownMenu(
expanded = isExpanded,
onDismissRequest = onDismiss,
modifier = Modifier
.wrapContentSize()
.background(TangemTheme.colors.background.secondary),
) {
items.forEach { item ->
DropdownMenuItem(
onClick = {
onItemClick.invoke(item)
},
) {
Text(
text = getTitleForApproveType(approveType = item),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body1,
maxLines = 1,
)
}
}
}
}
@Composable
private fun FeeItem(fee: String) {
InformationItem(
subtitle = stringResource(id = R.string.common_network_fee_title),
value = fee,
)
}
@Composable
private fun SubtitleItem(subtitle: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = subtitle,
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body2,
)
}
@Composable
private fun getTitleForApproveType(approveType: ApproveType): String = when (approveType) {
ApproveType.LIMITED -> stringResource(id = R.string.swapping_permission_current_transaction)
ApproveType.UNLIMITED -> stringResource(id = R.string.swapping_permission_unlimited)
}
// region preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_AgreementBottomSheet() {
TangemThemePreview {
SwapPermissionBottomSheetContent(content = previewData)
}
}
private val previewData = GivePermissionBottomSheetConfig(
data = SwapPermissionState.ReadyForRequest(
currency = "DAI",
amount = "",
walletAddress = "",
spenderAddress = "",
fee = TextReference.Str("2,14$"),
approveType = ApproveType.UNLIMITED,
approveButton = ApprovePermissionButton(true) {},
cancelButton = CancelPermissionButton(true),
onChangeApproveType = { ApproveType.UNLIMITED },
),
onCancel = {},
)

View file

@ -6,13 +6,14 @@ import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.tangem.common.ui.bottomsheets.GiveTxPermissionBottomSheet
import com.tangem.common.ui.bottomsheets.state.GiveTxPermissionBottomSheetConfig
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig
import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig
import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig
import com.tangem.feature.swap.models.states.WebViewBottomSheetConfig
import com.tangem.feature.swap.presentation.R
@ -40,8 +41,8 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) {
stateHolder.bottomSheetConfig?.let { config ->
when (config.content) {
is GivePermissionBottomSheetConfig -> {
SwapPermissionBottomSheet(config = config)
is GiveTxPermissionBottomSheetConfig -> {
GiveTxPermissionBottomSheet(config = config)
}
is ChooseProviderBottomSheetConfig -> {
ChooseProviderBottomSheet(config = config)

View file

@ -23,6 +23,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.constraintlayout.compose.ConstraintLayout
import com.tangem.common.ui.bottomsheets.state.GiveTxPermissionState
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
@ -414,7 +415,7 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U
state.warnings.any { it is SwapWarning.PermissionNeeded } -> {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.swapping_give_permission),
text = stringResource(id = R.string.give_permission_title),
enabled = true,
onClick = onPermissionWarningClick,
)
@ -495,7 +496,7 @@ private val state = SwapStateHolder(
onRefresh = {},
onBackClicked = {},
onChangeCardsClicked = {},
permissionState = SwapPermissionState.InProgress,
permissionState = GiveTxPermissionState.InProgress,
blockchainId = "POLYGON",
providerState = ProviderState.Loading(),
priceImpact = PriceImpact.Empty(),

View file

@ -1,5 +1,6 @@
package com.tangem.feature.swap.viewmodels
import com.tangem.common.ui.bottomsheets.state.ApproveType
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
import com.tangem.feature.swap.domain.models.domain.SwapProvider
@ -7,7 +8,6 @@ import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress
import com.tangem.feature.swap.domain.models.ui.TxFee
import com.tangem.feature.swap.models.ApproveType
data class SwapProcessDataState(
// Initial network id

View file

@ -9,6 +9,7 @@ import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
import com.tangem.common.ui.bottomsheets.state.ApproveType
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
@ -73,7 +74,7 @@ internal class SwapViewModel @Inject constructor(
) : ViewModel(), DefaultLifecycleObserver {
private val initialCryptoCurrency: CryptoCurrency = savedStateHandle.get<Bundle>(AppRoute.Swap.CURRENCY_BUNDLE_KEY)
?.let { it.unbundle(CryptoCurrency.serializer()) }
?.unbundle(CryptoCurrency.serializer())
?: error("no expected parameter CryptoCurrency found`")
private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus
@ -1173,6 +1174,13 @@ internal class SwapViewModel @Inject constructor(
)
}
private fun ApproveType.toDomainApproveType(): SwapApproveType {
return when (this) {
ApproveType.LIMITED -> SwapApproveType.LIMITED
ApproveType.UNLIMITED -> SwapApproveType.UNLIMITED
}
}
private companion object {
const val loggingTag = "SwapViewModel"
const val INITIAL_AMOUNT = ""