Updated on 2026-08-14
This commit is contained in:
commit
557e8eceb6
12 changed files with 692 additions and 8 deletions
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<TangemBottomSheetConfigContent.Empty>(
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,10 +3,15 @@ package com.tangem.features.approval.impl.di
|
||||||
import com.tangem.core.decompose.di.ModelComponent
|
import com.tangem.core.decompose.di.ModelComponent
|
||||||
import com.tangem.core.decompose.model.Model
|
import com.tangem.core.decompose.model.Model
|
||||||
import com.tangem.features.approval.api.GiveApprovalComponent
|
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.GiveApprovalFeatureToggles
|
||||||
|
import com.tangem.features.approval.api.SelectApprovalTypeComponent
|
||||||
import com.tangem.features.approval.impl.DefaultGiveApprovalComponent
|
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.DefaultGiveApprovalFeatureToggles
|
||||||
|
import com.tangem.features.approval.impl.DefaultSelectApprovalTypeComponent
|
||||||
import com.tangem.features.approval.impl.model.GiveApprovalModel
|
import com.tangem.features.approval.impl.model.GiveApprovalModel
|
||||||
|
import com.tangem.features.approval.impl.model.SelectApprovalTypeModel
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -26,6 +31,18 @@ internal interface GiveApprovalFeatureModule {
|
||||||
@Binds
|
@Binds
|
||||||
@Singleton
|
@Singleton
|
||||||
fun bindComponentFactory(factory: DefaultGiveApprovalComponent.Factory): GiveApprovalComponent.Factory
|
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
|
@Module
|
||||||
|
|
@ -36,4 +53,9 @@ internal interface GiveApprovalModelModule {
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@ClassKey(GiveApprovalModel::class)
|
@ClassKey(GiveApprovalModel::class)
|
||||||
fun bindModel(model: GiveApprovalModel): Model
|
fun bindModel(model: GiveApprovalModel): Model
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ClassKey(SelectApprovalTypeModel::class)
|
||||||
|
fun bindSelectApprovalTypeModel(model: SelectApprovalTypeModel): Model
|
||||||
}
|
}
|
||||||
|
|
@ -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<SelectApprovalTypeUM>
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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> = ApproveType.entries.toImmutableList(),
|
||||||
|
)
|
||||||
|
|
@ -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<ApproveType>,
|
||||||
|
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<ApproveType>,
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<SelectApprovalTypePreviewParams> {
|
||||||
|
override val values: Sequence<SelectApprovalTypePreviewParams>
|
||||||
|
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
|
||||||
|
|
@ -29,6 +29,7 @@ sealed class ProviderState {
|
||||||
val additionalBadge: AdditionalBadge,
|
val additionalBadge: AdditionalBadge,
|
||||||
val percentLowerThenBest: PercentDifference = PercentDifference.Empty,
|
val percentLowerThenBest: PercentDifference = PercentDifference.Empty,
|
||||||
val namePrefix: PrefixType,
|
val namePrefix: PrefixType,
|
||||||
|
val approvalSettings: ApprovalSettings = ApprovalSettings.Empty,
|
||||||
override val onProviderClick: (String) -> Unit,
|
override val onProviderClick: (String) -> Unit,
|
||||||
) : ProviderState()
|
) : ProviderState()
|
||||||
|
|
||||||
|
|
@ -61,6 +62,14 @@ sealed class ProviderState {
|
||||||
enum class PrefixType {
|
enum class PrefixType {
|
||||||
NONE, PROVIDED_BY
|
NONE, PROVIDED_BY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed class ApprovalSettings {
|
||||||
|
data object Empty : ApprovalSettings()
|
||||||
|
data class Content(
|
||||||
|
val onApprovalSelectClick: () -> Unit,
|
||||||
|
) : ApprovalSettings()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
|
||||||
|
|
@ -4,26 +4,32 @@ import android.content.res.Configuration
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.ripple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.SubcomposeAsyncImage
|
import coil.compose.SubcomposeAsyncImage
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
import com.tangem.core.ui.components.RectangleShimmer
|
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.resolveReference
|
||||||
import com.tangem.core.ui.extensions.stringReference
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
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)
|
ProviderChevron(selectionType = state.selectionType, isSelected = isSelected)
|
||||||
|
|
@ -442,10 +465,9 @@ private fun ProviderItemPreview(
|
||||||
@PreviewParameter(ProviderItemParameterProvider::class) state: Pair<ProviderState, Boolean>,
|
@PreviewParameter(ProviderItemParameterProvider::class) state: Pair<ProviderState, Boolean>,
|
||||||
) {
|
) {
|
||||||
TangemThemePreview {
|
TangemThemePreview {
|
||||||
ProviderItem(
|
ProviderItemBlock(
|
||||||
modifier = Modifier.background(TangemTheme.colors.background.action),
|
modifier = Modifier.background(TangemTheme.colors.background.action),
|
||||||
state = state.first,
|
state = state.first,
|
||||||
isSelected = state.second,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -460,22 +482,28 @@ private class ProviderItemParameterProvider : CollectionPreviewParameterProvider
|
||||||
subtitle = stringReference(value = "0,64554846 DAI ≈ 1 MATIC"),
|
subtitle = stringReference(value = "0,64554846 DAI ≈ 1 MATIC"),
|
||||||
additionalBadge = ProviderState.AdditionalBadge.Empty,
|
additionalBadge = ProviderState.AdditionalBadge.Empty,
|
||||||
percentLowerThenBest = PercentDifference.Value(value = 12.0f),
|
percentLowerThenBest = PercentDifference.Value(value = 12.0f),
|
||||||
selectionType = ProviderState.SelectionType.SELECT,
|
selectionType = ProviderState.SelectionType.NONE,
|
||||||
namePrefix = ProviderState.PrefixType.PROVIDED_BY,
|
namePrefix = ProviderState.PrefixType.PROVIDED_BY,
|
||||||
|
approvalSettings = ProviderState.ApprovalSettings.Empty,
|
||||||
onProviderClick = {},
|
onProviderClick = {},
|
||||||
)
|
)
|
||||||
val contentState2 = contentState.copy(
|
val contentStatePermissionRequired = contentState.copy(
|
||||||
subtitle = stringReference(value = "1 132,46 MATIC"),
|
subtitle = stringReference(value = "1 132,46 MATIC"),
|
||||||
additionalBadge = ProviderState.AdditionalBadge.PermissionRequired,
|
additionalBadge = ProviderState.AdditionalBadge.PermissionRequired,
|
||||||
percentLowerThenBest = PercentDifference.Value(value = 5f),
|
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(
|
val unavailableState = ProviderState.Unavailable(
|
||||||
id = "1",
|
id = "1",
|
||||||
name = "1inch",
|
name = "1inch",
|
||||||
type = "DEX",
|
type = "DEX",
|
||||||
iconUrl = "",
|
iconUrl = "",
|
||||||
alertText = stringReference(value = "Not available"),
|
alertText = stringReference(value = "Not available"),
|
||||||
selectionType = ProviderState.SelectionType.SELECT,
|
selectionType = ProviderState.SelectionType.NONE,
|
||||||
onProviderClick = {},
|
onProviderClick = {},
|
||||||
)
|
)
|
||||||
val loadingState = ProviderState.Loading()
|
val loadingState = ProviderState.Loading()
|
||||||
|
|
@ -483,8 +511,11 @@ private class ProviderItemParameterProvider : CollectionPreviewParameterProvider
|
||||||
add(contentState to true)
|
add(contentState to true)
|
||||||
add(contentState to false)
|
add(contentState to false)
|
||||||
|
|
||||||
add(contentState2 to true)
|
add(contentStatePermissionRequired to true)
|
||||||
add(contentState2 to false)
|
add(contentStatePermissionRequired to false)
|
||||||
|
|
||||||
|
add(contentStatePermissionIntegrated to true)
|
||||||
|
add(contentStatePermissionIntegrated to false)
|
||||||
|
|
||||||
add(unavailableState to true)
|
add(unavailableState to true)
|
||||||
add(unavailableState to false)
|
add(unavailableState to false)
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@ private class SimpleProviderPreview : PreviewParameterProvider<ProviderState> {
|
||||||
additionalBadge = ProviderState.AdditionalBadge.Empty,
|
additionalBadge = ProviderState.AdditionalBadge.Empty,
|
||||||
percentLowerThenBest = PercentDifference.Empty,
|
percentLowerThenBest = PercentDifference.Empty,
|
||||||
namePrefix = ProviderState.PrefixType.NONE,
|
namePrefix = ProviderState.PrefixType.NONE,
|
||||||
|
approvalSettings = ProviderState.ApprovalSettings.Empty,
|
||||||
onProviderClick = {},
|
onProviderClick = {},
|
||||||
),
|
),
|
||||||
ProviderState.Loading(),
|
ProviderState.Loading(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue