Updated on 2026-08-14
This commit is contained in:
parent
dea7034aad
commit
b94e3f814b
16 changed files with 867 additions and 0 deletions
|
|
@ -305,6 +305,8 @@ dependencies {
|
|||
implementation(projects.features.tokenRecieve.impl)
|
||||
implementation(projects.features.yieldSupply.api)
|
||||
implementation(projects.features.yieldSupply.impl)
|
||||
implementation(projects.features.approval.api)
|
||||
implementation(projects.features.approval.impl)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.core.ktx)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Deprecated("Use GiveApprovalComponent")
|
||||
@Composable
|
||||
fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) {
|
||||
var isPermissionAlertShow by remember { mutableStateOf(false) }
|
||||
|
|
|
|||
|
|
@ -55,5 +55,9 @@
|
|||
{
|
||||
"name": "WALLET_REORDER_FEATURE_ENABLED",
|
||||
"version": "5.34"
|
||||
},
|
||||
{
|
||||
"name": "GASLESS_APPROVAL_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
28
features/approval/api/build.gradle.kts
Normal file
28
features/approval/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.approval.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.runtime)
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.features.approval.api
|
||||
|
||||
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.UserWallet
|
||||
|
||||
interface GiveApprovalComponent : ComposableBottomSheetComponent {
|
||||
|
||||
data class Params(
|
||||
val userWallet: UserWallet,
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
val amount: String,
|
||||
val spenderAddress: String,
|
||||
val subtitle: TextReference,
|
||||
val callback: Callback,
|
||||
)
|
||||
|
||||
interface Callback {
|
||||
fun onApproveDone()
|
||||
fun onApproveFailed()
|
||||
fun onCancelClick()
|
||||
}
|
||||
|
||||
interface Factory {
|
||||
fun create(context: AppComponentContext, params: Params): GiveApprovalComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.approval.api
|
||||
|
||||
interface GiveApprovalFeatureToggles {
|
||||
|
||||
val isGaslessApprovalEnabled: Boolean
|
||||
}
|
||||
57
features/approval/impl/build.gradle.kts
Normal file
57
features/approval/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.approval.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Feature */
|
||||
implementation(projects.features.approval.api)
|
||||
implementation(projects.features.sendV2.api)
|
||||
|
||||
/** Core */
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
||||
/** SDK */
|
||||
implementation(tangemDeps.blockchain) {
|
||||
exclude(module = "joda-time")
|
||||
}
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.transaction.models)
|
||||
implementation(projects.domain.transaction)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.runtime)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.decompose)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.arrow.core)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
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.common.ui.userwallet.ext.walletInterationIcon
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
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.GiveApprovalComponent
|
||||
import com.tangem.features.approval.impl.model.GiveApprovalModel
|
||||
import com.tangem.features.approval.impl.ui.GiveApprovalContent
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import com.tangem.common.ui.R as CommonUiR
|
||||
|
||||
internal class DefaultGiveApprovalComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: GiveApprovalComponent.Params,
|
||||
feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory,
|
||||
) : GiveApprovalComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: GiveApprovalModel = getOrCreateModel(params = params)
|
||||
|
||||
private val feeSelectorBlockComponent = feeSelectorBlockComponentFactory.create(
|
||||
context = child("giveApprovalFeeSelector"),
|
||||
params = FeeSelectorParams.FeeSelectorBlockParams(
|
||||
state = FeeSelectorUM.Loading,
|
||||
onLoadFee = { model.loadFee() },
|
||||
onLoadFeeExtended = { selectedFeeToken -> model.loadFeeExtended(selectedFeeToken) },
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
feeStateConfiguration = FeeSelectorParams.FeeStateConfiguration.None,
|
||||
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
|
||||
analyticsCategoryName = CommonSendAnalyticEvents.APPROVE_CATEGORY,
|
||||
analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.Approve,
|
||||
userWalletId = params.userWallet.walletId,
|
||||
),
|
||||
onResult = model::onFeeResult,
|
||||
)
|
||||
|
||||
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.secondary,
|
||||
titleText = resourceReference(CommonUiR.string.give_permission_title),
|
||||
titleAction = TopAppBarButtonUM.Icon(
|
||||
iconRes = CommonUiR.drawable.ic_information_24,
|
||||
onClicked = model::showPermissionInfoDialog,
|
||||
),
|
||||
) {
|
||||
GiveApprovalContent(
|
||||
currency = currency,
|
||||
subtitle = params.subtitle,
|
||||
approveType = uiState.approveType,
|
||||
approveItems = uiState.approveItems,
|
||||
onChangeApproveType = model::onChangeApproveType,
|
||||
walletInteractionIcon = walletInterationIcon(params.userWallet),
|
||||
isApproveEnabled = uiState.isApproveButtonEnabled,
|
||||
isApproveLoading = uiState.isApproveLoading,
|
||||
onApproveClick = model::onApproveClick,
|
||||
onCancelClick = model::onCancelClick,
|
||||
onOpenLearnMoreAboutApproveClick = model::onOpenLearnMoreAboutApproveClick,
|
||||
feeSelectorBlockComponent = feeSelectorBlockComponent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : GiveApprovalComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: GiveApprovalComponent.Params,
|
||||
): DefaultGiveApprovalComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.features.approval.impl
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.approval.api.GiveApprovalFeatureToggles
|
||||
|
||||
internal class DefaultGiveApprovalFeatureToggles(
|
||||
private val featureToggles: FeatureTogglesManager,
|
||||
) : GiveApprovalFeatureToggles {
|
||||
|
||||
// Remove GiveTxPermissionBottomSheet and all dependencies with this toggle
|
||||
override val isGaslessApprovalEnabled: Boolean
|
||||
get() = featureToggles.isFeatureEnabled("GASLESS_APPROVAL_ENABLED")
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
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.impl.DefaultGiveApprovalComponent
|
||||
import com.tangem.features.approval.impl.model.GiveApprovalModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Singleton
|
||||
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@Module
|
||||
internal interface GiveApprovalFeatureModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindComponentFactory(factory: DefaultGiveApprovalComponent.Factory): GiveApprovalComponent.Factory
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface GiveApprovalModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(GiveApprovalModel::class)
|
||||
fun bindModel(model: GiveApprovalModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package com.tangem.features.approval.impl.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
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.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
|
||||
import com.tangem.features.approval.api.GiveApprovalComponent
|
||||
import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class GiveApprovalModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
paramsContainer: ParamsContainer,
|
||||
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val getFeeUseCase: GetFeeUseCase,
|
||||
private val getFeeForGaslessUseCase: GetFeeForGaslessUseCase,
|
||||
private val getFeeForTokenUseCase: GetFeeForTokenUseCase,
|
||||
private val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val urlOpener: UrlOpener,
|
||||
) : Model(), FeeSelectorModelCallback {
|
||||
|
||||
private val params: GiveApprovalComponent.Params = paramsContainer.require()
|
||||
|
||||
val uiState: StateFlow<GiveApprovalUM>
|
||||
field = MutableStateFlow(
|
||||
GiveApprovalUM(
|
||||
approveType = ApproveType.LIMITED,
|
||||
isApproveButtonEnabled = false,
|
||||
isApproveLoading = false,
|
||||
),
|
||||
)
|
||||
|
||||
private var feeSelectorUM: FeeSelectorUM = FeeSelectorUM.Loading
|
||||
|
||||
override fun onFeeResult(feeSelectorUM: FeeSelectorUM) {
|
||||
this.feeSelectorUM = feeSelectorUM
|
||||
uiState.update { it.copy(isApproveButtonEnabled = feeSelectorUM.isPrimaryButtonEnabled) }
|
||||
}
|
||||
|
||||
fun onApproveClick() {
|
||||
uiState.update { it.copy(isApproveLoading = true) }
|
||||
modelScope.launch(dispatchers.main) {
|
||||
val isSuccess = sendApprovalTransaction()
|
||||
uiState.update { it.copy(isApproveLoading = false) }
|
||||
if (isSuccess) {
|
||||
params.callback.onApproveDone()
|
||||
} else {
|
||||
params.callback.onApproveFailed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onCancelClick() {
|
||||
params.callback.onCancelClick()
|
||||
}
|
||||
|
||||
fun onChangeApproveType(approveType: ApproveType) {
|
||||
uiState.update { it.copy(approveType = approveType) }
|
||||
}
|
||||
|
||||
fun onOpenLearnMoreAboutApproveClick() {
|
||||
urlOpener.openUrl(RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP)
|
||||
}
|
||||
|
||||
fun showPermissionInfoDialog() {
|
||||
uiMessageSender.send(
|
||||
DialogMessage(
|
||||
message = resourceReference(com.tangem.common.ui.R.string.give_permission_staking_footer),
|
||||
title = resourceReference(com.tangem.common.ui.R.string.common_approve),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun prepareApprovalTransaction(): Either<Throwable, TransactionData> {
|
||||
val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
||||
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token
|
||||
?: return Either.Left(IllegalStateException("Currency is not a token"))
|
||||
|
||||
return createApprovalTransactionUseCase(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
userWalletId = params.userWallet.walletId,
|
||||
amount = getApprovalAmount(),
|
||||
contractAddress = tokenCurrency.contractAddress,
|
||||
spenderAddress = params.spenderAddress,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
|
||||
val approvalTransaction = prepareApprovalTransaction()
|
||||
.getOrElse { return GetFeeError.DataError(it).left() }
|
||||
|
||||
return getFeeUseCase(
|
||||
transactionData = approvalTransaction,
|
||||
userWallet = params.userWallet,
|
||||
network = params.cryptoCurrencyStatus.currency.network,
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadFeeExtended(maybeToken: CryptoCurrencyStatus?): Either<GetFeeError, TransactionFeeExtended> {
|
||||
val approvalTransaction = prepareApprovalTransaction()
|
||||
.getOrElse { return GetFeeError.DataError(it).left() }
|
||||
|
||||
return if (maybeToken == null) {
|
||||
getFeeForGaslessUseCase(
|
||||
transactionData = approvalTransaction,
|
||||
userWallet = params.userWallet,
|
||||
network = params.cryptoCurrencyStatus.currency.network,
|
||||
)
|
||||
} else {
|
||||
getFeeForTokenUseCase(
|
||||
transactionData = approvalTransaction,
|
||||
userWallet = params.userWallet,
|
||||
token = maybeToken.currency,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendApprovalTransaction(): Boolean {
|
||||
val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
||||
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return false
|
||||
|
||||
val feeContent = feeSelectorUM as? FeeSelectorUM.Content ?: return false
|
||||
val selectedFee = feeContent.selectedFeeItem.fee
|
||||
val feeExtended = feeContent.feeExtraInfo.transactionFeeExtended
|
||||
|
||||
val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency
|
||||
|
||||
val transactionData = createApprovalTransactionUseCase(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
userWalletId = params.userWallet.walletId,
|
||||
amount = getApprovalAmount(),
|
||||
fee = selectedFee,
|
||||
contractAddress = tokenCurrency.contractAddress,
|
||||
spenderAddress = params.spenderAddress,
|
||||
).getOrElse { error ->
|
||||
Timber.e(error, "Failed to create approval transaction")
|
||||
return false
|
||||
}
|
||||
|
||||
return if (isFeeInTokenCurrency) {
|
||||
createAndSendGaslessTransactionUseCase(
|
||||
userWallet = params.userWallet,
|
||||
transactionData = transactionData,
|
||||
fee = feeExtended,
|
||||
)
|
||||
} else {
|
||||
sendTransactionUseCase(
|
||||
txData = transactionData,
|
||||
userWallet = params.userWallet,
|
||||
network = tokenCurrency.network,
|
||||
)
|
||||
}.fold(
|
||||
ifLeft = { error ->
|
||||
Timber.e("Failed to send approval transaction: $error")
|
||||
false
|
||||
},
|
||||
ifRight = { true },
|
||||
)
|
||||
}
|
||||
|
||||
private fun getApprovalAmount(): BigDecimal? {
|
||||
return if (uiState.value.approveType == ApproveType.LIMITED) {
|
||||
params.amount.toBigDecimalOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.approval.impl.model
|
||||
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal data class GiveApprovalUM(
|
||||
val approveType: ApproveType,
|
||||
val approveItems: ImmutableList<ApproveType> = ApproveType.entries.toImmutableList(),
|
||||
val isApproveButtonEnabled: Boolean,
|
||||
val isApproveLoading: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,353 @@
|
|||
package com.tangem.features.approval.impl.ui
|
||||
|
||||
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.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.vectorResource
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withLink
|
||||
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.DpOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.containers.FooterContainer
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import com.tangem.common.ui.R as CommonUiR
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
internal fun GiveApprovalContent(
|
||||
currency: String,
|
||||
subtitle: TextReference,
|
||||
approveType: ApproveType,
|
||||
approveItems: ImmutableList<ApproveType>,
|
||||
onChangeApproveType: (ApproveType) -> Unit,
|
||||
walletInteractionIcon: Int?,
|
||||
isApproveEnabled: Boolean,
|
||||
isApproveLoading: Boolean,
|
||||
onApproveClick: () -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onOpenLearnMoreAboutApproveClick: () -> Unit,
|
||||
feeSelectorBlockComponent: FeeSelectorBlockComponent,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(color = TangemTheme.colors.background.secondary)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
text = subtitle.resolveReference(),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body2,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing24),
|
||||
)
|
||||
|
||||
SpacerH16()
|
||||
|
||||
ApprovalInfo(
|
||||
currency = currency,
|
||||
approveType = approveType,
|
||||
approveItems = approveItems,
|
||||
onChangeApproveType = onChangeApproveType,
|
||||
onOpenLearnMoreAboutApproveClick = onOpenLearnMoreAboutApproveClick,
|
||||
feeSelectorBlockComponent = feeSelectorBlockComponent,
|
||||
)
|
||||
|
||||
SpacerH(height = TangemTheme.dimens.spacing20)
|
||||
|
||||
PrimaryButtonIconEnd(
|
||||
text = stringResourceSafe(id = CommonUiR.string.common_approve),
|
||||
iconResId = walletInteractionIcon,
|
||||
showProgress = isApproveLoading,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
onClick = onApproveClick,
|
||||
enabled = isApproveEnabled,
|
||||
)
|
||||
|
||||
SpacerH12()
|
||||
|
||||
SecondaryButton(
|
||||
text = stringResourceSafe(id = CommonUiR.string.common_cancel),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
onClick = onCancelClick,
|
||||
)
|
||||
|
||||
SpacerH16()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun ApprovalInfo(
|
||||
currency: String,
|
||||
approveType: ApproveType,
|
||||
approveItems: ImmutableList<ApproveType>,
|
||||
onChangeApproveType: (ApproveType) -> Unit,
|
||||
onOpenLearnMoreAboutApproveClick: () -> Unit,
|
||||
feeSelectorBlockComponent: FeeSelectorBlockComponent,
|
||||
) {
|
||||
FooterContainer(
|
||||
footer = annotatedReference {
|
||||
append(stringResourceSafe(CommonUiR.string.swap_approve_description))
|
||||
append(" ")
|
||||
withLink(
|
||||
link = LinkAnnotation.Clickable(
|
||||
tag = "APPROVE_TAG",
|
||||
linkInteractionListener = { onOpenLearnMoreAboutApproveClick() },
|
||||
),
|
||||
block = {
|
||||
appendColored(
|
||||
text = stringResourceSafe(CommonUiR.string.common_learn_more),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
AmountItem(
|
||||
currency = currency,
|
||||
approveType = approveType,
|
||||
onChangeApproveType = onChangeApproveType,
|
||||
approveItems = approveItems,
|
||||
)
|
||||
}
|
||||
SpacerH16()
|
||||
FooterContainer(
|
||||
footer = resourceReference(CommonUiR.string.give_permission_policy_type_footer),
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
feeSelectorBlockComponent.Content(
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@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 = ripple(),
|
||||
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.spacing12,
|
||||
),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(id = CommonUiR.string.give_permission_rows_amount, currency),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerWMax()
|
||||
Text(
|
||||
text = approveType.text.resolveReference(),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body1,
|
||||
maxLines = 1,
|
||||
)
|
||||
Icon(
|
||||
painter = rememberVectorPainter(ImageVector.vectorResource(id = CommonUiR.drawable.ic_chevron_24)),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
}
|
||||
DropdownSelector(
|
||||
isExpanded = isExpandSelector,
|
||||
onDismiss = { isExpandSelector = false },
|
||||
onItemClick = { type ->
|
||||
isExpandSelector = false
|
||||
onChangeApproveType(type)
|
||||
},
|
||||
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
|
||||
|
||||
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 -> stringResourceSafe(
|
||||
id = CommonUiR.string.give_permission_current_transaction,
|
||||
)
|
||||
ApproveType.UNLIMITED -> stringResourceSafe(
|
||||
id = CommonUiR.string.give_permission_unlimited,
|
||||
)
|
||||
},
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.body1,
|
||||
maxLines = 1,
|
||||
)
|
||||
SpacerWMax()
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
image = ImageVector.vectorResource(id = CommonUiR.drawable.ic_check_24),
|
||||
),
|
||||
tint = color,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.size20),
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
onItemClick.invoke(item)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun GiveApprovalContentPreview(
|
||||
@PreviewParameter(GiveApprovalContentPreviewProvider::class) params: GiveApprovalPreviewParams,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
GiveApprovalContent(
|
||||
currency = params.currency,
|
||||
subtitle = params.subtitle,
|
||||
approveType = params.approveType,
|
||||
approveItems = params.approveItems,
|
||||
onChangeApproveType = {},
|
||||
walletInteractionIcon = params.walletInteractionIcon,
|
||||
isApproveEnabled = params.isApproveEnabled,
|
||||
isApproveLoading = params.isApproveLoading,
|
||||
onApproveClick = {},
|
||||
onCancelClick = {},
|
||||
onOpenLearnMoreAboutApproveClick = {},
|
||||
feeSelectorBlockComponent = PreviewFeeSelectorBlockComponent(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class GiveApprovalPreviewParams(
|
||||
val currency: String,
|
||||
val subtitle: TextReference,
|
||||
val approveType: ApproveType,
|
||||
val approveItems: ImmutableList<ApproveType>,
|
||||
val walletInteractionIcon: Int?,
|
||||
val isApproveEnabled: Boolean,
|
||||
val isApproveLoading: Boolean,
|
||||
)
|
||||
|
||||
private class GiveApprovalContentPreviewProvider : PreviewParameterProvider<GiveApprovalPreviewParams> {
|
||||
override val values: Sequence<GiveApprovalPreviewParams>
|
||||
get() = sequenceOf(
|
||||
GiveApprovalPreviewParams(
|
||||
currency = "USDT",
|
||||
subtitle = stringReference("Allow this app to access your USDT"),
|
||||
approveType = ApproveType.LIMITED,
|
||||
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
|
||||
walletInteractionIcon = CommonUiR.drawable.ic_tangem_24,
|
||||
isApproveEnabled = true,
|
||||
isApproveLoading = false,
|
||||
),
|
||||
GiveApprovalPreviewParams(
|
||||
currency = "USDC",
|
||||
subtitle = stringReference("Allow this app to access your USDC"),
|
||||
approveType = ApproveType.UNLIMITED,
|
||||
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
|
||||
walletInteractionIcon = CommonUiR.drawable.ic_tangem_24,
|
||||
isApproveEnabled = false,
|
||||
isApproveLoading = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.approval.impl.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
|
||||
internal class PreviewFeeSelectorBlockComponent : FeeSelectorBlockComponent {
|
||||
override fun updateState(feeSelectorUM: FeeSelectorUM) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
}
|
||||
}
|
||||
|
|
@ -211,6 +211,7 @@ sealed class CommonSendAnalyticEvents(
|
|||
const val SEND_CATEGORY = "Token / Send"
|
||||
const val SWAP_CATEGORY = "Swap"
|
||||
const val NFT_SEND_CATEGORY = "NFT"
|
||||
const val APPROVE_CATEGORY = "Approve"
|
||||
}
|
||||
|
||||
enum class SendScreenSource {
|
||||
|
|
@ -226,5 +227,6 @@ sealed class CommonSendAnalyticEvents(
|
|||
SendWithSwap("Send&Swap"),
|
||||
WalletConnect("WalletConnect"),
|
||||
NFT("NFT"),
|
||||
Approve("Approve"),
|
||||
}
|
||||
}
|
||||
|
|
@ -300,6 +300,9 @@ include(":features:token-recieve:impl")
|
|||
include(":features:yield-supply:api")
|
||||
include(":features:yield-supply:impl")
|
||||
|
||||
include(":features:approval:api")
|
||||
include(":features:approval:impl")
|
||||
|
||||
include(":features:feed:api")
|
||||
include(":features:feed:impl")
|
||||
// endregion Feature modules
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue