From f25adffe547b43db5c1cd6f10aae3c0b5e144cfe Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 May 2026 21:04:28 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../api/GiveApprovalEntryComponent.kt | 40 ++++ .../api/SelectApprovalTypeComponent.kt | 39 ++++ .../impl/DefaultGiveApprovalEntryComponent.kt | 58 ++++++ .../DefaultSelectApprovalTypeComponent.kt | 80 ++++++++ .../impl/di/GiveApprovalBindsModule.kt | 22 +++ .../impl/model/SelectApprovalTypeModel.kt | 52 +++++ .../impl/model/SelectApprovalTypeUM.kt | 12 ++ .../impl/ui/ApprovalTypeSelectorRow.kt | 183 ++++++++++++++++++ .../impl/ui/SelectApprovalTypeContent.kt | 157 +++++++++++++++ .../swap/models/states/ProviderState.kt | 9 + .../tangem/feature/swap/ui/ProviderItem.kt | 47 ++++- .../feature/swap/ui/ProviderItemSimple.kt | 1 + 12 files changed, 692 insertions(+), 8 deletions(-) create mode 100644 features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalEntryComponent.kt create mode 100644 features/approval/api/src/main/java/com/tangem/features/approval/api/SelectApprovalTypeComponent.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalEntryComponent.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultSelectApprovalTypeComponent.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/SelectApprovalTypeModel.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/SelectApprovalTypeUM.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/ApprovalTypeSelectorRow.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/SelectApprovalTypeContent.kt diff --git a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalEntryComponent.kt b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalEntryComponent.kt new file mode 100644 index 0000000000..f585d0ed4d --- /dev/null +++ b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalEntryComponent.kt @@ -0,0 +1,40 @@ +package com.tangem.features.approval.api + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent + +/** + * Entry component that wraps the two approval-flow variants: + * + * - [Mode.FullApproval] — original [GiveApprovalComponent] which renders the approval-type + * selector together with the fee selector and submits the approval transaction. + * - [Mode.SelectOnly] — [SelectApprovalTypeComponent] which only collects the approval-type + * choice and returns it to the caller via its own [SelectApprovalTypeComponent.Callback]. + * + * Callers depend only on this single factory and pass the appropriate [Mode]; the entry + * component internally creates the corresponding child and delegates the bottom sheet + * rendering and dismissal to it. + */ +interface GiveApprovalEntryComponent : ComposableBottomSheetComponent { + + data class Params( + val mode: Mode, + ) + + sealed interface Mode { + + /** Full flow: approval-type selector + fee selector + transaction submission. */ + data class FullApproval( + val params: GiveApprovalComponent.Params, + ) : Mode + + /** Selection-only flow: returns the chosen approval type without sending anything. */ + data class SelectOnly( + val params: SelectApprovalTypeComponent.Params, + ) : Mode + } + + interface Factory { + fun create(context: AppComponentContext, params: Params): GiveApprovalEntryComponent + } +} \ No newline at end of file diff --git a/features/approval/api/src/main/java/com/tangem/features/approval/api/SelectApprovalTypeComponent.kt b/features/approval/api/src/main/java/com/tangem/features/approval/api/SelectApprovalTypeComponent.kt new file mode 100644 index 0000000000..193d11d9d8 --- /dev/null +++ b/features/approval/api/src/main/java/com/tangem/features/approval/api/SelectApprovalTypeComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.approval.api + +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Selection-only variant of [GiveApprovalComponent]. + * + * Shows the same approval-type selector UI (LIMITED vs UNLIMITED) but does NOT submit the + * approval transaction. Instead, the chosen [ApproveType] is returned to the caller via + * [Callback.onApproveTypeSelected] when the user confirms. The caller is responsible for any + * downstream action (e.g. building the transaction, sending it, navigation). + * + * Intended for flows where the approval-type choice has to be collected separately from the + * actual fee selection / transaction submission step. + */ +interface SelectApprovalTypeComponent : ComposableBottomSheetComponent { + + data class Params( + val userWalletId: UserWalletId, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val amountFooter: TextReference, + val initialApproveType: ApproveType = ApproveType.LIMITED, + val callback: Callback, + ) + + interface Callback { + fun onApproveTypeSelected(approveType: ApproveType) + fun onCancelClick() + } + + interface Factory { + fun create(context: AppComponentContext, params: Params): SelectApprovalTypeComponent + } +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalEntryComponent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalEntryComponent.kt new file mode 100644 index 0000000000..fa2bd4c082 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalEntryComponent.kt @@ -0,0 +1,58 @@ +package com.tangem.features.approval.impl + +import androidx.compose.runtime.Composable +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.approval.api.GiveApprovalEntryComponent +import com.tangem.features.approval.api.SelectApprovalTypeComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** + * Default implementation of [GiveApprovalEntryComponent]. + * + * Picks the concrete child component (full [GiveApprovalComponent] or selection-only + * [SelectApprovalTypeComponent]) at construction time based on + * [GiveApprovalEntryComponent.Params.mode] and delegates [BottomSheet] and [dismiss] to it. + * + * Callers only need to depend on [GiveApprovalEntryComponent.Factory] regardless of the + * underlying mode. + */ +internal class DefaultGiveApprovalEntryComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: GiveApprovalEntryComponent.Params, + giveApprovalComponentFactory: GiveApprovalComponent.Factory, + selectApprovalTypeComponentFactory: SelectApprovalTypeComponent.Factory, +) : GiveApprovalEntryComponent, AppComponentContext by appComponentContext { + + private val delegate: ComposableBottomSheetComponent = when (val mode = params.mode) { + is GiveApprovalEntryComponent.Mode.FullApproval -> giveApprovalComponentFactory.create( + context = child("giveApprovalEntry_full"), + params = mode.params, + ) + is GiveApprovalEntryComponent.Mode.SelectOnly -> selectApprovalTypeComponentFactory.create( + context = child("giveApprovalEntry_select"), + params = mode.params, + ) + } + + override fun dismiss() { + delegate.dismiss() + } + + @Composable + override fun BottomSheet() { + delegate.BottomSheet() + } + + @AssistedFactory + interface Factory : GiveApprovalEntryComponent.Factory { + override fun create( + context: AppComponentContext, + params: GiveApprovalEntryComponent.Params, + ): DefaultGiveApprovalEntryComponent + } +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultSelectApprovalTypeComponent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultSelectApprovalTypeComponent.kt new file mode 100644 index 0000000000..dec40f2914 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultSelectApprovalTypeComponent.kt @@ -0,0 +1,80 @@ +package com.tangem.features.approval.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.R +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.approval.api.SelectApprovalTypeComponent +import com.tangem.features.approval.impl.model.SelectApprovalTypeModel +import com.tangem.features.approval.impl.ui.SelectApprovalTypeContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** + * Default implementation of [SelectApprovalTypeComponent]. + * + * Renders the same selection UI as the full [com.tangem.features.approval.api.GiveApprovalComponent] + * but without the fee selector block and without dispatching the on-chain approval transaction. + * Dismissing the bottom sheet (close button or external dismiss) is treated as a cancel. + */ +internal class DefaultSelectApprovalTypeComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: SelectApprovalTypeComponent.Params, +) : SelectApprovalTypeComponent, AppComponentContext by appComponentContext { + + private val model: SelectApprovalTypeModel = getOrCreateModel(params = params) + + private val currency: String = params.cryptoCurrencyStatus.currency.symbol + + override fun dismiss() { + params.callback.onCancelClick() + } + + @Composable + override fun BottomSheet() { + val uiState by model.uiState.collectAsStateWithLifecycle() + + val config = remember { + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ) + } + + TangemBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.tertiary, + titleText = resourceReference(R.string.give_permission_title), + titleAction = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_close_new_20, + onClicked = model::onCancelClick, + ), + ) { + SelectApprovalTypeContent( + currency = currency, + uiState = uiState, + onChangeApproveType = model::onChangeApproveType, + onConfirmClick = model::onConfirmClick, + ) + } + } + + @AssistedFactory + interface Factory : SelectApprovalTypeComponent.Factory { + override fun create( + context: AppComponentContext, + params: SelectApprovalTypeComponent.Params, + ): DefaultSelectApprovalTypeComponent + } +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt index 3f6fdce55a..dd4f46f896 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt @@ -3,10 +3,15 @@ package com.tangem.features.approval.impl.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.approval.api.GiveApprovalEntryComponent import com.tangem.features.approval.api.GiveApprovalFeatureToggles +import com.tangem.features.approval.api.SelectApprovalTypeComponent import com.tangem.features.approval.impl.DefaultGiveApprovalComponent +import com.tangem.features.approval.impl.DefaultGiveApprovalEntryComponent import com.tangem.features.approval.impl.DefaultGiveApprovalFeatureToggles +import com.tangem.features.approval.impl.DefaultSelectApprovalTypeComponent import com.tangem.features.approval.impl.model.GiveApprovalModel +import com.tangem.features.approval.impl.model.SelectApprovalTypeModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -26,6 +31,18 @@ internal interface GiveApprovalFeatureModule { @Binds @Singleton fun bindComponentFactory(factory: DefaultGiveApprovalComponent.Factory): GiveApprovalComponent.Factory + + @Binds + @Singleton + fun bindSelectApprovalTypeComponentFactory( + factory: DefaultSelectApprovalTypeComponent.Factory, + ): SelectApprovalTypeComponent.Factory + + @Binds + @Singleton + fun bindGiveApprovalEntryComponentFactory( + factory: DefaultGiveApprovalEntryComponent.Factory, + ): GiveApprovalEntryComponent.Factory } @Module @@ -36,4 +53,9 @@ internal interface GiveApprovalModelModule { @IntoMap @ClassKey(GiveApprovalModel::class) fun bindModel(model: GiveApprovalModel): Model + + @Binds + @IntoMap + @ClassKey(SelectApprovalTypeModel::class) + fun bindSelectApprovalTypeModel(model: SelectApprovalTypeModel): Model } \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/SelectApprovalTypeModel.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/SelectApprovalTypeModel.kt new file mode 100644 index 0000000000..e7e063601a --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/SelectApprovalTypeModel.kt @@ -0,0 +1,52 @@ +package com.tangem.features.approval.impl.model + +import androidx.compose.runtime.Stable +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.approval.api.SelectApprovalTypeComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +/** + * Model for [SelectApprovalTypeComponent]. + * + * Keeps the currently selected [ApproveType] and exposes intents to change it, open the + * learn-more URL, confirm the selection, and cancel. Unlike [GiveApprovalModel] this model + * does NOT load fees or submit any transaction — confirmation simply notifies the caller + * via the params callback with the selected [ApproveType]. + */ +@Stable +@ModelScoped +internal class SelectApprovalTypeModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: SelectApprovalTypeComponent.Params = paramsContainer.require() + + val uiState: StateFlow + field = MutableStateFlow( + SelectApprovalTypeUM( + approveType = params.initialApproveType, + subtitle = params.amountFooter, + ), + ) + + fun onChangeApproveType(approveType: ApproveType) { + if (uiState.value.approveType == approveType) return + uiState.update { it.copy(approveType = approveType) } + } + + fun onConfirmClick() { + params.callback.onApproveTypeSelected(uiState.value.approveType) + } + + fun onCancelClick() { + params.callback.onCancelClick() + } +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/SelectApprovalTypeUM.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/SelectApprovalTypeUM.kt new file mode 100644 index 0000000000..47023909ea --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/SelectApprovalTypeUM.kt @@ -0,0 +1,12 @@ +package com.tangem.features.approval.impl.model + +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal data class SelectApprovalTypeUM( + val subtitle: TextReference, + val approveType: ApproveType, + val approveItems: ImmutableList = ApproveType.entries.toImmutableList(), +) \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/ApprovalTypeSelectorRow.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/ApprovalTypeSelectorRow.kt new file mode 100644 index 0000000000..e6d9ea6f97 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/ApprovalTypeSelectorRow.kt @@ -0,0 +1,183 @@ +package com.tangem.features.approval.impl.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +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.vectorResource +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.PopupProperties +import androidx.compose.material3.Text as M3Text +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList + +/** + * Reusable row that shows "Amount for {currency}" on the left and the currently selected + * [ApproveType] on the right, with a dropdown to switch between the available types. + * + * Used by both [GiveApprovalContent] (full approval flow) and [SelectApprovalTypeContent] + * (selection-only flow). + */ +@Composable +internal fun ApprovalTypeSelectorRow( + currency: String, + approveType: ApproveType, + approveItems: ImmutableList, + onChangeApproveType: (ApproveType) -> Unit, + modifier: Modifier = Modifier, +) { + 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 = ripple(), + onClick = { isExpandSelector = true }, + ), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .onSizeChanged { amountSize = it } + .padding(vertical = 12.dp, horizontal = 14.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + M3Text( + text = stringResourceSafe(id = R.string.give_permission_rows_amount, currency), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + maxLines = 1, + ) + SpacerWMax() + M3Text( + text = approveType.text.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body1, + maxLines = 1, + ) + Icon( + painter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.ic_chevron_24)), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing2), + ) + } + ApprovalTypeDropdown( + isExpanded = isExpandSelector, + onDismiss = { isExpandSelector = false }, + onItemClick = { type -> + isExpandSelector = false + onChangeApproveType(type) + }, + items = approveItems, + selectedType = approveType, + amountSize = amountSize, + ) + } +} + +@Suppress("LongParameterList") +@Composable +private fun ApprovalTypeDropdown( + isExpanded: Boolean, + onDismiss: () -> Unit, + onItemClick: (ApproveType) -> Unit, + items: ImmutableList, + selectedType: ApproveType, + amountSize: IntSize, +) { + var dropDownWidth by remember { mutableStateOf(IntSize.Zero) } + val offsetY = amountSize.height.times(-1) + val offsetX = amountSize.width - dropDownWidth.width + + 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 { + M3Text( + text = when (item) { + ApproveType.LIMITED -> stringResourceSafe( + id = R.string.give_permission_current_transaction, + ) + ApproveType.UNLIMITED -> stringResourceSafe( + 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) + }, + ) + } + } + } +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/SelectApprovalTypeContent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/SelectApprovalTypeContent.kt new file mode 100644 index 0000000000..d7f7363b9f --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/SelectApprovalTypeContent.kt @@ -0,0 +1,157 @@ +package com.tangem.features.approval.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.core.ui.R +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.SpacerH18 +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.approval.impl.model.SelectApprovalTypeUM +import kotlinx.collections.immutable.persistentListOf + +/** + * UI for the selection-only approval variant. Reuses [ApprovalTypeSelectorRow] for the + * approval-type picker. The primary button calls [onConfirmClick] which is wired to a + * callback that returns the chosen [ApproveType] + * to the caller (instead of submitting an on-chain transaction). + */ +@Composable +@Suppress("LongParameterList") +internal fun SelectApprovalTypeContent( + currency: String, + uiState: SelectApprovalTypeUM, + onChangeApproveType: (ApproveType) -> Unit, + onConfirmClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = uiState.subtitle.resolveAnnotatedReference(), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + textAlign = TextAlign.Center, + modifier = Modifier.padding( + top = 2.dp, + start = 16.dp, + end = 16.dp, + ), + ) + + SpacerH18() + + ApprovalTypeSelectorRow( + currency = currency, + approveType = uiState.approveType, + approveItems = uiState.approveItems, + onChangeApproveType = onChangeApproveType, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) + + SpacerH(height = TangemTheme.dimens.spacing20) + + PrimaryButton( + text = stringResourceSafe(id = R.string.common_continue), + onClick = onConfirmClick, + enabled = true, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + ) + + SpacerH16() + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun SelectApprovalTypeContentPreview( + @PreviewParameter(SelectApprovalTypeContentPreviewProvider::class) params: SelectApprovalTypePreviewParams, +) { + TangemThemePreview { + SelectApprovalTypeContent( + currency = params.currency, + uiState = params.uiState, + onChangeApproveType = {}, + onConfirmClick = {}, + ) + } +} + +private data class SelectApprovalTypePreviewParams( + val currency: String, + val uiState: SelectApprovalTypeUM, +) + +private class SelectApprovalTypeContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + SelectApprovalTypePreviewParams( + currency = "USDT", + uiState = SelectApprovalTypeUM( + subtitle = combinedReference( + resourceReference( + id = R.string.give_permission_swap_subtitle_v2, + // Arg is only used in iOS + formatArgs = wrappedList(""), + ), + styledResourceReference( + id = R.string.common_learn_more, + spanStyleReference = { + TangemTheme.typography.caption2 + .copy(color = TangemTheme.colors.text.accent) + .toSpanStyle() + }, + onClick = { }, + ), + ), + approveType = ApproveType.LIMITED, + approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED), + ), + ), + SelectApprovalTypePreviewParams( + currency = "USDC", + uiState = SelectApprovalTypeUM( + subtitle = combinedReference( + resourceReference( + id = com.tangem.common.ui.R.string.give_permission_swap_subtitle_v2, + // Arg is only used in iOS + formatArgs = wrappedList(""), + ), + styledResourceReference( + id = com.tangem.common.ui.R.string.common_learn_more, + spanStyleReference = { + TangemTheme.typography.caption2 + .copy(color = TangemTheme.colors.text.accent) + .toSpanStyle() + }, + onClick = {}, + ), + ), + approveType = ApproveType.UNLIMITED, + approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED), + ), + ), + ) +} +// endregion \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt index 8ab97bb746..b36a3305f1 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt @@ -29,6 +29,7 @@ sealed class ProviderState { val additionalBadge: AdditionalBadge, val percentLowerThenBest: PercentDifference = PercentDifference.Empty, val namePrefix: PrefixType, + val approvalSettings: ApprovalSettings = ApprovalSettings.Empty, override val onProviderClick: (String) -> Unit, ) : ProviderState() @@ -61,6 +62,14 @@ sealed class ProviderState { enum class PrefixType { NONE, PROVIDED_BY } + + @Immutable + sealed class ApprovalSettings { + data object Empty : ApprovalSettings() + data class Content( + val onApprovalSelectClick: () -> Unit, + ) : ApprovalSettings() + } } @Immutable diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 4c8aca99fe..c5ce3a57e2 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -4,26 +4,32 @@ import android.content.res.Configuration import androidx.compose.animation.AnimatedContent 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.layout.Arrangement import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.Text +import androidx.compose.material3.ripple 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.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe @@ -194,6 +200,23 @@ private fun ProviderContentState( } } } + + if (state.approvalSettings is ProviderState.ApprovalSettings.Content) { + SpacerWMax() + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_filter_default_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier + .padding(end = 14.dp) + .size(20.dp) + .clickable( + indication = ripple(false), + interactionSource = remember { MutableInteractionSource() }, + onClick = state.approvalSettings.onApprovalSelectClick, + ), + ) + } } ProviderChevron(selectionType = state.selectionType, isSelected = isSelected) @@ -442,10 +465,9 @@ private fun ProviderItemPreview( @PreviewParameter(ProviderItemParameterProvider::class) state: Pair, ) { TangemThemePreview { - ProviderItem( + ProviderItemBlock( modifier = Modifier.background(TangemTheme.colors.background.action), state = state.first, - isSelected = state.second, ) } } @@ -460,22 +482,28 @@ private class ProviderItemParameterProvider : CollectionPreviewParameterProvider subtitle = stringReference(value = "0,64554846 DAI ≈ 1 MATIC"), additionalBadge = ProviderState.AdditionalBadge.Empty, percentLowerThenBest = PercentDifference.Value(value = 12.0f), - selectionType = ProviderState.SelectionType.SELECT, + selectionType = ProviderState.SelectionType.NONE, namePrefix = ProviderState.PrefixType.PROVIDED_BY, + approvalSettings = ProviderState.ApprovalSettings.Empty, onProviderClick = {}, ) - val contentState2 = contentState.copy( + val contentStatePermissionRequired = contentState.copy( subtitle = stringReference(value = "1 132,46 MATIC"), additionalBadge = ProviderState.AdditionalBadge.PermissionRequired, percentLowerThenBest = PercentDifference.Value(value = 5f), ) + val contentStatePermissionIntegrated = contentState.copy( + subtitle = stringReference(value = "1 132,46 MATIC"), + percentLowerThenBest = PercentDifference.Value(value = 5f), + approvalSettings = ProviderState.ApprovalSettings.Content({}), + ) val unavailableState = ProviderState.Unavailable( id = "1", name = "1inch", type = "DEX", iconUrl = "", alertText = stringReference(value = "Not available"), - selectionType = ProviderState.SelectionType.SELECT, + selectionType = ProviderState.SelectionType.NONE, onProviderClick = {}, ) val loadingState = ProviderState.Loading() @@ -483,8 +511,11 @@ private class ProviderItemParameterProvider : CollectionPreviewParameterProvider add(contentState to true) add(contentState to false) - add(contentState2 to true) - add(contentState2 to false) + add(contentStatePermissionRequired to true) + add(contentStatePermissionRequired to false) + + add(contentStatePermissionIntegrated to true) + add(contentStatePermissionIntegrated to false) add(unavailableState to true) add(unavailableState to false) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItemSimple.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItemSimple.kt index 71cd53b67e..ac2315b41f 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItemSimple.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItemSimple.kt @@ -140,6 +140,7 @@ private class SimpleProviderPreview : PreviewParameterProvider { additionalBadge = ProviderState.AdditionalBadge.Empty, percentLowerThenBest = PercentDifference.Empty, namePrefix = ProviderState.PrefixType.NONE, + approvalSettings = ProviderState.ApprovalSettings.Empty, onProviderClick = {}, ), ProviderState.Loading(),