From b94e3f814b29c5d3fe297c207b2829672fe903eb Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 11:46:23 +0400 Subject: [PATCH] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../GiveTxPermisssionBottomSheet.kt | 1 + .../configs/feature_toggles_config.json | 4 + features/approval/api/build.gradle.kts | 28 ++ .../approval/api/GiveApprovalComponent.kt | 30 ++ .../api/GiveApprovalFeatureToggles.kt | 6 + features/approval/impl/build.gradle.kts | 57 +++ .../impl/DefaultGiveApprovalComponent.kt | 105 ++++++ .../impl/DefaultGiveApprovalFeatureToggles.kt | 13 + .../impl/di/GiveApprovalBindsModule.kt | 33 ++ .../approval/impl/model/GiveApprovalModel.kt | 203 ++++++++++ .../approval/impl/model/GiveApprovalUM.kt | 12 + .../approval/impl/ui/GiveApprovalContent.kt | 353 ++++++++++++++++++ .../ui/PreviewFeeSelectorBlockComponent.kt | 15 + .../api/analytics/CommonSendAnalyticEvents.kt | 2 + settings.gradle.kts | 3 + 16 files changed, 867 insertions(+) create mode 100644 features/approval/api/build.gradle.kts create mode 100644 features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt create mode 100644 features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalFeatureToggles.kt create mode 100644 features/approval/impl/build.gradle.kts create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/PreviewFeeSelectorBlockComponent.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7b732b629d..53abf2183b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index 4b4ca2eb11..7af3019d50 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -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) } diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 23de2f59f4..23b909ebad 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -55,5 +55,9 @@ { "name": "WALLET_REORDER_FEATURE_ENABLED", "version": "5.34" + }, + { + "name": "GASLESS_APPROVAL_ENABLED", + "version": "undefined" } ] diff --git a/features/approval/api/build.gradle.kts b/features/approval/api/build.gradle.kts new file mode 100644 index 0000000000..0401b93110 --- /dev/null +++ b/features/approval/api/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt new file mode 100644 index 0000000000..b680b9470b --- /dev/null +++ b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt @@ -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 + } +} \ No newline at end of file diff --git a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalFeatureToggles.kt b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalFeatureToggles.kt new file mode 100644 index 0000000000..46410d3fbf --- /dev/null +++ b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalFeatureToggles.kt @@ -0,0 +1,6 @@ +package com.tangem.features.approval.api + +interface GiveApprovalFeatureToggles { + + val isGaslessApprovalEnabled: Boolean +} \ No newline at end of file diff --git a/features/approval/impl/build.gradle.kts b/features/approval/impl/build.gradle.kts new file mode 100644 index 0000000000..01c43ad67f --- /dev/null +++ b/features/approval/impl/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt new file mode 100644 index 0000000000..51c2fe3986 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt @@ -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( + 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 + } +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt new file mode 100644 index 0000000000..07867cc6a6 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt @@ -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") +} \ 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 new file mode 100644 index 0000000000..94d432e9b9 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt @@ -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 +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt new file mode 100644 index 0000000000..2b3fb3fa8f --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt @@ -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 + 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 { + 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 { + 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 { + 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 + } + } +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt new file mode 100644 index 0000000000..83bf60054d --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt @@ -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.entries.toImmutableList(), + val isApproveButtonEnabled: Boolean, + val isApproveLoading: Boolean, +) \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt new file mode 100644 index 0000000000..2d3386f430 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt @@ -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, + 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, + 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, + 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, + 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, + val walletInteractionIcon: Int?, + val isApproveEnabled: Boolean, + val isApproveLoading: Boolean, +) + +private class GiveApprovalContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + 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 \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/PreviewFeeSelectorBlockComponent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/PreviewFeeSelectorBlockComponent.kt new file mode 100644 index 0000000000..a08d796627 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/PreviewFeeSelectorBlockComponent.kt @@ -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) { + } +} \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt index 597de0c5d3..855c07c8e2 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt @@ -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"), } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 88b32b89bb..f77aab113a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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