From f8d0276d456ebf861b3249d935ca2662b789e813 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Mar 2025 18:03:37 +0300 Subject: [PATCH 001/138] Updated on 2026-08-14 --- .../model/OnboardingVisaPinCodeModel.kt | 17 ++++---- .../child/pincode/model/PinCodeValidation.kt | 31 ++++++++++++++ .../child/pincode/ui/OnboardingVisaPinCode.kt | 41 +++++++++++++++---- .../ui/state/OnboardingVisaPinCodeUM.kt | 3 ++ 4 files changed, 75 insertions(+), 17 deletions(-) create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/PinCodeValidation.kt diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt index 0b02574d50..5f5f95375e 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt @@ -5,6 +5,7 @@ import com.tangem.common.extensions.toHexString 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.ui.extensions.stringReference import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.visa.SetVisaPinCodeUseCase @@ -57,24 +58,28 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( } private fun onPinCodeChange(pin: String) { - if (pin.all { it.isDigit() }) { + if (PinCodeValidation.validateAllDigits(pin)) { + val isError = PinCodeValidation.validateLength(pin) && PinCodeValidation.validate(pin).not() + _uiState.update { it.copy( pinCode = pin, - submitButtonEnabled = checkPinCode(pin), + submitButtonEnabled = PinCodeValidation.validate(pin), + error = if (isError) stringReference("Invalid PIN: avoid sequences or repeats") else null, ) } } } private fun onSubmitClick() { - if (checkPinCode(_uiState.value.pinCode).not()) return + val pinCode = _uiState.value.pinCode + if (PinCodeValidation.validate(pinCode).not()) return modelScope.launch { loading(true) setVisaPinCodeUseCase( - pinCode = _uiState.value.pinCode, + pinCode = pinCode, visaCardId = visaCardId, activationOrderId = params.activationOrderInfo.orderId, ).onLeft { @@ -88,10 +93,6 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( } } - private fun checkPinCode(pin: String): Boolean { - return pin.length == PIN_CODE_LENGTH - } - private fun loading(state: Boolean) { _uiState.update { it.copy(submitButtonLoading = state) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/PinCodeValidation.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/PinCodeValidation.kt new file mode 100644 index 0000000000..99c721134d --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/PinCodeValidation.kt @@ -0,0 +1,31 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.pincode.model + +internal object PinCodeValidation { + + private const val PIN_CODE_LENGTH = 4 + + fun validate(pinCode: String): Boolean { + return sequenceOf( + ::validateAllDigits, + ::validateLength, + ::validateNoRepeatedDigits, + ::validateNoConsecutiveDigits, + ).all { it(pinCode) } + } + + fun validateAllDigits(pinCode: String): Boolean { + return pinCode.all { it.isDigit() } + } + + fun validateLength(pinCode: String): Boolean { + return pinCode.length == PIN_CODE_LENGTH + } + + private fun validateNoRepeatedDigits(pinCode: String): Boolean { + return pinCode.toSet().size == PIN_CODE_LENGTH + } + + private fun validateNoConsecutiveDigits(pinCode: String): Boolean { + return pinCode.windowed(2).all { it[0] + 1 == it[1] }.not() + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt index c65e7afaeb..744711c9db 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -31,7 +32,10 @@ import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM @@ -91,13 +95,29 @@ internal fun OnboardingVisaPinCode(state: OnboardingVisaPinCodeUM, modifier: Mod @Composable private fun PinCodeSection(state: OnboardingVisaPinCodeUM, modifier: Modifier = Modifier) { val focusRequester = remember { FocusRequester() } + Column(horizontalAlignment = Alignment.CenterHorizontally) { + PinCode( + modifier = modifier, + value = state.pinCode, + onValueChange = state.onPinCodeChange, + focusRequester = focusRequester, + ) - PinCode( - modifier = modifier, - value = state.pinCode, - onValueChange = state.onPinCodeChange, - focusRequester = focusRequester, - ) + AnimatedVisibility( + visible = state.error != null, + ) { + val error = remember(this) { requireNotNull(state.error) } + Column { + SpacerH4() + Text( + text = error.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.warning, + textAlign = TextAlign.Center, + ) + } + } + } LaunchedEffect(Unit) { delay(timeMillis = 300) @@ -134,9 +154,11 @@ private fun PinCode( keyboardType = KeyboardType.Number, imeAction = ImeAction.Done, ), - keyboardActions = KeyboardActions(onDone = { - keyboardController?.hide() - },), + keyboardActions = KeyboardActions( + onDone = { + keyboardController?.hide() + }, + ), modifier = Modifier .alpha(alpha = 0.01f) .focusRequester(focusRequester), @@ -194,6 +216,7 @@ private fun Preview() { OnboardingVisaPinCode( state = OnboardingVisaPinCodeUM( pinCode = "1234", + error = stringReference("PIN Code error"), ), ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt index 9b17f93c5a..adc4df53d1 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt @@ -1,7 +1,10 @@ package com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state +import com.tangem.core.ui.extensions.TextReference + internal data class OnboardingVisaPinCodeUM( val pinCode: String = "", + val error: TextReference? = null, val onPinCodeChange: (String) -> Unit = {}, val submitButtonLoading: Boolean = false, val submitButtonEnabled: Boolean = true, From 61591dd8f654f8c83dd8acff83d059d496c7425e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Mar 2025 12:06:15 +0300 Subject: [PATCH 002/138] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 20a7e31c0b..67b61d21e3 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 @@ -57,11 +57,11 @@ }, { "name": "TX_HISTORY_REFACTORING_ENABLED", - "version": "undefined" + "version": "5.22.0" }, { "name": "ASK_BIOMETRY_REFACTORING_ENABLED", - "version": "undefined" + "version": "5.22.0" }, { "name": "NOTE_REFACTORING_ENABLED", From 342ca3b53025e061dacd26e014aff1e18e76d479 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Mar 2025 13:30:00 +0000 Subject: [PATCH 003/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index c191bacee5..7b378fad02 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "release-app_5.22-982" +tangemBlockchainSdk = "develop-977" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.22-440" +tangemCardSdk = "develop-439" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 64d429f64263da8dcadbdecb330348f8bc948caa Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Mar 2025 17:11:22 +0300 Subject: [PATCH 004/138] Updated on 2026-08-14 --- data/visa/build.gradle.kts | 1 + .../tangem/data/visa/DefaultVisaRepository.kt | 16 +++--- .../data/visa/utils/VisaCurrencyFactory.kt | 41 ++++++++++++++- .../tangem/domain/visa/model/VisaCurrency.kt | 7 ++- .../wallet/model/intents/VisaWalletIntents.kt | 51 ------------------- .../wallet/state/model/DepositButtonState.kt | 6 --- .../wallet/state/model/WalletState.kt | 1 - .../SetRefreshStateTransformer.kt | 7 +-- ...ansformer.kt => SetVisaInfoTransformer.kt} | 49 ++++++++++++++++-- .../state/utils/WalletLoadingStateFactory.kt | 10 +++- .../subscribers/VisaWalletSubscriber.kt | 4 +- .../presentation/wallet/ui/WalletScreen.kt | 6 --- .../ui/components/visa/VisaDepositButton.kt | 21 -------- .../visa/DefaultVisaContractInfoProvider.kt | 9 +++- .../tangem/lib/visa/model/VisaContractInfo.kt | 1 + 15 files changed, 120 insertions(+), 110 deletions(-) delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/DepositButtonState.kt rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/{SetBalancesAndLimitsTransformer.kt => SetVisaInfoTransformer.kt} (68%) delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaDepositButton.kt diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 035040383b..f8999494d2 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.tokens.models) /** Project - Utils */ implementation(projects.core.utils) diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index 6a78abbf06..b513244dfc 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -43,11 +43,9 @@ internal class DefaultVisaRepository @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val visaApiRequestMaker: VisaApiRequestMaker, private val visaApi: TangemVisaApi, + private val visaCurrencyFactory: VisaCurrencyFactory, ) : VisaRepository { - private val currencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) { - VisaCurrencyFactory() - } private val txDetailsFactory by lazy(mode = LazyThreadSafetyMode.NONE) { VisaTxDetailsFactory() } @@ -79,20 +77,22 @@ internal class DefaultVisaRepository @Inject constructor( private suspend fun fetchVisaCurrency(userWalletId: UserWalletId, address: String) { val contractInfoProvider = visaLibLoader.getOrCreateProvider() + val paymentAccountAddress = getPaymentAccountAddress(userWalletId) + val userWallet = findVisaUserWallet(userWalletId) parZip( dispatchers.io, { contractInfoProvider.getContractInfo( walletAddress = address, - paymentAccountAddress = getPaymentAccountAddress(userWalletId), + paymentAccountAddress = paymentAccountAddress, ) }, { getFiatRate() }, { contractInfo, fiatRate -> fetchedCurrencies.update { value -> value.apply { - put(address, currencyFactory.create(contractInfo, fiatRate)) + put(address, visaCurrencyFactory.create(userWallet, contractInfo, fiatRate)) } } }, @@ -157,7 +157,7 @@ internal class DefaultVisaRepository @Inject constructor( ) } - // TODO select correct account when multiple accounts are available (will be implemented when backend is ready) + // There will be only one payment account (backend has filtered for us), so we can take the first one customerInfo.paymentAccounts.firstOrNull()?.paymentAccountAddress }.getOrNull() @@ -187,14 +187,14 @@ internal class DefaultVisaRepository @Inject constructor( } } - private suspend fun getFiatRate(): BigDecimal? { + private suspend fun getFiatRate(): BigDecimal { val fiatCurrencyId = VisaConstants.fiatCurrency.code.lowercase() val quotes = tangemTechApi.getQuotes( currencyId = fiatCurrencyId, coinIds = VisaConstants.TOKEN_ID, ).getOrThrow() - return quotes.quotes[VisaConstants.TOKEN_ID]?.price + return quotes.quotes[VisaConstants.TOKEN_ID]?.price ?: error("No price found") } private fun makeWalletAddresses(userWallet: UserWallet): Set
{ diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt index 989a2d504c..7765fc8608 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt @@ -1,16 +1,28 @@ package com.tangem.data.visa.utils +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.currency.getNetwork +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.visa.model.VisaCurrency +import com.tangem.domain.wallets.models.UserWallet import com.tangem.lib.visa.model.VisaContractInfo import org.joda.time.DateTime import org.joda.time.DateTimeZone import org.joda.time.Instant import java.math.BigDecimal import java.math.BigInteger +import javax.inject.Inject -internal class VisaCurrencyFactory { +internal class VisaCurrencyFactory @Inject constructor( + private val cryptoCurrencyFactory: CryptoCurrencyFactory, + private val excludedBlockchains: ExcludedBlockchains, +) { - fun create(contractInfo: VisaContractInfo, fiatRate: BigDecimal?): VisaCurrency { + fun create(userWallet: UserWallet, contractInfo: VisaContractInfo, fiatRate: BigDecimal): VisaCurrency { val now = Instant.now() val currentLimit = if (contractInfo.limitsChangeDate > now) { contractInfo.oldLimits @@ -19,11 +31,30 @@ internal class VisaCurrencyFactory { } val remainingOtpLimit = getRemainingOtp(currentLimit, now) + val currencyNetwork = getNetwork( + blockchain = Blockchain.Polygon, + extraDerivationPath = null, + derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, + excludedBlockchains = excludedBlockchains, + canHandleTokens = true, + ) ?: error("Unable to create network for Visa currency") + + val cryptoCurrency = cryptoCurrencyFactory.createToken( + network = currencyNetwork, + rawId = CryptoCurrency.RawID(VisaConstants.TOKEN_ID), + name = contractInfo.token.name, + symbol = contractInfo.token.symbol, + decimals = contractInfo.token.decimals, + contractAddress = contractInfo.token.address, + ) + return VisaCurrency( + cryptoCurrency = cryptoCurrency, symbol = contractInfo.token.symbol, networkName = VisaConstants.NETWORK_NAME, decimals = contractInfo.token.decimals, fiatRate = fiatRate, + priceChange = BigDecimal.ZERO, // [Second Visa Iteration] pass to create() params if needed fiatCurrency = VisaConstants.fiatCurrency, balances = with(contractInfo) { VisaCurrency.Balances( @@ -40,6 +71,12 @@ internal class VisaCurrencyFactory { singleTransaction = currentLimit.singleTransactionLimit, expirationDate = getLimitsExpirationDate(currentLimit, now), ), + paymentAccountAddress = NetworkAddress.Single( + NetworkAddress.Address( + value = contractInfo.paymentAccountAddress, + type = NetworkAddress.Address.Type.Primary, + ), + ), ) } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaCurrency.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaCurrency.kt index b21df662c3..d17b1d726e 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaCurrency.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaCurrency.kt @@ -1,17 +1,22 @@ package com.tangem.domain.visa.model import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.NetworkAddress import org.joda.time.DateTime import java.math.BigDecimal data class VisaCurrency( + val cryptoCurrency: CryptoCurrency, val networkName: String, val symbol: String, val decimals: Int, - val fiatRate: BigDecimal?, + val fiatRate: BigDecimal, + val priceChange: BigDecimal, val fiatCurrency: AppCurrency, val balances: Balances, val limits: Limits, + val paymentAccountAddress: NetworkAddress, ) { data class Balances( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt index 4a0281d436..8fe27fb45f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt @@ -2,29 +2,19 @@ package com.tangem.feature.wallet.child.wallet.model.intents import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels -import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Network import com.tangem.domain.visa.GetVisaCurrencyUseCase import com.tangem.domain.visa.GetVisaTxDetailsUseCase -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.BalancesAndLimitsBottomSheetConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.VisaTxDetailsBottomSheetConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject internal interface VisaWalletIntents { - fun onDepositClick() - fun onBalancesAndLimitsClick() fun onVisaTransactionClick(id: String) @@ -36,7 +26,6 @@ internal interface VisaWalletIntents { internal class VisaWalletIntentsImplementor @Inject constructor( private val stateController: WalletStateController, private val eventSender: WalletEventSender, - private val getCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase, private val getVisaCurrencyUseCase: GetVisaCurrencyUseCase, private val getVisaTxDetailsUseCase: GetVisaTxDetailsUseCase, private val dispatchers: CoroutineDispatcherProvider, @@ -46,38 +35,6 @@ internal class VisaWalletIntentsImplementor @Inject constructor( BalancesAndLimitsBottomSheetConverter(eventSender) } - override fun onDepositClick() { - val userWalletId = stateController.getSelectedWalletId() - - modelScope.launch(dispatchers.main) { - val currencyStatus = getPrimaryCurrencyStatus(userWalletId) ?: return@launch - - createReceiveBottomSheetContent(currencyStatus)?.let { content -> - stateController.showBottomSheet(content) - } - } - } - - private fun createReceiveBottomSheetContent(currencyStatus: CryptoCurrencyStatus): TangemBottomSheetConfigContent? { - val currency = currencyStatus.currency - val addresses = currencyStatus.value.networkAddress?.availableAddresses - - if (addresses == null) { - Timber.e("Addresses should not be null") - return null - } - - return TokenReceiveBottomSheetConfig( - name = currency.name, - symbol = currency.symbol, - network = currency.network.name, - addresses = addresses.mapToAddressModels(currency).toImmutableList(), - showMemoDisclaimer = currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, - onCopyClick = { /* no-op */ }, - onShareClick = { /* no-op */ }, - ) - } - override fun onBalancesAndLimitsClick() { modelScope.launch(dispatchers.main) { val userWalletId = stateController.getSelectedWalletId() @@ -95,14 +52,6 @@ internal class VisaWalletIntentsImplementor @Inject constructor( } } - private suspend fun getPrimaryCurrencyStatus(userWalletId: UserWalletId): CryptoCurrencyStatus? { - return getCurrencyStatusUseCase(userWalletId) - .getOrElse { - Timber.e("Failed to get primary currency $it") - null - } - } - override fun onVisaTransactionClick(id: String) { modelScope.launch(dispatchers.main) { val userWalletId = stateController.getSelectedWalletId() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/DepositButtonState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/DepositButtonState.kt deleted file mode 100644 index 9a490bf52e..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/DepositButtonState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.model - -internal data class DepositButtonState( - val isEnabled: Boolean, - val onClick: () -> Unit, -) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 42b4839fc4..786c3ebb5c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -96,7 +96,6 @@ internal sealed interface WalletState : WalletStateHolder { override val bottomSheetConfig: TangemBottomSheetConfig?, override val balancesAndLimitBlockState: BalancesAndLimitsBlockState, override val txHistoryState: TxHistoryState, - val depositButtonState: DepositButtonState, ) : Visa() data class Locked( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt index 1465ccc33a..4ba7560ab0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.BalancesAndLimitsBlockState -import com.tangem.feature.wallet.presentation.wallet.state.model.DepositButtonState import com.tangem.feature.wallet.presentation.wallet.state.model.* import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.mutate @@ -29,8 +28,8 @@ internal class SetRefreshStateTransformer( } is WalletState.Visa.Content -> { prevState.copy( + buttons = prevState.buttons.toUpdatedState(), pullToRefreshConfig = prevState.pullToRefreshConfig.toUpdatedState(isRefreshing), - depositButtonState = prevState.depositButtonState.toUpdatedState(isRefreshing), balancesAndLimitBlockState = prevState.balancesAndLimitBlockState.toUpdatedState(isRefreshing), ) } @@ -75,10 +74,6 @@ internal class SetRefreshStateTransformer( } } - private fun DepositButtonState.toUpdatedState(isRefreshing: Boolean): DepositButtonState { - return copy(isEnabled = !isRefreshing) - } - private fun BalancesAndLimitsBlockState.toUpdatedState(isRefreshing: Boolean): BalancesAndLimitsBlockState { return when (this) { is BalancesAndLimitsBlockState.Content -> copy(isEnabled = !isRefreshing) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt similarity index 68% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt index 2655da4ba9..1d65f129ae 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.common.util.getCardsCount +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.visa.exception.RefreshTokenExpiredException import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.wallets.models.UserWallet @@ -15,11 +16,14 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditiona import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton import com.tangem.utils.extensions.isZero +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf import org.joda.time.DateTime import org.joda.time.Days -internal class SetBalancesAndLimitsTransformer( +internal class SetVisaInfoTransformer( private val userWallet: UserWallet, private val maybeVisaCurrency: Either, private val clickIntents: WalletClickIntents, @@ -35,15 +39,15 @@ internal class SetBalancesAndLimitsTransformer( } return prevState.copy( + buttons = createVisaButtonsDimmed(), walletCardState = getErrorWalletCardState(prevState.walletCardState), - depositButtonState = prevState.depositButtonState.copy(isEnabled = false), balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error, ) } return prevState.copy( + buttons = createVisaButtons(visaCurrency = visaCurrency), walletCardState = getContentWalletCardState(prevState.walletCardState, visaCurrency), - depositButtonState = prevState.depositButtonState.copy(isEnabled = true), balancesAndLimitBlockState = getContentBlockState(visaCurrency), ) } @@ -115,4 +119,43 @@ internal class SetBalancesAndLimitsTransformer( onUnlockVisaAccessNotificationClick = clickIntents::onUnlockVisaAccessClick, ) } + + private fun createVisaButtonsDimmed(): PersistentList { + return persistentListOf( + WalletManageButton.Receive(enabled = true, dimContent = true, onClick = {}, onLongClick = null), + WalletManageButton.Buy(enabled = true, dimContent = true, onClick = {}), + ) + } + + private fun createVisaButtons(visaCurrency: VisaCurrency): PersistentList { + // [Second Visa Iteration] Make VisaCurrency contain CryptoCurrencyStatus + val cryptoCurrencyStatus = CryptoCurrencyStatus( + currency = visaCurrency.cryptoCurrency, + value = CryptoCurrencyStatus.Loaded( + amount = visaCurrency.balances.available, + fiatAmount = visaCurrency.balances.available.multiply(visaCurrency.fiatRate), + fiatRate = visaCurrency.fiatRate, + priceChange = visaCurrency.priceChange, + yieldBalance = null, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = visaCurrency.paymentAccountAddress, + sources = CryptoCurrencyStatus.Sources(), + ), + ) + + return return persistentListOf( + WalletManageButton.Receive( + enabled = true, + dimContent = false, + onClick = { clickIntents.onReceiveClick(cryptoCurrencyStatus = cryptoCurrencyStatus) }, + onLongClick = { clickIntents.onCopyAddressLongClick(cryptoCurrencyStatus = cryptoCurrencyStatus) }, + ), + WalletManageButton.Buy( + enabled = true, + dimContent = false, + onClick = { clickIntents.onMultiWalletBuyClick(userWalletId = userWallet.walletId) }, + ), + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 6223bc1d05..599932a80e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -67,7 +67,7 @@ internal class WalletLoadingStateFactory( return WalletState.Visa.Content( pullToRefreshConfig = createPullToRefreshConfig(), walletCardState = userWallet.toLoadingWalletCardState(), - buttons = createMultiWalletActions(userWallet), + buttons = createVisaDimmedButtons(), warnings = persistentListOf(), bottomSheetConfig = null, balancesAndLimitBlockState = BalancesAndLimitsBlockState.Loading, @@ -76,7 +76,6 @@ internal class WalletLoadingStateFactory( value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), ), ), - depositButtonState = DepositButtonState(isEnabled = false, clickIntents::onDepositClick), ) } @@ -117,6 +116,13 @@ internal class WalletLoadingStateFactory( ) } + private fun createVisaDimmedButtons(): PersistentList { + return persistentListOf( + WalletManageButton.Receive(enabled = true, dimContent = true, onClick = {}, onLongClick = null), + WalletManageButton.Buy(enabled = true, dimContent = true, onClick = {}), + ) + } + private fun createDimmedButtons(): PersistentList { return persistentListOf( WalletManageButton.Receive(enabled = true, dimContent = true, onClick = {}, onLongClick = null), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt index 239ee8c80d..03e49e67f0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt @@ -13,7 +13,7 @@ import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetBalancesAndLimitsTransformer +import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetVisaInfoTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryCountTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsErrorTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsTransformer @@ -62,7 +62,7 @@ internal class VisaWalletSubscriber( private fun setLoadedCurrencyState(maybeCurrency: Either) { stateController.update( - SetBalancesAndLimitsTransformer( + SetVisaInfoTransformer( userWallet = userWallet, maybeVisaCurrency = maybeCurrency, clickIntents = clickIntents, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 69c2f73c77..3a8e681611 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -85,7 +85,6 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrenc import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.BalancesAndLimitsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDetailsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock -import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.entry.MarketsEntryComponent @@ -230,11 +229,6 @@ private fun WalletContent( } (selectedWallet as? WalletState.Visa.Content)?.let { - depositButton( - modifier = itemModifier.fillMaxWidth(), - state = it.depositButtonState, - ) - balancesAndLimitsBlock( modifier = itemModifier, state = it.balancesAndLimitBlockState, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaDepositButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaDepositButton.kt deleted file mode 100644 index 395e9499a4..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaDepositButton.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.visa - -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.PrimaryButtonIconStart -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.wallet.state.model.DepositButtonState - -private const val DEPOSIT_BUTTON_CONTENT_TYPE = "DepositButton" - -internal fun LazyListScope.depositButton(state: DepositButtonState, modifier: Modifier = Modifier) { - item(key = DEPOSIT_BUTTON_CONTENT_TYPE, contentType = DEPOSIT_BUTTON_CONTENT_TYPE) { - PrimaryButtonIconStart( - modifier = modifier, - text = "Deposit", - iconResId = R.drawable.ic_arrow_down_24, - onClick = state.onClick, - enabled = state.isEnabled, - ) - } -} \ No newline at end of file diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt index 395904554e..957b817ab8 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt @@ -81,7 +81,14 @@ internal class DefaultVisaContractInfoProvider( { fetchBalances(paymentAccount, paymentToken) }, { fetchLimits(paymentAccount, paymentToken, walletAddress) }, { token, balances, (oldLimit, newLimit, changeDate) -> - VisaContractInfo(token, balances, oldLimit, newLimit, changeDate) + VisaContractInfo( + token = token, + balances = balances, + oldLimits = oldLimit, + newLimits = newLimit, + paymentAccountAddress = paymentAccount.contractAddress, + limitsChangeDate = changeDate, + ) }, ) diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaContractInfo.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaContractInfo.kt index e50c6e3c89..b15cc9a7e7 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaContractInfo.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaContractInfo.kt @@ -9,6 +9,7 @@ data class VisaContractInfo( val balances: Balances, val oldLimits: Limits, val newLimits: Limits, + val paymentAccountAddress: String, val limitsChangeDate: Instant, ) { From 06cfbc6a272820bf1045e072303f6fee906eeec3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Mar 2025 18:05:21 +0300 Subject: [PATCH 005/138] Updated on 2026-08-14 --- .../wallet/presentation/wallet/domain/Wallet2CobrandImage.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt index 3c468610ba..d49cdd69fb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -40,7 +40,7 @@ internal enum class Wallet2CobrandImage( BitcoinGold( cards2ResId = R.drawable.ill_bitcoingold_card2_120_106, cards3ResId = R.drawable.ill_bitcoingold_card3_120_106, - batchIds = setOf("AF71"), + batchIds = setOf("AF71", "AF990016", "AF990009"), ), BitcoinPizzaDay( From 0ba686b450162ff93262f85fc5b74edaa379dc90 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Mar 2025 13:02:59 +0400 Subject: [PATCH 006/138] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 5 ++++- .../java/com/tangem/tap/TangemApplication.kt | 5 +++++ .../com/tangem/tap/domain/extensions/Card.kt | 2 +- .../features/onboarding/OnboardingManager.kt | 2 +- .../tangem/tap/proxy/redux/DaggerGraphState.kt | 2 ++ .../assets/configs/feature_toggles_config.json | 4 ++++ .../wallets/usecase/GetCardImageUseCase.kt | 12 +++++------- gradle/tangem_dependencies.toml | 2 +- libs/tangem-sdk-api/build.gradle.kts | 1 + .../sdk/api/di/CardSdkFeatureTogglesModule.kt | 18 ++++++++++++++++++ .../com/tangem/sdk/api/di/CardSdkModule.kt | 17 +++++++++++++++++ .../featuretoggles/CardSdkFeatureToggles.kt | 11 +++++++++++ .../DefaultCardSdkFeatureToggles.kt | 12 ++++++++++++ 13 files changed, 82 insertions(+), 11 deletions(-) create mode 100644 libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkFeatureTogglesModule.kt create mode 100644 libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkModule.kt create mode 100644 libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/CardSdkFeatureToggles.kt create mode 100644 libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/DefaultCardSdkFeatureToggles.kt diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 2b76d7c7eb..e37127cb50 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -3,7 +3,6 @@ package com.tangem.tap import androidx.hilt.work.HiltWorkerFactory import com.tangem.TangemSdkLogger import com.tangem.blockchainsdk.BlockchainSDKFactory -import com.tangem.data.card.TransactionSignerFactory import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.filter.OneTimeEventFilter @@ -15,6 +14,7 @@ import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.data.card.TransactionSignerFactory import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage @@ -39,6 +39,7 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onramp.OnrampFeatureToggles +import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles @@ -142,4 +143,6 @@ interface ApplicationEntryPoint { fun getUiMessageSender(): UiMessageSender fun getWorkerFactory(): HiltWorkerFactory + + fun getOnlineCardVerifier(): OnlineCardVerifier } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 32e3b46f66..43f0e36ce0 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -53,6 +53,7 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onramp.OnrampFeatureToggles +import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler @@ -220,6 +221,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat .setWorkerFactory(workerFactory) .build() + private val onlineCardVerifier: OnlineCardVerifier + get() = entryPoint.getOnlineCardVerifier() + // endregion override fun onCreate() { @@ -322,6 +326,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat clipboardManager = clipboardManager, settingsManager = settingsManager, uiMessageSender = uiMessageSender, + onlineCardVerifier = onlineCardVerifier, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt index 9497f54791..52fadf58cc 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt @@ -6,8 +6,8 @@ import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.getTwinCardNumber import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.models.Artwork -import com.tangem.operations.attestation.CardVerifyAndGetInfo import com.tangem.operations.attestation.OnlineCardVerifier +import com.tangem.operations.attestation.api.models.CardVerifyAndGetInfo fun CardDTO.signedHashesCount(): Int { return wallets.sumOf { it.totalSignedHashes ?: 0 } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt index a0db9b19f8..7150916d2e 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt @@ -7,8 +7,8 @@ import com.tangem.common.extensions.isZero import com.tangem.common.services.Result import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.models.scan.ScanResponse -import com.tangem.operations.attestation.CardVerifyAndGetInfo import com.tangem.operations.attestation.OnlineCardVerifier +import com.tangem.operations.attestation.api.models.CardVerifyAndGetInfo import com.tangem.tap.common.entities.ProgressState import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.safeUpdate diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 08b6c9ba8a..4dd2bd2a00 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -33,6 +33,7 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onramp.OnrampFeatureToggles +import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor @@ -77,4 +78,5 @@ data class DaggerGraphState( val clipboardManager: ClipboardManager? = null, val settingsManager: SettingsManager? = null, val uiMessageSender: UiMessageSender? = null, + val onlineCardVerifier: OnlineCardVerifier? = null, ) : StateType \ No newline at end of file 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 67b61d21e3..71052ae476 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 @@ -66,5 +66,9 @@ { "name": "NOTE_REFACTORING_ENABLED", "version": "undefined" + }, + { + "name": "NEW_ATTESTATION_ENABLED", + "version": "undefined" } ] diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt index 67080c0682..27281d6c33 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt @@ -6,7 +6,6 @@ import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.TwinsHelper import com.tangem.domain.wallets.models.Artwork import com.tangem.operations.attestation.OnlineCardVerifier -import com.tangem.operations.attestation.TangemApi /** * Use case for getting card image url @@ -30,7 +29,11 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier = OnlineCardV if (artworkId.isNullOrEmpty()) { getFallbackArtworkUrl(cardId) } else { - getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId) + OnlineCardVerifier.getUrlForArtwork( + cardId = cardId, + cardPublicKey = cardPublicKey.toHexString(), + artworkId = artworkId, + ) } } @@ -51,9 +54,4 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier = OnlineCardV } } } - - private fun getUrlForArtwork(cardId: String, cardPublicKeyHex: String, artworkId: String): String { - return TangemApi.Companion.BaseUrl.VERIFY.url + TangemApi.ARTWORK + - "?artworkId=$artworkId&CID=$cardId&publicKey=$cardPublicKeyHex" - } } \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 7b378fad02..e71e1ff89f 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] tangemBlockchainSdk = "develop-977" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-439" +tangemCardSdk = "develop-441" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/tangem-sdk-api/build.gradle.kts b/libs/tangem-sdk-api/build.gradle.kts index 577365a357..f780f554c2 100644 --- a/libs/tangem-sdk-api/build.gradle.kts +++ b/libs/tangem-sdk-api/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.visa.models) + implementation(projects.core.configToggles) implementation(projects.core.res) /** Tangem libraries */ diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkFeatureTogglesModule.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkFeatureTogglesModule.kt new file mode 100644 index 0000000000..a7389193ee --- /dev/null +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkFeatureTogglesModule.kt @@ -0,0 +1,18 @@ +package com.tangem.sdk.api.di + +import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles +import com.tangem.sdk.api.featuretoggles.DefaultCardSdkFeatureToggles +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface CardSdkFeatureTogglesModule { + + @Binds + @Singleton + fun bindCardSdkFeatureToggles(impl: DefaultCardSdkFeatureToggles): CardSdkFeatureToggles +} \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkModule.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkModule.kt new file mode 100644 index 0000000000..6a6cfde251 --- /dev/null +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkModule.kt @@ -0,0 +1,17 @@ +package com.tangem.sdk.api.di + +import com.tangem.operations.attestation.OnlineCardVerifier +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object CardSdkModule { + + @Provides + @Singleton + fun provideOnlineCardVerifier(): OnlineCardVerifier = OnlineCardVerifier() +} \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/CardSdkFeatureToggles.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/CardSdkFeatureToggles.kt new file mode 100644 index 0000000000..b90f1577cc --- /dev/null +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/CardSdkFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.sdk.api.featuretoggles + +/** + * Card SDK feature toggles + * +[REDACTED_AUTHOR] + */ +interface CardSdkFeatureToggles { + + val isNewAttestationEnabled: Boolean +} \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/DefaultCardSdkFeatureToggles.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/DefaultCardSdkFeatureToggles.kt new file mode 100644 index 0000000000..4146304590 --- /dev/null +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/DefaultCardSdkFeatureToggles.kt @@ -0,0 +1,12 @@ +package com.tangem.sdk.api.featuretoggles + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import javax.inject.Inject + +internal class DefaultCardSdkFeatureToggles @Inject constructor( + private val featureTogglesManager: FeatureTogglesManager, +) : CardSdkFeatureToggles { + + override val isNewAttestationEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "NEW_ATTESTATION_ENABLED") +} \ No newline at end of file From c1250bc31f3fc2ea85c7f2574652ba825dc9508d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Mar 2025 13:06:30 +0400 Subject: [PATCH 007/138] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 9 +++------ .../java/com/tangem/tap/TangemApplication.kt | 14 ++++--------- .../tap/di/domain/WalletsDomainModule.kt | 8 ++++++++ .../com/tangem/tap/features/home/HomeModel.kt | 8 ++------ .../tap/features/home/redux/HomeMiddleware.kt | 6 +++--- .../features/onboarding/OnboardingHelper.kt | 6 +++--- .../redux/OnboardingWalletMiddleware.kt | 6 +++--- .../saveWallet/redux/SaveWalletMiddleware.kt | 7 ++++--- .../welcome/redux/WelcomeMiddleware.kt | 8 +++----- .../tap/proxy/redux/DaggerGraphState.kt | 6 ++---- domain/wallets/build.gradle.kts | 8 ++++++++ .../wallets/builder/UserWalletBuilder.kt | 17 ++++++++++++---- .../wallets/usecase/GetCardImageUseCase.kt | 4 ++-- .../features/details/utils/UserWalletSaver.kt | 20 ++++++++++--------- .../model/Wallet1ChooseOptionModel.kt | 5 ++--- .../model/MultiWalletCreateWalletModel.kt | 5 ++--- .../model/MultiWalletFinalizeModel.kt | 5 ++--- .../impl/model/OnboardingMultiWalletModel.kt | 10 +++++++--- .../model/OnboardingVisaInProgressModel.kt | 8 +++----- .../model/OnboardingVisaPinCodeModel.kt | 8 +++----- .../ScanCardToUnlockWalletClickHandler.kt | 5 ++--- 21 files changed, 90 insertions(+), 83 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index e37127cb50..ee95e2ba40 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -32,11 +32,10 @@ import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.operations.attestation.OnlineCardVerifier @@ -95,8 +94,6 @@ interface ApplicationEntryPoint { fun getSaveTwinsOnboardingShownUseCase(): SaveTwinsOnboardingShownUseCase - fun getWalletNameGenerateUseCase(): GenerateWalletNameUseCase - fun getCardRepository(): CardRepository fun getTangemSdkLogger(): TangemSdkLogger @@ -119,8 +116,6 @@ interface ApplicationEntryPoint { fun getTransactionSignerFactory(): TransactionSignerFactory - fun getGetUserCountryCodeUseCase(): GetUserCountryUseCase - fun getOnrampFeatureToggles(): OnrampFeatureToggles fun getOnboardingV2FeatureToggles(): OnboardingV2FeatureToggles @@ -145,4 +140,6 @@ interface ApplicationEntryPoint { fun getWorkerFactory(): HiltWorkerFactory fun getOnlineCardVerifier(): OnlineCardVerifier + + fun getUserWalletBuilderFactory(): UserWalletBuilder.Factory } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 43f0e36ce0..e73bf509da 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -46,11 +46,10 @@ import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.operations.attestation.OnlineCardVerifier @@ -147,9 +146,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase get() = entryPoint.getSaveTwinsOnboardingShownUseCase() - private val generateWalletNameUseCase: GenerateWalletNameUseCase - get() = entryPoint.getWalletNameGenerateUseCase() - private val cardRepository: CardRepository get() = entryPoint.getCardRepository() @@ -183,9 +179,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val transactionSignerFactory: TransactionSignerFactory get() = entryPoint.getTransactionSignerFactory() - private val getUserCountryUseCase: GetUserCountryUseCase - get() = entryPoint.getGetUserCountryCodeUseCase() - private val onrampFeatureToggles: OnrampFeatureToggles get() = entryPoint.getOnrampFeatureToggles() @@ -224,6 +217,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val onlineCardVerifier: OnlineCardVerifier get() = entryPoint.getOnlineCardVerifier() + private val userWalletBuilderFactory: UserWalletBuilder.Factory + get() = entryPoint.getUserWalletBuilderFactory() // endregion override fun onCreate() { @@ -305,7 +300,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat generalUserWalletsListManager = generalUserWalletsListManager, wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase, saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase, - generateWalletNameUseCase = generateWalletNameUseCase, cardRepository = cardRepository, settingsRepository = settingsRepository, blockchainSDKFactory = blockchainSDKFactory, @@ -316,7 +310,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat shareManager = shareManager, appRouter = appRouter, transactionSignerFactory = transactionSignerFactory, - getUserCountryUseCase = getUserCountryUseCase, onrampFeatureToggles = onrampFeatureToggles, environmentConfigStorage = environmentConfigStorage, onboardingV2FeatureToggles = onboardingV2FeatureToggles, @@ -327,6 +320,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat settingsManager = settingsManager, uiMessageSender = uiMessageSender, onlineCardVerifier = onlineCardVerifier, + userWalletBuilderFactory = userWalletBuilderFactory, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 49fdf2c1be..9ede57293c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -11,6 +11,7 @@ import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase +import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -18,6 +19,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +@Suppress("TooManyFunctions") @Module @InstallIn(SingletonComponent::class) internal object WalletsDomainModule { @@ -165,4 +167,10 @@ internal object WalletsDomainModule { fun providesSeedPhraseNotificationUseCase(walletsRepository: WalletsRepository): SeedPhraseNotificationUseCase { return SeedPhraseNotificationUseCase(walletsRepository = walletsRepository) } + + @Provides + @Singleton + fun provideGetCardImageUseCase(onlineCardVerifier: OnlineCardVerifier): GetCardImageUseCase { + return GetCardImageUseCase(verifier = onlineCardVerifier) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt index 8511f53ab2..ce4cd7ef14 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt @@ -19,7 +19,6 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.tokens.TokensAction import com.tangem.domain.wallets.builder.UserWalletBuilder -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.IntroductionProcess @@ -44,12 +43,12 @@ import javax.inject.Inject internal class HomeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val scanCardProcessor: ScanCardProcessor, - private val generateWalletNameUseCase: GenerateWalletNameUseCase, private val saveWalletUseCase: SaveWalletUseCase, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsRepository: SettingsRepository, private val urlOpener: UrlOpener, private val analyticsEventHandler: AnalyticsEventHandler, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, ) : Model() { private val tangemErrorHandler = TangemTangemErrorsHandler(store) @@ -100,10 +99,7 @@ internal class HomeModel @Inject constructor( } private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) { - val userWallet = UserWalletBuilder( - scanResponse = scanResponse, - generateWalletNameUseCase = generateWalletNameUseCase, - ).build() + val userWallet = userWalletBuilderFactory.create(scanResponse = scanResponse).build() if (userWallet == null) { Timber.e("User wallet not created") diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index b16af4eab3..ccae0217e2 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -10,7 +10,6 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.extensions.dispatchNavigationAction @@ -91,8 +90,9 @@ private suspend fun readCard() { } private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch { - val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) - val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase).build().guard { + val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory).create(scanResponse) + + val userWallet = userWalletBuilder.build().guard { Timber.e("User wallet not created") return@launch } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 2d53df4f9b..907f45bf6a 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -14,7 +14,6 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.visa.model.VisaCardActivationStatus -import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter @@ -255,8 +254,9 @@ object OnboardingHelper { hasBackupError: Boolean, alreadyCreatedWallet: UserWallet? = null, ) { - val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) - val userWallet = alreadyCreatedWallet ?: UserWalletBuilder(scanResponse, walletNameGenerateUseCase) + val userWalletBuilderFactory = store.inject(DaggerGraphState::userWalletBuilderFactory) + + val userWallet = alreadyCreatedWallet ?: userWalletBuilderFactory.create(scanResponse) .hasBackupError(hasBackupError) .backupCardsIds(backupCardsIds?.toSet()) .build() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index df9ee2e6b7..fe30b20603 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -24,7 +24,6 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.Artwork @@ -782,9 +781,10 @@ private suspend fun updateWallet( } private suspend fun createUserWallet(scanResponse: ScanResponse, backupState: BackupState): UserWallet { - val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) + val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory).create(scanResponse) + return requireNotNull( - value = UserWalletBuilder(scanResponse, walletNameGenerateUseCase) + value = userWalletBuilder .backupCardsIds(backupState.backupCardIds.toSet()) .hasBackupError(backupState.hasBackupError) .build(), diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 02fb7c271c..d8f15fd3d7 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -6,7 +6,6 @@ import com.tangem.common.extensions.guard import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent -import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Onboarding @@ -70,8 +69,10 @@ internal class SaveWalletMiddleware { scope.launch { val backupInfo = state.backupInfo ?: error("Backup info is null") - val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) - val userWallet = UserWalletBuilder(backupInfo.scanResponse, walletNameGenerateUseCase) + val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory) + .create(scanResponse = backupInfo.scanResponse) + + val userWallet = userWalletBuilder .backupCardsIds(state.backupInfo.backupCardsIds) .hasBackupError(hasBackupError) .build() diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index b9f8660cf1..f03175888a 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -11,10 +11,8 @@ import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic - import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.legacy.unlockIfLockable import com.tangem.tap.backupService @@ -118,9 +116,9 @@ internal class WelcomeMiddleware { ) scanCardInternal { scanResponse -> - val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) - val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase).build() - ?: return@scanCardInternal + val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory).create(scanResponse) + + val userWallet = userWalletBuilder.build() ?: return@scanCardInternal val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.save(userWallet, canOverride = true) diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 4dd2bd2a00..2525bdd3cd 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -26,11 +26,10 @@ import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.operations.attestation.OnlineCardVerifier @@ -57,7 +56,6 @@ data class DaggerGraphState( val generalUserWalletsListManager: UserWalletsListManager? = null, val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null, val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null, - val generateWalletNameUseCase: GenerateWalletNameUseCase? = null, val cardRepository: CardRepository? = null, val settingsRepository: SettingsRepository? = null, val blockchainSDKFactory: BlockchainSDKFactory? = null, @@ -68,7 +66,6 @@ data class DaggerGraphState( val shareManager: ShareManager? = null, val appRouter: AppRouter? = null, val transactionSignerFactory: TransactionSignerFactory? = null, - val getUserCountryUseCase: GetUserCountryUseCase? = null, val onrampFeatureToggles: OnrampFeatureToggles? = null, val environmentConfigStorage: EnvironmentConfigStorage? = null, val onboardingV2FeatureToggles: OnboardingV2FeatureToggles? = null, @@ -79,4 +76,5 @@ data class DaggerGraphState( val settingsManager: SettingsManager? = null, val uiMessageSender: UiMessageSender? = null, val onlineCardVerifier: OnlineCardVerifier? = null, + val userWalletBuilderFactory: UserWalletBuilder.Factory? = null, ) : StateType \ No newline at end of file diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 616cbdcc21..6ca312b904 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -1,6 +1,8 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) id("configuration") } @@ -19,6 +21,7 @@ dependencies { api(projects.domain.core) implementation(projects.domain.legacy) implementation(projects.libs.blockchainSdk) + implementation(projects.libs.tangemSdkApi) implementation(projects.domain.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) @@ -29,4 +32,9 @@ dependencies { implementation(tangemDeps.blockchain) // android-library implementation(tangemDeps.card.core) // endregion + + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // end } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt index ffa69e47ae..dfaec8126c 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt @@ -3,14 +3,17 @@ package com.tangem.domain.wallets.builder import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.domain.wallets.usecase.GetCardImageUseCase +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject -class UserWalletBuilder( - private val scanResponse: ScanResponse, +class UserWalletBuilder @AssistedInject constructor( + @Assisted private val scanResponse: ScanResponse, private val generateWalletNameUseCase: GenerateWalletNameUseCase, - private val getCardImageUseCase: GetCardImageUseCase = GetCardImageUseCase(), + private val getCardImageUseCase: GetCardImageUseCase, ) { private var backupCardsIds: Set = emptySet() private var hasBackupError: Boolean = false @@ -56,4 +59,10 @@ class UserWalletBuilder( } } } + + @AssistedFactory + interface Factory { + + fun create(scanResponse: ScanResponse): UserWalletBuilder + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt index 27281d6c33..b44e440301 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt @@ -10,11 +10,11 @@ import com.tangem.operations.attestation.OnlineCardVerifier /** * Use case for getting card image url * - * @property verifier REST API + * @property verifier online card verifier * [REDACTED_AUTHOR] */ -class GetCardImageUseCase(private val verifier: OnlineCardVerifier = OnlineCardVerifier()) { +class GetCardImageUseCase(private val verifier: OnlineCardVerifier) { /** * Get card image url diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt index 6badebac2d..570693940c 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -23,7 +23,6 @@ import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.SaveWalletError import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase import com.tangem.features.details.impl.R @@ -38,7 +37,7 @@ import kotlin.coroutines.resume internal class UserWalletSaver @Inject constructor( private val scanCardProcessor: ScanCardProcessor, private val saveWalletUseCase: SaveWalletUseCase, - private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, private val shouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, private val reduxStateHolder: ReduxStateHolder, private val messageSender: UiMessageSender, @@ -56,17 +55,20 @@ internal class UserWalletSaver @Inject constructor( } }, ifRight = { scanResponse -> - recover(block = { - scanResponse ?: return - val userWallet = createUserWallet(scanResponse) - saveWallet(userWallet) - }, recover = { + recover( + block = { + scanResponse ?: return + val userWallet = createUserWallet(scanResponse) + saveWallet(userWallet) + }, + recover = { val message = it.message if (!message.isNullOrEmpty()) { messageSender.send(SnackbarMessage(message)) } - },) + }, + ) }, ) } @@ -110,7 +112,7 @@ internal class UserWalletSaver @Inject constructor( } private suspend fun Raise.createUserWallet(response: ScanResponse): UserWallet { - val userWallet = UserWalletBuilder(response, generateWalletNameUseCase).build() + val userWallet = userWalletBuilderFactory.create(scanResponse = response).build() return ensureNotNull(userWallet) { Error.Unknown } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt index c460852063..2bf7b82cbd 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt @@ -10,7 +10,6 @@ import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams @@ -25,7 +24,7 @@ import javax.inject.Inject internal class Wallet1ChooseOptionModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, - private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, private val cardRepository: CardRepository, private val saveWalletUseCase: SaveWalletUseCase, private val analyticsHandler: AnalyticsEventHandler, @@ -67,7 +66,7 @@ internal class Wallet1ChooseOptionModel @Inject constructor( private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet { return requireNotNull( - value = UserWalletBuilder(scanResponse, generateWalletNameUseCase).build(), + value = userWalletBuilderFactory.create(scanResponse = scanResponse).build(), lazyMessage = { "User wallet not created" }, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt index 235a5a45ff..7304aefcae 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt @@ -15,7 +15,6 @@ import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent @@ -43,7 +42,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor( private val getCardInfoUseCase: GetCardInfoUseCase, private val cardRepository: CardRepository, private val analyticsHandler: AnalyticsEventHandler, - private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, private val saveWalletUseCase: SaveWalletUseCase, ) : Model() { @@ -146,7 +145,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor( private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet { return requireNotNull( - value = UserWalletBuilder(scanResponse, generateWalletNameUseCase).build(), + value = userWalletBuilderFactory.create(scanResponse = scanResponse).build(), lazyMessage = { "User wallet not created" }, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index a84ab61103..77270eb8a2 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -19,7 +19,6 @@ import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams @@ -47,7 +46,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( private val tangemSdkManager: TangemSdkManager, private val getCardInfoUseCase: GetCardInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, - private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, private val userWalletsListManager: UserWalletsListManager, private val cardRepository: CardRepository, private val onboardingRepository: OnboardingRepository, @@ -276,7 +275,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet { return requireNotNull( - value = UserWalletBuilder(scanResponse, generateWalletNameUseCase) + value = userWalletBuilderFactory.create(scanResponse = scanResponse) .backupCardsIds(backupCardIds.toSet()) .hasBackupError(walletHasBackupError) .build(), diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt index 8ca4e7faf4..a41c53f68c 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt @@ -19,21 +19,25 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMul import com.tangem.operations.backup.BackupService import com.tangem.sdk.api.BackupServiceHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class OnboardingMultiWalletModel @Inject constructor( paramsContainer: ParamsContainer, + analyticsHandler: AnalyticsEventHandler, override val dispatchers: CoroutineDispatcherProvider, private val router: Router, - private val analyticsHandler: AnalyticsEventHandler, private val backupServiceHolder: BackupServiceHolder, private val appPreferencesStore: AppPreferencesStore, + private val getCardImageUseCase: GetCardImageUseCase, ) : Model() { private val params = paramsContainer.require() - private val getCardImageUseCase = GetCardImageUseCase() private val _uiState = MutableStateFlow(OnboardingMultiWalletUM()) val state = MutableStateFlow( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt index 3d79bce2c1..57f424606e 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -14,7 +14,6 @@ import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Config import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Params import com.tangem.features.onboarding.v2.visa.impl.route.OnboardingVisaRoute @@ -35,7 +34,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( private val visaAuthRepository: VisaAuthRepository, private val visaAuthTokenStorage: VisaAuthTokenStorage, private val otpStorage: VisaOTPStorage, - private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, private val userWalletsListManager: UserWalletsListManager, ) : Model() { @@ -110,9 +109,8 @@ internal class OnboardingVisaInProgressModel @Inject constructor( val newActivationStatus = VisaCardActivationStatus.Activated(visaAuthTokens = authTokens) requireNotNull( - value = UserWalletBuilder( - scanResponse.copy(visaCardActivationStatus = newActivationStatus), - generateWalletNameUseCase, + value = userWalletBuilderFactory.create( + scanResponse = scanResponse.copy(visaCardActivationStatus = newActivationStatus), ).build(), lazyMessage = { "User wallet not created" }, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt index 5f5f95375e..3e590ec818 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt @@ -14,7 +14,6 @@ import com.tangem.domain.visa.model.VisaCardId import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.visa.impl.child.pincode.OnboardingVisaPinCodeComponent import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -32,7 +31,7 @@ import javax.inject.Inject internal class OnboardingVisaPinCodeModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, - private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, private val userWalletsListManager: UserWalletsListManager, private val authTokenStorage: VisaAuthTokenStorage, private val otpStorage: VisaAuthTokenStorage, @@ -112,9 +111,8 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( ) requireNotNull( - value = UserWalletBuilder( - scanResponse.copy(visaCardActivationStatus = newActivationStatus), - generateWalletNameUseCase, + value = userWalletBuilderFactory.create( + scanResponse = scanResponse.copy(visaCardActivationStatus = newActivationStatus), ).build(), lazyMessage = { "User wallet not created" }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt index a487bd79bd..136f5c811d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt @@ -8,7 +8,6 @@ import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.wallets.builder.UserWalletBuilder -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.SaveWalletUseCase import javax.inject.Inject @@ -16,7 +15,7 @@ import javax.inject.Inject internal class ScanCardToUnlockWalletClickHandler @Inject constructor( private val scanCardProcessor: ScanCardProcessor, private val saveWalletUseCase: SaveWalletUseCase, - private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, ) { private var scanFailsCounter = 0 @@ -34,7 +33,7 @@ internal class ScanCardToUnlockWalletClickHandler @Inject constructor( scanFailsCounter = 0 // If card's public key is null then user wallet will be null - val scannedWallet = UserWalletBuilder(result.data, generateWalletNameUseCase).build() + val scannedWallet = userWalletBuilderFactory.create(scanResponse = result.data).build() ensure(walletId == scannedWallet?.walletId) { ScanCardToUnlockWalletError.WrongCardIsScanned From 48150bcb5df5d2c635eb7595dc657fe7365dd578 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Mar 2025 10:46:43 +0000 Subject: [PATCH 008/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index c191bacee5..e71e1ff89f 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "release-app_5.22-982" +tangemBlockchainSdk = "develop-977" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.22-440" +tangemCardSdk = "develop-441" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From fb2f23c8a9a4cc2742992a48d9c4c69267699a23 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Mar 2025 15:59:22 +0500 Subject: [PATCH 009/138] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 40 +++ .../tap/di/domain/WalletsDomainModule.kt | 14 + .../local/nft/DefaultNFTPersistenceStore.kt | 2 +- .../local/nft/NFTPersistenceStore.kt | 2 +- .../local/nft/NFTPersistenceStoreFactory.kt | 77 +++-- .../nft/converter/NFTSdkAssetConverter.kt | 9 +- .../converter/NFTSdkCollectionConverter.kt | 9 +- .../tangem/data/nft/DefaultNFTRepository.kt | 284 ++++++++++-------- .../data/wallets/DefaultWalletsRepository.kt | 4 + .../domain/nft/models/NFTCollections.kt | 10 + .../domain/nft/FetchNFTCollectionsUseCase.kt | 16 + .../domain/nft/GetNFTCollectionsUseCase.kt | 25 ++ .../wallets/repository/WalletsRepository.kt | 2 + features/nft/impl/build.gradle.kts | 3 + features/wallet/impl/build.gradle.kts | 3 + .../wallet/child/wallet/model/WalletModel.kt | 35 ++- .../model/WalletsUpdateActionResolver.kt | 5 +- .../model/intents/WalletClickIntents.kt | 8 + .../common/preview/WalletScreenPreviewData.kt | 6 + .../domain/IsWalletNFTEnabledSyncUseCase.kt | 22 ++ .../implementors/MultiWalletContentLoader.kt | 15 + .../MultiWalletContentLoaderFactory.kt | 9 + .../wallet/state/WalletStateController.kt | 1 + .../wallet/state/model/WalletState.kt | 3 + .../InitializeWalletsTransformer.kt | 4 +- .../ReinitializeWalletTransformer.kt | 8 +- .../RemoveNFTCollectionsTransformer.kt | 23 ++ .../SetNFTCollectionsTransformer.kt | 95 ++++++ .../transformers/UnlockWalletTransformer.kt | 4 +- .../state/utils/WalletLoadingStateFactory.kt | 10 +- .../subscribers/WalletNFTListSubscriber.kt | 50 +++ .../presentation/wallet/ui/WalletScreen.kt | 13 + .../wallet/ui/components/WalletNFTItem.kt | 34 ++- .../MultiCurrencyNFTCollections.kt | 18 ++ gradle/tangem_dependencies.toml | 2 +- 35 files changed, 674 insertions(+), 191 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionsUseCase.kt create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/IsWalletNFTEnabledSyncUseCase.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt new file mode 100644 index 0000000000..039fa951ff --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -0,0 +1,40 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.nft.FetchNFTCollectionsUseCase +import com.tangem.domain.nft.GetNFTCollectionsUseCase +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object NFTDomainModule { + + @Provides + @Singleton + fun providesGetNFTCollectionsUseCase( + currenciesRepository: CurrenciesRepository, + nftRepository: NFTRepository, + ): GetNFTCollectionsUseCase { + return GetNFTCollectionsUseCase( + currenciesRepository = currenciesRepository, + nftRepository = nftRepository, + ) + } + + @Provides + @Singleton + fun providesFetchNFTCollectionsUseCase( + currenciesRepository: CurrenciesRepository, + nftRepository: NFTRepository, + ): FetchNFTCollectionsUseCase { + return FetchNFTCollectionsUseCase( + currenciesRepository = currenciesRepository, + nftRepository = nftRepository, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 9ede57293c..9be71f685e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -10,8 +10,10 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* +import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase import com.tangem.operations.attestation.OnlineCardVerifier +import com.tangem.features.nft.NFTFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -173,4 +175,16 @@ internal object WalletsDomainModule { fun provideGetCardImageUseCase(onlineCardVerifier: OnlineCardVerifier): GetCardImageUseCase { return GetCardImageUseCase(verifier = onlineCardVerifier) } + + @Provides + @Singleton + fun providesIsWalletNFTEnabledSyncUseCase( + walletsRepository: WalletsRepository, + nftFeatureToggles: NFTFeatureToggles, + ): IsWalletNFTEnabledSyncUseCase { + return IsWalletNFTEnabledSyncUseCase( + walletsRepository = walletsRepository, + nftFeatureToggles = nftFeatureToggles, + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt index 43d82aaa8d..a418b31a5d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt @@ -12,7 +12,7 @@ internal class DefaultNFTPersistenceStore( private val pricesPersistenceStore: DataStore>, ) : NFTPersistenceStore { - override fun getCollections(): Flow> = collectionsPersistenceStore.data + override fun getCollections(): Flow?> = collectionsPersistenceStore.data override suspend fun getCollectionsSync(): List? = collectionsPersistenceStore .data diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStore.kt index ecd7275a6a..3a6b27d9f3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStore.kt @@ -5,7 +5,7 @@ import com.tangem.blockchain.nft.models.NFTCollection import kotlinx.coroutines.flow.Flow interface NFTPersistenceStore { - fun getCollections(): Flow> + fun getCollections(): Flow?> suspend fun getCollectionsSync(): List? diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt index 49d0ce8415..cb2e6399bf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.local.nft import android.content.Context +import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi @@ -11,10 +12,12 @@ import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.listTypes import com.tangem.datasource.utils.mapWithCustomKeyTypes import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob +import java.lang.reflect.ParameterizedType import javax.inject.Inject import javax.inject.Singleton @@ -25,41 +28,51 @@ class NFTPersistenceStoreFactory @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, ) { - fun provide(network: Network): NFTPersistenceStore { - val networkStringIdentifier = listOfNotNull( - network.id.value, - network.derivationPath.value, - ).joinToString("_") { - // remove all non-alphanumeric characters - it - .toCharArray() - .filter(Char::isLetterOrDigit) - .joinToString("") - .lowercase() - } + fun provide(userWalletId: UserWalletId, network: Network): NFTPersistenceStore { + // simplify network identifier so that is correct for a file name + // e.g. eth_m4460000 or theopennetwork_m446070 + val networkStringId = + network.id.formatted() + network.derivationPath.formatted()?.let { "_$it" }.orEmpty() + // simplify user wallet id so that is correct for a file name + // e.g. 9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a + val userWalletStringId = userWalletId.formatted() return DefaultNFTPersistenceStore( - collectionsPersistenceStore = DataStoreFactory.create( - serializer = MoshiDataStoreSerializer( - moshi = moshi, - types = listTypes(), - defaultValue = emptyList(), - ), - produceFile = { - context.dataStoreFile(fileName = "nft_${networkStringIdentifier}_collections") - }, - scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + collectionsPersistenceStore = createPersistenceStore( + // result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_eth_m4460000_collections + // result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_collections + fileName = "nft_${userWalletStringId}_${networkStringId}_collections", + types = listTypes(), + defaultValue = emptyList(), ), - pricesPersistenceStore = DataStoreFactory.create( - serializer = MoshiDataStoreSerializer( - moshi = moshi, - types = mapWithCustomKeyTypes(), - defaultValue = emptyMap(), - ), - produceFile = { - context.dataStoreFile(fileName = "nft_${networkStringIdentifier}_prices") - }, - scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + pricesPersistenceStore = createPersistenceStore( + // result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_eth_m4460000_prices + // result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_prices + fileName = "nft_${userWalletStringId}_${networkStringId}_prices", + types = mapWithCustomKeyTypes(), + defaultValue = emptyMap(), ), ) } + + private fun createPersistenceStore(fileName: String, types: ParameterizedType, defaultValue: T): DataStore = + DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = types, + defaultValue = defaultValue, + ), + produceFile = { context.dataStoreFile(fileName = fileName) }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ) + + private fun Network.ID.formatted(): String = value + .filter(Char::isLetterOrDigit) + .lowercase() + + private fun Network.DerivationPath.formatted(): String? = value + ?.filter(Char::isLetterOrDigit) + ?.lowercase() + + private fun UserWalletId.formatted(): String = stringValue + .lowercase() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt index ce67e88dd6..add09e2c0a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt @@ -7,14 +7,11 @@ import com.tangem.domain.tokens.model.Network import com.tangem.utils.converter.Converter import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset -class NFTSdkAssetConverter( - private val nftSdkAssetIdentifierConverter: NFTSdkAssetIdentifierConverter, - private val nftSdkCollectionIdentifierConverter: NFTSdkCollectionIdentifierConverter, -) : Converter, NFTAsset> { +object NFTSdkAssetConverter : Converter, NFTAsset> { override fun convert(value: Pair): NFTAsset { val (network, asset) = value - val assetId = nftSdkAssetIdentifierConverter.convert(asset.identifier) - val collectionId = nftSdkCollectionIdentifierConverter.convert(asset.collectionIdentifier) + val assetId = NFTSdkAssetIdentifierConverter.convert(asset.identifier) + val collectionId = NFTSdkCollectionIdentifierConverter.convert(asset.collectionIdentifier) return NFTAsset.Value( id = assetId, collectionId = collectionId, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt index 7b1be28a4e..ed9a5ed3ec 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt @@ -6,13 +6,10 @@ import com.tangem.domain.tokens.model.Network import com.tangem.utils.converter.Converter import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection -class NFTSdkCollectionConverter( - private val nftSdkCollectionIdentifierConverter: NFTSdkCollectionIdentifierConverter, - private val nftSdkAssetConverter: NFTSdkAssetConverter, -) : Converter, NFTCollection> { +object NFTSdkCollectionConverter : Converter, NFTCollection> { override fun convert(value: Pair): NFTCollection { val (network, collection) = value - val collectionId = nftSdkCollectionIdentifierConverter.convert(collection.identifier) + val collectionId = NFTSdkCollectionIdentifierConverter.convert(collection.identifier) return NFTCollection( id = collectionId, network = network, @@ -22,7 +19,7 @@ class NFTSdkCollectionConverter( count = collection.count, assets = collection.assets .map { asset -> - nftSdkAssetConverter.convert(network to asset) + NFTSdkAssetConverter.convert(network to asset) } .filter { it.id !is NFTAsset.Identifier.Unknown diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index f0c4c72fea..5a89fbe57f 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -1,14 +1,14 @@ package com.tangem.data.nft import arrow.core.Either +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.datasource.local.nft.NFTPersistenceStore import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory import com.tangem.datasource.local.nft.NFTRuntimeStore import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory -import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter import com.tangem.datasource.local.nft.converter.NFTSdkAssetIdentifierConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter -import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter import com.tangem.domain.models.StatusSource import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollections @@ -20,8 +20,10 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn -import kotlinx.coroutines.* +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch import javax.inject.Inject import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection @@ -34,82 +36,82 @@ internal class DefaultNFTRepository @Inject constructor( private val jobs = mutableMapOf() - private val nftRuntimeStores = mutableMapOf() - private val nftPersistenceStores = mutableMapOf() - - private val nftSdkAssetConverter = NFTSdkAssetConverter( - nftSdkAssetIdentifierConverter = NFTSdkAssetIdentifierConverter, - nftSdkCollectionIdentifierConverter = NFTSdkCollectionIdentifierConverter, - ) - private val collectionConverter = NFTSdkCollectionConverter( - nftSdkCollectionIdentifierConverter = NFTSdkCollectionIdentifierConverter, - nftSdkAssetConverter = nftSdkAssetConverter, - ) + private val nftRuntimeStores = mutableMapOf() + private val nftPersistenceStores = mutableMapOf() override fun observeCollections(userWalletId: UserWalletId, networks: List): Flow> = - flow { - emitAll( - combine( - networks.map { getNFTRuntimeStore(it).getCollections() }, - ) { it.asList() }, - ) - }.onStart { - refreshCollections(userWalletId, networks) - } + flow { emitAll(observeCollectionsInternal(userWalletId, networks)) } + + private suspend fun observeCollectionsInternal( + userWalletId: UserWalletId, + networks: List, + ): Flow> = combine( + networks.map { observeCollectionsInternal(userWalletId, it) }, + ) { it.asList() } + + private suspend fun observeCollectionsInternal( + userWalletId: UserWalletId, + network: Network, + ): Flow = if (network.canHandleNFTs()) { + getNFTRuntimeStore(userWalletId, network).getCollections() + } else { + flowOf(NFTCollections.empty(network)) + } override suspend fun refreshCollections(userWalletId: UserWalletId, networks: List) = coroutineScope { - networks.map { network -> - launch(dispatchers.io) { - Either.catch { - expireCollections(network) - walletManagersFacade.getNFTCollections(userWalletId, network) - }.onLeft { - saveFailedStateInRuntime( - network = network, - error = it, - ) - }.onRight { - val mergedCollections = it.mergeWithStoredAssets(network) + networks.mapNotNull { network -> + if (network.canHandleNFTs()) { + launch(dispatchers.io) { + Either.catch { + expireCollections(userWalletId, network) + walletManagersFacade.getNFTCollections(userWalletId, network) + }.onLeft { + saveFailedStateInRuntime( + userWalletId = userWalletId, + network = network, + error = it, + ) + }.onRight { + val mergedCollections = it.mergeWithStoredAssets(userWalletId, network) - saveCollectionsInRuntime( - network = network, - collections = mergedCollections, - ) - saveCollectionsInPersistence( - network = network, - collections = mergedCollections, - ) - } - }.saveIn(getJobHolder(network)) + saveCollectionsInRuntime( + userWalletId = userWalletId, + network = network, + collections = mergedCollections, + ) + saveCollectionsInPersistence( + userWalletId = userWalletId, + network = network, + collections = mergedCollections, + ) + } + }.saveIn(getJobHolder(network)) + } else { + null + } }.joinAll() } - private suspend fun expireCollections(network: Network) { - val runtimeStore = getNFTRuntimeStore(network) - + private suspend fun expireCollections(userWalletId: UserWalletId, network: Network) { + val runtimeStore = getNFTRuntimeStore(userWalletId, network) val expiredCollections = runtimeStore .getCollectionsSync() - .let { collections -> - collections.copy( - content = when (val content = collections.content) { - is NFTCollections.Content.Collections -> content.copy( - source = StatusSource.CACHE, - ) - is NFTCollections.Content.Error -> content - }, - ) - } + .changeStatusSource(StatusSource.CACHE) runtimeStore.saveCollections(expiredCollections) } - private suspend fun saveCollectionsInRuntime(network: Network, collections: List) { - getNFTRuntimeStore(network).saveCollections( + private suspend fun saveCollectionsInRuntime( + userWalletId: UserWalletId, + network: Network, + collections: List, + ) { + getNFTRuntimeStore(userWalletId, network).saveCollections( NFTCollections( network = network, content = NFTCollections.Content.Collections( collections = collections .map { collection -> - collectionConverter.convert(network to collection) + NFTSdkCollectionConverter.convert(network to collection) } .filter { it.id !is NFTCollection.Identifier.Unknown @@ -120,19 +122,30 @@ internal class DefaultNFTRepository @Inject constructor( ) } - private suspend fun saveFailedStateInRuntime(network: Network, error: Throwable) { - getNFTRuntimeStore(network).saveCollections( - NFTCollections( - network = network, - content = NFTCollections.Content.Error( - error = error, - ), - ), - ) + private suspend fun saveFailedStateInRuntime(userWalletId: UserWalletId, network: Network, error: Throwable) { + getNFTRuntimeStore(userWalletId, network).let { store -> + val storedCollections = store.getCollectionsSync() + val content = storedCollections.content + val updatedCollections = if (content is NFTCollections.Content.Collections && content.collections != null) { + // if there is any cached collections in store, then mark them as not actual and emit anyway + storedCollections.changeStatusSource(StatusSource.ONLY_CACHE) + } else { + // otherwise, just emit an error + NFTCollections( + network = network, + content = NFTCollections.Content.Error(error), + ) + } + store.saveCollections(updatedCollections) + } } - private suspend fun saveCollectionsInPersistence(network: Network, collections: List) { - getNFTPersistenceStore(network).saveCollections(collections) + private suspend fun saveCollectionsInPersistence( + userWalletId: UserWalletId, + network: Network, + collections: List, + ) { + getNFTPersistenceStore(userWalletId, network).saveCollections(collections) } private fun getJobHolder(network: Network): JobHolder = jobs[network] ?: run { @@ -141,65 +154,85 @@ internal class DefaultNFTRepository @Inject constructor( } } - private fun getNFTPersistenceStore(network: Network): NFTPersistenceStore = nftPersistenceStores[network] ?: run { - nftPersistenceStoreFactory.provide(network).also { - nftPersistenceStores[network] = it + private fun getNFTPersistenceStore(userWalletId: UserWalletId, network: Network): NFTPersistenceStore { + val storeId = (userWalletId to network).formatted() + return nftPersistenceStores[storeId] ?: run { + nftPersistenceStoreFactory.provide(userWalletId, network).also { + nftPersistenceStores[storeId] = it + } } } - private suspend fun getNFTRuntimeStore(network: Network): NFTRuntimeStore = nftRuntimeStores[network] ?: run { - nftRuntimeStoreFactory.provide(network).also { - nftRuntimeStores[network] = it - val storedCollections = getStoredCollections(network) - val storedPrices = getStoredPrices(network) - it.initialize( - collections = storedCollections, - prices = storedPrices, - ) + private suspend fun getNFTRuntimeStore(userWalletId: UserWalletId, network: Network): NFTRuntimeStore { + val storeId = (userWalletId to network).formatted() + return nftRuntimeStores[storeId] ?: run { + nftRuntimeStoreFactory.provide(network).also { + nftRuntimeStores[storeId] = it + val storedCollections = getStoredCollections(userWalletId, network) + val storedPrices = getStoredPrices(userWalletId, network) + it.initialize( + collections = storedCollections, + prices = storedPrices, + ) + } } } - private suspend fun getStoredCollections(network: Network) = getNFTPersistenceStore(network) - .getCollectionsSync() - .let { - NFTCollections( - network = network, - content = NFTCollections.Content.Collections( - collections = it - ?.map { collection -> - collectionConverter.convert(network to collection) - } - ?.filter { - it.id !is NFTCollection.Identifier.Unknown - }, - source = StatusSource.CACHE, - ), - ) - } - - private suspend fun getStoredPrices(network: Network) = getNFTPersistenceStore(network) - .getSalePricesSync() - .orEmpty() - .let { prices -> - prices - .mapKeys { - val (assetId, _) = it - NFTSdkAssetIdentifierConverter.convert(assetId) - } - .mapValues { - val (assetId, price) = it - NFTSalePrice.Value( - assetId = assetId, - value = price.value, - symbol = price.symbol, + private suspend fun getStoredCollections(userWalletId: UserWalletId, network: Network) = + getNFTPersistenceStore(userWalletId, network) + .getCollectionsSync() + .let { + NFTCollections( + network = network, + content = NFTCollections.Content.Collections( + collections = it + ?.map { collection -> + NFTSdkCollectionConverter.convert(network to collection) + } + ?.filter { + it.id !is NFTCollection.Identifier.Unknown + }, source = StatusSource.CACHE, - ) - } - } + ), + ) + } - private suspend fun List.mergeWithStoredAssets(network: Network): List { + private suspend fun getStoredPrices(userWalletId: UserWalletId, network: Network) = + getNFTPersistenceStore(userWalletId, network) + .getSalePricesSync() + .orEmpty() + .let { prices -> + prices + .mapKeys { + val (assetId, _) = it + NFTSdkAssetIdentifierConverter.convert(assetId) + } + .mapValues { + val (assetId, price) = it + NFTSalePrice.Value( + assetId = assetId, + value = price.value, + symbol = price.symbol, + source = StatusSource.CACHE, + ) + } + } + + private fun NFTCollections.changeStatusSource(source: StatusSource) = copy( + content = when (val content = content) { + is NFTCollections.Content.Collections -> content.copy( + source = source, + ) + is NFTCollections.Content.Error -> content + }, + ) + + private suspend fun List.mergeWithStoredAssets( + userWalletId: UserWalletId, + network: Network, + ): List { val storedCollections = - getNFTPersistenceStore(network) + getNFTPersistenceStore(userWalletId, network) .getCollectionsSync() .orEmpty() .associateBy(SdkNFTCollection::identifier) @@ -224,4 +257,11 @@ internal class DefaultNFTRepository @Inject constructor( } } } + + private fun Pair.formatted(): String { + val (walletId, network) = this + return walletId.stringValue + "_" + network.id.value + "_" + network.derivationPath.value + } + + private fun Network.canHandleNFTs(): Boolean = Blockchain.fromNetworkId(backendId)?.canHandleNFTs() == true } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index e17b2c880c..35bc64c215 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -208,6 +208,10 @@ internal class DefaultWalletsRepository( .getObjectMap(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY) .map { it[userWalletId.stringValue] == true } + override fun nftEnabledStatuses(): Flow> = appPreferencesStore + .getObjectMap(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY) + .map { it.mapKeys { UserWalletId(it.key) } } + override suspend fun enableNFT(userWalletId: UserWalletId) { appPreferencesStore.editData { it.setObjectMap( diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollections.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollections.kt index b4e4accc54..bd97bf4cfe 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollections.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollections.kt @@ -17,4 +17,14 @@ data class NFTCollections( val error: Throwable, ) : Content() } + + companion object { + fun empty(network: Network) = NFTCollections( + network = network, + content = Content.Collections( + collections = null, + source = StatusSource.ACTUAL, + ), + ) + } } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionsUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionsUseCase.kt new file mode 100644 index 0000000000..e8ecd813c0 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionsUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.nft + +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId + +class FetchNFTCollectionsUseCase( + private val currenciesRepository: CurrenciesRepository, + private val nftRepository: NFTRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId) { + val currencies = currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) + nftRepository.refreshCollections(userWalletId, currencies.map { it.network }.distinct()) + } +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt new file mode 100644 index 0000000000..f59d8578b6 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.nft + +import com.tangem.domain.nft.models.NFTCollections +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest + +class GetNFTCollectionsUseCase( + private val currenciesRepository: CurrenciesRepository, + private val nftRepository: NFTRepository, +) { + + @OptIn(ExperimentalCoroutinesApi::class) + fun launch(userWalletId: UserWalletId): Flow> = currenciesRepository + .getWalletCurrenciesUpdates(userWalletId) + .flatMapLatest { + val networks = it + .map { cryptoCurrency -> cryptoCurrency.network } + .distinct() + nftRepository.observeCollections(userWalletId, networks) + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index b1e526ce5c..6ec22a4eaf 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -32,6 +32,8 @@ interface WalletsRepository { fun nftEnabledStatus(userWalletId: UserWalletId): Flow + fun nftEnabledStatuses(): Flow> + suspend fun enableNFT(userWalletId: UserWalletId) suspend fun disableNFT(userWalletId: UserWalletId) diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index 40d3b149c9..7c051fa34e 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -26,6 +26,9 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.datasource) + /** Domain modules */ + implementation(projects.domain.nft.models) + /** Common */ implementation(projects.common.ui) implementation(projects.common.routing) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index f786056d52..8416888824 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -90,6 +90,8 @@ dependencies { implementation(projects.domain.onramp) implementation(projects.domain.promo) implementation(projects.domain.promo.models) + implementation(projects.domain.nft) + implementation(projects.domain.nft.models) //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) @@ -108,6 +110,7 @@ dependencies { implementation(projects.features.wallet.api) implementation(projects.features.walletSettings.api) implementation(projects.features.biometry.api) + implementation(projects.features.nft.api) /** Common modules */ implementation(projects.common) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 590d0d0bec..7848653a92 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -10,9 +10,11 @@ import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.nft.FetchNFTCollectionsUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -41,6 +43,7 @@ import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.* +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -76,6 +79,8 @@ internal class WalletModel @Inject constructor( private val walletFeatureToggles: WalletFeatureToggles, private val biometryFeatureToggles: BiometryFeatureToggles, private val analyticsEventsHandler: AnalyticsEventHandler, + private val fetchNFTCollectionsUseCase: FetchNFTCollectionsUseCase, + private val walletsRepository: WalletsRepository, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -86,6 +91,7 @@ internal class WalletModel @Inject constructor( private val walletsUpdateJobHolder = JobHolder() private val refreshWalletJobHolder = JobHolder() private val expressStatusJobHolder = JobHolder() + private val walletsNFTsUpdateJobHolder = JobHolder() private var needToRefreshWallet = false private var expressTxStatusTaskScheduler = SingleTaskScheduler() @@ -103,6 +109,7 @@ internal class WalletModel @Inject constructor( subscribeToScreenBackgroundState() subscribeOnPushNotificationsPermission() subscribeOnExpressTransactionsUpdates() + subscribeOnNFTUpdates() clickIntents.initialize(innerWalletRouter, modelScope) } @@ -160,7 +167,10 @@ internal class WalletModel @Inject constructor( .conflate() .distinctUntilChanged() .map { - walletsUpdateActionResolver.resolve(wallets = it, currentState = stateHolder.value) + walletsUpdateActionResolver.resolve( + wallets = it, + currentState = stateHolder.value, + ) } .onEach(::updateWallets) .flowOn(dispatchers.main) @@ -256,6 +266,29 @@ internal class WalletModel @Inject constructor( }.saveIn(expressStatusJobHolder) } + @OptIn(ExperimentalCoroutinesApi::class) + private fun subscribeOnNFTUpdates() { + getWalletsUseCase() + .conflate() + .distinctUntilChanged() + .flatMapLatest { wallets -> + wallets + .map { wallet -> + walletsRepository.nftEnabledStatus(wallet.walletId) + .distinctUntilChanged() + .onEach { nftEnabled -> + if (nftEnabled) { + fetchNFTCollectionsUseCase.invoke(wallet.walletId) + } + } + } + .merge() + } + .flowOn(dispatchers.main) + .launchIn(modelScope) + .saveIn(walletsNFTsUpdateJobHolder) + } + private fun needToRefreshTimer() { modelScope.launch { delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt index 5594553aab..35cf3a44cd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt @@ -231,7 +231,10 @@ internal class WalletsUpdateActionResolver @Inject constructor( * @property prevWalletId previous selected wallet id * @property selectedWallet selected wallet */ - data class ReinitializeWallet(val prevWalletId: UserWalletId, val selectedWallet: UserWallet) : Action() { + data class ReinitializeWallet( + val prevWalletId: UserWalletId, + val selectedWallet: UserWallet, + ) : Action() { override fun toString(): String { return "ReinitializeWallet(prevWalletId = $prevWalletId, selectedWallet = ${selectedWallet.walletId})" diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index e84eccf060..a78fac8554 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -5,6 +5,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.nft.FetchNFTCollectionsUseCase import com.tangem.domain.onramp.FetchHotCryptoUseCase import com.tangem.domain.settings.NeverToShowWalletsScrollPreview import com.tangem.domain.tokens.FetchCardTokenListUseCase @@ -14,6 +15,7 @@ import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -50,6 +52,8 @@ internal class WalletClickIntents @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val onrampFeatureToggles: OnrampFeatureToggles, private val fetchHotCryptoUseCase: FetchHotCryptoUseCase, + private val fetchNFTCollectionsUseCase: FetchNFTCollectionsUseCase, + private val isWalletNFTEnabledSyncUseCase: IsWalletNFTEnabledSyncUseCase, ) : BaseWalletClickIntents(), WalletCardClickIntents by walletCardClickIntentsImplementor, WalletWarningsClickIntents by warningsClickIntentsImplementer, @@ -140,6 +144,10 @@ internal class WalletClickIntents @Inject constructor( if (onrampFeatureToggles.isHotTokensEnabled) { async { fetchHotCryptoUseCase() }.let(::add) } + + if (isWalletNFTEnabledSyncUseCase.invoke(userWallet.walletId)) { + async { fetchNFTCollectionsUseCase(userWalletId = userWallet.walletId) }.let(::add) + } } .awaitAll() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index 4f8cb9bdfd..b8cf556b05 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -120,6 +120,12 @@ internal object WalletScreenPreviewData { ), bottomSheetConfig = null, tokensListState = textContentTokensState, + nftState = WalletNFTItemUM.Content( + previews = persistentListOf(WalletNFTItemUM.Content.CollectionPreview.Image("img1")), + collectionsCount = 1, + assetsCount = 3, + isFlickering = false, + ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/IsWalletNFTEnabledSyncUseCase.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/IsWalletNFTEnabledSyncUseCase.kt new file mode 100644 index 0000000000..e931183ffe --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/IsWalletNFTEnabledSyncUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.features.nft.NFTFeatureToggles +import kotlinx.coroutines.flow.firstOrNull + +class IsWalletNFTEnabledSyncUseCase( + private val walletsRepository: WalletsRepository, + private val nftFeatureToggles: NFTFeatureToggles, +) { + + suspend operator fun invoke(userWalletId: UserWalletId): Boolean = if (nftFeatureToggles.isNFTEnabled) { + walletsRepository + .nftEnabledStatuses() + .firstOrNull() + ?.let { it[userWalletId] } + ?: false + } else { + false + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 402b03dec0..c69025ad0a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -3,10 +3,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -21,6 +23,7 @@ import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletActi import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletTokenListSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber +import com.tangem.features.nft.NFTFeatureToggles import com.tangem.features.swap.SwapFeatureToggles @Suppress("LongParameterList") @@ -34,6 +37,7 @@ internal class MultiWalletContentLoader( private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, private val walletWithFundsChecker: WalletWithFundsChecker, private val tokenListStore: MultiWalletTokenListStore, + private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, @@ -42,6 +46,8 @@ internal class MultiWalletContentLoader( private val getStoryContentUseCase: GetStoryContentUseCase, private val swapFeatureToggles: SwapFeatureToggles, private val deepLinksRegistry: DeepLinksRegistry, + private val nftFeatureToggles: NFTFeatureToggles, + private val walletsRepository: WalletsRepository, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -58,6 +64,15 @@ internal class MultiWalletContentLoader( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, deepLinksRegistry = deepLinksRegistry, ).let(::add) + if (nftFeatureToggles.isNFTEnabled) { + WalletNFTListSubscriber( + userWallet = userWallet, + getNFTCollectionsUseCase = getNFTCollectionsUseCase, + stateHolder = stateHolder, + walletsRepository = walletsRepository, + clickIntents = clickIntents, + ).let(::add) + } MultiWalletWarningsSubscriber( userWallet = userWallet, stateHolder = stateHolder, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 7bc706075d..f5cb299df3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -3,10 +3,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -16,6 +18,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarnin import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.features.nft.NFTFeatureToggles import com.tangem.features.swap.SwapFeatureToggles import javax.inject.Inject @@ -36,6 +39,9 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val getStoryContentUseCase: GetStoryContentUseCase, private val swapFeatureToggles: SwapFeatureToggles, private val deepLinksRegistry: DeepLinksRegistry, + private val nftFeatureToggles: NFTFeatureToggles, + private val walletsRepository: WalletsRepository, + private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader { @@ -56,6 +62,9 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, swapFeatureToggles = swapFeatureToggles, deepLinksRegistry = deepLinksRegistry, + nftFeatureToggles = nftFeatureToggles, + walletsRepository = walletsRepository, + getNFTCollectionsUseCase = getNFTCollectionsUseCase, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index cf9c56b44c..e9771676b7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.event.consumedEvent import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 786c3ebb5c..cd4ed81df9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -22,6 +22,7 @@ internal sealed interface WalletState : WalletStateHolder { sealed class MultiCurrency : WalletState { abstract val tokensListState: WalletTokensListState + abstract val nftState: WalletNFTItemUM data class Content( override val pullToRefreshConfig: PullToRefreshConfig, @@ -30,6 +31,7 @@ internal sealed interface WalletState : WalletStateHolder { override val warnings: ImmutableList, override val bottomSheetConfig: TangemBottomSheetConfig?, override val tokensListState: WalletTokensListState, + override val nftState: WalletNFTItemUM, ) : MultiCurrency() data class Locked( @@ -46,6 +48,7 @@ internal sealed interface WalletState : WalletStateHolder { ) { override val tokensListState = WalletTokensListState.ContentState.Locked + override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index ac0c59bc42..b22b7ea772 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt @@ -38,7 +38,9 @@ internal class InitializeWalletsTransformer( if (userWallet.isLocked) { createLockedState(userWallet) } else { - walletLoadingStateFactory.create(userWallet) + walletLoadingStateFactory.create( + userWallet = userWallet, + ) } } .toImmutableList(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt index ceecfc9cfc..02d33a5f4c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt @@ -2,10 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.toImmutableList @@ -38,7 +38,11 @@ internal class ReinitializeWalletTransformer( return prevState.copy( wallets = prevState.wallets .filterNot { it.walletCardState.id == prevWalletId } - .plus(element = walletLoadingStateFactory.create(newUserWallet)) + .plus( + element = walletLoadingStateFactory.create( + userWallet = newUserWallet, + ), + ) .toImmutableList(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt new file mode 100644 index 0000000000..b66ed6ba9a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState + +internal class RemoveNFTCollectionsTransformer( + userWalletId: UserWalletId, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState = when (prevState) { + is WalletState.MultiCurrency.Content -> prevState.copy( + nftState = WalletNFTItemUM.Hidden, + ) + is WalletState.SingleCurrency.Content, + is WalletState.Visa.Content, + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + is WalletState.Visa.Locked, + is WalletState.Visa.AccessTokenLocked, + -> prevState + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt new file mode 100644 index 0000000000..f713b64077 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt @@ -0,0 +1,95 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.nft.models.NFTCollections +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import kotlinx.collections.immutable.toPersistentList + +internal class SetNFTCollectionsTransformer( + userWalletId: UserWalletId, + private val nftCollections: List, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState = when (prevState) { + is WalletState.MultiCurrency.Content -> prevState.copy( + nftState = when { + allCollectionsFailed() -> WalletNFTItemUM.Failed + anyCollectionFailed() && allLoadedCollectionsEmpty() -> WalletNFTItemUM.Failed + allCollectionsLoaded() && allCollectionsEmpty() -> WalletNFTItemUM.Empty + !allCollectionsLoaded() && allCollectionsEmpty() -> WalletNFTItemUM.Loading + else -> createContentNFTItemUM() + }, + ) + is WalletState.SingleCurrency.Content, + is WalletState.Visa.Content, + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + is WalletState.Visa.Locked, + is WalletState.Visa.AccessTokenLocked, + -> prevState + } + + private fun createContentNFTItemUM(): WalletNFTItemUM.Content { + val collectionsContent = nftCollections + .map { it.content } + .filterIsInstance() + + val isFlickering = collectionsContent + .map { it.source } + .any { it == StatusSource.CACHE } + + val collections = collectionsContent + .mapNotNull { it.collections } + .flatten() + + return WalletNFTItemUM.Content( + previews = if (collections.size > NFT_COLLECTIONS_MAX_PREVIEWS_COUNT) { + collections + .take(NFT_COLLECTIONS_MAX_PREVIEWS_COUNT - 1) + .map { WalletNFTItemUM.Content.CollectionPreview.Image(it.logoUrl.orEmpty()) } + .plus(WalletNFTItemUM.Content.CollectionPreview.More) + .toPersistentList() + } else { + collections + .take(NFT_COLLECTIONS_MAX_PREVIEWS_COUNT) + .map { WalletNFTItemUM.Content.CollectionPreview.Image(it.logoUrl.orEmpty()) } + .toPersistentList() + }, + collectionsCount = collections.size, + assetsCount = collections + .sumOf { it.count }, + isFlickering = isFlickering, + ) + } + + private fun allCollectionsFailed() = nftCollections.all { + it.content is NFTCollections.Content.Error + } + + private fun anyCollectionFailed() = nftCollections.any { + it.content is NFTCollections.Content.Error + } + + private fun allLoadedCollectionsEmpty() = nftCollections + .map { it.content } + .filterIsInstance() + .all { it.collections.isNullOrEmpty() } + + private fun allCollectionsLoaded() = nftCollections.all { + val content = it.content + content is NFTCollections.Content.Collections && + content.source != StatusSource.CACHE + } + + private fun allCollectionsEmpty() = nftCollections.all { + val content = it.content + content is NFTCollections.Content.Collections && + content.collections.isNullOrEmpty() + } + + companion object { + private const val NFT_COLLECTIONS_MAX_PREVIEWS_COUNT = 4 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt index 61cb2c07b4..847bf806a1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt @@ -46,7 +46,9 @@ internal class UnlockWalletTransformer( is WalletState.MultiCurrency.Locked, is WalletState.SingleCurrency.Locked, is WalletState.Visa.Locked, - -> walletLoadingStateFactory.create(userWallet = unlockedWallet) + -> walletLoadingStateFactory.create( + userWallet = unlockedWallet, + ) is WalletState.MultiCurrency.Content, is WalletState.SingleCurrency.Content, is WalletState.Visa.Content, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 599932a80e..562cbd95c1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -1,14 +1,14 @@ package com.tangem.feature.wallet.presentation.wallet.state.utils +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.* -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -41,6 +41,7 @@ internal class WalletLoadingStateFactory( warnings = persistentListOf(), bottomSheetConfig = null, tokensListState = WalletTokensListState.ContentState.Loading, + nftState = WalletNFTItemUM.Hidden, ) } @@ -80,7 +81,10 @@ internal class WalletLoadingStateFactory( } private fun createPullToRefreshConfig(): PullToRefreshConfig { - return PullToRefreshConfig(onRefresh = { clickIntents.onRefreshSwipe(it.value) }, isRefreshing = false) + return PullToRefreshConfig( + onRefresh = { clickIntents.onRefreshSwipe(it.value) }, + isRefreshing = false, + ) } private fun UserWallet.toLoadingWalletCardState(): WalletCardState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt new file mode 100644 index 0000000000..476770ce55 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt @@ -0,0 +1,50 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.nft.GetNFTCollectionsUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.transformers.RemoveNFTCollectionsTransformer +import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetNFTCollectionsTransformer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* + +@Suppress("UnusedPrivateMember") +internal class WalletNFTListSubscriber( + private val userWallet: UserWallet, + private val stateHolder: WalletStateController, + private val walletsRepository: WalletsRepository, + private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, + clickIntents: WalletClickIntents, +) : WalletSubscriber() { + + @OptIn(ExperimentalCoroutinesApi::class) + override fun create(coroutineScope: CoroutineScope): Flow<*> = walletsRepository + .nftEnabledStatus(userWallet.walletId) + .distinctUntilChanged() + .flatMapLatest { nftEnabled -> + // if NFT is enabled for this wallet, then start observing changes from store and apply transformer if need + if (nftEnabled) { + getNFTCollectionsUseCase + .launch(userWallet.walletId) + .shareIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(), + replay = 1, + ) + .onEach { + stateHolder.update( + SetNFTCollectionsTransformer(userWallet.walletId, it), + ) + } + } else { + // otherwise, hide NFT from wallet + stateHolder.update( + RemoveNFTCollectionsTransformer(userWallet.walletId), + ) + emptyFlow() + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 3a8e681611..ff7362b3c2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -80,6 +80,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsB import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* import com.tangem.feature.wallet.presentation.wallet.ui.components.common.actions +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.nftCollections import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.BalancesAndLimitsBottomSheet @@ -242,6 +243,8 @@ private fun WalletContent( modifier = movableItemModifier, ) + nftCollections(state = selectedWallet, itemModifier = itemModifier) + organizeTokens(state = selectedWallet, itemModifier = itemModifier) } @@ -700,6 +703,16 @@ internal fun LazyListScope.organizeTokens(state: WalletState, itemModifier: Modi } } +internal fun LazyListScope.nftCollections(state: WalletState, itemModifier: Modifier) { + (state as? WalletState.MultiCurrency)?.let { + nftCollections( + modifier = itemModifier, + state = it.nftState, + onClick = {}, + ) + } +} + @Composable private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { if (bottomSheetConfig != null) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt index d31a09e214..850b0d78ab 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt @@ -131,7 +131,20 @@ private fun WalletNFTItemFailed(modifier: Modifier = Modifier) { RowContentContainer( modifier = modifier, icon = { - CollectionsPreviewsPlaceholder() + Box( + modifier = Modifier + .size(TangemTheme.dimens.size36) + .clip(RoundedCornerShape(TangemTheme.dimens.radius8)) + .background(TangemTheme.colors.field.primary), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size20), + painter = painterResource(R.drawable.ic_error_sync_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } }, text = { Text( @@ -158,7 +171,12 @@ private fun WalletNFTItemLoading(modifier: Modifier = Modifier) { RowContentContainer( modifier = modifier, icon = { - CollectionsPreviewsPlaceholder() + Box( + modifier = Modifier + .size(TangemTheme.dimens.size36) + .clip(RoundedCornerShape(TangemTheme.dimens.radius8)) + .background(TangemTheme.colors.field.primary), + ) }, text = { TextShimmer( @@ -178,16 +196,6 @@ private fun WalletNFTItemLoading(modifier: Modifier = Modifier) { ) } -@Composable -private fun CollectionsPreviewsPlaceholder(modifier: Modifier = Modifier) { - Box( - modifier = modifier - .size(TangemTheme.dimens.size36) - .clip(RoundedCornerShape(TangemTheme.dimens.radius8)) - .background(TangemTheme.colors.field.primary), - ) -} - @Composable @Suppress("MagicNumber") private fun BoxScope.CollectionsPreviews(previews: ImmutableList, modifier: Modifier = Modifier) { @@ -207,7 +215,7 @@ private fun BoxScope.CollectionsPreviews(previews: ImmutableList { SubcomposeAsyncImage( modifier = modifier, - model = s, + model = s.url, loading = { RectangleShimmer(radius = 0.dp) }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt new file mode 100644 index 0000000000..da4ce8f1d5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM +import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletNFTItem + +private const val NFT_COLLECTIONS_CONTENT_TYPE = "NFTCollections" + +internal fun LazyListScope.nftCollections(state: WalletNFTItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + item(key = NFT_COLLECTIONS_CONTENT_TYPE, contentType = NFT_COLLECTIONS_CONTENT_TYPE) { + WalletNFTItem( + modifier = modifier, + state = state, + onClick = onClick, + ) + } +} \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index e71e1ff89f..4351692f4c 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,5 +1,5 @@ [versions] -tangemBlockchainSdk = "develop-977" +tangemBlockchainSdk = "develop-984" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-441" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 0b9ab4392f24b8d095651cae965a90fe4a930521 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Mar 2025 15:22:15 +0300 Subject: [PATCH 010/138] Updated on 2026-08-14 --- .../features/onboarding/OnboardingHelper.kt | 7 +++- .../configs/feature_toggles_config.json | 4 +++ .../v2/OnboardingV2FeatureToggles.kt | 1 + .../v2/DefaultOnboardingV2FeatureToggles.kt | 2 ++ .../v2/twin/api/OnboardingTwinComponent.kt | 21 ++++++++++++ .../impl/DefaultOnboardingTwinComponent.kt | 32 ++++++++++++++++++ .../v2/twin/impl/di/ComponentModule.kt | 33 +++++++++++++++++++ .../v2/twin/impl/model/OnboardingTwinModel.kt | 13 ++++++++ 8 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/di/ComponentModule.kt create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 4276465908..4d81a04308 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -69,6 +69,7 @@ object OnboardingHelper { } } + @Suppress("CyclomaticComplexMethod") fun whereToNavigate(scanResponse: ScanResponse): AppRoute { val newOnboardingSupportTypes = scanResponse.productType == ProductType.Wallet2 || scanResponse.productType == ProductType.Ring || @@ -103,7 +104,11 @@ object OnboardingHelper { } else { AppRoute.OnboardingOther } - ProductType.Twins -> AppRoute.OnboardingTwins + ProductType.Twins -> if (featureToggles.isTwinRefactoringEnabled) { + AppRoute.Onboarding(scanResponse = scanResponse) + } else { + AppRoute.OnboardingTwins + } ProductType.Start2Coin, ProductType.Visa, -> throw UnsupportedOperationException("Onboarding for ${type.name} cards is not supported") 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 71052ae476..a3e83e6903 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 @@ -70,5 +70,9 @@ { "name": "NEW_ATTESTATION_ENABLED", "version": "undefined" + }, + { + "name": "TWIN_REFACTORING_ENABLED", + "version": "undefined" } ] diff --git a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt index 1845005df6..dbeb952224 100644 --- a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt +++ b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt @@ -4,4 +4,5 @@ interface OnboardingV2FeatureToggles { val isOnboardingV2Enabled: Boolean val isVisaOnboardingEnabled: Boolean val isNoteRefactoringEnabled: Boolean + val isTwinRefactoringEnabled: Boolean } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt index 41b7796f7a..80bbb18b70 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt @@ -12,4 +12,6 @@ internal class DefaultOnboardingV2FeatureToggles @Inject constructor( get() = featureTogglesManager.isFeatureEnabled("VISA_ONBOARDING_ENABLED") override val isNoteRefactoringEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled("NOTE_REFACTORING_ENABLED") + override val isTwinRefactoringEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("TWIN_REFACTORING_ENABLED") } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt new file mode 100644 index 0000000000..7fe9075114 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt @@ -0,0 +1,21 @@ +package com.tangem.features.onboarding.v2.twin.api + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.features.onboarding.v2.TitleProvider + +interface OnboardingTwinComponent : ComposableContentComponent { + + data class Params( + val titleProvider: TitleProvider, + val scanResponse: ScanResponse, + val modelCallbacks: ModelCallbacks, + ) + + interface ModelCallbacks { + fun onDone() + } + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt new file mode 100644 index 0000000000..402c426566 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt @@ -0,0 +1,32 @@ +package com.tangem.features.onboarding.v2.twin.impl + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent +import com.tangem.features.onboarding.v2.twin.impl.model.OnboardingTwinModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +class DefaultOnboardingTwinComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: OnboardingTwinComponent.Params, +) : OnboardingTwinComponent, AppComponentContext by appComponentContext { + + private val model: OnboardingTwinModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + TODO("Not yet implemented") + } + + @AssistedFactory + interface Factory : OnboardingTwinComponent.Factory { + override fun create( + context: AppComponentContext, + params: OnboardingTwinComponent.Params, + ): DefaultOnboardingTwinComponent + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..41e30634d5 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/di/ComponentModule.kt @@ -0,0 +1,33 @@ +package com.tangem.features.onboarding.v2.twin.impl.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent +import com.tangem.features.onboarding.v2.twin.impl.DefaultOnboardingTwinComponent +import com.tangem.features.onboarding.v2.twin.impl.model.OnboardingTwinModel +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 + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindComponent(factory: DefaultOnboardingTwinComponent.Factory): OnboardingTwinComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(OnboardingTwinModel::class) + fun provideModel(model: OnboardingTwinModel): Model +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt new file mode 100644 index 0000000000..39c6c4aa1a --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -0,0 +1,13 @@ +package com.tangem.features.onboarding.v2.twin.impl.model + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import javax.inject.Inject + +@ModelScoped +class OnboardingTwinModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + // TODO +} \ No newline at end of file From 06d435a58fbd1f937690aa5c69ccfd78bdcb6a32 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Mar 2025 10:04:38 +0400 Subject: [PATCH 011/138] Updated on 2026-08-14 --- .../com/tangem/tap/data/DefaultCardSdkProvider.kt | 6 +++++- .../java/com/tangem/tap/domain/extensions/Card.kt | 7 +++++-- .../tap/features/onboarding/OnboardingManager.kt | 11 ++++++++--- .../wallet/redux/OnboardingWalletMiddleware.kt | 4 +++- gradle/tangem_dependencies.toml | 2 +- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt index a1751aff85..87267d0c59 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt @@ -15,6 +15,7 @@ import com.tangem.crypto.bip39.Wordlist import com.tangem.data.card.sdk.CardSdkOwner import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.sdk.DefaultSessionViewDelegate +import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles import com.tangem.sdk.extensions.* import com.tangem.sdk.nfc.AndroidNfcAvailabilityProvider import com.tangem.sdk.nfc.NfcManager @@ -34,6 +35,7 @@ import javax.inject.Singleton internal class DefaultCardSdkProvider @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, + private val cardSdkFeatureToggles: CardSdkFeatureToggles, ) : CardSdkProvider, CardSdkOwner { private val observer = Observer() @@ -108,7 +110,9 @@ internal class DefaultCardSdkProvider @Inject constructor( authenticationManager = authenticationManager, keystoreManager = keystoreManager, wordlist = Wordlist.getWordlist(activity), - config = config, + config = config.apply { + isNewOnlineAttestationEnabled = cardSdkFeatureToggles.isNewAttestationEnabled + }, ) holder = Holder( diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt index 52fadf58cc..7117819925 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt @@ -13,7 +13,10 @@ fun CardDTO.signedHashesCount(): Int { return wallets.sumOf { it.totalSignedHashes ?: 0 } } -suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result? = null): String { +suspend fun CardDTO.getOrLoadCardArtworkUrl( + cardInfo: Result? = null, + onlineCardVerifier: OnlineCardVerifier, +): String { fun ifAnyError(): String { return when { cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL @@ -28,7 +31,7 @@ suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result { val artworkId = cardInfoResult.data.artwork?.id if (artworkId.isNullOrEmpty()) { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt index 7150916d2e..fbb3d27204 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt @@ -28,14 +28,19 @@ import java.math.BigDecimal */ class OnboardingManager(var scanResponse: ScanResponse) { - private val cardRepository: CardRepository = store.inject(DaggerGraphState::cardRepository) + private val cardRepository: CardRepository by lazy { store.inject(DaggerGraphState::cardRepository) } + + private val onlineCardVerifier: OnlineCardVerifier by lazy { store.inject(DaggerGraphState::onlineCardVerifier) } + private var cardInfo: Result? = null suspend fun loadArtworkUrl(): String { val cardInfo = cardInfo - ?: OnlineCardVerifier().getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey) + ?: onlineCardVerifier.getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey) + this.cardInfo = cardInfo - return scanResponse.card.getOrLoadCardArtworkUrl(cardInfo) + + return scanResponse.card.getOrLoadCardArtworkUrl(cardInfo, onlineCardVerifier) } suspend fun updateBalance(walletManager: WalletManager): OnboardingWalletBalance { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 0604cfb21d..310cf7bc01 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -242,7 +242,9 @@ private suspend fun readCard(): CompletionResult { } private suspend fun loadArtworkForCard(cardId: String, cardPublicKey: ByteArray, defaultArtwork: Uri?): Uri { - return when (val cardInfo = OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)) { + val onlineCardVerifier = store.inject(DaggerGraphState::onlineCardVerifier) + + return when (val cardInfo = onlineCardVerifier.getCardInfo(cardId, cardPublicKey)) { is Result.Success -> { val artworkId = cardInfo.data.artwork?.id if (artworkId.isNullOrEmpty()) { diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 4351692f4c..9a39f98772 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] tangemBlockchainSdk = "develop-984" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-441" +tangemCardSdk = "develop-443" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From d4fe3896f3e03a0f6292b4065e82121da6ebde76 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Mar 2025 13:24:37 +0500 Subject: [PATCH 012/138] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 9 ++ .../data/staking/DefaultStakingRepository.kt | 2 +- .../DefaultWalletManagersFacade.kt | 6 ++ .../walletmanager/WalletManagersFacade.kt | 6 ++ domain/staking/build.gradle.kts | 1 + .../staking/CheckAccountInitializedUseCase.kt | 13 +++ .../impl/presentation/model/StakingModel.kt | 92 +++++++++++-------- .../state/events/StakingAlertUM.kt | 7 ++ .../state/events/StakingEventFactory.kt | 5 + gradle/tangem_dependencies.toml | 2 +- 10 files changed, 102 insertions(+), 41 deletions(-) create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/CheckAccountInitializedUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index db77c4ceba..cb6976b9b8 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.staking.* import com.tangem.domain.staking.repositories.* +import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -209,4 +210,12 @@ internal object StakingDomainModule { fun provideGetStakingIntegrationIdUseCase(stakingRepository: StakingRepository): GetStakingIntegrationIdUseCase { return GetStakingIntegrationIdUseCase(stakingRepository) } + + @Provides + @Singleton + fun provideCheckAccountInitializedUseCase( + walletManagersFacade: WalletManagersFacade, + ): CheckAccountInitializedUseCase { + return CheckAccountInitializedUseCase(walletManagersFacade) + } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index d08938d3fe..9275635df5 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -738,7 +738,7 @@ internal class DefaultStakingRepository( private companion object { const val YIELDS_STORE_KEY = "yields" - const val TON_INTEGRATION_ID = "ton-ton-tonwhales-pools-staking" + const val TON_INTEGRATION_ID = "ton-ton-chorus-one-pools-staking" const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" const val ETHEREUM_POLYGON_INTEGRATION_ID = "ethereum-matic-native-staking" diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index ce2a817c95..4beba64f1c 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -682,6 +682,12 @@ class DefaultWalletManagersFacade( return walletManager.getAsset(collectionIdentifier, assetIdentifier) } + override suspend fun isAccountInitialized(userWalletId: UserWalletId, network: Network): Boolean { + val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network) + val initializableAccountWalletManger = walletManager as? InitializableAccount ?: return false + return initializableAccountWalletManger.accountInitializationState == InitializableAccount.State.INITIALIZED + } + private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { if (tokens.isEmpty()) return diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index d1554a2118..e71d19ab0f 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -265,4 +265,10 @@ interface WalletManagersFacade { collectionIdentifier: NFTCollection.Identifier, assetIdentifier: NFTAsset.Identifier, ): NFTAsset? + + /** + * If wallet manager implements [InitializableAccount] then returns [InitializableAccount.isAccountInitialized] + * value. Otherwise always return true + */ + suspend fun isAccountInitialized(userWalletId: UserWalletId, network: Network): Boolean } \ No newline at end of file diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index 917cd0ad5f..d87b8d595b 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation(deps.kotlin.serialization) implementation(deps.jodatime) + implementation(projects.domain.legacy) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/CheckAccountInitializedUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/CheckAccountInitializedUseCase.kt new file mode 100644 index 0000000000..99d0d2fec7 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/CheckAccountInitializedUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId + +class CheckAccountInitializedUseCase(private val walletManagersFacade: WalletManagersFacade) { + + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either { + return Either.catch { walletManagersFacade.isAccountInitialized(userWalletId, network) } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 9696fb48cd..38987cadd6 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -114,6 +114,7 @@ internal class StakingModel @Inject constructor( private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val getActionsUseCase: GetActionsUseCase, private val getYieldUseCase: GetYieldUseCase, + private val checkAccountInitializedUseCase: CheckAccountInitializedUseCase, private val paramsInterceptorHolder: ParamsInterceptorHolder, private val shareManager: ShareManager, @DelayedWork private val coroutineScope: CoroutineScope, @@ -229,49 +230,62 @@ internal class StakingModel @Inject constructor( } override fun onNextClick(balanceState: BalanceState?) { - val isInitialInfoStep = value.currentStep == StakingStep.InitialInfo - val noBalanceState = balanceState == null - val noYieldBalanceData = cryptoCurrencyStatus.value.yieldBalance !is YieldBalance.Data + modelScope.launch { + val isInitialInfoStep = value.currentStep == StakingStep.InitialInfo + val noBalanceState = balanceState == null + val noYieldBalanceData = cryptoCurrencyStatus.value.yieldBalance !is YieldBalance.Data + val isAccountInitialized = checkAccountInitializedUseCase.invoke( + userWalletId = userWalletId, + network = cryptoCurrencyStatus.currency.network, + ).getOrElse { + Timber.e(it) + false + } - when { - isInitialInfoStep && noBalanceState && yield.allValidatorsFull && noYieldBalanceData -> { - stakingEventFactory.createStakingValidatorsUnavailableAlert() - return - } - isInitialInfoStep && noBalanceState -> { - val list = buildList { - SetConfirmationStateInitTransformer( - isEnter = true, - isExplicitExit = false, - balanceState = null, - cryptoCurrencyStatus = cryptoCurrencyStatus, - stakingApproval = stakingApproval, - stakingAllowance = stakingAllowance, - yieldArgs = yield.args, - ).let(::add) - if (yield.args.enter.isPartialAmountDisabled) { - ValidatorSelectChangeTransformer( - selectedValidator = yield.preferredValidators.firstOrNull(), - yield = yield, - ).let(::add) - SetAmountDataTransformer( - clickIntents = this@StakingModel, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - userWalletProvider = Provider { userWallet }, - appCurrencyProvider = Provider { appCurrency }, - ).let(::add) - AmountMaxValueStateTransformer( - cryptoCurrencyStatus = cryptoCurrencyStatus, - minimumTransactionAmount = minimumTransactionAmount, - actionType = uiState.value.actionType, - yield = yield, - ).let(::add) - } + when { + isInitialInfoStep && noBalanceState && yield.allValidatorsFull && noYieldBalanceData -> { + stakingEventFactory.createStakingValidatorsUnavailableAlert() + return@launch + } + isInitialInfoStep && noBalanceState && !isAccountInitialized -> { + stakingEventFactory.createInitializeAccountAlert() + return@launch + } + isInitialInfoStep && noBalanceState -> { + val list = buildList { + SetConfirmationStateInitTransformer( + isEnter = true, + isExplicitExit = false, + balanceState = null, + cryptoCurrencyStatus = cryptoCurrencyStatus, + stakingApproval = stakingApproval, + stakingAllowance = stakingAllowance, + yieldArgs = yield.args, + ).let(::add) + if (yield.args.enter.isPartialAmountDisabled) { + ValidatorSelectChangeTransformer( + selectedValidator = yield.preferredValidators.firstOrNull(), + yield = yield, + ).let(::add) + SetAmountDataTransformer( + clickIntents = this@StakingModel, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + userWalletProvider = Provider { userWallet }, + appCurrencyProvider = Provider { appCurrency }, + ).let(::add) + AmountMaxValueStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minimumTransactionAmount, + actionType = uiState.value.actionType, + yield = yield, + ).let(::add) + } + } + stateController.updateAll(*list.toTypedArray()) } - stateController.updateAll(*list.toTypedArray()) } + stakingStateRouter.onNextClick() } - stakingStateRouter.onNextClick() } override fun getFee() { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt index 2e6c9aebf2..a9b1445d41 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt @@ -61,4 +61,11 @@ internal sealed class StakingAlertUM : AlertUM { ) override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) } + + data object InitializeAccount : StakingAlertUM() { + override val onConfirmClick: (() -> Unit)? = null + override val title: TextReference = resourceReference(id = R.string.staking_error_no_validators_title) + override val message: TextReference = resourceReference(id = R.string.staking_notification_ton_activate_account) + override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt index 6d54195e71..c206124f55 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt @@ -46,4 +46,9 @@ internal class StakingEventFactory( val alert = StakingEvent.ShowAlert(alert = StakingAlertUM.ValidatorsUnavailable) stateController.updateEvent(alert) } + + fun createInitializeAccountAlert() { + val alert = StakingEvent.ShowAlert(alert = StakingAlertUM.InitializeAccount) + stateController.updateEvent(alert) + } } \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 9a39f98772..6367a68925 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,5 +1,5 @@ [versions] -tangemBlockchainSdk = "develop-984" +tangemBlockchainSdk = "develop-986" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-443" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 87351d28b8304d08396536777c07567af8be6a21 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Mar 2025 13:14:04 +0400 Subject: [PATCH 013/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 6367a68925..85b8e9a1f8 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] tangemBlockchainSdk = "develop-986" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-443" +tangemCardSdk = "develop-444" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 99b1e02c0a56d8c824048022161afd9b477fe874 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Mar 2025 12:33:00 +0300 Subject: [PATCH 014/138] Updated on 2026-08-14 --- .../onboarding/v2/common/ui/RefreshButton.kt | 51 +++ .../child/topup/ui/OnboardingNoteTopUp.kt | 2 +- ...Header.kt => OnboardingNoteTopUpHeader.kt} | 47 +-- .../v2/twin/api/OnboardingTwinComponent.kt | 7 +- .../impl/DefaultOnboardingTwinComponent.kt | 2 +- .../v2/twin/impl/model/OnboardingTwinModel.kt | 8 +- .../v2/twin/impl/ui/OnboardingTwin.kt | 26 ++ .../v2/twin/impl/ui/TwinWalletArtwork.kt | 378 ++++++++++++++++++ 8 files changed, 473 insertions(+), 48 deletions(-) create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/RefreshButton.kt rename features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/{OnboardinNoteTopUpHeader.kt => OnboardingNoteTopUpHeader.kt} (60%) create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/RefreshButton.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/RefreshButton.kt new file mode 100644 index 0000000000..fcea5b5883 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/RefreshButton.kt @@ -0,0 +1,51 @@ +package com.tangem.features.onboarding.v2.common.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onboarding.v2.impl.R + +@Composable +internal fun RefreshButton(isRefreshing: Boolean, onRefreshBalanceClick: () -> Unit, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size48) + .shadow(elevation = 2.dp, shape = CircleShape) + .background(TangemTheme.colors.background.action) + .clickable( + enabled = !isRefreshing, + onClick = onRefreshBalanceClick, + indication = ripple(), + interactionSource = remember { MutableInteractionSource() }, + ), + ) { + if (isRefreshing) { + CircularProgressIndicator( + modifier = Modifier.padding(TangemTheme.dimens.spacing12), + color = TangemTheme.colors.icon.primary1, + strokeWidth = TangemTheme.dimens.size2, + ) + } else { + Icon( + painter = painterResource(id = R.drawable.ic_refresh_24), + contentDescription = null, + tint = TangemTheme.colors.text.disabled, + modifier = Modifier.padding(TangemTheme.dimens.spacing12), + ) + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt index a850b14829..cd5e8f6af6 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt @@ -28,7 +28,7 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif .navigationBarsPadding(), verticalArrangement = Arrangement.Bottom, ) { - OnboardinNoteTopUpHeader( + OnboardingNoteTopUpHeader( balance = state.balance, cardArtworkUrl = state.cardArtworkUrl, onRefreshBalanceClick = state.onRefreshBalanceClick, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardinNoteTopUpHeader.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt similarity index 60% rename from features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardinNoteTopUpHeader.kt rename to features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt index e0c96f997b..502271e7dc 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardinNoteTopUpHeader.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt @@ -1,20 +1,11 @@ package com.tangem.features.onboarding.v2.note.impl.child.topup.ui 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.CircleShape -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.material3.ripple import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -23,11 +14,12 @@ import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.common.ui.RefreshButton import com.tangem.features.onboarding.v2.common.ui.WalletCard import com.tangem.features.onboarding.v2.impl.R @Composable -fun OnboardinNoteTopUpHeader( +fun OnboardingNoteTopUpHeader( balance: String?, cardArtworkUrl: String?, isRefreshing: Boolean, @@ -78,44 +70,11 @@ fun OnboardinNoteTopUpHeader( } } -@Composable -private fun RefreshButton(isRefreshing: Boolean, onRefreshBalanceClick: () -> Unit, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .size(TangemTheme.dimens.size48) - .shadow(elevation = 2.dp, shape = CircleShape) - .background(TangemTheme.colors.background.action) - .clickable( - enabled = !isRefreshing, - onClick = onRefreshBalanceClick, - indication = ripple(), - interactionSource = remember { MutableInteractionSource() }, - ), - ) { - if (isRefreshing) { - Box { - CircularProgressIndicator( - modifier = Modifier.padding(TangemTheme.dimens.spacing12), - color = TangemTheme.colors.icon.primary1, - strokeWidth = TangemTheme.dimens.size2, - ) - } - } else { - Icon( - painter = painterResource(id = R.drawable.ic_refresh_24), - contentDescription = null, - tint = TangemTheme.colors.text.disabled, - modifier = Modifier.padding(TangemTheme.dimens.spacing12), - ) - } - } -} - @Preview(showBackground = true) @Composable private fun OnboardinNoteTopUpHeaderPreview() { TangemThemePreview { - OnboardinNoteTopUpHeader( + OnboardingNoteTopUpHeader( balance = "0.00000001 BTC", cardArtworkUrl = "", onRefreshBalanceClick = {}, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt index 7fe9075114..20893e09f8 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt @@ -11,7 +11,12 @@ interface OnboardingTwinComponent : ComposableContentComponent { val titleProvider: TitleProvider, val scanResponse: ScanResponse, val modelCallbacks: ModelCallbacks, - ) + val mode: Mode, + ) { + enum class Mode { + CreateWallet, RecreateWallet + } + } interface ModelCallbacks { fun onDone() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt index 402c426566..318143250b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt @@ -10,7 +10,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -class DefaultOnboardingTwinComponent @AssistedInject constructor( +internal class DefaultOnboardingTwinComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted params: OnboardingTwinComponent.Params, ) : OnboardingTwinComponent, AppComponentContext by appComponentContext { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index 39c6c4aa1a..047e897976 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -2,12 +2,18 @@ package com.tangem.features.onboarding.v2.twin.impl.model 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.onboarding.v2.twin.api.OnboardingTwinComponent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import javax.inject.Inject @ModelScoped -class OnboardingTwinModel @Inject constructor( +internal class OnboardingTwinModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, ) : Model() { + + val params = paramsContainer.require() + // TODO } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt new file mode 100644 index 0000000000..2a8405c685 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt @@ -0,0 +1,26 @@ +package com.tangem.features.onboarding.v2.twin.impl.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemThemePreview + +@Suppress("UnusedPrivateMember") +@Composable +fun OnboardingTwin(modifier: Modifier = Modifier) { + Column { + Box { + // TODO + } + } +} + +@Preview +@Composable +private fun Preview() { + TangemThemePreview { + OnboardingTwin() + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt new file mode 100644 index 0000000000..c2a89c2b69 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt @@ -0,0 +1,378 @@ +package com.tangem.features.onboarding.v2.twin.impl.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.zIndex +import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.SpacerHMax +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.wallets.models.Artwork +import com.tangem.features.onboarding.v2.common.ui.RefreshButton +import com.tangem.features.onboarding.v2.common.ui.WalletCard +import com.tangem.features.onboarding.v2.impl.R +import kotlinx.coroutines.delay +import java.util.concurrent.TimeUnit + +internal sealed class TwinWalletArtworkState { + data object Spread : TwinWalletArtworkState() + + data class Leapfrog( + val step: Step, + ) : TwinWalletArtworkState() { + enum class Step { + FirstCard, SecondCard + } + } + + data object TopUp : TwinWalletArtworkState() +} + +private data class CardsTransitionState( + val walletCard1: WalletCardTransitionState, + val walletCard2: WalletCardTransitionState, +) + +private data class WalletCardTransitionState( + val alpha: Float = 1f, + val xTranslation: Float = 0f, + val yTranslation: Float = 0f, + val xScale: Float = 1f, + val yScale: Float = 1f, + val rotation: Float = 0f, + val zIndex: Float = 0f, +) + +@Suppress("LongMethod") +@Composable +internal fun TwinWalletArtworks( + state: TwinWalletArtworkState, + balance: String, + isRefreshing: Boolean, + onRefreshBalanceClick: () -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithConstraints( + modifier + .heightIn(min = 180.dp) + .widthIn(max = 450.dp), + ) { + val animationWidth = maxWidth + val density = LocalDensity.current.density + + var animationState by remember { + mutableStateOf( + state.toTransitionSetState( + maxWidthDp = animationWidth.value, + density = density, + maxHeightDp = maxHeight.value, + ).first(), + ) + } + + val transition1 = updateTransition(animationState.walletCard1, label = "transition") + val transition2 = updateTransition(animationState.walletCard2, label = "transition") + + LaunchedEffect(state) { + val animationSetState = state.toTransitionSetState( + maxWidthDp = animationWidth.value, + density = density, + maxHeightDp = maxHeight.value, + ) + animationSetState.fastForEach { + animationState = it + val maxTime = maxOf( + transition1.totalDurationNanos, + transition2.totalDurationNanos, + ) + delay(TimeUnit.NANOSECONDS.toMillis(maxTime)) + } + } + + AnimatedVisibility( + visible = state == TwinWalletArtworkState.TopUp, + enter = fadeIn(), + exit = fadeOut(), + ) { + Box( + modifier = Modifier + .padding(vertical = 24.dp, horizontal = 32.dp) + .fillMaxSize() + .background( + TangemTheme.colors.button.secondary, + shape = TangemTheme.shapes.roundedCornersMedium, + ), + ) + } + + AnimatedTwinCards( + transition1 = transition1, + transition2 = transition2, + modifier = Modifier + .widthIn(max = 450.dp) + .matchParentSize(), + ) + + AnimatedVisibility( + modifier = Modifier.align(Alignment.Center), + visible = state == TwinWalletArtworkState.TopUp, + enter = fadeIn(), + exit = fadeOut(), + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SpacerHMax() + Text( + text = stringResourceSafe(R.string.common_balance_title), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + SpacerH8() + Text( + text = balance.orEmpty(), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + SpacerHMax() + } + } + + AnimatedVisibility( + modifier = Modifier.align(Alignment.BottomCenter), + visible = state == TwinWalletArtworkState.TopUp, + enter = fadeIn(), + exit = fadeOut(), + ) { + RefreshButton( + isRefreshing = isRefreshing, + onRefreshBalanceClick = onRefreshBalanceClick, + ) + } + } +} + +@Composable +private fun AnimatedTwinCards( + transition1: Transition, + transition2: Transition, + modifier: Modifier = Modifier, +) { + val alpha1 by transition1.animateFloat(label = "alpha1") { it.alpha } + val translationX1 by transition1.animateFloat(label = "translationX1") { it.xTranslation } + val translationY1 by transition1.animateFloat(label = "translationY1") { it.yTranslation } + val rotation1 by transition1.animateFloat(label = "rotation1") { it.rotation } + val scaleX1 by transition1.animateFloat(label = "scaleX1") { it.xScale } + val scaleY1 by transition1.animateFloat(label = "scaleY1") { it.yScale } + val zIndex1 = transition1.currentState.zIndex + + val alpha2 by transition2.animateFloat(label = "alpha2") { it.alpha } + val translationX2 by transition2.animateFloat(label = "translationX2") { it.xTranslation } + val translationY2 by transition2.animateFloat(label = "translationY2") { it.yTranslation } + val rotation2 by transition2.animateFloat(label = "rotation2") { it.rotation } + val scaleX2 by transition2.animateFloat(label = "scaleX2") { it.xScale } + val scaleY2 by transition2.animateFloat(label = "scaleY2") { it.yScale } + val zIndex2 = transition2.currentState.zIndex + + Box(modifier) { + WalletCard( + modifier = Modifier + .zIndex(zIndex2) + .matchParentSize() + .graphicsLayer { + translationX = translationX2 + translationY = translationY2 + } + .graphicsLayer { + alpha = alpha2 + rotationZ = rotation2 + scaleX = scaleX2 + scaleY = scaleY2 + }, + url = Artwork.TWIN_CARD_2_URL, + ) + + WalletCard( + modifier = Modifier + .zIndex(zIndex1) + .matchParentSize() + .graphicsLayer { + scaleX = scaleX1 + scaleY = scaleY1 + } + .graphicsLayer { + alpha = alpha1 + translationX = translationX1 + translationY = translationY1 + rotationZ = rotation1 + }, + url = Artwork.TWIN_CARD_1_URL, + ) + } +} + +@Suppress("MagicNumber", "LongMethod", "UnnecessaryParentheses") +private fun TwinWalletArtworkState.toTransitionSetState( + maxWidthDp: Float, + maxHeightDp: Float, + density: Float, +): List = when (this) { + TwinWalletArtworkState.Spread -> { + val scale = 0.8f + listOf( + CardsTransitionState( + walletCard1 = WalletCardTransitionState( + xTranslation = maxWidthDp * -0.15f * density, + yTranslation = -65 * density, + rotation = -5f, + xScale = scale, + yScale = scale, + zIndex = 2f, + ), + walletCard2 = WalletCardTransitionState( + xTranslation = maxWidthDp * 0.15f * density, + yTranslation = 65 * density, + rotation = -5f, + xScale = scale, + yScale = scale, + zIndex = 1f, + ), + ), + ) + } + is TwinWalletArtworkState.Leapfrog -> { + val translationY = -(maxHeightDp / 2) * density + val secondCardPlace = WalletCardTransitionState().copy( + xTranslation = 0f, + yTranslation = ((maxHeightDp * (1 - 0.83f)) / 4 + 12) * density, + rotation = 0f, + xScale = 0.83f, + yScale = 0.83f, + ) + + if (step == TwinWalletArtworkState.Leapfrog.Step.FirstCard) { + listOf( + CardsTransitionState( + walletCard1 = WalletCardTransitionState( + yTranslation = translationY, + zIndex = 2f, + ), + walletCard2 = WalletCardTransitionState(), + ), + CardsTransitionState( + walletCard1 = WalletCardTransitionState( + zIndex = 2f, + ), + walletCard2 = secondCardPlace, + ), + ) + } else { + listOf( + CardsTransitionState( + walletCard1 = WalletCardTransitionState(), + walletCard2 = WalletCardTransitionState( + yTranslation = translationY, + zIndex = 2f, + ), + ), + CardsTransitionState( + walletCard1 = secondCardPlace, + walletCard2 = WalletCardTransitionState( + zIndex = 2f, + ), + ), + ) + } + } + TwinWalletArtworkState.TopUp -> { + val scale = 0.4f + val yTranslation = -maxHeightDp * density - 24 * density + listOf( + CardsTransitionState( + walletCard1 = WalletCardTransitionState( + yTranslation = yTranslation, + xScale = scale, + yScale = scale, + zIndex = 2f, + ), + walletCard2 = WalletCardTransitionState( + yTranslation = yTranslation * 0.35f, + xScale = scale * 0.8f, + yScale = scale * 0.8f, + zIndex = 1f, + ), + ), + ) + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 640) +@Preview(showBackground = true, widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + var state: TwinWalletArtworkState by remember { mutableStateOf(TwinWalletArtworkState.TopUp) } + + TwinWalletArtworks( + state = state, + modifier = Modifier + .padding(top = 250.dp) + .fillMaxWidth(), + balance = "1 USD", + isRefreshing = false, + onRefreshBalanceClick = {}, + ) + + var index by remember { mutableIntStateOf(0) } + + Button( + modifier = Modifier + .zIndex(5f) + .align(Alignment.TopStart), + onClick = { + val list = listOf( + TwinWalletArtworkState.Spread, + TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.FirstCard), + TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.SecondCard), + TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.FirstCard), + TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.SecondCard), + TwinWalletArtworkState.TopUp, + ) + + state = list[index % list.size] + index++ + }, + ) { + Text("Animate $state") + } + } + } +} \ No newline at end of file From 91e095657603070f70a59dc9f51678525f10df8e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Mar 2025 15:01:00 +0500 Subject: [PATCH 015/138] Updated on 2026-08-14 --- .../products/note/OnboardingNoteFragment.kt | 4 +- .../ui/dialogs/UnfinishedBackupFoundDialog.kt | 2 +- features/onboarding-v2/impl/build.gradle.kts | 2 + .../analytics/OnboardingEvent.kt | 12 +- .../onboarding/v2/common/ui/Dialogs.kt | 25 ++++ .../impl => }/common/ui/OnboardingDialogUM.kt | 2 +- .../onboarding/v2/common/ui/RefreshButton.kt | 2 +- .../DefaultOnboardingMultiWalletComponent.kt | 2 +- .../model/MultiWalletAccessCodeModel.kt | 2 +- .../backup/model/MultiWalletBackupModel.kt | 2 +- .../model/Wallet1ChooseOptionModel.kt | 2 +- .../model/MultiWalletCreateWalletModel.kt | 2 +- .../ui/state/MultiWalletCreateWalletUM.kt | 2 +- .../ui/state/MultiWalletFinalizeUM.kt | 2 +- .../model/MultiWalletSeedPhraseModel.kt | 4 +- .../ui/state/MultiWalletSeedPhraseUM.kt | 2 +- .../v2/multiwallet/impl/common/ui/Dialogs.kt | 1 + .../impl/model/OnboardingMultiWalletModel.kt | 2 +- .../impl/ui/state/OnboardingMultiWalletUM.kt | 2 +- .../impl/DefaultOnboardingNoteComponent.kt | 20 +-- .../OnboardingNoteCreateWalletComponent.kt | 5 +- .../model/OnboardingNoteCreateWalletModel.kt | 82 +++++++++++- .../topup/model/OnboardingNoteTopUpModel.kt | 119 +++++++++++++++++- .../child/topup/ui/OnboardingNoteTopUp.kt | 8 +- .../topup/ui/OnboardingNoteTopUpHeader.kt | 6 +- .../impl/model/OnboardingNoteCommonState.kt | 10 ++ .../v2/note/impl/model/OnboardingNoteModel.kt | 66 ++++++++-- .../v2/note/impl/model/OnboardingNoteState.kt | 5 - 28 files changed, 338 insertions(+), 57 deletions(-) rename features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/{multiwallet/impl => common}/analytics/OnboardingEvent.kt (88%) create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/Dialogs.kt rename features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/{multiwallet/impl => }/common/ui/OnboardingDialogUM.kt (87%) create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt delete mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteState.kt diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt index 97746d2d70..8346f444cd 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt @@ -26,9 +26,7 @@ import com.tangem.tap.store import com.tangem.utils.extensions.stripZeroPlainString import com.tangem.wallet.R -/** -[REDACTED_AUTHOR] - */ +@Deprecated("Use OnboardingNoteComponent") class OnboardingNoteFragment : BaseOnboardingFragment() { private var previousStep: OnboardingNoteStep = OnboardingNoteStep.None diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt index ec80c29c24..fbeff5f6f4 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt @@ -5,7 +5,7 @@ import androidx.appcompat.app.AlertDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.core.analytics.Analytics import com.tangem.domain.models.scan.ScanResponse -import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index a848d97068..6b65a2561d 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -43,6 +43,8 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.onboarding) implementation(projects.domain.visa) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) /** Tangem libraries */ implementation(projects.libs.tangemSdkApi) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/analytics/OnboardingEvent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt similarity index 88% rename from features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/analytics/OnboardingEvent.kt rename to features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt index f6ffdafe88..f795e51d51 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/analytics/OnboardingEvent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onboarding.v2.multiwallet.impl.analytics +package com.tangem.features.onboarding.v2.common.analytics import com.tangem.core.analytics.models.AnalyticsEvent @@ -38,6 +38,16 @@ sealed class OnboardingEvent( } } + sealed class Topup( + event: String, + params: Map = mapOf(), + ) : OnboardingEvent("Onboarding / Top Up", event, params) { + + object ScreenOpened : Topup("Activation Screen Opened") + + object ButtonShowWalletAddress : Topup("Button - Show the Wallet Address") + } + sealed class Backup( event: String, params: Map = mapOf(), diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/Dialogs.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/Dialogs.kt new file mode 100644 index 0000000000..2d26b8608b --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/Dialogs.kt @@ -0,0 +1,25 @@ +package com.tangem.features.onboarding.v2.common.ui + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.features.onboarding.v2.impl.R + +internal fun exitOnboardingDialog(onConfirm: () -> Unit): DialogMessage { + return DialogMessage( + title = resourceReference(R.string.onboarding_exit_alert_title), + message = resourceReference(R.string.onboarding_exit_alert_message), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_yes), + onClick = onConfirm, + ) + }, + secondActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_cancel), + onClick = {}, + ) + }, + ) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/OnboardingDialogUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt similarity index 87% rename from features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/OnboardingDialogUM.kt rename to features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt index d5050f50e6..53e6ba8d82 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/OnboardingDialogUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onboarding.v2.multiwallet.impl.common.ui +package com.tangem.features.onboarding.v2.common.ui import com.tangem.common.ui.alerts.models.AlertUM import com.tangem.core.ui.extensions.TextReference diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/RefreshButton.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/RefreshButton.kt index fcea5b5883..12cde207b1 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/RefreshButton.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/RefreshButton.kt @@ -43,7 +43,7 @@ internal fun RefreshButton(isRefreshing: Boolean, onRefreshBalanceClick: () -> U Icon( painter = painterResource(id = R.drawable.ic_refresh_24), contentDescription = null, - tint = TangemTheme.colors.text.disabled, + tint = TangemTheme.colors.text.primary1, modifier = Modifier.padding(TangemTheme.dimens.spacing12), ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt index b7d96ab79a..ff138a6d70 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt @@ -26,7 +26,7 @@ import com.tangem.core.decompose.navigation.inner.InnerNavigation import com.tangem.core.decompose.navigation.inner.InnerNavigationState import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent -import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.MultiWalletAccessCodeComponent import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWalletBackupComponent diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt index bd4a37a0b5..a432deb8d0 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt @@ -6,7 +6,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler 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.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui.state.MultiWalletAccessCodeUM import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt index 6728f170e4..94dcb94eba 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt @@ -19,8 +19,8 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.ProductType +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.impl.R -import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWalletBackupComponent import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.backupCardAttestationFailedDialog diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt index 2bf7b82cbd..c5cd73b286 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt @@ -11,7 +11,7 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableSharedFlow diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt index 7304aefcae..86226a42b6 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt @@ -16,8 +16,8 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.impl.R -import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDialog diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/state/MultiWalletCreateWalletUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/state/MultiWalletCreateWalletUM.kt index 6f1af9d0a3..2a6a97474f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/state/MultiWalletCreateWalletUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/state/MultiWalletCreateWalletUM.kt @@ -1,7 +1,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state import com.tangem.core.ui.extensions.TextReference -import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM +import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM internal data class MultiWalletCreateWalletUM( val title: TextReference, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/state/MultiWalletFinalizeUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/state/MultiWalletFinalizeUM.kt index b367e365b4..00387df368 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/state/MultiWalletFinalizeUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/state/MultiWalletFinalizeUM.kt @@ -1,6 +1,6 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.ui.state -import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM +import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM internal data class MultiWalletFinalizeUM( val step: Step = Step.Primary, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt index 7b75b80c4e..fcb2c47870 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt @@ -13,13 +13,13 @@ import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.GenerateSeedPhraseUiStateBuilder import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.ImportSeedPhraseUiStateBuilder import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.SeedPhraseCheckUiStateBuilder import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM -import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDialog import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/state/MultiWalletSeedPhraseUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/state/MultiWalletSeedPhraseUM.kt index f6dd64808c..c167a59481 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/state/MultiWalletSeedPhraseUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/state/MultiWalletSeedPhraseUM.kt @@ -4,8 +4,8 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.text.input.TextFieldValue import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.GeneratedWordsType -import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/Dialogs.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/Dialogs.kt index ecf06866f2..771c7b9085 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/Dialogs.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/Dialogs.kt @@ -1,6 +1,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.common.ui import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM import com.tangem.features.onboarding.v2.impl.R internal fun resetCardDialog(onConfirm: () -> Unit, dismiss: () -> Unit, onDismissButtonClick: () -> Unit) = diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt index 4556e9711e..f868e3915a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt @@ -10,7 +10,7 @@ import com.tangem.domain.models.scan.ProductType import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent -import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.interruptBackupDialog import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.FinalizeStage diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt index 5aa205dc81..74f7cc7bec 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt @@ -1,6 +1,6 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.ui.state -import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM +import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM internal data class OnboardingMultiWalletUM( val artwork1Url: String? = null, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt index d3ca96fa30..bea41ef278 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt @@ -25,15 +25,16 @@ import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.child.create.OnboardingNoteCreateWalletComponent import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel +import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteCommonState import com.tangem.features.onboarding.v2.note.impl.route.ONBOARDING_NOTE_STEPS_COUNT import com.tangem.features.onboarding.v2.note.impl.route.OnboardingNoteRoute import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch internal class DefaultOnboardingNoteComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @@ -47,6 +48,7 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor( } private val childParams = ChildParams( + commonState = model.commonUiState, onBack = ::onChildBack, parentBackEvent = currentChildBackEventHandle, ) @@ -70,15 +72,13 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor( override val state: StateFlow = model.innerNavigationState override fun pop(onComplete: (Boolean) -> Unit) { - // TODO [REDACTED_TASK_KEY] + model.onChildBack( + currentRoute = childStack.value.active.configuration, + ) } } init { - componentScope.launch { - model.onDone.collect { params.onDone() } - } - // sets title and stepper value childStack.subscribe(lifecycle) { stack -> val currentRoute = stack.active.configuration @@ -96,14 +96,17 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor( appComponentContext = factoryContext, params = OnboardingNoteCreateWalletComponent.Params( childParams = childParams, - onDone = { model.stackNavigation.push(OnboardingNoteRoute.TopUp) }, + onWalletCreated = { userWallet -> + model.onWalletCreated(userWallet) + model.stackNavigation.push(OnboardingNoteRoute.TopUp) + }, ), ) OnboardingNoteRoute.TopUp -> OnboardingNoteTopUpComponent( appComponentContext = factoryContext, params = OnboardingNoteTopUpComponent.Params( childParams = childParams, - onDone = { model.onDone.tryEmit(Unit) }, + onDone = { params.onDone() }, ), ) } @@ -134,6 +137,7 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor( } data class ChildParams( + val commonState: MutableStateFlow, val onBack: () -> Unit, val parentBackEvent: SharedFlow, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/OnboardingNoteCreateWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/OnboardingNoteCreateWalletComponent.kt index 5751eb80ba..603ce3d688 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/OnboardingNoteCreateWalletComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/OnboardingNoteCreateWalletComponent.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.child.create.model.OnboardingNoteCreateWalletModel import com.tangem.features.onboarding.v2.note.impl.child.create.ui.OnboardingNoteCreateWallet @@ -24,7 +25,7 @@ internal class OnboardingNoteCreateWalletComponent( override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() - BackHandler(onBack = remember(this) { { params.childParams.onBack() } }) + BackHandler(onBack = remember(this) { { params.childParams.onBack } }) OnboardingNoteCreateWallet( modifier = modifier, @@ -34,6 +35,6 @@ internal class OnboardingNoteCreateWalletComponent( data class Params( val childParams: DefaultOnboardingNoteComponent.ChildParams, - val onDone: () -> Unit, + val onWalletCreated: (UserWallet) -> Unit, ) } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt index 93634beb79..b8ca1d76a2 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt @@ -1,26 +1,104 @@ package com.tangem.features.onboarding.v2.note.impl.child.create.model +import com.tangem.common.CompletionResult +import com.tangem.core.analytics.Analytics import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.card.repository.CardRepository +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.note.impl.child.create.OnboardingNoteCreateWalletComponent import com.tangem.features.onboarding.v2.note.impl.child.create.ui.state.OnboardingNoteCreateWalletUM import com.tangem.sdk.api.TangemSdkManager 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 javax.inject.Inject -@Suppress("UnusedPrivateMember") @ModelScoped internal class OnboardingNoteCreateWalletModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val tangemSdkManager: TangemSdkManager, + private val cardRepository: CardRepository, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, + private val saveWalletUseCase: SaveWalletUseCase, ) : Model() { + private val params = paramsContainer.require() + private val _uiState = MutableStateFlow( - OnboardingNoteCreateWalletUM(), + OnboardingNoteCreateWalletUM( + onCreateClick = ::onCreateWalletClick, + ), ) + init { + Analytics.send(OnboardingEvent.CreateWallet.ScreenOpened) + modelScope.launch { + val scanResponse = params.childParams.commonState.value.scanResponse ?: return@launch + if (!cardRepository.isActivationStarted(scanResponse.card.cardId)) { + Analytics.send(OnboardingEvent.Started) + } + } + observeArtwork() + } + val uiState: StateFlow = _uiState + + private fun onCreateWalletClick() { + modelScope.launch { + _uiState.update { + it.copy(createWalletInProgress = true) + } + val scanResponse = params.childParams.commonState.value.scanResponse ?: return@launch + val result = tangemSdkManager.createProductWallet(scanResponse) + when (result) { + is CompletionResult.Success -> { + Analytics.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) + cardRepository.startCardActivation(scanResponse.card.cardId) + createWalletAndNavigateBackWithDone(scanResponse.copy(card = result.data.card)) + } + is CompletionResult.Failure -> _uiState.update { + it.copy(createWalletInProgress = false) + } + } + } + } + + private fun createWalletAndNavigateBackWithDone(scanResponse: ScanResponse) { + modelScope.launch { + val userWallet = createUserWallet(scanResponse) + saveWalletUseCase(userWallet, canOverride = true).onRight { + cardRepository.finishCardActivation(scanResponse.card.cardId) + params.onWalletCreated(userWallet) + } + _uiState.update { + it.copy(createWalletInProgress = false) + } + } + } + + private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet { + return requireNotNull( + value = userWalletBuilderFactory.create(scanResponse = scanResponse).build(), + lazyMessage = { "User wallet not created" }, + ) + } + + private fun observeArtwork() { + modelScope.launch { + params.childParams.commonState.collect { + _uiState.value = _uiState.value.copy( + artworkUrl = it.cardArtworkUrl, + ) + } + } + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt index f2a8e3bf60..a7956a0cb8 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt @@ -1,12 +1,23 @@ package com.tangem.features.onboarding.v2.note.impl.child.topup.model +import com.tangem.core.analytics.Analytics 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.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.FetchCurrencyStatusUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject @Suppress("UnusedPrivateMember") @@ -14,11 +25,113 @@ import javax.inject.Inject internal class OnboardingNoteTopUpModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, ) : Model() { + private val params = paramsContainer.require() + private val scanResponse = params.childParams.commonState.value.scanResponse + private var userWallet: UserWallet? = null + private val _uiState = MutableStateFlow( - OnboardingNoteTopUpUM(), + OnboardingNoteTopUpUM( + onRefreshBalanceClick = ::refreshBalance, + onBuyCryptoClick = ::onBuyCryptoClick, + onShowWalletAddressClick = ::onShowWalletAddressClick, + ), ) val uiState: StateFlow = _uiState + + init { + Analytics.send(OnboardingEvent.Topup.ScreenOpened) + observeArtwork() + modelScope.launch { + createUserWalletIfNull() + observeCryptoCurrencyStatus() + refreshBalance() + } + } + + private fun refreshBalance() { + modelScope.launch { + showBalanceLoadingProgress(true) + createUserWalletIfNull() + val userWalletId = requireNotNull(userWallet?.walletId) + fetchCurrencyStatusUseCase( + userWalletId = userWalletId, + refresh = true, + ) + showBalanceLoadingProgress(false) + } + } + + private fun onBuyCryptoClick() { + // TODO [REDACTED_TASK_KEY] + } + + private fun onShowWalletAddressClick() { + // TODO [REDACTED_TASK_KEY] + } + + private suspend fun createUserWalletIfNull() { + if (userWallet != null) { + return + } + val commonState = params.childParams.commonState.value + userWallet = commonState.userWallet ?: createUserWallet(scanResponse) + } + + private fun observeArtwork() { + modelScope.launch { + params.childParams.commonState.collect { + _uiState.value = _uiState.value.copy( + cardArtworkUrl = it.cardArtworkUrl, + ) + } + } + } + + private fun observeCryptoCurrencyStatus() { + val userWalletId = userWallet?.walletId ?: return + getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId).map { it.getOrNull() }.filterNotNull() + .onEach(::applyCryptoCurrencyStatusToState).launchIn(modelScope) + } + + private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { + _uiState.update { + it.copy( + amountToCreateAccount = (status.value as? CryptoCurrencyStatus.NoAccount) + ?.amountToCreateAccount + ?.format( + { + crypto( + symbol = status.currency.symbol, + decimals = status.currency.decimals, + ) + }, + ), + balance = (status.value as? CryptoCurrencyStatus.Loaded)?.amount?.format({ + crypto( + symbol = status.currency.symbol, + decimals = status.currency.decimals, + ) + },), + ) + } + } + + private fun showBalanceLoadingProgress(value: Boolean) { + _uiState.update { + it.copy(isRefreshing = value) + } + } + + private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet { + return requireNotNull( + value = userWalletBuilderFactory.create(scanResponse = scanResponse).build(), + lazyMessage = { "User wallet not created" }, + ) + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt index cd5e8f6af6..307bc1efaa 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt @@ -17,7 +17,6 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R -import com.tangem.features.onboarding.v2.note.impl.ALL_STEPS_TOP_CONTAINER_WEIGHT import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM @Composable @@ -35,15 +34,16 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif isRefreshing = state.isRefreshing, modifier = Modifier .padding(horizontal = 34.dp) - .padding(top = 20.dp) - .weight(ALL_STEPS_TOP_CONTAINER_WEIGHT) + .padding(top = 16.dp) + .height(228.dp) .fillMaxWidth(), ) Column( modifier = Modifier .fillMaxWidth() .verticalScroll(rememberScrollState()) - .padding(32.dp) + .padding(horizontal = 32.dp) + .padding(top = 16.dp) .weight(1.0f), horizontalAlignment = Alignment.CenterHorizontally, ) { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt index 502271e7dc..2e598ac4d9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt @@ -27,12 +27,14 @@ fun OnboardingNoteTopUpHeader( modifier: Modifier = Modifier, ) { Box( - modifier = modifier.fillMaxWidth(), + modifier = modifier + .heightIn(min = 228.dp) + .widthIn(max = 450.dp), contentAlignment = Alignment.Center, ) { Box( modifier = Modifier - .padding(vertical = 24.dp, horizontal = 32.dp) + .padding(vertical = 24.dp) .fillMaxSize() .background( TangemTheme.colors.button.secondary, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt new file mode 100644 index 0000000000..7f082a0f19 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onboarding.v2.note.impl.model + +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.models.UserWallet + +internal data class OnboardingNoteCommonState( + val scanResponse: ScanResponse, + val cardArtworkUrl: String? = null, + val userWallet: UserWallet? = null, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt index 87a00a91e1..30733d3ff8 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt @@ -4,46 +4,54 @@ import com.arkivanov.decompose.router.stack.StackNavigation 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.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetCardImageUseCase +import com.tangem.features.onboarding.v2.common.ui.exitOnboardingDialog import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.OnboardingNoteInnerNavigationState import com.tangem.features.onboarding.v2.note.impl.route.OnboardingNoteRoute import com.tangem.features.onboarding.v2.note.impl.route.stepNum import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @ModelScoped internal class OnboardingNoteModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val messageSender: UiMessageSender, + private val getCardImageUseCase: GetCardImageUseCase, ) : Model() { @Suppress("UnusedPrivateMember") private val params = paramsContainer.require() - private val _currentScanResponse = MutableStateFlow(params.scanResponse) - - val initialRoute = OnboardingNoteRoute.CreateWallet // TODO [REDACTED_TASK_KEY] + val initialRoute = initializeRoute() private val _innerNavigationState = MutableStateFlow(OnboardingNoteInnerNavigationState(stackSize = initialRoute.stepNum())) - val currentScanResponse = _currentScanResponse.asStateFlow() - val innerNavigationState = _innerNavigationState.asStateFlow() - val onDone = MutableSharedFlow() - val stackNavigation = StackNavigation() private val _uiState = MutableStateFlow( - OnboardingNoteState(), + OnboardingNoteCommonState( + scanResponse = params.scanResponse, + ), ) - val uiState: StateFlow = _uiState + val commonUiState: MutableStateFlow = _uiState + + init { + loadArtworkUrl() + } fun updateStepForNewRoute(route: OnboardingNoteRoute) { _innerNavigationState.value = innerNavigationState.value.copy( @@ -53,6 +61,40 @@ internal class OnboardingNoteModel @Inject constructor( @Suppress("UnusedPrivateMember") fun onChildBack(currentRoute: OnboardingNoteRoute) { - // TODO [REDACTED_TASK_KEY] + messageSender.send( + exitOnboardingDialog( + onConfirm = { + router.pop() + }, + ), + ) + } + + fun onWalletCreated(userWallet: UserWallet) { + commonUiState.update { + it.copy(userWallet = userWallet) + } + } + + private fun initializeRoute(): OnboardingNoteRoute { + val card = params.scanResponse.card + return if (card.wallets.isEmpty()) { + OnboardingNoteRoute.CreateWallet + } else { + OnboardingNoteRoute.TopUp + } + } + + private fun loadArtworkUrl() { + modelScope.launch { + val cardInfo = params.scanResponse.card + val url = getCardImageUseCase( + cardId = cardInfo.cardId, + cardPublicKey = cardInfo.cardPublicKey, + ) + _uiState.update { + it.copy(cardArtworkUrl = url) + } + } } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteState.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteState.kt deleted file mode 100644 index 7d183852bb..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.model - -data class OnboardingNoteState( - val cardArtworkUrl: String? = null, -) \ No newline at end of file From 3eb843a05fd378bae69143b57bb040cbe08be860 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Mar 2025 15:44:13 +0300 Subject: [PATCH 016/138] Updated on 2026-08-14 --- .../domain/tokens/GetCryptoCurrencyUseCase.kt | 42 ------------------- .../model/OnrampSuccessComponentModel.kt | 28 ++++++++----- .../send/impl/presentation/model/SendModel.kt | 5 ++- 3 files changed, 21 insertions(+), 54 deletions(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt index 4363b74633..199e9ad236 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt @@ -36,36 +36,6 @@ class GetCryptoCurrencyUseCase( } } - /** - * Returns specific cryptocurrency for a given user wallet. - * - * @param userWalletId The ID of the user's wallet. - * @param id The ID of the cryptocurrency. - * @return An [Either] representing success (Right) or an error (Left) in fetching the status. - */ - suspend operator fun invoke( - userWalletId: UserWalletId, - id: CryptoCurrency.ID, - ): Either { - return either { getCurrency(userWalletId, id) } - } - - /** - * Returns specific cryptocurrency for a given user wallet. - * - * !!! Important Use only [CryptoCurrency.ID.value] as cryptoCurrencyId - * - * @param userWalletId The ID of the user's wallet. - * @param cryptoCurrencyId String representation of the [CryptoCurrency.ID.value] of the cryptocurrency. - * @return An [Either] representing success (Right) or an error (Left) in fetching the status. - */ - suspend operator fun invoke( - userWalletId: UserWalletId, - cryptoCurrencyId: String, - ): Either { - return either { getCurrency(userWalletId, cryptoCurrencyId) } - } - /** * Returns the primary cryptocurrency for a given user wallet. * @@ -76,18 +46,6 @@ class GetCryptoCurrencyUseCase( return either { getPrimaryCurrency(userWalletId) } } - private suspend fun Raise.getCurrency( - userWalletId: UserWalletId, - id: CryptoCurrency.ID, - ): CryptoCurrency { - return catch( - block = { - currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id) - }, - catch = { raise(CurrencyStatusError.DataError(it)) }, - ) - } - private suspend fun Raise.getCurrency( userWalletId: UserWalletId, id: String, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index 34f4ff9f39..622c7055ba 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -19,6 +19,7 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.tokens.GetCryptoCurrencyUseCase import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.onramp.component.OnrampSuccessComponent import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.success.entity.OnrampSuccessClickIntents @@ -42,6 +43,7 @@ internal class OnrampSuccessComponentModel @Inject constructor( private val getOnrampStatusUseCase: GetOnrampStatusUseCase, private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val messageSender: UiMessageSender, private val router: Router, @@ -79,17 +81,21 @@ internal class OnrampSuccessComponentModel @Inject constructor( } private suspend fun loadTransactionStatus(transaction: OnrampTransaction) { - val multiCryptoCurrency = getCryptoCurrencyUseCase( - transaction.userWalletId, - transaction.toCurrencyId, - ).getOrNull() - - val cryptoCurrency = multiCryptoCurrency - ?: getCryptoCurrencyUseCase.invoke(transaction.userWalletId).getOrElse { - Timber.e("Crypto currency not found") - showErrorAlert(OnrampError.DomainError(null)) - return - } + val userWallet = getUserWalletUseCase(transaction.userWalletId).getOrNull() + if (userWallet == null) { + Timber.e("UserWallet found") + // this case should never happened + showErrorAlert(OnrampError.DomainError("UserWallet not found")) + return + } + val cryptoCurrency = getCryptoCurrencyUseCase( + userWallet = userWallet, + cryptoCurrencyId = transaction.toCurrencyId, + ).getOrElse { + Timber.e("Crypto currency not found") + showErrorAlert(OnrampError.DomainError(null)) + return + } getOnrampStatusUseCase(txId = transaction.txId) .fold( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt index 55e0edc294..a316c282f7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt @@ -419,7 +419,10 @@ internal class SendModel @Inject constructor( return filterNot { it.isLocked } .mapNotNull { wallet -> val addresses = if (!wallet.isMultiCurrency) { - getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let { + getCryptoCurrencyUseCase( + userWallet = wallet, + cryptoCurrencyId = cryptoCurrency.id.value, + ).getOrNull()?.let { if (it.network.id == cryptoCurrency.network.id) { getNetworkAddressesUseCase.invokeSync(wallet.walletId, it.network) } else { From a49564f6d0fe45bcba0eb01cd1d8d9db4cfc424b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 20 Mar 2025 08:37:33 +0000 Subject: [PATCH 017/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 856feac4bb..85b8e9a1f8 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "release-app_5.22-985" +tangemBlockchainSdk = "develop-986" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.22-440" +tangemCardSdk = "develop-444" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 9c6d5fc4fac81fbcfb7eaa93573bf55deb93b516 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Mar 2025 11:56:37 +0500 Subject: [PATCH 018/138] Updated on 2026-08-14 --- .../response/model/YieldBalanceWrapperDTO.kt | 13 +++++- .../PendingActionConstraintsConverter.kt | 20 +++++++++ .../token/converter/YieldBalanceConverter.kt | 2 + .../staking/model/stakekit/YieldBalance.kt | 22 ++++++++++ .../InvalidatePendingTransactionsUseCase.kt | 1 + .../impl/presentation/model/StakingModel.kt | 39 ++++++++++++----- .../state/InnerYieldBalanceState.kt | 18 +++++--- .../converters/YieldBalancesConverter.kt | 42 +++++++++++------- .../state/events/StakingAlertUM.kt | 13 ++++++ .../state/events/StakingEventFactory.kt | 11 +++++ .../previewdata/InitialStakingStatePreview.kt | 12 ++++-- .../SetConfirmationStateInitTransformer.kt | 43 +++++++++---------- .../AddStakingNotificationsTransformer.kt | 4 +- .../ui/StakingInitialInfoContent.kt | 17 +++++--- .../TokenDetailsStakingInfoConverter.kt | 8 ++-- 15 files changed, 192 insertions(+), 73 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConstraintsConverter.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt index a83ebdd931..121730cdcf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt @@ -3,6 +3,7 @@ package com.tangem.datasource.api.stakekit.models.response.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.tangem.datasource.api.stakekit.models.request.Address +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO.PendingAction.PendingActionArgs.Amount import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO import org.joda.time.DateTime import java.math.BigDecimal @@ -31,6 +32,8 @@ data class BalanceDTO( val pricePerShare: BigDecimal, @Json(name = "pendingActions") val pendingActions: List, + @Json(name = "pendingActionConstraints") + val pendingActionConstraints: List?, @Json(name = "token") val tokenDTO: TokenDTO, @Json(name = "validatorAddress") @@ -99,7 +102,7 @@ data class BalanceDTO( @JsonClass(generateAdapter = true) data class Amount( @Json(name = "required") - val required: Boolean, + val required: Boolean = true, @Json(name = "minimum") val minimum: BigDecimal?, @Json(name = "maximum") @@ -136,6 +139,14 @@ data class BalanceDTO( } } + @JsonClass(generateAdapter = true) + data class PendingActionConstraints( + @Json(name = "type") + val type: StakingActionTypeDTO, + @Json(name = "amount") + val amountArg: Amount, + ) + @JsonClass(generateAdapter = true) data class Required( @Json(name = "required") diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConstraintsConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConstraintsConverter.kt new file mode 100644 index 0000000000..2284023080 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConstraintsConverter.kt @@ -0,0 +1,20 @@ +package com.tangem.datasource.local.token.converter + +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.staking.model.stakekit.PendingActionConstraints +import com.tangem.utils.converter.Converter + +internal object PendingActionConstraintsConverter : + Converter { + override fun convert(value: BalanceDTO.PendingActionConstraints): PendingActionConstraints { + return PendingActionConstraints( + type = StakingActionTypeConverter.convert(value.type), + amountArg = PendingAction.PendingActionArgs.Amount( + required = value.amountArg.required, + minimum = value.amountArg.minimum, + maximum = value.amountArg.maximum, + ), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt index 6fc465801a..051e63780e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt @@ -38,6 +38,8 @@ internal class YieldBalanceConverter( pendingActions = PendingActionConverter .convertList(item.pendingActions) .sortedBy { it.passthrough }, + pendingActionsConstraints = PendingActionConstraintsConverter + .convertList(item.pendingActionConstraints.orEmpty()), isPending = false, ) } diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index 7456b68af6..8856f70ef3 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -63,9 +63,15 @@ data class BalanceItem( val validatorAddress: String?, val date: DateTime?, val pendingActions: List, + val pendingActionsConstraints: List, val isPending: Boolean, ) +data class PendingActionConstraints( + val type: StakingActionType, + val amountArg: PendingAction.PendingActionArgs.Amount?, +) + data class PendingAction( val type: StakingActionType, val passthrough: String, @@ -135,5 +141,21 @@ enum class BalanceType(val order: Int) { enum class RewardBlockType { NoRewards, Rewards, + RewardsRequirementsError, RewardUnavailable, + ; + + /** + * Indicated whether action can be performed on available reward + * For example, pending action CLAIM_REWARDS could be called + */ + val isActionable: Boolean + get() = when (this) { + NoRewards, + RewardUnavailable, + -> false + RewardsRequirementsError, + Rewards, + -> true + } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt index 29e7148ee2..45892df5b3 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt @@ -88,6 +88,7 @@ class InvalidatePendingTransactionsUseCase( validatorAddress = action.validatorAddress ?: action.validatorAddresses?.get(0) ?: "", date = null, pendingActions = emptyList(), + pendingActionsConstraints = emptyList(), isPending = true, ), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 38987cadd6..2c3eda379d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -16,6 +16,8 @@ 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.navigation.share.ShareManager +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -508,19 +510,34 @@ internal class StakingModel @Inject constructor( analyticsEventHandler.send(StakingAnalyticsEvent.ButtonRewards) val rewardsValidators = stateController.value.rewardsValidatorsState as? StakingStates.RewardsValidatorsState.Data - val rewards = rewardsValidators?.rewards - if (rewards != null && rewards.isSingleItem()) { - onActiveStake(rewards.first()) - } else { - analyticsEventHandler.send( - StakingAnalyticsEvent.ButtonValidator( - source = StakeScreenSource.Info, - ), + + val initialInfoState = stateController.value.initialInfoState as? StakingStates.InitialInfoState.Data + val yieldBalance = initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data + val rewardBlockType = yieldBalance?.reward?.rewardBlockType + val rewardPendingActionConstraints = yieldBalance?.reward?.rewardConstraints + + if (rewardBlockType == RewardBlockType.RewardsRequirementsError) { + stakingEventFactory.createStakingRewardsMinimumRequirementsErrorAlert( + cryptoCurrencyName = cryptoCurrencyStatus.currency.name, + cryptoAmountValue = rewardPendingActionConstraints?.amountArg?.minimum?.format { + crypto(cryptoCurrencyStatus.currency) + }.orEmpty(), ) - stateController.update { - value.copy(actionType = StakingActionCommonType.Pending.Rewards) + } else { + val rewards = rewardsValidators?.rewards + if (rewards != null && rewards.isSingleItem()) { + onActiveStake(rewards.first()) + } else { + analyticsEventHandler.send( + StakingAnalyticsEvent.ButtonValidator( + source = StakeScreenSource.Info, + ), + ) + stateController.update { + value.copy(actionType = StakingActionCommonType.Pending.Rewards) + } + stakingStateRouter.showRewardsValidators() } - stakingStateRouter.showRewardsValidators() } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index f795c3195d..db102751d3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -2,18 +2,14 @@ package com.tangem.features.staking.impl.presentation.state import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.PendingAction -import com.tangem.domain.staking.model.stakekit.RewardBlockType -import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.* import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal +@Immutable internal sealed class InnerYieldBalanceState { data class Data( - val rewardsCrypto: String, - val rewardsFiat: String, - val rewardBlockType: RewardBlockType, + val reward: YieldReward, val isActionable: Boolean, val balances: ImmutableList, ) : InnerYieldBalanceState() @@ -37,4 +33,12 @@ internal data class BalanceState( val validator: Yield.Validator?, val pendingActions: ImmutableList, val isPending: Boolean, +) + +@Immutable +internal data class YieldReward( + val rewardsCrypto: String, + val rewardsFiat: String, + val rewardBlockType: RewardBlockType, + val rewardConstraints: PendingActionConstraints?, ) \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index ed5cb2cdad..02a6356ad2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -6,8 +6,10 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.* +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.features.staking.impl.presentation.state.YieldReward import com.tangem.lib.crypto.BlockchainUtils.isBSC import com.tangem.lib.crypto.BlockchainUtils.isSolana import com.tangem.utils.Provider @@ -33,7 +35,8 @@ internal class YieldBalancesConverter( val balanceToShowItems = balancesToShowProvider() return if (yieldBalance is YieldBalance.Data || balanceToShowItems.any { it.isPending }) { - val cryptoRewardsValue = (yieldBalance as? YieldBalance.Data)?.getRewardStakingBalance() + val yieldBalance = yieldBalance as? YieldBalance.Data + val cryptoRewardsValue = yieldBalance?.getRewardStakingBalance() val fiatRate = cryptoCurrencyStatus.value.fiatRate val fiatRewardsValue = if (fiatRate != null && cryptoRewardsValue != null) { @@ -41,17 +44,25 @@ internal class YieldBalancesConverter( } else { null } - val (type, isActionable) = getRewardBlockType() + val type = getRewardBlockType() + val pendingRewardsConstraints = yieldBalance?.balance?.items + ?.firstOrNull { it.type == BalanceType.REWARDS } + ?.pendingActionsConstraints + ?.firstOrNull { it.type == StakingActionType.CLAIM_REWARDS } + InnerYieldBalanceState.Data( - rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) }, - rewardsFiat = fiatRewardsValue.format { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - }, - rewardBlockType = type, - isActionable = isActionable, + reward = YieldReward( + rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) }, + rewardsFiat = fiatRewardsValue.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + rewardBlockType = type, + rewardConstraints = pendingRewardsConstraints, + ), + isActionable = type.isActionable, balances = balanceToShowItems.mapBalances(), ) } else { @@ -66,7 +77,7 @@ internal class YieldBalancesConverter( .sortedBy { it.type.order } .toPersistentList() - private fun getRewardBlockType(): Pair { + private fun getRewardBlockType(): RewardBlockType { val blockchainId = cryptoCurrencyStatus.currency.network.id.value val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data val rewards = yieldBalance?.balance?.items @@ -76,9 +87,10 @@ internal class YieldBalancesConverter( val isRewardsClaimable = rewards?.isNotEmpty() == true return when { - isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable to false - isRewardsClaimable -> RewardBlockType.Rewards to isActionable - else -> RewardBlockType.NoRewards to false + isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable + isRewardsClaimable && isActionable -> RewardBlockType.Rewards + isRewardsClaimable && !isActionable -> RewardBlockType.RewardsRequirementsError + else -> RewardBlockType.NoRewards } } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt index a9b1445d41..661b19506c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt @@ -68,4 +68,17 @@ internal sealed class StakingAlertUM : AlertUM { override val message: TextReference = resourceReference(id = R.string.staking_notification_ton_activate_account) override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) } + + data class RewardsMinimumRequirementsError( + val cryptoCurrencyName: String, + val cryptoAmountValue: String, + ) : StakingAlertUM() { + override val onConfirmClick: (() -> Unit)? = null + override val title: TextReference? = null + override val message: TextReference = resourceReference( + id = R.string.staking_details_min_rewards_notification, + formatArgs = wrappedList(cryptoCurrencyName, cryptoAmountValue), + ) + override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt index c206124f55..bb1b446118 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt @@ -51,4 +51,15 @@ internal class StakingEventFactory( val alert = StakingEvent.ShowAlert(alert = StakingAlertUM.InitializeAccount) stateController.updateEvent(alert) } + + fun createStakingRewardsMinimumRequirementsErrorAlert(cryptoCurrencyName: String, cryptoAmountValue: String) { + stateController.updateEvent( + StakingEvent.ShowAlert( + alert = StakingAlertUM.RewardsMinimumRequirementsError( + cryptoCurrencyName = cryptoCurrencyName, + cryptoAmountValue = cryptoAmountValue, + ), + ), + ) + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 0aa07c6011..1c9a799920 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -1,9 +1,9 @@ package com.tangem.features.staking.impl.presentation.state.previewdata +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.domain.staking.model.stakekit.Yield @@ -11,6 +11,7 @@ import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.YieldReward import kotlinx.collections.immutable.persistentListOf internal object InitialStakingStatePreview { @@ -63,9 +64,12 @@ internal object InitialStakingStatePreview { val stateWithYield = defaultState.copy( yieldBalance = InnerYieldBalanceState.Data( - rewardsFiat = "100 $", - rewardsCrypto = "100 SOL", - rewardBlockType = RewardBlockType.RewardUnavailable, + reward = YieldReward( + rewardsFiat = "100 $", + rewardsCrypto = "100 SOL", + rewardBlockType = RewardBlockType.RewardUnavailable, + rewardConstraints = null, + ), isActionable = true, balances = persistentListOf( BalanceState( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt index 29c70b9c15..ab97ccdcdf 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt @@ -43,28 +43,7 @@ internal class SetConfirmationStateInitTransformer( @Suppress("CyclomaticComplexMethod") override fun transform(prevState: StakingUiState): StakingUiState { - val actionType = when { - isEnter -> StakingActionCommonType.Enter( - skipEnterAmount = yieldArgs.enter.isPartialAmountDisabled, - ) - isImplicitExit || isExplicitExit -> StakingActionCommonType.Exit(isPartiallyUnstakeDisabled(prevState)) - else -> when (pendingAction?.type) { - StakingActionType.STAKE -> StakingActionCommonType.Pending.Stake( - skipEnterAmount = yieldArgs.enter.isPartialAmountDisabled, - ) - StakingActionType.UNSTAKE -> StakingActionCommonType.Exit( - partiallyUnstakeDisabled = isPartiallyUnstakeDisabled(prevState), - ) - StakingActionType.CLAIM_REWARDS, - StakingActionType.RESTAKE_REWARDS, - -> StakingActionCommonType.Pending.Rewards - StakingActionType.VOTE_LOCKED, - StakingActionType.RESTAKE, - -> StakingActionCommonType.Pending.Restake - else -> StakingActionCommonType.Pending.Other - } - } - + val actionType = getActionType(prevState) return prevState.copy( actionType = actionType, balanceState = balanceState, @@ -88,6 +67,26 @@ internal class SetConfirmationStateInitTransformer( ) } + private fun getActionType(prevState: StakingUiState): StakingActionCommonType { + val isPartialEnterAmountDisabled = yieldArgs.enter.isPartialAmountDisabled + val isPartialExitAmountDisabled = isPartiallyUnstakeDisabled(prevState) + return when { + isEnter -> StakingActionCommonType.Enter(isPartialEnterAmountDisabled) + isImplicitExit || isExplicitExit -> StakingActionCommonType.Exit(isPartialExitAmountDisabled) + else -> when (pendingAction?.type) { + StakingActionType.STAKE -> StakingActionCommonType.Pending.Stake(isPartialEnterAmountDisabled) + StakingActionType.UNSTAKE -> StakingActionCommonType.Exit(isPartialExitAmountDisabled) + StakingActionType.CLAIM_REWARDS, + StakingActionType.RESTAKE_REWARDS, + -> StakingActionCommonType.Pending.Rewards + StakingActionType.VOTE_LOCKED, + StakingActionType.RESTAKE, + -> StakingActionCommonType.Pending.Restake + else -> StakingActionCommonType.Pending.Other + } + } + } + private fun isPartiallyUnstakeDisabled(state: StakingUiState): Boolean { val isSolana = BlockchainUtils.isSolana(state.cryptoCurrencyBlockchainId) val isValidatorPreferred = balanceState?.validator?.preferred == true diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index f82dff2923..2ac8c6e2ef 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -98,9 +98,7 @@ internal class AddStakingNotificationsTransformer( onReload = prevState.clickIntents::getFee, feeValue = feeValue, ) - addStakingErrorNotifications( - stakingError = stakingError, - ) + addStakingErrorNotifications(stakingError = stakingError) // warnings addWarningNotifications( prevState = prevState, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index a815b1b370..5e8683d6a9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.unit.Density import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.components.list.roundedListWithDividersItems @@ -38,19 +39,18 @@ import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.previewdata.InitialStakingStatePreview import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub -import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.extensions.orZero @@ -214,14 +214,17 @@ private fun StakingRewardBlock( onRewardsClick: () -> Unit, isBalanceHidden: Boolean, ) { - val (text, textColor) = when (yieldBalanceState.rewardBlockType) { - RewardBlockType.Rewards -> { + val reward = yieldBalanceState.reward + val (text, textColor) = when (yieldBalanceState.reward.rewardBlockType) { + RewardBlockType.RewardsRequirementsError, + RewardBlockType.Rewards, + -> { annotatedReference { - append(yieldBalanceState.rewardsFiat.orMaskWithStars(isBalanceHidden)) + append(reward.rewardsFiat.orMaskWithStars(isBalanceHidden)) appendSpace() append(DOT) appendSpace() - append(yieldBalanceState.rewardsCrypto.orMaskWithStars(isBalanceHidden)) + append(reward.rewardsCrypto.orMaskWithStars(isBalanceHidden)) } to TangemTheme.colors.text.primary1 } RewardBlockType.RewardUnavailable -> { @@ -232,7 +235,7 @@ private fun StakingRewardBlock( resourceReference(R.string.staking_details_no_rewards_to_claim) to TangemTheme.colors.text.tertiary } } - val isShowIcon = yieldBalanceState.rewardBlockType == RewardBlockType.Rewards && yieldBalanceState.isActionable + val isShowIcon = reward.rewardBlockType == RewardBlockType.Rewards && yieldBalanceState.isActionable InputRowDefault( title = resourceReference(R.string.staking_rewards), text = text, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt index ef4a99cb22..72d2697d5e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt @@ -155,7 +155,11 @@ internal class TokenDetailsStakingInfoConverter( } return when (rewardBlockType) { - RewardBlockType.Rewards -> resourceReference( + RewardBlockType.NoRewards -> resourceReference(R.string.staking_details_no_rewards_to_claim) + RewardBlockType.RewardUnavailable -> TextReference.EMPTY + RewardBlockType.RewardsRequirementsError, + RewardBlockType.Rewards, + -> resourceReference( R.string.staking_details_rewards_to_claim, wrappedList( stakingRewardAmount.format { @@ -166,8 +170,6 @@ internal class TokenDetailsStakingInfoConverter( }, ), ) - RewardBlockType.NoRewards -> resourceReference(R.string.staking_details_no_rewards_to_claim) - RewardBlockType.RewardUnavailable -> TextReference.EMPTY } } } \ No newline at end of file From 9e948f77036c196a8b999b74ad47aff451651979 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 20 Mar 2025 12:46:07 +0500 Subject: [PATCH 019/138] Updated on 2026-08-14 --- .../java/com/tangem/core/ui/decorations/RoundedDecorations.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt index 02615d9faa..9b16d77616 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt @@ -32,7 +32,7 @@ fun Modifier.roundedShapeItemDecoration( val applyShape: Modifier.(shape: RoundedCornerShape?) -> Modifier = { shape -> if (backgroundColor != null) { if (shape != null) { - background(color = backgroundColor, shape = shape) + clip(shape).background(color = backgroundColor, shape = shape) } else { background(color = backgroundColor) } From 147625bda1e5a742172283dea238868e1c0c19dc Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 20 Mar 2025 14:15:57 +0200 Subject: [PATCH 020/138] Updated on 2026-08-14 --- .../common/analytics/AnalyticsEventsLogger.kt | 6 +-- .../tap/common/analytics/events/Push.kt | 1 - .../tap/common/analytics/events/Settings.kt | 12 ++---- .../tap/common/analytics/events/SignIn.kt | 3 +- .../tap/common/analytics/events/Token.kt | 3 +- .../common/analytics/events/WalletConnect.kt | 3 +- .../amplitude/AmplitudeAnalyticsHandler.kt | 8 +++- .../firebase/FirebaseAnalyticsHandler.kt | 39 +++++++------------ .../handlers/firebase/FirebaseClient.kt | 16 ++++---- .../handlers/firebase/FirebaseLogClient.kt | 4 +- .../tangem/tap/data/DefaultCardSdkProvider.kt | 38 +++++++++++++++--- .../tangem/tap/data/TangemSdkWarningEvent.kt | 9 ----- .../tangem/tap/di/routing/AppRouterModule.kt | 12 +++++- .../domain/scanCard/UseCaseScanProcessor.kt | 15 ++++++- .../sdk/impl/DefaultTangemSdkManager.kt | 9 ++++- .../securitymode/model/SecurityModeModel.kt | 15 +++++-- .../redux/OnboardingWalletMiddleware.kt | 6 +-- .../com/tangem/tap/routing/ProxyAppRouter.kt | 6 ++- .../core/analytics/models/AnalyticsEvent.kt | 1 - .../com/tangem/core/analytics/models/Basic.kt | 8 +--- .../models/ExceptionAnalyticsEvent.kt | 8 ++++ .../com/tangem/core/analytics/Analytics.kt | 24 +++++++++++- .../tangem/core/analytics/api/ClientApi.kt | 6 ++- .../core/analytics/api/EventHandlerApi.kt | 16 +++++--- .../core/analytics/di/AnalyticsModule.kt | 14 +++++++ .../analytics/TokenExchangeAnalyticsEvent.kt | 2 +- .../analytics/TokenOnrampAnalyticsEvent.kt | 2 +- .../analytics/TokenReceiveAnalyticsEvent.kt | 2 +- .../analytics/TokenScreenAnalyticsEvent.kt | 3 +- .../backup/model/MultiWalletBackupModel.kt | 6 ++- .../feature/swap/analytics/SwapEvents.kt | 1 + .../walletsettings/analytics/Settings.kt | 3 +- .../analytics/WalletScreenAnalyticsEvent.kt | 3 +- gradle/dependencies.toml | 2 +- libs/blockchain-sdk/build.gradle.kts | 1 + .../di/ExcludedBlockchainsModule.kt | 10 +++++ .../BlockchainProvidersResponseLoader.kt | 3 +- .../BlockchainProvidersResponseMerger.kt | 24 +++++++----- .../BlockchainProvidersResponseLoaderTest.kt | 8 ++++ .../BlockchainProvidersResponseMergerTest.kt | 33 ++++++++++------ 40 files changed, 254 insertions(+), 131 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/data/TangemSdkWarningEvent.kt create mode 100644 core/analytics/models/src/main/java/com/tangem/core/analytics/models/ExceptionAnalyticsEvent.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt index 39291d12f6..29b584fa05 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt @@ -1,20 +1,20 @@ package com.tangem.tap.common.analytics import com.tangem.common.json.MoshiJsonConverter -import com.tangem.core.analytics.api.ErrorEventLogger +import com.tangem.core.analytics.api.ExceptionLogger import com.tangem.core.analytics.api.EventLogger import timber.log.Timber class AnalyticsEventsLogger( private val name: String, private val jsonConverter: MoshiJsonConverter, -) : EventLogger, ErrorEventLogger { +) : EventLogger, ExceptionLogger { override fun logEvent(event: String, params: Map) { Timber.d(jsonConverter.prettyPrint(PrintEventModel(name, event, params))) } - override fun logErrorEvent(error: Throwable, params: Map) { + override fun logException(error: Throwable, params: Map) { Timber.e(error, jsonConverter.prettyPrint(PrintEventModel(name, "error", params))) } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt index 46c647b06f..c4345c597f 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt @@ -6,7 +6,6 @@ internal sealed class Push(event: String) : AnalyticsEvent( category = "Push", event = event, params = emptyMap(), - error = null, ) { data object PushNotificationOpened : Push(event = "Push Notification Opened") diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt index 98b0289289..4234256e8c 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt @@ -9,8 +9,7 @@ sealed class Settings( category: String = "Settings", event: String, params: Map = mapOf(), - error: Throwable? = null, -) : AnalyticsEvent(category, event, params, error) { +) : AnalyticsEvent(category, event, params) { class ScreenOpened : Settings(event = "Settings Screen Opened") class ButtonStartWalletConnectSession : Settings(event = "Button - Start Wallet Connect Session") @@ -19,16 +18,14 @@ sealed class Settings( sealed class CardSettings( event: String, params: Map = mapOf(), - error: Throwable? = null, - ) : Settings("Settings / Card Settings", event, params, error) { + ) : Settings("Settings / Card Settings", event, params) { class ButtonFactoryReset : CardSettings("Button - Factory Reset") - class FactoryResetFinished(cardsCount: Int? = null, error: Throwable? = null) : CardSettings( + class FactoryResetFinished(cardsCount: Int? = null) : CardSettings( event = "Factory Reset Finished", params = buildMap { cardsCount?.let { put("Cards Count", "$it") } }, - error = error, ) class FactoryResetCanceled(cardsCount: Int) : CardSettings( @@ -44,10 +41,9 @@ sealed class Settings( params = mapOf("Type" to type.value), ) - class SecurityModeChanged(mode: AnalyticsParam.SecurityMode, error: Throwable? = null) : CardSettings( + class SecurityModeChanged(mode: AnalyticsParam.SecurityMode) : CardSettings( event = "Security Mode Changed", params = mapOf("Mode" to mode.value), - error = error, ) class AccessCodeRecoveryChanged(status: AnalyticsParam.AccessCodeRecoveryStatus) : CardSettings( diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt index 212969481a..a9a52c5e25 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt @@ -8,8 +8,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent sealed class SignIn( event: String, params: Map = mapOf(), - error: Throwable? = null, -) : AnalyticsEvent("Sign In", event, params, error) { +) : AnalyticsEvent("Sign In", event, params) { class ScreenOpened : SignIn(event = "Sign In Screen Opened") diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt index bf0bdc539a..9f6214a78a 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt @@ -10,8 +10,7 @@ sealed class Token( category: String, event: String, params: Map = mapOf(), - error: Throwable? = null, -) : AnalyticsEvent(category, event, params, error) { +) : AnalyticsEvent(category, event, params) { sealed class Receive( event: String, diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt index 1725eabaaa..a6c39d4f3c 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt @@ -9,8 +9,7 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.O internal sealed class WalletConnect( event: String, params: Map = mapOf(), - error: Throwable? = null, -) : AnalyticsEvent("Wallet Connect", event, params, error) { +) : AnalyticsEvent("Wallet Connect", event, params) { class ScreenOpened : WalletConnect(event = "WC Screen Opened") class NewSessionInitiated(source: SourceType) : WalletConnect( diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt index 2aa36c5413..a618ce78ab 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt @@ -1,11 +1,13 @@ package com.tangem.tap.common.analytics.handlers.amplitude +import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsHandler +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder class AmplitudeAnalyticsHandler( private val client: AmplitudeAnalyticsClient, -) : AnalyticsHandler { +) : AnalyticsHandler, AnalyticsErrorHandler { override fun id(): String = ID @@ -13,6 +15,10 @@ class AmplitudeAnalyticsHandler( client.logEvent(eventId, params) } + override fun sendErrorEvent(event: AnalyticsEvent) { + send(event.id, event.params) + } + companion object { const val ID = "Amplitude" } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt index 10b399382c..298874dcef 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt @@ -1,16 +1,18 @@ package com.tangem.tap.common.analytics.handlers.firebase -import com.google.firebase.analytics.FirebaseAnalytics +import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsHandler -import com.tangem.core.analytics.api.ErrorEventHandler +import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.converters.AnalyticsErrorConverter -import com.tangem.tap.common.analytics.events.Shop class FirebaseAnalyticsHandler( private val client: FirebaseAnalyticsClient, -) : AnalyticsHandler, ErrorEventHandler { +) : AnalyticsHandler, AnalyticsErrorHandler, AnalyticsExceptionHandler { + + private val errorConverter = AnalyticsErrorConverter() override fun id(): String = ID @@ -18,30 +20,17 @@ class FirebaseAnalyticsHandler( client.logEvent(eventId, params) } - override fun send(event: AnalyticsEvent) { - val error = event.error - when { - error != null -> { - val errorConverter = AnalyticsErrorConverter() - if (!errorConverter.canBeHandled(error)) return + override fun sendException(event: ExceptionAnalyticsEvent) { + if (!errorConverter.canBeHandled(event.exception)) return - val errorParams = errorConverter.convert(error).toMutableMap() - errorParams["Category"] = event.category - errorParams["Event"] = event.event - errorParams.putAll(event.params) - send(error, errorParams) - } - event is Shop.Purchased -> { - send(FirebaseAnalytics.Event.PURCHASE, event.params) - } - else -> { - super.send(event) - } - } + val errorParams = errorConverter.convert(event.exception).toMutableMap() + errorParams.putAll(event.params) + + client.logException(event.exception, event.params) } - override fun send(error: Throwable, params: Map) { - client.logErrorEvent(error, params) + override fun sendErrorEvent(event: AnalyticsEvent) { + send(event) } companion object { diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt index 7e4e5085bc..47b97a7dbc 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt @@ -4,14 +4,15 @@ import android.os.Bundle import androidx.core.os.bundleOf import com.google.firebase.analytics.ktx.analytics import com.google.firebase.crashlytics.ktx.crashlytics +import com.google.firebase.crashlytics.recordException import com.google.firebase.ktx.Firebase -import com.tangem.core.analytics.api.ErrorEventLogger +import com.tangem.core.analytics.api.ExceptionLogger import com.tangem.core.analytics.api.EventLogger /** [REDACTED_AUTHOR] */ -interface FirebaseAnalyticsClient : EventLogger, ErrorEventLogger +interface FirebaseAnalyticsClient : EventLogger, ExceptionLogger internal class FirebaseClient : FirebaseAnalyticsClient { @@ -27,11 +28,12 @@ internal class FirebaseClient : FirebaseAnalyticsClient { ) } - override fun logErrorEvent(error: Throwable, params: Map) { - eventConverter.convertEventParams(params) - .forEach { fbCrashlytics.setCustomKey(it.key, it.value) } - - fbCrashlytics.recordException(error) + // TODO [REDACTED_TASK_KEY] fix recordException usages in the app + override fun logException(error: Throwable, params: Map) { + fbCrashlytics.recordException(error) { + eventConverter.convertEventParams(params) + .forEach { key(it.key, it.value) } + } } private fun Map.toBundle(): Bundle = bundleOf(*this.toList().toTypedArray()) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt index e460f9dbf9..4b818bdf51 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt @@ -16,7 +16,7 @@ internal class FirebaseLogClient( logger.logEvent(event, params) } - override fun logErrorEvent(error: Throwable, params: Map) { - logger.logErrorEvent(error, params) + override fun logException(error: Throwable, params: Map) { + logger.logException(error, params) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt index 87267d0c59..4c9bbe446d 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt @@ -10,7 +10,8 @@ import com.tangem.common.authentication.AuthenticationManager import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.Config import com.tangem.common.services.secure.SecureStorage -import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.crypto.bip39.Wordlist import com.tangem.data.card.sdk.CardSdkOwner import com.tangem.data.card.sdk.CardSdkProvider @@ -33,7 +34,7 @@ import javax.inject.Singleton */ @Singleton internal class DefaultCardSdkProvider @Inject constructor( - private val analyticsEventHandler: AnalyticsEventHandler, + private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val dispatchers: CoroutineDispatcherProvider, private val cardSdkFeatureToggles: CardSdkFeatureToggles, ) : CardSdkProvider, CardSdkOwner { @@ -48,7 +49,12 @@ internal class DefaultCardSdkProvider @Inject constructor( override fun register(activity: FragmentActivity) = runBlocking(dispatchers.mainImmediate) { if (activity.isDestroyed || activity.isFinishing || activity.isChangingConfigurations) { val message = "Tangem SDK owner registration skipped: activity is destroyed or finishing" - analyticsEventHandler.send(TangemSdkWarningEvent(message)) + analyticsExceptionHandler.sendException( + ExceptionAnalyticsEvent( + exception = IllegalStateException(message), + params = errorParams, + ), + ) Log.info { message } return@runBlocking } @@ -66,14 +72,24 @@ internal class DefaultCardSdkProvider @Inject constructor( private fun tryToRegisterWithForegroundActivity(): TangemSdk = runBlocking(dispatchers.mainImmediate) { val warning = "Tangem SDK holder is null, trying to recreate it with foreground activity" - analyticsEventHandler.send(TangemSdkWarningEvent(warning)) + analyticsExceptionHandler.sendException( + ExceptionAnalyticsEvent( + exception = IllegalStateException(warning), + params = errorParams, + ), + ) Log.warning { warning } val activity = foregroundActivityObserver.foregroundActivity if (activity == null) { val error = "Tangem SDK holder is null and foreground activity is null" - analyticsEventHandler.send(TangemSdkWarningEvent(error)) + analyticsExceptionHandler.sendException( + ExceptionAnalyticsEvent( + exception = IllegalStateException(error), + params = errorParams, + ), + ) Log.error { error } error(error) } @@ -84,7 +100,12 @@ internal class DefaultCardSdkProvider @Inject constructor( if (sdk == null) { val error = "Tangem SDK is null after re-registering with foreground activity" - analyticsEventHandler.send(TangemSdkWarningEvent(error)) + analyticsExceptionHandler.sendException( + ExceptionAnalyticsEvent( + exception = IllegalStateException(error), + params = errorParams, + ), + ) Log.error { error } error(error) } @@ -174,5 +195,10 @@ internal class DefaultCardSdkProvider @Inject constructor( ), ), ) + + val errorParams = mapOf( + "Category" to "Tangem SDK", + "Event" to "Warning", + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/data/TangemSdkWarningEvent.kt b/app/src/main/java/com/tangem/tap/data/TangemSdkWarningEvent.kt deleted file mode 100644 index 1393bfbf57..0000000000 --- a/app/src/main/java/com/tangem/tap/data/TangemSdkWarningEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.data - -import com.tangem.core.analytics.models.AnalyticsEvent - -internal class TangemSdkWarningEvent(message: String) : AnalyticsEvent( - category = "Tangem SDK", - event = "Warning", - error = IllegalStateException(message), -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt b/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt index 4f4aa7e79b..3bfc0b3a44 100644 --- a/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt +++ b/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di.routing import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.tap.routing.ProxyAppRouter import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.configurator.MutableAppRouterConfig @@ -17,8 +18,15 @@ internal object AppRouterModule { @Provides @Singleton - fun provideAppRouter(config: AppRouterConfig, dispatchers: CoroutineDispatcherProvider): AppRouter = - ProxyAppRouter(config, dispatchers) + fun provideAppRouter( + config: AppRouterConfig, + dispatchers: CoroutineDispatcherProvider, + analyticsExceptionHandler: AnalyticsExceptionHandler, + ): AppRouter = ProxyAppRouter( + config = config, + dispatchers = dispatchers, + analyticsExceptionHandler = analyticsExceptionHandler, + ) @Provides @Singleton diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt index 09fe732b6d..772960fb03 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt @@ -7,6 +7,7 @@ import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.domain.card.ScanCardException import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.StateDialog @@ -34,7 +35,12 @@ internal object UseCaseScanProcessor { ifLeft = { val error = scanCardExceptionConverter.convertBack(it) - Analytics.send(Basic.ScanError(error)) + Analytics.sendException( + ExceptionAnalyticsEvent( + exception = error, + params = mapOf("Event" to "Scan"), + ), + ) CompletionResult.Failure(error) }, ifRight = { CompletionResult.Success(it) }, @@ -97,7 +103,12 @@ internal object UseCaseScanProcessor { -> { val error = scanCardExceptionConverter.convertBack(exception) - Analytics.send(Basic.ScanError(error)) + Analytics.sendException( + ExceptionAnalyticsEvent( + exception = error, + params = mapOf("Event" to "Scan"), + ), + ) onFailure(error) } } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index abf3ce3f3c..bc1616ef60 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -14,7 +14,7 @@ import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.services.secure.SecureStorage import com.tangem.common.usersCode.UserCodeRepository import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.res.getStringSafe import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.hdWallet.DerivationPath @@ -224,7 +224,12 @@ internal class DefaultTangemSdkManager( private fun sendScanResultsToAnalytics(result: CompletionResult) { if (result is CompletionResult.Failure) { (result.error as? TangemSdkError)?.let { error -> - Analytics.send(Basic.ScanError(error)) + Analytics.sendException( + ExceptionAnalyticsEvent( + exception = error, + params = mapOf("Event" to "Scan"), + ), + ) } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/model/SecurityModeModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/model/SecurityModeModel.kt index 39ae0e13fb..6568f05a99 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/model/SecurityModeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/model/SecurityModeModel.kt @@ -4,7 +4,9 @@ import androidx.compose.runtime.Stable import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.routing.AppRouter -import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.common.util.cardTypesResolver @@ -30,6 +32,8 @@ internal class SecurityModeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val tangemSdkManager: TangemSdkManager, private val cardSettingsInteractor: CardSettingsInteractor, + private val analyticsEventHandler: AnalyticsEventHandler, + private val analyticsExceptionHandler: AnalyticsExceptionHandler, ) : Model() { private val scannedScanResponse = cardSettingsInteractor.scannedScanResponse.value @@ -85,14 +89,19 @@ internal class SecurityModeModel @Inject constructor( val paramValue = AnalyticsParam.SecurityMode.from(selectedOption) when (result) { is CompletionResult.Success -> { - Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue)) + analyticsEventHandler.send(Settings.CardSettings.SecurityModeChanged(paramValue)) store.dispatchNavigationAction(AppRouter::pop) } is CompletionResult.Failure -> { val error = result.error if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) { - Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue, error)) + analyticsExceptionHandler.sendException( + ExceptionAnalyticsEvent( + exception = error, + params = mapOf("Event" to "Security Mode Changed"), + ), + ) } } else -> Unit diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 310cf7bc01..984f638acc 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -1,7 +1,6 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import android.net.Uri -import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.common.CompletionResult import com.tangem.common.card.Card import com.tangem.common.core.TangemSdkError @@ -12,6 +11,7 @@ import com.tangem.common.routing.AppRouter import com.tangem.common.services.Result import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList @@ -454,8 +454,6 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) store.dispatchOnMain(BackupAction.AddBackupCard.Success(result.data)) } is CompletionResult.Failure -> { - val crashlytics = FirebaseCrashlytics.getInstance() - when (val error = result.error) { is TangemSdkError.CardVerificationFailed -> { Analytics.send( @@ -492,7 +490,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) GlobalAction.ShowDialog(BackupDialog.AttestationFailed), ) } - else -> crashlytics.recordException(error) + else -> Analytics.sendException(ExceptionAnalyticsEvent(error)) } } } diff --git a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt index d744032000..bcebe2fe83 100644 --- a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt +++ b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt @@ -1,8 +1,9 @@ package com.tangem.tap.routing -import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.decompose.navigation.Router import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -15,6 +16,7 @@ import kotlin.reflect.KClass internal class ProxyAppRouter( private val config: AppRouterConfig, private val dispatchers: CoroutineDispatcherProvider, + private val analyticsExceptionHandler: AnalyticsExceptionHandler, ) : AppRouter { private val routerScope: CoroutineScope @@ -76,7 +78,7 @@ internal class ProxyAppRouter( override fun defaultCompletionHandler(isSuccess: Boolean, errorMessage: String) { if (!isSuccess) { - FirebaseCrashlytics.getInstance().recordException(RuntimeException(errorMessage)) + analyticsExceptionHandler.sendException(ExceptionAnalyticsEvent(RuntimeException(errorMessage))) Timber.w(errorMessage) with(receiver = config.snackbarHandler ?: return) { diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt index 025a6c1038..8c12f046df 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt @@ -7,7 +7,6 @@ open class AnalyticsEvent( val category: String, val event: String, var params: Map = mapOf(), - val error: Throwable? = null, ) { val id: String = "[$category] $event" diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt index c623358a84..8f2960f7a1 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt @@ -3,8 +3,7 @@ package com.tangem.core.analytics.models sealed class Basic( event: String, params: Map = mapOf(), - error: Throwable? = null, -) : AnalyticsEvent("Basic", event, params, error) { +) : AnalyticsEvent("Basic", event, params) { class CardWasScanned( source: AnalyticsParam.ScreensSources, @@ -73,11 +72,6 @@ sealed class Basic( } } - class ScanError(error: Throwable) : Basic( - event = "Scan", - error = error, - ) - class ButtonSupport(source: AnalyticsParam.ScreensSources) : Basic( event = "Request Support", params = mapOf( diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/ExceptionAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/ExceptionAnalyticsEvent.kt new file mode 100644 index 0000000000..82372912e5 --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/ExceptionAnalyticsEvent.kt @@ -0,0 +1,8 @@ +package com.tangem.core.analytics.models + +class ExceptionAnalyticsEvent( + val exception: Throwable, + // val category: String, + // val event: String, + var params: Map = mapOf(), +) \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt index 239573bed5..081a583c06 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt @@ -2,6 +2,7 @@ package com.tangem.core.analytics import com.tangem.core.analytics.api.* import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex @@ -16,7 +17,9 @@ interface GlobalAnalyticsEventHandler : AnalyticsEventHandler, AnalyticsHandlerHolder, AnalyticsFilterHolder, - ParamsInterceptorHolder + ParamsInterceptorHolder, + AnalyticsErrorHandler, + AnalyticsExceptionHandler object Analytics : GlobalAnalyticsEventHandler { @@ -72,6 +75,25 @@ object Analytics : GlobalAnalyticsEventHandler { } } + override fun sendErrorEvent(event: AnalyticsEvent) { + analyticsScope.launch { + event.params = applyParamsInterceptors(event) + analyticsMutex.withLock { + analyticsHandlers.filterIsInstance() + .forEach { handler -> handler.sendErrorEvent(event) } + } + } + } + + override fun sendException(event: ExceptionAnalyticsEvent) { + analyticsScope.launch { + analyticsMutex.withLock { + analyticsHandlers.filterIsInstance() + .forEach { it.sendException(event) } + } + } + } + private suspend fun applyParamsInterceptors(event: AnalyticsEvent): MutableMap { val interceptedParams = event.params.toMutableMap() analyticsMutex.withLock { diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/ClientApi.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/ClientApi.kt index f44a0133cd..523ac41b71 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/api/ClientApi.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/ClientApi.kt @@ -8,5 +8,9 @@ interface EventLogger { } interface ErrorEventLogger { - fun logErrorEvent(error: Throwable, params: Map = emptyMap()) + fun logErrorEvent(event: String, params: Map = emptyMap()) +} + +interface ExceptionLogger { + fun logException(error: Throwable, params: Map = emptyMap()) } \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt index 7bedfebff3..6c98379d6d 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt @@ -1,15 +1,25 @@ package com.tangem.core.analytics.api import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent /** -[REDACTED_AUTHOR] + * Sends usual analytics events */ interface AnalyticsEventHandler { fun send(event: AnalyticsEvent) } +interface AnalyticsErrorHandler { + fun sendErrorEvent(event: AnalyticsEvent) +} + +interface AnalyticsExceptionHandler { + fun sendException(event: ExceptionAnalyticsEvent) +} + interface AnalyticsHandler : AnalyticsEventHandler { + fun id(): String fun send(eventId: String, params: Map = emptyMap()) @@ -19,10 +29,6 @@ interface AnalyticsHandler : AnalyticsEventHandler { } } -interface ErrorEventHandler { - fun send(error: Throwable, params: Map = emptyMap()) -} - interface AnalyticsHandlerHolder { fun addHandler(name: String, handler: AnalyticsHandler) fun removeHandler(name: String): AnalyticsHandler? diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt b/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt index 18a95d3a02..de53ef6a99 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt @@ -1,7 +1,9 @@ package com.tangem.core.analytics.di import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.api.ParamsInterceptorHolder import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.domain.analytics.repository.AnalyticsRepository @@ -21,6 +23,18 @@ internal object AnalyticsModule { return Analytics // todo replace after refactoring calling Analytics in whole project } + @Singleton + @Provides + fun provideAnalyticsErrorHandler(): AnalyticsErrorHandler { + return Analytics + } + + @Singleton + @Provides + fun provideAnalyticsExceptionHandler(): AnalyticsExceptionHandler { + return Analytics + } + @Singleton @Provides fun provideParamsInterceptorHolder(): ParamsInterceptorHolder { diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt index 7fe8dfdf64..4d139661e9 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt @@ -9,7 +9,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM class TokenExchangeAnalyticsEvent( event: String, params: Map = mapOf(), -) : AnalyticsEvent("Token", event, params, null) { +) : AnalyticsEvent("Token", event, params) { class CexTxStatusOpened(token: String) : TokenScreenAnalyticsEvent( event = "Swap Status Opened", diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt index 69ba4f794b..ef07a4082d 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt @@ -8,7 +8,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM class TokenOnrampAnalyticsEvent( event: String, params: Map = mapOf(), -) : AnalyticsEvent("Onramp", event, params, null) { +) : AnalyticsEvent("Onramp", event, params) { class OnrampStatusOpened(tokenSymbol: String, provider: String, fiatCurrency: String) : TokenScreenAnalyticsEvent( event = "Onramp Status Opened", diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveAnalyticsEvent.kt index 45d43689a8..7de884bf84 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveAnalyticsEvent.kt @@ -6,7 +6,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM sealed class TokenReceiveAnalyticsEvent( event: String, params: Map = mapOf(), -) : AnalyticsEvent("Token / Receive", event, params, null) { +) : AnalyticsEvent("Token / Receive", event, params) { class ReceiveScreenOpened(token: String) : TokenReceiveAnalyticsEvent( event = "Receive Screen Opened", diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index 9e8a010f52..844923dee0 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -13,8 +13,7 @@ import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason sealed class TokenScreenAnalyticsEvent( event: String, params: Map = mapOf(), - error: Throwable? = null, -) : AnalyticsEvent("Token", event, params, error) { +) : AnalyticsEvent("Token", event, params) { /** Legacy event. It has a unique category, but it also is sent on TokenScreen */ class DetailsScreenOpened(token: String) : AnalyticsEvent( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt index 94dcb94eba..e9be6ab78d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt @@ -1,11 +1,12 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model import androidx.compose.runtime.Stable -import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -48,6 +49,7 @@ class MultiWalletBackupModel @Inject constructor( private val cardSdkConfigRepository: CardSdkConfigRepository, private val tangemSdkManager: TangemSdkManager, private val analyticsEventHandler: AnalyticsEventHandler, + private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val uiMessageSender: UiMessageSender, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val cardRepository: CardRepository, @@ -241,7 +243,7 @@ class MultiWalletBackupModel @Inject constructor( ) } } - else -> FirebaseCrashlytics.getInstance().recordException(result.error) + else -> analyticsExceptionHandler.sendException(ExceptionAnalyticsEvent(result.error)) } } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 68d9795026..0f2e700cd8 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -117,6 +117,7 @@ sealed class SwapEvents( "Error Code" to errorCode.toString(), ), ) + // TODO parameters // region Promo activity data class ChangellyActivity( diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt index 99b401cfda..51c265394a 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt @@ -6,8 +6,7 @@ internal sealed class Settings( category: String = "Settings", event: String, params: Map = mapOf(), - error: Throwable? = null, -) : AnalyticsEvent(category, event, params, error) { +) : AnalyticsEvent(category, event, params) { data object ButtonCreateBackup : Settings(event = "Button - Create Backup") diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index c39efabf74..a07053ba04 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -10,8 +10,7 @@ sealed class WalletScreenAnalyticsEvent { sealed class Basic( event: String, params: Map = mapOf(), - error: Throwable? = null, - ) : AnalyticsEvent(category = "Basic", event = event, params = params, error = error) { + ) : AnalyticsEvent(category = "Basic", event = event, params = params) { class WalletToppedUp(userWalletId: UserWalletId, walletType: AnalyticsParam.WalletType) : Basic( diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 461c560532..972cbd4bd7 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -47,7 +47,7 @@ coil = "2.1.0" compose-shimmer = "1.0.3" coroutine = "1.7.2" # 1.8+ is not compatible with tangem-sdk desugarJdkLibs = "1.1.5" -firebase = "33.7.0" +firebase = "33.10.0" googleMaterialComponent = "1.6.1" googlePlayReview = "2.0.1" googlePlayReviewKtx = "2.0.1" diff --git a/libs/blockchain-sdk/build.gradle.kts b/libs/blockchain-sdk/build.gradle.kts index 3a1001135f..5b0077a1d2 100644 --- a/libs/blockchain-sdk/build.gradle.kts +++ b/libs/blockchain-sdk/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.configToggles) implementation(projects.core.utils) + implementation(projects.core.analytics) // endregion // region AndroidX libraries diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/ExcludedBlockchainsModule.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/ExcludedBlockchainsModule.kt index c63d1dd071..35b4d2ff9b 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/ExcludedBlockchainsModule.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/ExcludedBlockchainsModule.kt @@ -1,6 +1,8 @@ package com.tangem.blockchainsdk.di +import com.tangem.blockchainsdk.providers.BlockchainProvidersResponseMerger import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager import dagger.Module import dagger.Provides @@ -17,4 +19,12 @@ internal object ExcludedBlockchainsModule { fun bindExcludedBlockchains(excludedBlockchainsManager: ExcludedBlockchainsManager): ExcludedBlockchains { return ExcludedBlockchains(excludedBlockchainsManager) } + + @Provides + @Singleton + fun bindBlockchainProvidersResponseMerger( + analyticsExceptionHandler: AnalyticsExceptionHandler, + ): BlockchainProvidersResponseMerger { + return BlockchainProvidersResponseMerger(analyticsExceptionHandler) + } } \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt index 4336fc4db4..60c5024818 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt @@ -22,6 +22,7 @@ import javax.inject.Singleton internal class BlockchainProvidersResponseLoader @Inject constructor( private val tangemTechApi: TangemTechApi, private val blockchainProvidersStorage: BlockchainProvidersStorage, + private val blockchainProvidersResponseMerger: BlockchainProvidersResponseMerger, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -31,7 +32,7 @@ internal class BlockchainProvidersResponseLoader @Inject constructor( return loadRemote().fold( onSuccess = { remoteResponse -> - BlockchainProvidersResponseMerger.merge( + blockchainProvidersResponseMerger.merge( local = localResponse, remote = remoteResponse, ) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt index ec57ec97ac..e64cc2f00e 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt @@ -1,19 +1,21 @@ package com.tangem.blockchainsdk.providers import androidx.core.util.PatternsCompat -import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.datasource.local.config.providers.models.ProviderModel import timber.log.Timber +import javax.inject.Inject /** * Merger of [BlockchainProvidersResponse] * [REDACTED_AUTHOR] */ -internal object BlockchainProvidersResponseMerger { - - private val firebaseCrashlytics by lazy(FirebaseCrashlytics::getInstance) +internal class BlockchainProvidersResponseMerger @Inject internal constructor( + private val analyticsExceptionHandler: AnalyticsExceptionHandler, +) { private val forbiddenSchemes = listOf("wss://") @@ -41,7 +43,7 @@ internal object BlockchainProvidersResponseMerger { val missingBlockchains = result.keys - remote.keys val blockchainsWithoutProviders = remote.filterValues { it.isEmpty() }.keys - recordException(missingBlockchains = missingBlockchains + blockchainsWithoutProviders) + logException(missingBlockchains = missingBlockchains + blockchainsWithoutProviders) } return result.guaranteeUrlsEndWithSlash() @@ -74,15 +76,19 @@ internal object BlockchainProvidersResponseMerger { return PatternsCompat.WEB_URL.matcher(inputUrl).matches() } - private fun recordException(missingBlockchains: Set) { + private fun logException(missingBlockchains: Set) { val exception = IllegalStateException( - "Remote config does not contain required blockchains or providers information: " + - missingBlockchains.joinToString(), + "Remote config does not contain some blockchains or providers information", ) Timber.e(exception) - firebaseCrashlytics.recordException(exception) + analyticsExceptionHandler.sendException( + ExceptionAnalyticsEvent( + exception = exception, + params = mapOf("Missing blockchains" to missingBlockchains.joinToString()), + ), + ) } /* diff --git a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoaderTest.kt b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoaderTest.kt index 54d99add4a..64d82467cc 100644 --- a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoaderTest.kt +++ b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoaderTest.kt @@ -4,6 +4,8 @@ import com.google.common.truth.Truth import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchainsdk.providers.BlockchainProvidersResponseMergerTest.Companion.localResponse import com.tangem.blockchainsdk.providers.BlockchainProvidersResponseMergerTest.Companion.remoteResponse +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage import com.tangem.datasource.local.config.providers.models.ProviderModel @@ -20,10 +22,16 @@ internal class BlockchainProvidersResponseLoaderTest { private val tangemTechApi = mockk() private val blockchainProvidersStorage = mockk() + private val analyticsExceptionHandler = object : AnalyticsExceptionHandler { + override fun sendException(event: ExceptionAnalyticsEvent) { + FirebaseCrashlytics.getInstance().recordException(event.exception) + } + } private val loader = BlockchainProvidersResponseLoader( tangemTechApi = tangemTechApi, blockchainProvidersStorage = blockchainProvidersStorage, + blockchainProvidersResponseMerger = BlockchainProvidersResponseMerger(analyticsExceptionHandler), dispatchers = TestingCoroutineDispatcherProvider(), ) diff --git a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMergerTest.kt b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMergerTest.kt index 53004c77b9..2fc0251d41 100644 --- a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMergerTest.kt +++ b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMergerTest.kt @@ -2,7 +2,10 @@ package com.tangem.blockchainsdk.providers import com.google.common.truth.Truth import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.firebase.crashlytics.recordException import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.datasource.local.config.providers.models.ProviderModel import io.mockk.* import org.junit.Before @@ -13,6 +16,14 @@ import org.junit.Test */ internal class BlockchainProvidersResponseMergerTest { + private val blockchainProvidersResponseMerger = BlockchainProvidersResponseMerger( + object : AnalyticsExceptionHandler { + override fun sendException(event: ExceptionAnalyticsEvent) { + FirebaseCrashlytics.getInstance().recordException(event.exception) + } + }, + ) + @Before fun setup() { mockkStatic(FirebaseCrashlytics::class) @@ -25,7 +36,7 @@ internal class BlockchainProvidersResponseMergerTest { fun test_if_both_configs_are_empty() { val expected = emptyMap>() - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = emptyMap(), remote = emptyMap(), ) @@ -37,7 +48,7 @@ internal class BlockchainProvidersResponseMergerTest { fun test_if_local_config_is_empty() { val expected = remoteResponse - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = emptyMap(), remote = remoteResponse, ) @@ -49,7 +60,7 @@ internal class BlockchainProvidersResponseMergerTest { fun test_if_remote_config_is_empty() { val expected = localResponse - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = localResponse, remote = emptyMap(), ) @@ -61,7 +72,7 @@ internal class BlockchainProvidersResponseMergerTest { fun test_if_both_configs_are_not_empty() { val expected = remoteResponse - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = localResponse, remote = remoteResponse, ) @@ -73,7 +84,7 @@ internal class BlockchainProvidersResponseMergerTest { fun test_if_configs_are_equal() { val expected = remoteResponse - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = remoteResponse, remote = remoteResponse, ) @@ -88,7 +99,7 @@ internal class BlockchainProvidersResponseMergerTest { val expected = localResponseWithEth + remoteResponse - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = localResponseWithEth, remote = remoteResponse, ) @@ -103,7 +114,7 @@ internal class BlockchainProvidersResponseMergerTest { val eth = "ethereum" to emptyList() val remoteWithEth = remoteResponse + eth - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = localResponse, remote = remoteWithEth, ) @@ -121,7 +132,7 @@ internal class BlockchainProvidersResponseMergerTest { */ val expected = remoteResponse - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = localResponse, remote = remoteWithoutLocal, ) @@ -137,7 +148,7 @@ internal class BlockchainProvidersResponseMergerTest { val expected = remoteResponse + ("ethereum" to listOf(nowNodesProvider)) - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = localResponse, remote = remoteWithEth, ) @@ -152,7 +163,7 @@ internal class BlockchainProvidersResponseMergerTest { val expected = remoteResponse - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = localResponse, remote = remoteWithEth, ) @@ -170,7 +181,7 @@ internal class BlockchainProvidersResponseMergerTest { val expected = remoteResponse + eth.addSlash() + kaspa.addSlash() - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = localWithKaspa, remote = remoteWithEth, ) From 78fafc0ee2a0fb71b5daca3f1b729ceffb6b9da5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 20 Mar 2025 14:32:49 +0200 Subject: [PATCH 021/138] Updated on 2026-08-14 --- .../transformers/validator/ValidatorSelectChangeTransformer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt index 480126eb20..f9f71dd51e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt @@ -38,7 +38,7 @@ internal class ValidatorSelectChangeTransformer( chosenValidator = selectedValidator ?: yield.preferredValidators.first(), availableValidators = filteredValidators, isPrimaryButtonEnabled = true, - isClickable = true, + isClickable = yield.preferredValidators.size > 1, activeValidator = activeValidator, isVisibleOnConfirmation = isEnter || isRestake || isVoteLocked, ), From 74fa237c0df5b62da545ed35faa9fbb2eaae6d57 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 20 Mar 2025 13:11:49 +0400 Subject: [PATCH 022/138] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 3 + .../tap/di/domain/MarketsDomainModule.kt | 5 ++ .../tap/di/domain/TokensDomainModule.kt | 4 ++ .../managetokens/SaveManagedTokensUseCase.kt | 12 ++++ domain/markets/build.gradle.kts | 1 + .../domain/markets/SaveMarketTokensUseCase.kt | 40 +++++++++-- .../tokens/AddCryptoCurrenciesUseCase.kt | 24 +++++++ .../CachedCurrenciesStatusesOperations.kt | 70 ++++++++++++++----- 8 files changed, 136 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index c3520a41cf..8d32208885 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -7,6 +7,7 @@ import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module import dagger.Provides @@ -67,6 +68,7 @@ internal object ManageTokensDomainModule { networksRepository: NetworksRepository, derivationsRepository: DerivationsRepository, stakingRepository: StakingRepository, + quotesRepository: QuotesRepository, ): SaveManagedTokensUseCase { return SaveManagedTokensUseCase( customTokensRepository = customTokensRepository, @@ -75,6 +77,7 @@ internal object ManageTokensDomainModule { networksRepository = networksRepository, derivationsRepository = derivationsRepository, stakingRepository = stakingRepository, + quotesRepository = quotesRepository, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 4498836964..72b1e2451c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -4,6 +4,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository @@ -57,12 +58,16 @@ object MarketsDomainModule { marketsTokenRepository: MarketsTokenRepository, currenciesRepository: CurrenciesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, + quotesRepository: QuotesRepository, ): SaveMarketTokensUseCase { return SaveMarketTokensUseCase( derivationsRepository = derivationsRepository, marketsTokenRepository = marketsTokenRepository, currenciesRepository = currenciesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, + quotesRepository = quotesRepository, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index b719015a7a..73ecbf344e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -27,10 +27,14 @@ internal object TokensDomainModule { fun provideAddCryptoCurrenciesUseCase( currenciesRepository: CurrenciesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, + quotesRepository: QuotesRepository, ): AddCryptoCurrenciesUseCase { return AddCryptoCurrenciesUseCase( currenciesRepository = currenciesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, + quotesRepository = quotesRepository, ) } diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt index 4143444b7b..1cec10d0c1 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt @@ -10,9 +10,11 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId +@Suppress("LongParameterList") class SaveManagedTokensUseCase( private val customTokensRepository: CustomTokensRepository, private val walletManagersFacade: WalletManagersFacade, @@ -20,6 +22,7 @@ class SaveManagedTokensUseCase( private val networksRepository: NetworksRepository, private val derivationsRepository: DerivationsRepository, private val stakingRepository: StakingRepository, + private val quotesRepository: QuotesRepository, ) { suspend operator fun invoke( @@ -54,6 +57,8 @@ class SaveManagedTokensUseCase( ) refreshUpdatedYieldBalances(userWalletId, existingCurrencies) + + refreshUpdatedQuotes(addingCurrencies) } private suspend fun removeCurrenciesFromWalletManager( @@ -105,6 +110,13 @@ class SaveManagedTokensUseCase( ) } + private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { + quotesRepository.fetchQuotes( + currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, + refresh = true, + ) + } + /** * Determines if the [existingCurrencies] list contains a coin that corresponds * to the given [network]. diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts index b3d59216c4..f5a22e3d18 100644 --- a/domain/markets/build.gradle.kts +++ b/domain/markets/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { api(projects.domain.models) api(projects.domain.legacy) api(projects.domain.wallets) + api(projects.domain.staking) implementation(projects.domain.tokens.models) implementation(projects.domain.tokens) diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt index f5b43a9bee..352fb55d85 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt @@ -3,10 +3,12 @@ package com.tangem.domain.markets import arrow.core.Either import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId /** @@ -23,6 +25,8 @@ class SaveMarketTokensUseCase( private val marketsTokenRepository: MarketsTokenRepository, private val currenciesRepository: CurrenciesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, + private val quotesRepository: QuotesRepository, ) { suspend operator fun invoke( @@ -60,11 +64,37 @@ class SaveMarketTokensUseCase( currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = addedCurrencies) - networksRepository.getNetworkStatusesSync( - userWalletId = userWalletId, - networks = addedCurrencies.map(CryptoCurrency::network).toSet(), - refresh = true, - ) + refreshUpdatedNetworks(userWalletId, addedCurrencies) + + refreshUpdatedYieldBalances(userWalletId, addedCurrencies) + + refreshUpdatedQuotes(addedCurrencies) } } + + private suspend fun refreshUpdatedNetworks(userWalletId: UserWalletId, addedCurrencies: List) { + networksRepository.getNetworkStatusesSync( + userWalletId = userWalletId, + networks = addedCurrencies.map(CryptoCurrency::network).toSet(), + refresh = true, + ) + } + + private suspend fun refreshUpdatedYieldBalances( + userWalletId: UserWalletId, + existingCurrencies: List, + ) { + stakingRepository.fetchMultiYieldBalance( + userWalletId = userWalletId, + cryptoCurrencies = existingCurrencies, + refresh = true, + ) + } + + private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { + quotesRepository.fetchQuotes( + currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, + refresh = true, + ) + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt index 01223c61a3..370a29ee8e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt @@ -5,10 +5,12 @@ import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.toNonEmptyListOrNull +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId /** @@ -21,6 +23,8 @@ import com.tangem.domain.wallets.models.UserWalletId class AddCryptoCurrenciesUseCase( private val currenciesRepository: CurrenciesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, + private val quotesRepository: QuotesRepository, ) { /** @@ -105,6 +109,8 @@ class AddCryptoCurrenciesUseCase( val tokenToAdd = createTokenCurrency(userWalletId, contractAddress, networkId) addCurrencies(userWalletId, listOf(tokenToAdd)) refreshUpdatedNetworks(userWalletId, listOf(tokenToAdd), existingCurrencies) + refreshUpdatedYieldBalances(userWalletId, existingCurrencies) + refreshUpdatedQuotes(existingCurrencies) tokenToAdd } @@ -139,6 +145,24 @@ class AddCryptoCurrenciesUseCase( } } + private suspend fun refreshUpdatedYieldBalances( + userWalletId: UserWalletId, + existingCurrencies: List, + ) { + stakingRepository.fetchMultiYieldBalance( + userWalletId = userWalletId, + cryptoCurrencies = existingCurrencies, + refresh = true, + ) + } + + private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { + quotesRepository.fetchQuotes( + currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, + refresh = true, + ) + } + private suspend fun Raise.createTokenCurrency( userWalletId: UserWalletId, contractAddress: String, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index 739ad63738..a14f37e601 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -7,6 +7,7 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lce import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList @@ -44,30 +45,49 @@ class CachedCurrenciesStatusesOperations( userWalletId: UserWalletId, currenciesFlow: EitherFlow>, ): LceFlow> = lceFlow { - currenciesFlow.flatMapLatest { maybeCurrencies -> - val isUpdating = MutableStateFlow(value = true) + val prevStatuses = MutableStateFlow(value = emptyList()) - val nonEmptyCurrencies = maybeCurrencies + val isUpdating = MutableStateFlow(value = true) + val isFetchingStarted = MutableStateFlow(value = false) + + val nonEmptyCurrencies = currenciesFlow.mapNotNull { it.getOrNull() }.firstOrNull()?.toNonEmptyListOrNull() + + if (nonEmptyCurrencies != null) { + launch { + isFetchingStarted.value = true + + val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + fetchComponents(userWalletId, networks, currenciesIds, nonEmptyCurrencies) + } + .invokeOnCompletion { isUpdating.value = false } + } + + currenciesFlow.flatMapLatest { maybeCurrencies -> + val currencies = maybeCurrencies .getOrElse { return@flatMapLatest flowOf(it.lceError()) } .toNonEmptyListOrNull() - if (nonEmptyCurrencies.isNullOrEmpty()) { + if (currencies.isNullOrEmpty()) { + prevStatuses.value = emptyList() return@flatMapLatest flowOf(TokenListError.EmptyTokens.lceError()) } // This is only 'true' when the flow here is empty, such as during initial loading if (isLoading.get()) { val loadingCurrencies = createCurrenciesStatuses( - currencies = nonEmptyCurrencies, + currencies = currencies, maybeNetworkStatuses = null, maybeQuotes = null, maybeYieldBalances = null, isUpdating = true, ) + + loadingCurrencies.getOrNull()?.let { prevStatuses.value = it } + send(loadingCurrencies) } - val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + val (networks, currenciesIds) = getIds(currencies) fun createCurrenciesStatuses( maybeQuotes: Either>?, @@ -75,26 +95,46 @@ class CachedCurrenciesStatusesOperations( maybeYieldBalances: Either?, isUpdating: Boolean, ) = createCurrenciesStatuses( - currencies = nonEmptyCurrencies, + currencies = currencies, maybeQuotes = maybeQuotes, maybeNetworkStatuses = maybeNetworkStatuses, maybeYieldBalances = maybeYieldBalances, isUpdating = isUpdating, ) - launch { fetchComponents(userWalletId, networks, currenciesIds, nonEmptyCurrencies) } - .invokeOnCompletion { isUpdating.value = false } + // removing token + val prevStatusesValue = prevStatuses.value + if (prevStatusesValue.size - currencies.size == 1) { + val removed = prevStatusesValue.map { it.currency } - currencies + + return@flatMapLatest flowOf( + prevStatusesValue.filter { it.currency !in removed }.lceContent(), + ) + } + + if (!isFetchingStarted.value) { + launch { + isFetchingStarted.value = true + + fetchComponents(userWalletId, networks, currenciesIds, currencies) + } + .invokeOnCompletion { isUpdating.value = false } + } combine( flow = getQuotes(currenciesIds), flow2 = getNetworksStatuses(userWalletId, networks), - flow3 = getYieldBalances(userWalletId, nonEmptyCurrencies), + flow3 = getYieldBalances(userWalletId, currencies), flow4 = isUpdating, transform = ::createCurrenciesStatuses, ) .distinctUntilChanged() } - .onEach(::send) + .onEach { statusesLce -> + statusesLce.getOrNull()?.let { prevStatuses.value = it } + + send(statusesLce) + } .launchIn(scope = this) } @@ -112,13 +152,7 @@ class CachedCurrenciesStatusesOperations( val rawCurrenciesIds = currenciesIds.mapNotNullTo(mutableSetOf()) { it.rawCurrencyId } quotesRepository.fetchQuotes(rawCurrenciesIds) }, - async { - if (currencies.size == 1) { - stakingRepository.fetchSingleYieldBalance(userWalletId, currencies.first()) - } else { - stakingRepository.fetchMultiYieldBalance(userWalletId, currencies) - } - }, + async { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies) }, ) } .map { } From 3b269e6f6af45516773773a3fd398b1d6b21403d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 20 Mar 2025 20:38:05 +0500 Subject: [PATCH 023/138] Updated on 2026-08-14 --- .../tap/di/domain/OnrampDomainModule.kt | 7 +++ .../features/onboarding/OnboardingManager.kt | 1 + .../mercuryo/MercuryoService.kt | 1 + .../tangem/data/onramp/di/OnrampDataModule.kt | 15 ++++++ .../legacy}/MercuryoBlockchainMapping.kt | 5 +- .../onramp/legacy/MercuryoTopUpRepository.kt | 47 +++++++++++++++++++ .../domain/onramp/GetLegacyTopUpUrlUseCase.kt | 22 +++++++++ .../repositories/LegacyTopUpRepository.kt | 23 +++++++++ features/onboarding-v2/impl/build.gradle.kts | 1 + .../topup/model/OnboardingNoteTopUpModel.kt | 32 +++++++++---- .../child/topup/ui/OnboardingNoteTopUp.kt | 12 ++++- .../topup/ui/state/OnboardingNoteTopUpUM.kt | 1 + .../impl/model/OnboardingNoteCommonState.kt | 2 + 13 files changed, 158 insertions(+), 11 deletions(-) rename {app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo => data/onramp/src/main/java/com/tangem/data/onramp/legacy}/MercuryoBlockchainMapping.kt (97%) create mode 100644 data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt create mode 100644 domain/onramp/src/main/java/com/tangem/domain/onramp/GetLegacyTopUpUrlUseCase.kt create mode 100644 domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/LegacyTopUpRepository.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index 87afb55912..77bd0c1526 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -1,5 +1,6 @@ package com.tangem.tap.di.domain +import com.tangem.domain.onramp.repositories.LegacyTopUpRepository import com.tangem.domain.onramp.* import com.tangem.domain.onramp.repositories.HotCryptoRepository import com.tangem.domain.onramp.repositories.OnrampErrorResolver @@ -242,4 +243,10 @@ internal object OnrampDomainModule { fun provideFetchHotCryptoUseCase(hotCryptoRepository: HotCryptoRepository): FetchHotCryptoUseCase { return FetchHotCryptoUseCase(hotCryptoRepository) } + + @Provides + @Singleton + fun provideGetLegacyTopUpUrlUseCase(legacyTopUpRepository: LegacyTopUpRepository): GetLegacyTopUpUrlUseCase { + return GetLegacyTopUpUrlUseCase(legacyTopUpRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt index fbb3d27204..e982cd5f2e 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt @@ -26,6 +26,7 @@ import java.math.BigDecimal /** [REDACTED_AUTHOR] */ +@Deprecated("Remove when navigation refactoring will be implemented") class OnboardingManager(var scanResponse: ScanResponse) { private val cardRepository: CardRepository by lazy { store.inject(DaggerGraphState::cardRepository) } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt index a892310a6f..b398116c05 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt @@ -6,6 +6,7 @@ import com.tangem.common.extensions.calculateSha512 import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.common.services.performRequest +import com.tangem.data.onramp.legacy.mercuryoNetwork import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index 91b4be5185..3ecf2df253 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -8,6 +8,8 @@ import com.tangem.data.onramp.DefaultOnrampErrorResolver import com.tangem.data.onramp.DefaultOnrampRepository import com.tangem.data.onramp.DefaultOnrampTransactionRepository import com.tangem.data.onramp.converters.error.OnrampErrorConverter +import com.tangem.domain.onramp.repositories.LegacyTopUpRepository +import com.tangem.data.onramp.legacy.MercuryoTopUpRepository import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.api.onramp.OnrampApi @@ -15,6 +17,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore +import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore @@ -112,4 +115,16 @@ internal object OnrampDataModule { analyticsEventHandler = analyticsEventHandler, ) } + + @Provides + @Singleton + fun provideMercuryoRepository( + environmentConfigStorage: EnvironmentConfigStorage, + dispatchersProvider: CoroutineDispatcherProvider, + ): LegacyTopUpRepository { + return MercuryoTopUpRepository( + environmentConfigStorage = environmentConfigStorage, + dispatchersProvider = dispatchersProvider, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt similarity index 97% rename from app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt rename to data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt index 3c20cb9f8e..aed9a68d2a 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt @@ -1,12 +1,13 @@ -package com.tangem.tap.network.exchangeServices.mercuryo +package com.tangem.data.onramp.legacy import com.tangem.blockchain.common.Blockchain /** + * Legacy onboarding note/twins to get top up URL from mercuryo * Map [Blockchain] to ids from [link](https://api.mercuryo.io/v1.6/lib/currencies) [crypto_currencies] */ @Suppress("CyclomaticComplexMethod") -internal val Blockchain.mercuryoNetwork: String? +public val Blockchain.mercuryoNetwork: String? get() { return when (this) { Blockchain.Algorand -> "ALGORAND" diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt new file mode 100644 index 0000000000..91bd1a5854 --- /dev/null +++ b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt @@ -0,0 +1,47 @@ +package com.tangem.data.onramp.legacy + +import android.net.Uri +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.extensions.calculateSha512 +import com.tangem.common.extensions.toHexString +import com.tangem.datasource.local.config.environment.EnvironmentConfig +import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.domain.onramp.repositories.LegacyTopUpRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal class MercuryoTopUpRepository @Inject constructor( + private val environmentConfigStorage: EnvironmentConfigStorage, + private val dispatchersProvider: CoroutineDispatcherProvider, +) : LegacyTopUpRepository { + + override suspend fun getTopUpUrl(cryptoCurrency: CryptoCurrency, walletAddress: String): String = + withContext(dispatchersProvider.default) { + val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value) + val environmentConfig = environmentConfigStorage.getConfigSync() + + val builder = Uri.Builder() + .scheme(LegacyTopUpRepository.SCHEME) + .authority("exchange.mercuryo.io") + .appendQueryParameter("widget_id", environmentConfig.mercuryoWidgetId) + .appendQueryParameter("type", "buy") + .appendQueryParameter("currency", cryptoCurrency.symbol) + .appendQueryParameter("address", walletAddress) + .appendQueryParameter("signature", environmentConfig.signature(walletAddress)) + .appendQueryParameter("fix_currency", "true") + .appendQueryParameter("redirect_url", LegacyTopUpRepository.SUCCESS_URL) + + blockchain.mercuryoNetwork?.let { + builder.appendQueryParameter("network", it) + } + + builder.build().toString() + } + + private fun EnvironmentConfig.signature(address: String) = (address + mercuryoSecret) + .calculateSha512() + .toHexString() + .lowercase() +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetLegacyTopUpUrlUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetLegacyTopUpUrlUseCase.kt new file mode 100644 index 0000000000..ab5a2807a0 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetLegacyTopUpUrlUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.domain.onramp.repositories.LegacyTopUpRepository +import com.tangem.domain.onramp.model.error.OnrampError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus + +class GetLegacyTopUpUrlUseCase( + private val legacyTopUpRepository: LegacyTopUpRepository, +) { + + suspend operator fun invoke(cryptoCurrencyStatus: CryptoCurrencyStatus): Either = + either { + val walletAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: raise(OnrampError.DomainError("Wallet address is null")) + legacyTopUpRepository.getTopUpUrl( + cryptoCurrency = cryptoCurrencyStatus.currency, + walletAddress = walletAddress, + ) + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/LegacyTopUpRepository.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/LegacyTopUpRepository.kt new file mode 100644 index 0000000000..0b69495806 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/LegacyTopUpRepository.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.onramp.repositories + +import com.tangem.domain.tokens.model.CryptoCurrency + +/** + * Support legacy onboarding note/twins to get top up URL from mercuryo + * + */ +interface LegacyTopUpRepository { + + /** + * Get top up URL from mercuryo + * + * @property cryptoCurrency crypto currency + * @property walletAddress address of wallet + */ + suspend fun getTopUpUrl(cryptoCurrency: CryptoCurrency, walletAddress: String): String + + companion object { + const val SCHEME = "https" + const val SUCCESS_URL = "https://tangem.com/success" + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index 6b65a2561d..00be841eed 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(projects.domain.visa) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.onramp) /** Tangem libraries */ implementation(projects.libs.tangemSdkApi) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt index a7956a0cb8..11f6c047b9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt @@ -4,9 +4,11 @@ import com.tangem.core.analytics.Analytics 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.navigation.url.UrlOpener import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -20,7 +22,7 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject -@Suppress("UnusedPrivateMember") +@Suppress("LongParameterList") @ModelScoped internal class OnboardingNoteTopUpModel @Inject constructor( paramsContainer: ParamsContainer, @@ -28,9 +30,12 @@ internal class OnboardingNoteTopUpModel @Inject constructor( private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val userWalletBuilderFactory: UserWalletBuilder.Factory, + private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase, + private val urlOpener: UrlOpener, ) : Model() { private val params = paramsContainer.require() + private val commonState = params.childParams.commonState private val scanResponse = params.childParams.commonState.value.scanResponse private var userWallet: UserWallet? = null @@ -68,7 +73,12 @@ internal class OnboardingNoteTopUpModel @Inject constructor( } private fun onBuyCryptoClick() { - // TODO [REDACTED_TASK_KEY] + val cryptoCurrencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return + modelScope.launch { + getLegacyTopUpUrlUseCase(cryptoCurrencyStatus).onRight { + urlOpener.openUrl(it) + } + } } private fun onShowWalletAddressClick() { @@ -100,6 +110,9 @@ internal class OnboardingNoteTopUpModel @Inject constructor( } private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { + commonState.update { + it.copy(cryptoCurrencyStatus = status) + } _uiState.update { it.copy( amountToCreateAccount = (status.value as? CryptoCurrencyStatus.NoAccount) @@ -112,12 +125,15 @@ internal class OnboardingNoteTopUpModel @Inject constructor( ) }, ), - balance = (status.value as? CryptoCurrencyStatus.Loaded)?.amount?.format({ - crypto( - symbol = status.currency.symbol, - decimals = status.currency.decimals, - ) - },), + balance = (status.value as? CryptoCurrencyStatus.Loaded)?.amount?.format( + { + crypto( + symbol = status.currency.symbol, + decimals = status.currency.decimals, + ) + }, + ), + isTopUpDataLoading = status.value.networkAddress == null, ) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt index 307bc1efaa..ff0bcdccb4 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt @@ -3,6 +3,7 @@ package com.tangem.features.onboarding.v2.note.impl.child.topup.ui import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -26,6 +27,7 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif .fillMaxSize() .navigationBarsPadding(), verticalArrangement = Arrangement.Bottom, + horizontalAlignment = Alignment.CenterHorizontally, ) { OnboardingNoteTopUpHeader( balance = state.balance, @@ -64,7 +66,15 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif ) } - BottomButtons(state) + if (state.isTopUpDataLoading) { + CircularProgressIndicator( + modifier = Modifier + .padding(all = 32.dp) + .size(36.dp), + ) + } else { + BottomButtons(state) + } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt index 762ccae2e8..5f91975c6a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt @@ -5,6 +5,7 @@ data class OnboardingNoteTopUpUM( val availableForBuy: Boolean = true, val balance: String? = null, val isRefreshing: Boolean = false, + val isTopUpDataLoading: Boolean = true, val amountToCreateAccount: String? = null, val onBuyCryptoClick: () -> Unit = {}, val onShowWalletAddressClick: () -> Unit = {}, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt index 7f082a0f19..52ff268627 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt @@ -1,10 +1,12 @@ package com.tangem.features.onboarding.v2.note.impl.model import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet internal data class OnboardingNoteCommonState( val scanResponse: ScanResponse, val cardArtworkUrl: String? = null, val userWallet: UserWallet? = null, + val cryptoCurrencyStatus: CryptoCurrencyStatus? = null, ) \ No newline at end of file From c40326c1c9988783c0d57fa71e780ef52a9ce8eb Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 21 Mar 2025 11:24:36 +0200 Subject: [PATCH 024/138] Updated on 2026-08-14 --- .../java/com/tangem/tap/TangemApplication.kt | 8 +- .../handlers/BlockchainExceptionHandler.kt | 6 +- .../common/response/ApiResponseCallAdapter.kt | 4 +- .../response/ApiResponseCallAdapterFactory.kt | 9 +- .../response/ApiResponseCallDelegate.kt | 6 +- .../api/common/response/ApiResponseError.kt | 82 +++++++++---------- .../api/common/response/ResponseExt.kt | 23 +++++- .../response/analytics/ApiErrorEvent.kt | 17 ++++ .../com/tangem/datasource/di/NetworkModule.kt | 23 +++++- .../markets/DefaultMarketsTokenRepository.kt | 2 +- .../data/onramp/DefaultHotCryptoRepository.kt | 2 +- .../tangem/feature/swap/model/SwapModel.kt | 4 +- 12 files changed, 128 insertions(+), 58 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/response/analytics/ApiErrorEvent.kt diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index e73bf509da..256468df9e 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -8,6 +8,7 @@ import coil.ImageLoaderFactory import com.chuckerteam.chucker.api.ChuckerInterceptor import com.tangem.Log import com.tangem.TangemSdkLogger +import com.tangem.blockchain.common.ExceptionHandler import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.blockchainsdk.utils.ExcludedBlockchains @@ -55,6 +56,7 @@ import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder +import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler import com.tangem.tap.common.images.createCoilImageLoader @@ -206,6 +208,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val uiMessageSender: UiMessageSender get() = entryPoint.getUiMessageSender() + private val blockchainExceptionHandler: BlockchainExceptionHandler + get() = entryPoint.getBlockchainExceptionHandler() + private val workerFactory: HiltWorkerFactory get() = entryPoint.getWorkerFactory() @@ -219,6 +224,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val userWalletBuilderFactory: UserWalletBuilder.Factory get() = entryPoint.getUserWalletBuilderFactory() + // endregion override fun onCreate() { @@ -264,7 +270,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat } loadNativeLibraries() - // ExceptionHandler.append(blockchainExceptionHandler) // TODO [REDACTED_TASK_KEY] Send only to Firebase + ExceptionHandler.append(blockchainExceptionHandler) if (LogConfig.network.blockchainSdkNetwork) { BlockchainSdkRetrofitBuilder.interceptors = listOf( createNetworkLoggingInterceptor(), diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt index b227a5f1f1..c6e47dcb6d 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt @@ -1,15 +1,15 @@ package com.tangem.tap.common.analytics.handlers import com.tangem.blockchain.common.ExceptionHandlerOutput -import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.tap.common.analytics.events.BlockchainExceptionEvent import javax.inject.Inject class BlockchainExceptionHandler @Inject constructor( - private val analyticsHandler: AnalyticsEventHandler, + private val analyticsErrorHandler: AnalyticsErrorHandler, ) : ExceptionHandlerOutput { override fun handleApiSwitch(currentHost: String, nextHost: String, message: String) { - analyticsHandler.send( + analyticsErrorHandler.sendErrorEvent( BlockchainExceptionEvent( selectedHost = nextHost, exceptionHost = currentHost, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapter.kt index 885b52ca28..6f9fbc020a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapter.kt @@ -1,16 +1,18 @@ package com.tangem.datasource.api.common.response +import com.tangem.core.analytics.api.AnalyticsErrorHandler import retrofit2.Call import retrofit2.CallAdapter import java.lang.reflect.Type internal class ApiResponseCallAdapter( private val resultType: Type, + private val analyticsErrorHandler: AnalyticsErrorHandler, ) : CallAdapter>> { override fun responseType(): Type = resultType override fun adapt(call: Call): Call> { - return ApiResponseCallDelegate(call) + return ApiResponseCallDelegate(call, analyticsErrorHandler) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapterFactory.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapterFactory.kt index 8f8c65e92d..b3bd217b7b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapterFactory.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapterFactory.kt @@ -1,12 +1,15 @@ package com.tangem.datasource.api.common.response +import com.tangem.core.analytics.api.AnalyticsErrorHandler import retrofit2.Call import retrofit2.CallAdapter import retrofit2.Retrofit import java.lang.reflect.ParameterizedType import java.lang.reflect.Type -class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory() { +class ApiResponseCallAdapterFactory private constructor( + private val analyticsErrorHandler: AnalyticsErrorHandler, +) : CallAdapter.Factory() { override fun get(returnType: Type, annotations: Array, retrofit: Retrofit): CallAdapter<*, *>? { if (getRawType(returnType) != Call::class.java) { @@ -19,11 +22,11 @@ class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory( } val resultType = getParameterUpperBound(0, callType as ParameterizedType) - return ApiResponseCallAdapter(resultType) + return ApiResponseCallAdapter(resultType, analyticsErrorHandler) } companion object { - fun create() = ApiResponseCallAdapterFactory() + fun create(analyticsErrorHandler: AnalyticsErrorHandler) = ApiResponseCallAdapterFactory(analyticsErrorHandler) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt index 919be8e848..50ee235c7b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt @@ -1,5 +1,6 @@ package com.tangem.datasource.api.common.response +import com.tangem.core.analytics.api.AnalyticsErrorHandler import okhttp3.Request import okio.Timeout import retrofit2.Call @@ -9,6 +10,7 @@ import timber.log.Timber internal class ApiResponseCallDelegate( private val wrappedCall: Call, + private val analyticsErrorHandler: AnalyticsErrorHandler, ) : Call> { override fun enqueue(callback: Callback>) { @@ -16,7 +18,7 @@ internal class ApiResponseCallDelegate( } override fun execute(): Response> = throw NotImplementedError() - override fun clone(): Call> = ApiResponseCallDelegate(wrappedCall.clone()) + override fun clone(): Call> = ApiResponseCallDelegate(wrappedCall.clone(), analyticsErrorHandler) override fun request(): Request = wrappedCall.request() override fun timeout(): Timeout = wrappedCall.timeout() override fun isExecuted(): Boolean = wrappedCall.isExecuted @@ -30,7 +32,7 @@ internal class ApiResponseCallDelegate( ) : Callback { override fun onResponse(call: Call, response: Response) { - val safeResponse = response.toSafeApiResponse() + val safeResponse = response.toSafeApiResponse(analyticsErrorHandler) responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse)) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt index 22162b76b4..10ec8add5d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt @@ -19,51 +19,51 @@ sealed class ApiResponseError : Exception() { ) : ApiResponseError() { // region Error Codes - enum class Code(val code: Int) { + enum class Code(val numericCode: Int) { // 4xx Server Errors - BAD_REQUEST(code = 400), - UNAUTHORIZED(code = 401), - PAYMENT_REQUIRED(code = 402), - FORBIDDEN(code = 403), - NOT_FOUND(code = 404), - METHOD_NOT_ALLOWED(code = 405), - NOT_ACCEPTABLE(code = 406), - PROXY_AUTHENTICATION_REQUIRED(code = 407), - REQUEST_TIMEOUT(code = 408), - CONFLICT(code = 409), - GONE(code = 410), - LENGTH_REQUIRED(code = 411), - PRECONDITION_FAILED(code = 412), - PAYLOAD_TOO_LARGE(code = 413), - URI_TOO_LONG(code = 414), - UNSUPPORTED_MEDIA_TYPE(code = 415), - RANGE_NOT_SATISFIABLE(code = 416), - EXPECTATION_FAILED(code = 417), - IM_A_TEAPOT(code = 418), // Not an error, but an April Fools' joke from RFC 2324 - UNPROCESSABLE_ENTITY(code = 422), - LOCKED(code = 423), - FAILED_DEPENDENCY(code = 424), - TOO_EARLY(code = 425), - UPGRADE_REQUIRED(code = 426), - PRECONDITION_REQUIRED(code = 428), - TOO_MANY_REQUESTS(code = 429), - REQUEST_HEADER_FIELDS_TOO_LARGE(code = 431), - UNAVAILABLE_FOR_LEGAL_REASONS(code = 451), + BAD_REQUEST(numericCode = 400), + UNAUTHORIZED(numericCode = 401), + PAYMENT_REQUIRED(numericCode = 402), + FORBIDDEN(numericCode = 403), + NOT_FOUND(numericCode = 404), + METHOD_NOT_ALLOWED(numericCode = 405), + NOT_ACCEPTABLE(numericCode = 406), + PROXY_AUTHENTICATION_REQUIRED(numericCode = 407), + REQUEST_TIMEOUT(numericCode = 408), + CONFLICT(numericCode = 409), + GONE(numericCode = 410), + LENGTH_REQUIRED(numericCode = 411), + PRECONDITION_FAILED(numericCode = 412), + PAYLOAD_TOO_LARGE(numericCode = 413), + URI_TOO_LONG(numericCode = 414), + UNSUPPORTED_MEDIA_TYPE(numericCode = 415), + RANGE_NOT_SATISFIABLE(numericCode = 416), + EXPECTATION_FAILED(numericCode = 417), + IM_A_TEAPOT(numericCode = 418), // Not an error, but an April Fools' joke from RFC 2324 + UNPROCESSABLE_ENTITY(numericCode = 422), + LOCKED(numericCode = 423), + FAILED_DEPENDENCY(numericCode = 424), + TOO_EARLY(numericCode = 425), + UPGRADE_REQUIRED(numericCode = 426), + PRECONDITION_REQUIRED(numericCode = 428), + TOO_MANY_REQUESTS(numericCode = 429), + REQUEST_HEADER_FIELDS_TOO_LARGE(numericCode = 431), + UNAVAILABLE_FOR_LEGAL_REASONS(numericCode = 451), // 5xx Server Errors - INTERNAL_SERVER_ERROR(code = 500), - NOT_IMPLEMENTED(code = 501), - BAD_GATEWAY(code = 502), - SERVICE_UNAVAILABLE(code = 503), - GATEWAY_TIMEOUT(code = 504), - HTTP_VERSION_NOT_SUPPORTED(code = 505), - VARIANT_ALSO_NEGOTIATES(code = 506), - INSUFFICIENT_STORAGE(code = 507), - LOOP_DETECTED(code = 508), - NOT_EXTENDED(code = 510), - NETWORK_AUTHENTICATION_REQUIRED(code = 511), + INTERNAL_SERVER_ERROR(numericCode = 500), + NOT_IMPLEMENTED(numericCode = 501), + BAD_GATEWAY(numericCode = 502), + SERVICE_UNAVAILABLE(numericCode = 503), + GATEWAY_TIMEOUT(numericCode = 504), + HTTP_VERSION_NOT_SUPPORTED(numericCode = 505), + VARIANT_ALSO_NEGOTIATES(numericCode = 506), + INSUFFICIENT_STORAGE(numericCode = 507), + LOOP_DETECTED(numericCode = 508), + NOT_EXTENDED(numericCode = 510), + NETWORK_AUTHENTICATION_REQUIRED(numericCode = 511), ; - override fun toString(): String = "$code - $name" + override fun toString(): String = "$numericCode - $name" companion object { val values = values() diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt index f696275bc3..1699dbc089 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt @@ -1,5 +1,7 @@ package com.tangem.datasource.api.common.response +import com.tangem.core.analytics.api.AnalyticsErrorHandler +import com.tangem.datasource.api.common.response.analytics.ApiErrorEvent import kotlinx.coroutines.TimeoutCancellationException import retrofit2.Response import timber.log.Timber @@ -9,18 +11,20 @@ import java.net.UnknownHostException import java.util.concurrent.TimeoutException import javax.net.ssl.SSLHandshakeException -internal fun Response.toSafeApiResponse(): ApiResponse { +internal fun Response.toSafeApiResponse(analyticsErrorHandler: AnalyticsErrorHandler): ApiResponse { val body = body() return if (isSuccessful && body != null) { apiSuccess(body) } else { val code = ApiResponseError.HttpException.Code.values - .firstOrNull { it.code == code() } + .firstOrNull { it.numericCode == code() } val e = try { if (code == null) { ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}")) } else { + sendHttpError(code, analyticsErrorHandler) + ApiResponseError.HttpException(code, message(), errorBody()?.string()) } } catch (e: Exception) { @@ -32,6 +36,21 @@ internal fun Response.toSafeApiResponse(): ApiResponse { } } +private fun Response.sendHttpError( + code: ApiResponseError.HttpException.Code, + analyticsErrorHandler: AnalyticsErrorHandler, +) { + val fullRequestUrl = raw().request.url.toUrl() + val shortUrl = fullRequestUrl.authority + fullRequestUrl.path + analyticsErrorHandler.sendErrorEvent( + ApiErrorEvent( + endpoint = shortUrl, + code = code.numericCode, + message = errorBody()?.string().orEmpty(), + ), + ) +} + internal fun Throwable.toApiError(): ApiResponseError = when (this) { is ConnectException, is UnknownHostException, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/analytics/ApiErrorEvent.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/analytics/ApiErrorEvent.kt new file mode 100644 index 0000000000..c01da30063 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/analytics/ApiErrorEvent.kt @@ -0,0 +1,17 @@ +package com.tangem.datasource.api.common.response.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +internal data class ApiErrorEvent( + val endpoint: String, + val code: Int, + val message: String, +) : AnalyticsEvent( + category = "Tangem API Service", + event = "Exception", + params = mapOf( + "Endpoint" to endpoint, + "Code" to code.toString(), + "Message" to message, + ), +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 29dbfa7630..dd14d5f63a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.di import android.content.Context import com.squareup.moshi.Moshi +import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiConfigs @@ -61,6 +62,7 @@ internal object NetworkModule { fun provideExpressApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, + analyticsErrorHandler: AnalyticsErrorHandler, apiConfigsManager: ApiConfigsManager, appLogsStore: AppLogsStore, ): TangemExpressApi { @@ -69,6 +71,7 @@ internal object NetworkModule { moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + analyticsErrorHandler = analyticsErrorHandler, clientBuilder = { addInterceptor( NetworkLogsSaveInterceptor(appLogsStore), @@ -83,6 +86,7 @@ internal object NetworkModule { @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, apiConfigsManager: ApiConfigsManager, + analyticsErrorHandler: AnalyticsErrorHandler, appLogsStore: AppLogsStore, ): StakeKitApi { return createApi( @@ -90,6 +94,7 @@ internal object NetworkModule { moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + analyticsErrorHandler = analyticsErrorHandler, timeouts = Timeouts( callTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS, connectTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS, @@ -109,6 +114,7 @@ internal object NetworkModule { fun provideOnrampApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, + analyticsErrorHandler: AnalyticsErrorHandler, apiConfigsManager: ApiConfigsManager, appLogsStore: AppLogsStore, ): OnrampApi { @@ -117,6 +123,7 @@ internal object NetworkModule { moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + analyticsErrorHandler = analyticsErrorHandler, clientBuilder = { addInterceptor( NetworkLogsSaveInterceptor(appLogsStore), @@ -130,6 +137,7 @@ internal object NetworkModule { fun provideTangemTechApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, + analyticsErrorHandler: AnalyticsErrorHandler, apiConfigsManager: ApiConfigsManager, ): TangemTechApi { return createApi( @@ -137,6 +145,7 @@ internal object NetworkModule { moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + analyticsErrorHandler = analyticsErrorHandler, clientBuilder = { applyTimeoutAnnotations() }, ) } @@ -147,6 +156,7 @@ internal object NetworkModule { fun provideTangemTechApiV2( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, + analyticsErrorHandler: AnalyticsErrorHandler, appVersionProvider: AppVersionProvider, ): TangemTechApiV2 { return provideTangemTechApiInternal( @@ -154,6 +164,7 @@ internal object NetworkModule { context = context, appVersionProvider = appVersionProvider, baseUrl = PROD_V2_TANGEM_TECH_BASE_URL, + analyticsErrorHandler = analyticsErrorHandler, ) } @@ -162,6 +173,7 @@ internal object NetworkModule { fun provideTangemTechMarketsApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, + analyticsErrorHandler: AnalyticsErrorHandler, apiConfigsManager: ApiConfigsManager, ): TangemTechMarketsApi { return createApi( @@ -169,6 +181,7 @@ internal object NetworkModule { moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + analyticsErrorHandler = analyticsErrorHandler, clientBuilder = { this.callTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS) .connectTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS) @@ -183,6 +196,7 @@ internal object NetworkModule { fun provideTangemVisaAuthApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, + analyticsErrorHandler: AnalyticsErrorHandler, apiConfigsManager: ApiConfigsManager, appLogsStore: AppLogsStore, ): TangemVisaAuthApi { @@ -191,6 +205,7 @@ internal object NetworkModule { moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + analyticsErrorHandler = analyticsErrorHandler, clientBuilder = { addInterceptor( NetworkLogsSaveInterceptor(appLogsStore), @@ -204,6 +219,7 @@ internal object NetworkModule { fun provideTangemVisaApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, + analyticsErrorHandler: AnalyticsErrorHandler, apiConfigsManager: ApiConfigsManager, appLogsStore: AppLogsStore, ): TangemVisaApi { @@ -212,6 +228,7 @@ internal object NetworkModule { moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + analyticsErrorHandler = analyticsErrorHandler, clientBuilder = { addInterceptor( NetworkLogsSaveInterceptor(appLogsStore), @@ -226,6 +243,7 @@ internal object NetworkModule { context: Context, appVersionProvider: AppVersionProvider, baseUrl: String, + analyticsErrorHandler: AnalyticsErrorHandler, timeouts: Timeouts = Timeouts(), requestHeaders: List = listOf(AppVersionPlatformHeaders(appVersionProvider)), ): T { @@ -257,7 +275,7 @@ internal object NetworkModule { return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) - .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) + .addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler)) .baseUrl(baseUrl) .client(client) .build() @@ -269,6 +287,7 @@ internal object NetworkModule { moshi: Moshi, context: Context, apiConfigsManager: ApiConfigsManager, + analyticsErrorHandler: AnalyticsErrorHandler, timeouts: Timeouts = Timeouts(), clientBuilder: OkHttpClient.Builder.() -> OkHttpClient.Builder = { this }, ): T { @@ -276,7 +295,7 @@ internal object NetworkModule { return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) - .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) + .addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler)) .baseUrl(environmentConfig.baseUrl) .client( OkHttpClient.Builder() diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 4b2f305bcf..91c229e3ef 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -335,7 +335,7 @@ internal class DefaultMarketsTokenRepository( ): T { return when (error) { is ApiResponseError.HttpException -> { - createEvent(MarketsDataAnalyticsEvent.Type.Http, error.code.code, error.message.orEmpty()) + createEvent(MarketsDataAnalyticsEvent.Type.Http, error.code.numericCode, error.message.orEmpty()) } is ApiResponseError.TimeoutException -> { createEvent(MarketsDataAnalyticsEvent.Type.Timeout, null, error.message.orEmpty()) diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt index 86807b4905..dd1fb830b0 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -131,7 +131,7 @@ internal class DefaultHotCryptoRepository( analyticsEventHandler.send( event = MainScreenAnalyticsEvent.HotTokenError( - errorCode = (it as? ApiResponseError.HttpException)?.code?.code?.toString().orEmpty(), + errorCode = (it as? ApiResponseError.HttpException)?.code?.numericCode?.toString().orEmpty(), ), ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 1728183620..c2eeff8a86 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -9,6 +9,7 @@ import arrow.core.getOrElse import com.tangem.common.routing.AppRouter import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState.InProgress.getApproveTypeOrNull +import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic @@ -82,6 +83,7 @@ internal class SwapModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val blockchainInteractor: BlockchainInteractor, private val analyticsEventHandler: AnalyticsEventHandler, + private val analyticsErrorEventHandler: AnalyticsErrorHandler, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, @@ -483,7 +485,7 @@ internal class SwapModel @Inject constructor( val receiveToken = dataState.toCryptoCurrency?.currency?.let { "${it.network.backendId}:${it.symbol}" } - analyticsEventHandler.send( + analyticsErrorEventHandler.sendErrorEvent( SwapEvents.NoticeProviderError( sendToken = "${initialCurrencyFrom.network.backendId}:${initialCurrencyFrom.symbol}", receiveToken = receiveToken ?: "", From 894b3fdc7163ab995f160256def144fb82773e77 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 21 Mar 2025 15:19:00 +0500 Subject: [PATCH 025/138] Updated on 2026-08-14 --- .../v2/common/analytics/OnboardingEvent.kt | 7 ++ ...kt => OnboardingNoteCreateWalletScreen.kt} | 0 .../topup/model/OnboardingNoteTopUpModel.kt | 77 ++++++++++++++++++- .../topup/ui/OnboardingNoteTopUpHeader.kt | 4 +- ...eTopUp.kt => OnboardingNoteTopUpScreen.kt} | 45 +++++------ .../topup/ui/state/OnboardingNoteTopUpUM.kt | 9 ++- 6 files changed, 112 insertions(+), 30 deletions(-) rename features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/{OnboardingNoteCreateWallet.kt => OnboardingNoteCreateWalletScreen.kt} (100%) rename features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/{OnboardingNoteTopUp.kt => OnboardingNoteTopUpScreen.kt} (79%) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt index f795e51d51..185b058558 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt @@ -1,6 +1,8 @@ package com.tangem.features.onboarding.v2.common.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.tokens.model.CryptoCurrency sealed class OnboardingEvent( category: String, @@ -46,6 +48,11 @@ sealed class OnboardingEvent( object ScreenOpened : Topup("Activation Screen Opened") object ButtonShowWalletAddress : Topup("Button - Show the Wallet Address") + + class ButtonBuyCrypto(currency: CryptoCurrency) : Topup( + event = "Button - Buy Crypto", + params = mapOf(AnalyticsParam.CURRENCY to currency.symbol), + ) } sealed class Backup( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/OnboardingNoteCreateWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/OnboardingNoteCreateWalletScreen.kt similarity index 100% rename from features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/OnboardingNoteCreateWallet.kt rename to features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/OnboardingNoteCreateWalletScreen.kt diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt index 11f6c047b9..2d7cbc4c04 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt @@ -4,20 +4,30 @@ import com.tangem.core.analytics.Analytics 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.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -32,6 +42,9 @@ internal class OnboardingNoteTopUpModel @Inject constructor( private val userWalletBuilderFactory: UserWalletBuilder.Factory, private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase, private val urlOpener: UrlOpener, + private val clipboardManager: ClipboardManager, + private val shareManager: ShareManager, + private val rampStateManager: RampStateManager, ) : Model() { private val params = paramsContainer.require() @@ -44,6 +57,7 @@ internal class OnboardingNoteTopUpModel @Inject constructor( onRefreshBalanceClick = ::refreshBalance, onBuyCryptoClick = ::onBuyCryptoClick, onShowWalletAddressClick = ::onShowWalletAddressClick, + onDismissBottomSheet = ::onDismissBottomSheet, ), ) @@ -79,10 +93,23 @@ internal class OnboardingNoteTopUpModel @Inject constructor( urlOpener.openUrl(it) } } + Analytics.send(OnboardingEvent.Topup.ButtonBuyCrypto(cryptoCurrencyStatus.currency)) } private fun onShowWalletAddressClick() { - // TODO [REDACTED_TASK_KEY] + val currencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return + val networkAddress = currencyStatus.value.networkAddress ?: return + + _uiState.update { + it.copy(addressBottomSheetConfig = createReceiveBS(currencyStatus, networkAddress)) + } + Analytics.send(OnboardingEvent.Topup.ButtonShowWalletAddress) + } + + private fun onDismissBottomSheet() { + _uiState.update { + it.copy(addressBottomSheetConfig = null) + } } private suspend fun createUserWalletIfNull() { @@ -110,9 +137,13 @@ internal class OnboardingNoteTopUpModel @Inject constructor( } private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { + if (commonState.value.cryptoCurrencyStatus == null) { + loadAvailableForBuy(status) + } commonState.update { it.copy(cryptoCurrencyStatus = status) } + _uiState.update { it.copy( amountToCreateAccount = (status.value as? CryptoCurrencyStatus.NoAccount) @@ -132,7 +163,7 @@ internal class OnboardingNoteTopUpModel @Inject constructor( decimals = status.currency.decimals, ) }, - ), + ).orEmpty(), isTopUpDataLoading = status.value.networkAddress == null, ) } @@ -144,6 +175,48 @@ internal class OnboardingNoteTopUpModel @Inject constructor( } } + private fun loadAvailableForBuy(cryptoCurrencyStatus: CryptoCurrencyStatus) { + val userWalletId = userWallet?.walletId ?: return + + modelScope.launch { + val availableForBuy = rampStateManager.availableForBuy( + scanResponse = scanResponse, + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + ) + _uiState.update { + it.copy( + availableForBuy = availableForBuy, + availableForBuyLoading = false, + ) + } + } + } + + private fun createReceiveBS(currencyStatus: CryptoCurrencyStatus, networkAddress: NetworkAddress) = + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = uiState.value.onDismissBottomSheet, + content = TokenReceiveBottomSheetConfig( + name = currencyStatus.currency.name, + symbol = currencyStatus.currency.symbol, + network = currencyStatus.currency.network.name, + addresses = networkAddress.availableAddresses + .mapToAddressModels(currencyStatus.currency) + .toImmutableList(), + showMemoDisclaimer = + currencyStatus.currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, + onCopyClick = { + Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currencyStatus.currency.symbol)) + clipboardManager.setText(text = it, isSensitive = true) + }, + onShareClick = { + Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currencyStatus.currency.symbol)) + shareManager.shareText(text = it) + }, + ), + ) + private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet { return requireNotNull( value = userWalletBuilderFactory.create(scanResponse = scanResponse).build(), diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt index 2e598ac4d9..572a257158 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt @@ -20,7 +20,7 @@ import com.tangem.features.onboarding.v2.impl.R @Composable fun OnboardingNoteTopUpHeader( - balance: String?, + balance: String, cardArtworkUrl: String?, isRefreshing: Boolean, onRefreshBalanceClick: () -> Unit, @@ -58,7 +58,7 @@ fun OnboardingNoteTopUpHeader( ) SpacerH8() Text( - text = balance.orEmpty(), + text = balance, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt similarity index 79% rename from features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt rename to features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt index ff0bcdccb4..8fbe707ec3 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt @@ -1,9 +1,9 @@ package com.tangem.features.onboarding.v2.note.impl.child.topup.ui +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -11,9 +11,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -66,14 +66,10 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif ) } - if (state.isTopUpDataLoading) { - CircularProgressIndicator( - modifier = Modifier - .padding(all = 32.dp) - .size(36.dp), - ) - } else { - BottomButtons(state) + BottomButtons(state) + + state.addressBottomSheetConfig?.let { config -> + TokenReceiveBottomSheet(config = config) } } } @@ -81,32 +77,33 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif @Composable private fun BottomButtons(state: OnboardingNoteTopUpUM) { if (state.availableForBuy) { - PrimaryButtonIconEnd( + PrimaryButton( modifier = Modifier - .padding(horizontal = 16.dp) + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) .fillMaxWidth(), - iconResId = R.drawable.ic_tangem_24, text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto), onClick = state.onBuyCryptoClick, ) - SpacerH8() - SecondaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address), - onClick = state.onShowWalletAddressClick, - ) } else { - PrimaryButtonIconEnd( + PrimaryButton( modifier = Modifier .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), - iconResId = R.drawable.ic_tangem_24, text = stringResourceSafe(R.string.onboarding_button_receive_crypto), onClick = state.onShowWalletAddressClick, ) } + AnimatedVisibility(visible = !state.availableForBuyLoading) { + if (state.availableForBuy) { + SecondaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address), + onClick = state.onShowWalletAddressClick, + ) + } + } } @Preview(showBackground = true) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt index 5f91975c6a..f61ef91a50 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt @@ -1,13 +1,18 @@ package com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig + data class OnboardingNoteTopUpUM( val cardArtworkUrl: String? = null, - val availableForBuy: Boolean = true, - val balance: String? = null, + val availableForBuy: Boolean = false, + val availableForBuyLoading: Boolean = true, + val balance: String = "–", val isRefreshing: Boolean = false, val isTopUpDataLoading: Boolean = true, val amountToCreateAccount: String? = null, + val addressBottomSheetConfig: TangemBottomSheetConfig? = null, val onBuyCryptoClick: () -> Unit = {}, val onShowWalletAddressClick: () -> Unit = {}, val onRefreshBalanceClick: () -> Unit = {}, + val onDismissBottomSheet: () -> Unit = {}, ) \ No newline at end of file From 5c6118d572aa3a9418bd7ad1eae7a3852eb6ab3e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 21 Mar 2025 10:28:59 +0000 Subject: [PATCH 026/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 58cb7cc642..85b8e9a1f8 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "release-app_5.22-987" +tangemBlockchainSdk = "develop-986" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.22-440" +tangemCardSdk = "develop-444" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 872df0ecb7128c7b210d9c2737709e2b43f10cb7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 21 Mar 2025 14:06:27 +0300 Subject: [PATCH 027/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 85b8e9a1f8..1e84ff3ed9 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,5 +1,5 @@ [versions] -tangemBlockchainSdk = "develop-986" +tangemBlockchainSdk = "develop-988" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-444" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 9b4b339b8d67e86e0e6b3b567e7e1c4651fca4cf Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 24 Mar 2025 11:20:28 +0300 Subject: [PATCH 028/138] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 30 ++++++- core/res/src/main/res/values-fr/strings.xml | 2 +- core/res/src/main/res/values-ja/strings.xml | 33 ++++++++ core/res/src/main/res/values/strings.xml | 83 ++++++++++++++++++- .../v2/done/api/OnboardingDoneComponent.kt | 9 +- .../impl/DefaultOnboardingDoneComponent.kt | 4 +- .../v2/done/impl/ui/OnboardingDone.kt | 20 ++++- .../entry/impl/model/OnboardingEntryModel.kt | 48 +++++++++-- .../impl/routing/OnboardingChildFactory.kt | 1 + .../v2/entry/impl/routing/OnboardingRoute.kt | 2 + .../impl/DefaultOnboardingVisaComponent.kt | 6 +- .../accesscode/ui/OnboardingVisaAccessCode.kt | 8 +- .../child/approve/ui/OnboardingVisaApprove.kt | 9 +- .../model/OnboardingVisaChooseWalletModel.kt | 6 +- .../ui/OnboardingVisaChooseWallet.kt | 11 ++- .../model/OnboardingVisaOtherWalletModel.kt | 4 +- .../ui/OnboardingVisaOtherWallet.kt | 11 ++- .../model/OnboardingVisaPinCodeModel.kt | 13 +-- .../child/pincode/ui/OnboardingVisaPinCode.kt | 10 ++- .../child/welcome/ui/OnboardingVisaWelcome.kt | 17 ++-- .../ui/state/OnboardingVisaWelcomeUM.kt | 1 - .../onboarding/v2/visa/impl/route/Titles.kt | 22 +++-- .../v2/visa/impl/ui/OnboardingVisa.kt | 9 -- 23 files changed, 278 insertions(+), 81 deletions(-) delete mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/ui/OnboardingVisa.kt diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index c5b765d329..1c51e92eb7 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -139,6 +139,7 @@ Markt Langsam Gebühren + Beenden Adressen synchronisieren Zum Anbieter gehen Zum Token @@ -158,6 +159,7 @@ Keine Adresse Jetzt OK + Im Browser öffnen Hauptkarte oder Ring Primärring Passphrase @@ -181,6 +183,7 @@ Senden Der Server ist nicht verfügbar. Bitte versuche es später erneut. Teilen + Link teilen Signieren Signieren und senden Staken @@ -495,6 +498,10 @@ Rufe dies auf oder tippe auf die Suchleiste, um Token direkt vom Markt hinzuzufügen Token hinzufügen NFC ist auf deinem Gerät nicht verfügbar + %1$d NFTs in der %2$d Sammlung + Tippe hier, um das erste NFT zu erhalten + NFT-Sammlungen + Die Daten konnten nicht geladen werden Du musst einen einzigen Zugangscode einrichten, um alle deine Geräte zu schützen Schützen Du kannst später auf jeder Karte oder Ring einen individuellen Zugangscode einrichten @@ -760,6 +767,8 @@ inklusive einer Netzgebühr von %1$s Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert %1$s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen. + Um diese Transaktion für die angegebene Adresse abzuschließen, ist ein Ziel-Tag (Memo) erforderlich. + Ziel-Tag erforderlich Transaktion gesendet Bereite das Scannen der Karte oder Ring vor, die du einrichten möchtest. Entferne diese Wallet @@ -857,6 +866,7 @@ Belohnungen Stake gesperrt Mehr staken + Beim Staking %1$s wird Dein gesamtes %2$s -Guthaben eingesetzt. Alle weiteren %2$s due Du in Deine Tangem-Wallet einzahlst, werden ebenfalls automatisch eingesetzt. Gestakeder Betrag Du setzt %1$s ein und ethältst Deine Belohnung %2$s Zum Entsperren antippen @@ -931,7 +941,7 @@ Die Aktion ist für benutzerdefinierte Token nicht verfügbar. Du hast kein Guthaben um dies zu Verkaufen. Lade dein Konto auf, um Geld von dort zu verkaufen. Du hast kein Guthaben zum Versenden. Lade dein Konto auf, um Geld von dort aus senden zu können. - Die Daten wurden noch nicht geladen. Dies kann einige Sekunden dauern. Bitte versuche es später erneut. + Die Daten werden gerade geladen. Dies kann einige Sekunden dauern. Bitte versuche es später noch einmal. Das Tauschen von %s wird von aktuellen Anbietern nicht unterstützt, aber wir arbeiten daran, weitere Optionen hinzuzufügen. Der angezeigte Kontostand kann aufgrund der Zwischenspeicherung veraltet sein. Der Verkauf von Geldmitteln ist möglich, sobald die ausstehende(n) Transaktion(en) im %s -Netzwerk abgeschlossen ist/sind. @@ -995,6 +1005,24 @@ Wallet umbenennen Alle freischalten Alle mit %s freischalten + + Verfügbar für %d Tag + Verfügbar für %d Tage + + Guthaben und Limits + Bitte wähle die Wallet aus, mit der Du den Registrierungsprozess gestartet hast, um die Transaktion zur Erstellung Deines Kontos auf der Blockchain zu signieren + Möchtest Du den Vorgang wirklich beenden? Du kannst später dort fortfahren, wo Du aufgehört hast. + Das wird nicht lange dauern. Wir richten gerade Dein Konto ein. + Das wird nicht lange dauern. Wir schließen die Aktivierung ab. + Alles wird vorbereitet! + Ungültige PIN: Sequenzen oder Wiederholungen vermeiden + Schon kann es losgehen! + Du kannst Deine Verbindung \n auf der Website des Drittanbieters \n herstellen und zurück zur Tangem-App gelangen + Zur Website gehen + Fahren wir mit der Einrichtung Deines Kontos fort. + Willkommen zurück! + Folge den Schritten, um Dein Konto einzurichten. + Willkommen! Freischalten Scanne Deine Karte, um den Zugriff freizuschalten Benötigte Freischaltung diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 8adef0ab59..83acd44b09 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1038,7 +1038,7 @@ BNB Beacon Chain va s\'arrêter de fonctionner Veuillez déposer environ %1$s pour couvrir les frais de réseau Fonds insuffisants pour couvrir les frais de réseau - Pas terrible + Peut mieux faire J\'aime Ok, compris! Vraiment cool ! diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 2ff3e6be5f..9c9b68a323 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -136,6 +136,7 @@ マーケット 遅い 速度と料金 + 終了 アドレスを同期する プロバイダーへ移動 トークンへ移動 @@ -155,6 +156,7 @@ アドレスがありません わかりました + ブラウザで開く プライマリーカード プライマリーリング パスフレーズ @@ -178,6 +180,7 @@ 送る サーバーが利用できません。しばらくしてからもう一度お試しください。 共有 + リンクを共有 署名 署名して送信 ステーキング @@ -489,6 +492,19 @@ これをドラッグするか、検索窓をタップして、マーケットから直接トークンを追加します トークンを追加 お使いのデバイスではNFCが使用できません + + アイテム + + ウォレットに送信されたNFTがここに表示されます。 + コレクションはまだありません + NFTを受け取る + NFTコレクション + 一部のデータが読み込まれない場合があります + 一時的な読み込みの問題 + %1$dコレクションの%2$dNFT + ここをタップして最初のNFTを受け取ります + NFTコレクション + データを読み込めません すべてのデバイスを保護するには、単一のアクセスコードを設定してください。 保護する 後で各カードおよびリングに個別のアクセスコードを設定できます。 @@ -750,6 +766,8 @@ ネットワーク手数料%1$sを含む 取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。 %1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTron(TRX)を入金する必要があります。 + 指定されたアドレスへの取引を完了するには、宛先タグ(メモ)が必要です。 + 宛先タグが必要です 取引が送信されました 設定したいカードまたはリングをスキャンするために準備してください。 ウォレット削除 @@ -847,6 +865,7 @@ 報酬 ステーキングはロックされています もっとステーキングする + %1$sをステーキングすると、 %2$s残高全体がステーキングされます。Tangemウォレットに入金した追加の%2$sも、自動的にステーキングされます。 ステーキング中の金額 %1$sをステーキングし、報酬%2$sを受け取ります タップしてロック解除 @@ -985,6 +1004,20 @@ ウォレット名の変更 すべてのロックを解除 %sですべてをロック解除 + + %d日間利用可能 + + 残高と限度額 + 本当に終了してもよろしいですか?中断したところから後で続行できます。 + 長くはかかりません。アカウントを設定しています。 + 長くはかかりません。アクティベーションを完了しています。 + 準備完了です! + 無効な暗証番号:連続や繰り返しを避けてください + ウェブサイトに移動 + アカウントの設定を続けましょう。 + お帰りなさい! + 手順に従ってアカウントを設定してください。 + ようこそ! ロック解除 カードをスキャンしてアクセスロックを解除する ロック解除が必要 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 85818c274f..453cfe5df9 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -139,6 +139,7 @@ Market Slow Speed and fee + Finish Synchronize addresses Go to provider Go to token @@ -158,6 +159,7 @@ No address Now OK + Open in Browser Primary card Primary ring Passphrase @@ -181,6 +183,7 @@ Send The server is not available, please try again later Share + Share Link Sign Sign and send Stake @@ -190,6 +193,7 @@ Success Support Swap + Tangem Wallet terms and conditions Terms of Use Today @@ -495,9 +499,19 @@ Pull this up or tap the search bar to add tokens directly from the market Add tokens NFC is not available on your device - NFT collections - Tap here to receive first NFT + + %d item + %d items + + NFTs sent to your wallet address will show up here. + No collections, yet + Receive NFT + NFT collections + Some data may not load + Temporary loading problems %1$d NFTs in %2$d collection + Tap here to receive first NFT + NFT collections Unable to load the data Set up a single access code to protect all your devices. Protect @@ -764,6 +778,8 @@ including a network fee of %1$s Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while %1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron (TRX) in your account. + A destination tag (memo) is required to complete this transaction for the specified address. + Destination Tag Required Transaction sent Prepare to scan card or ring you want to set up. Forget wallet @@ -1000,6 +1016,69 @@ Rename wallet Unlock all Unlock all with %s + AML verifired + Available + Blocked + Debt + Limits + Other (no-otp) + Single transaction + Total + + available for %d day + available for %d days + + Available balance is actual funds available, considering pending transactions, blocked amounts, and debit balance to prevent overdrafts. + Available till %1$s + Balances & Limits + Missing Payment account information. Please contact support + Unable to load information about token + Information is still loading, please wait + Limits are needed to control costs, improve security, manage risk. You can spend %1$s during the period for card payments in shops and %2$s for other transactions, e.g. subscriptions or debts. + The access code will be used manage your payment account and protect it from unauthorized access + Access Code + Account activation + Please choose the wallet you started the registration process with to sign the transaction for creating your account on the Blockchain + Cancel Activation + Are you sure you want to exit? You can continue later from where you left off. + This won\'t take long. We\'re setting up your account. + This won’t take long. We’re completing the activation. + Getting everything ready! + Other wallet + Set up a 4-digit code. 
It will be used for payments. + PIN code + Create PIN Code + Invalid PIN: avoid sequences or repeats + You\'re good to go! + Prepare the Tangem card and tap to approve + Prepare Tangem Wallet + You will be able to complete your connection \n on the third-party web-site \n and back to the Tangem app + Go to Website + Wallet connection + Choose wallet + Continue Activation + Let\'s continue setting up your account. + Welcome back! + Start Activation + Follow the steps to set up your account. + Welcome! + Blockchain amount + Currency code + Date + Error code + Transaction details + Merchant category code + Merchant city + Merchant country code + Merchant name + Request ID + Status + Transaction + Transaction amount + Transaction hash + Transaction request + Transaction status + Type Unlock Scan your card to unlock access Needed unlock diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/api/OnboardingDoneComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/api/OnboardingDoneComponent.kt index 079b169a7e..2863a3876a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/api/OnboardingDoneComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/api/OnboardingDoneComponent.kt @@ -5,7 +5,14 @@ import com.tangem.core.ui.decompose.ComposableContentComponent interface OnboardingDoneComponent : ComposableContentComponent { - data class Params(val onDone: () -> Unit) + data class Params( + val mode: Mode, + val onDone: () -> Unit, + ) + + enum class Mode { + WalletCreated, GoodToGo + } interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/DefaultOnboardingDoneComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/DefaultOnboardingDoneComponent.kt index f36c949bfe..a32bb8cc28 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/DefaultOnboardingDoneComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/DefaultOnboardingDoneComponent.kt @@ -1,7 +1,6 @@ package com.tangem.features.onboarding.v2.done.impl import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.tangem.core.decompose.context.AppComponentContext import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent @@ -18,7 +17,8 @@ internal class DefaultOnboardingDoneComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { OnboardingDone( - onContinueClick = remember(this) { params.onDone }, + mode = params.mode, + onContinueClick = params.onDone, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/ui/OnboardingDone.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/ui/OnboardingDone.kt index f3c8aa0111..309b940ac9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/ui/OnboardingDone.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/ui/OnboardingDone.kt @@ -20,10 +20,16 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.impl.R +@Suppress("LongMethod") @Composable -internal fun OnboardingDone(onContinueClick: () -> Unit, modifier: Modifier = Modifier) { +internal fun OnboardingDone( + mode: OnboardingDoneComponent.Mode, + onContinueClick: () -> Unit, + modifier: Modifier = Modifier, +) { val composition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.anim_confetti)) val progress by animateLottieCompositionAsState(composition) var showConfetti by remember { mutableStateOf(false) } @@ -57,7 +63,12 @@ internal fun OnboardingDone(onContinueClick: () -> Unit, modifier: Modifier = Mo SpacerH12() Text( modifier = Modifier.padding(horizontal = 32.dp), - text = stringResourceSafe(R.string.onboarding_subtitle_success_tangem_wallet_onboarding), + text = when (mode) { + OnboardingDoneComponent.Mode.WalletCreated -> + stringResourceSafe(R.string.onboarding_subtitle_success_tangem_wallet_onboarding) + OnboardingDoneComponent.Mode.GoodToGo -> + stringResourceSafe(R.string.visa_onboarding_success_screen_description) + }, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, @@ -100,6 +111,9 @@ internal fun OnboardingDone(onContinueClick: () -> Unit, modifier: Modifier = Mo @Composable private fun Preview() { TangemThemePreview { - OnboardingDone(onContinueClick = {}) + OnboardingDone( + mode = OnboardingDoneComponent.Mode.WalletCreated, + onContinueClick = {}, + ) } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index 45def47034..f8baf38360 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -20,6 +20,7 @@ import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.biometry.BiometryFeatureToggles import com.tangem.features.onboarding.v2.TitleProvider import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog +import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent import com.tangem.features.onboarding.v2.entry.impl.analytics.OnboardingEntryEvent import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute @@ -105,7 +106,12 @@ internal class OnboardingEntryModel @Inject constructor( private fun onMultiWalletOnboardingDone(userWallet: UserWallet) { when { params.multiWalletMode == OnboardingEntryComponent.MultiWalletMode.AddBackup -> { - stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen)) + stackNavigation.replaceAll( + OnboardingRoute.Done( + mode = OnboardingDoneComponent.Mode.WalletCreated, + onDone = ::navigateToWalletScreen, + ), + ) } userWallet.scanResponse.cardTypesResolver.isMultiwalletAllowed() -> { stackNavigation.replaceAll(OnboardingRoute.ManageTokens(userWallet)) @@ -117,34 +123,58 @@ internal class OnboardingEntryModel @Inject constructor( } private fun onVisaOnboardingDone() { - navigateToFinalScreenFlow() + navigateToFinalScreenFlow(doneMode = OnboardingDoneComponent.Mode.GoodToGo) } - private fun navigateToFinalScreenFlow() { + private fun navigateToFinalScreenFlow( + doneMode: OnboardingDoneComponent.Mode = OnboardingDoneComponent.Mode.WalletCreated, + ) { if (askBiometryFeatureToggles.isAskForBiometryEnabled) { modelScope.launch { if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) { stackNavigation.replaceAll( - OnboardingRoute.AskBiometry(modelCallbacks = AskBiometryModelCallbacks()), + OnboardingRoute.AskBiometry(modelCallbacks = AskBiometryModelCallbacks(doneMode)), ) } else { - stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen)) + stackNavigation.replaceAll( + OnboardingRoute.Done( + mode = doneMode, + onDone = ::navigateToWalletScreen, + ), + ) } } } else { - stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen)) + stackNavigation.replaceAll( + OnboardingRoute.Done( + mode = doneMode, + onDone = ::navigateToWalletScreen, + ), + ) } } - inner class AskBiometryModelCallbacks : AskBiometryComponent.ModelCallbacks { + inner class AskBiometryModelCallbacks( + private val doneMode: OnboardingDoneComponent.Mode, + ) : AskBiometryComponent.ModelCallbacks { override fun onAllowed() { analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.On)) - stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen)) + stackNavigation.replaceAll( + OnboardingRoute.Done( + mode = doneMode, + onDone = ::navigateToWalletScreen, + ), + ) } override fun onDenied() { analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.Off)) - stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen)) + stackNavigation.replaceAll( + OnboardingRoute.Done( + mode = doneMode, + onDone = ::navigateToWalletScreen, + ), + ) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt index 608d542ebe..edbc555782 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt @@ -48,6 +48,7 @@ internal class OnboardingChildFactory @Inject constructor( is OnboardingRoute.Done -> onboardingDoneComponentFactory.create( context = childContext, params = OnboardingDoneComponent.Params( + mode = route.mode, onDone = route.onDone, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt index b111c82d91..1175f2948b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt @@ -5,6 +5,7 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.onboarding.v2.TitleProvider +import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent sealed class OnboardingRoute : Route { @@ -40,6 +41,7 @@ sealed class OnboardingRoute : Route { ) : OnboardingRoute() data class Done( + val mode: OnboardingDoneComponent.Mode, val onDone: () -> Unit, ) : OnboardingRoute() } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt index b36dcef18f..108d8134bc 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt @@ -161,7 +161,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( params = OnboardingVisaOtherWalletComponent.Params( childParams = childParams, onDone = { - model.stackNavigation.push(OnboardingVisaRoute.PinCode(activationOrderInfo = it)) + model.stackNavigation.pushNew(OnboardingVisaRoute.PinCode(activationOrderInfo = it)) }, ), ) @@ -174,7 +174,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( params = OnboardingVisaPinCodeComponent.Params( childParams = childParams, onDone = { - model.stackNavigation.push( + model.stackNavigation.pushNew( OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.PinCode), ) }, @@ -190,7 +190,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( params = OnboardingVisaApproveComponent.Params( childParams = childParams, onDone = { - model.stackNavigation.push( + model.stackNavigation.pushNew( OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.Approve), ) }, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt index 1557683bfe..d80ef6209b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt @@ -23,6 +23,7 @@ import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemAnimations import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -87,9 +88,9 @@ private fun Content( modifier = Modifier .padding(horizontal = 32.dp), text = if (reEnterAccessCodeState) { - "Re-enter your access code" // TODO + stringResourceSafe(R.string.onboarding_access_code_repeat_code_title) } else { - "Create access code" // TODO + stringResourceSafe(R.string.onboarding_access_code_intro_title) }, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, @@ -98,11 +99,10 @@ private fun Content( SpacerH16() - // TODO Text( modifier = Modifier .padding(horizontal = 32.dp), - text = "The access code will be used manage your payment account and protect it from unauthorized access", + text = stringResourceSafe(R.string.visa_onboarding_access_code_description), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt index f93ebd7cd3..106973d0da 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt @@ -14,7 +14,8 @@ import androidx.compose.ui.unit.dp import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -43,7 +44,7 @@ internal fun OnboardingVisaApprove(state: OnboardingVisaApproveUM, modifier: Mod SpacerH(86.dp) Text( - text = "Prepare Tangem Wallet", + text = stringResourceSafe(R.string.visa_onboarding_tangem_approve_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -52,7 +53,7 @@ internal fun OnboardingVisaApprove(state: OnboardingVisaApproveUM, modifier: Mod SpacerH(12.dp) Text( - text = "Prepare the Tangem card\nand tap to approve", + text = stringResourceSafe(R.string.visa_onboarding_tangem_approve_description), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, @@ -65,7 +66,7 @@ internal fun OnboardingVisaApprove(state: OnboardingVisaApproveUM, modifier: Mod .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), primaryButton = NavigationButton( - textReference = TextReference.Str("Approve"), + textReference = resourceReference(R.string.common_approve), iconRes = R.drawable.ic_tangem_24, isIconVisible = true, showProgress = state.approveButtonLoading, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt index 381cfc11aa..3d48e57718 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt @@ -3,7 +3,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.model import androidx.compose.runtime.Stable import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.OnboardingVisaChooseWalletComponent.Params.Event import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state.OnboardingVisaChooseWalletUM @@ -32,7 +32,7 @@ internal class OnboardingVisaChooseWalletModel @Inject constructor( val selectedOption = SelectableChainRowUM( event = Event.TangemWallet, icon = R.drawable.ic_tangem_24, - text = TextReference.Str("Tangem Wallet"), + text = resourceReference(R.string.common_tangem_wallet), ) return OnboardingVisaChooseWalletUM( @@ -41,7 +41,7 @@ internal class OnboardingVisaChooseWalletModel @Inject constructor( SelectableChainRowUM( event = Event.OtherWallet, icon = R.drawable.ic_wallet_filled_24, - text = TextReference.Str("Other Wallet"), + text = resourceReference(R.string.visa_onboarding_other_wallet), ), ), selectedOption = selectedOption, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt index b44e28c2c6..5cd9a11f10 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt @@ -21,6 +21,8 @@ import com.tangem.core.ui.components.rows.RowContentContainer import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.outsetBorder import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -45,10 +47,7 @@ internal fun OnboardingVisaChooseWallet(state: OnboardingVisaChooseWalletUM, mod ) { Notification( config = NotificationConfig( - subtitle = TextReference.Str( - "Please choose the wallet you started the registration" + - " process with to sign the transaction for creating your account on the Blockchain.", - ), + subtitle = resourceReference(R.string.visa_onboarding_approve_wallet_selector_notification_message), iconResId = R.drawable.ic_alert_circle_24, ), iconTint = TangemTheme.colors.icon.accent, @@ -62,7 +61,7 @@ internal fun OnboardingVisaChooseWallet(state: OnboardingVisaChooseWalletUM, mod start = 12.dp, end = 12.dp, ), - text = "Choose wallet", + text = stringResourceSafe(R.string.visa_onboarding_wallet_list_header), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) @@ -79,7 +78,7 @@ internal fun OnboardingVisaChooseWallet(state: OnboardingVisaChooseWalletUM, mod .imePadding() .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), - text = "Continue", + text = stringResourceSafe(R.string.common_continue), onClick = state.onContinueClick, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt index 53c043fbea..875e5a4b49 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt @@ -68,10 +68,10 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor( } private fun onShareClicked() { - shareManager.shareText("https://tangem.com/") + shareManager.shareText("https://tangem.com/") // TODO } private fun onOpenInBrowserClicked() { - urlOpener.openUrl("https://tangem.com/") + urlOpener.openUrl("https://tangem.com/") // TODO } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/OnboardingVisaOtherWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/OnboardingVisaOtherWallet.kt index 4b0a8c2f3f..68b39dbc61 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/OnboardingVisaOtherWallet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/OnboardingVisaOtherWallet.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -44,7 +45,7 @@ internal fun OnboardingVisaOtherWallet(state: OnboardingVisaOtherWalletUM, modif SpacerH12() Text( - text = "Go to Website", + text = stringResourceSafe(R.string.visa_onboarding_wallet_connect_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -53,9 +54,7 @@ internal fun OnboardingVisaOtherWallet(state: OnboardingVisaOtherWalletUM, modif SpacerH12() Text( - text = "You will be able to complete your connection \n" + - "on the third-party web-site \n" + - "and back to the Tangem app", + text = stringResourceSafe(R.string.visa_onboarding_wallet_connect_description), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, @@ -68,7 +67,7 @@ internal fun OnboardingVisaOtherWallet(state: OnboardingVisaOtherWalletUM, modif modifier = Modifier .padding(horizontal = 16.dp) .fillMaxWidth(), - text = "Open in Browser", + text = stringResourceSafe(R.string.common_open_in_browser), onClick = state.onOpenInBrowserClick, ) @@ -78,7 +77,7 @@ internal fun OnboardingVisaOtherWallet(state: OnboardingVisaOtherWalletUM, modif modifier = Modifier .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), - text = "Share Link", + text = stringResourceSafe(R.string.common_share_link), onClick = state.onOpenInBrowserClick, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt index 3e590ec818..bcb96ade2f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt @@ -5,7 +5,7 @@ import com.tangem.common.extensions.toHexString 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.ui.extensions.stringReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.visa.SetVisaPinCodeUseCase @@ -14,6 +14,7 @@ import com.tangem.domain.visa.model.VisaCardId import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.visa.impl.child.pincode.OnboardingVisaPinCodeComponent import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -64,7 +65,11 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( it.copy( pinCode = pin, submitButtonEnabled = PinCodeValidation.validate(pin), - error = if (isError) stringReference("Invalid PIN: avoid sequences or repeats") else null, + error = if (isError) { + resourceReference(R.string.visa_onboarding_pin_validation_error_message) + } else { + null + }, ) } } @@ -117,8 +122,4 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( lazyMessage = { "User wallet not created" }, ) } - - private companion object { - const val PIN_CODE_LENGTH = 4 - } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt index 744711c9db..cb2d5993c8 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt @@ -33,11 +33,13 @@ import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.SpacerH4 -import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM import kotlinx.coroutines.delay @@ -57,7 +59,7 @@ internal fun OnboardingVisaPinCode(state: OnboardingVisaPinCodeUM, modifier: Mod horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = "Create\nPIN Code", + text = stringResourceSafe(R.string.visa_onboarding_pin_code_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -66,7 +68,7 @@ internal fun OnboardingVisaPinCode(state: OnboardingVisaPinCodeUM, modifier: Mod SpacerH16() Text( - text = "Set up a 4-digit code.\nIt will be used for payments.", + text = stringResourceSafe(R.string.visa_onboarding_pin_code_description), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, @@ -83,7 +85,7 @@ internal fun OnboardingVisaPinCode(state: OnboardingVisaPinCodeUM, modifier: Mod .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), primaryButton = NavigationButton( - textReference = TextReference.Str("Submit"), + textReference = resourceReference(R.string.common_submit), onClick = state.onSubmitClick, showProgress = state.submitButtonLoading, isEnabled = state.submitButtonEnabled, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt index 0a209235b0..9a01f706d6 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt @@ -16,6 +16,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -47,8 +48,10 @@ internal fun OnboardingVisaWelcome(state: OnboardingVisaWelcomeUM, modifier: Mod Text( text = when (state.mode) { - OnboardingVisaWelcomeUM.Mode.Hello -> "Hello, ${state.userName}!" - OnboardingVisaWelcomeUM.Mode.WelcomeBack -> "Welcome back, ${state.userName}!" + OnboardingVisaWelcomeUM.Mode.Hello -> + stringResourceSafe(R.string.visa_onboarding_welcome_title) + OnboardingVisaWelcomeUM.Mode.WelcomeBack -> + stringResourceSafe(R.string.visa_onboarding_welcome_back_title) }, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, @@ -58,8 +61,10 @@ internal fun OnboardingVisaWelcome(state: OnboardingVisaWelcomeUM, modifier: Mod Text( text = when (state.mode) { - OnboardingVisaWelcomeUM.Mode.Hello -> "bla bla bla //TODO" - OnboardingVisaWelcomeUM.Mode.WelcomeBack -> "bla bla bla? //TODO" + OnboardingVisaWelcomeUM.Mode.Hello -> + stringResourceSafe(R.string.visa_onboarding_welcome_description) + OnboardingVisaWelcomeUM.Mode.WelcomeBack -> + stringResourceSafe(R.string.visa_onboarding_welcome_back_description) }, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, @@ -72,7 +77,7 @@ internal fun OnboardingVisaWelcome(state: OnboardingVisaWelcomeUM, modifier: Mod modifier = Modifier .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), - text = "Start Activation", + text = stringResourceSafe(R.string.visa_onboarding_welcome_button_title), onClick = state.onContinueClick, ) } @@ -81,7 +86,7 @@ internal fun OnboardingVisaWelcome(state: OnboardingVisaWelcomeUM, modifier: Mod modifier = Modifier .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), - text = "Continue Activation", + text = stringResourceSafe(R.string.visa_onboarding_welcome_back_button_title), showProgress = state.continueButtonLoading, iconResId = R.drawable.ic_tangem_24, onClick = state.onContinueClick, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/state/OnboardingVisaWelcomeUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/state/OnboardingVisaWelcomeUM.kt index d1f975f9b3..069b63d30f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/state/OnboardingVisaWelcomeUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/state/OnboardingVisaWelcomeUM.kt @@ -2,7 +2,6 @@ package com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui.state internal data class OnboardingVisaWelcomeUM( val mode: Mode = Mode.Hello, - val userName: String = "", val continueButtonLoading: Boolean = false, val onContinueClick: () -> Unit = {}, ) { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Titles.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Titles.kt index 5da3a52e3d..87aaac10b3 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Titles.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Titles.kt @@ -1,16 +1,22 @@ package com.tangem.features.onboarding.v2.visa.impl.route import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onboarding.v2.impl.R -// TODO add translations [REDACTED_TASK_KEY] internal fun OnboardingVisaRoute.screenTitle(): TextReference = when (this) { - OnboardingVisaRoute.AccessCode -> TextReference.Str("Access code") - is OnboardingVisaRoute.ChooseWallet -> TextReference.Str("Account activation") - is OnboardingVisaRoute.InProgress -> TextReference.Str("In progress") - is OnboardingVisaRoute.OtherWalletApproveOption -> TextReference.Str("Wallet connection") - is OnboardingVisaRoute.PinCode -> TextReference.Str("PIN code") - is OnboardingVisaRoute.TangemWalletApproveOption -> TextReference.Str("Wallet connection") + OnboardingVisaRoute.AccessCode -> + resourceReference(R.string.visa_onboarding_access_code_navigation_title) + is OnboardingVisaRoute.ChooseWallet -> + resourceReference(R.string.visa_onboarding_account_activation_navigation_title) + is OnboardingVisaRoute.InProgress -> + resourceReference(R.string.common_in_progress) + is OnboardingVisaRoute.OtherWalletApproveOption, + is OnboardingVisaRoute.TangemWalletApproveOption, + -> resourceReference(R.string.visa_onboarding_wallet_connection_navigation_title) + is OnboardingVisaRoute.PinCode -> + resourceReference(R.string.visa_onboarding_pin_code_navigation_title) is OnboardingVisaRoute.Welcome, is OnboardingVisaRoute.WelcomeBack, - -> TextReference.Str("Getting started") + -> resourceReference(R.string.onboarding_getting_started) } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/ui/OnboardingVisa.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/ui/OnboardingVisa.kt deleted file mode 100644 index 40a9a8597d..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/ui/OnboardingVisa.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.features.onboarding.v2.visa.impl.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -internal fun OnboardingVisa(modifier: Modifier = Modifier, childContent: @Composable (Modifier) -> Unit) { - childContent(modifier) -} \ No newline at end of file From d84b06f80dcae511c5f264e66c1a5d3527db9c7d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 24 Mar 2025 11:57:42 +0200 Subject: [PATCH 029/138] Updated on 2026-08-14 --- .../common/ui/notifications/NotificationUM.kt | 5 +++++ .../ui/notifications/NotificationsFactory.kt | 14 ++++++++++++++ .../state/confirm/SendNotificationFactory.kt | 7 +++++-- gradle/tangem_dependencies.toml | 2 +- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index e37d15ae31..d546442104 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -159,6 +159,11 @@ sealed class NotificationUM(val config: NotificationConfig) { ), subtitle = resourceReference(id = R.string.send_notification_invalid_reserve_amount_text), ) + + data object DestinationMemoRequired : Error( + title = resourceReference(id = R.string.send_validation_destination_tag_required_title), + subtitle = resourceReference(id = R.string.send_validation_destination_tag_required_description), + ) } open class Warning( diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt index 347ac937ec..b5e98fa800 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.amountScreen.utils.getFiatString +import com.tangem.common.ui.notifications.NotificationsFactory.addRequireDestinationFlagErrorNotification import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format @@ -112,6 +113,10 @@ object NotificationsFactory { } } + fun MutableList.addRequireDestinationFlagErrorNotification() { + add(NotificationUM.Error.DestinationMemoRequired) + } + fun MutableList.addMinimumAmountErrorNotification( minimumSendAmount: BigDecimal?, sendingAmount: BigDecimal, @@ -326,6 +331,9 @@ object NotificationsFactory { error = validationError, onReduceClick = onReduceClick, ) + is BlockchainSdkError.XRP -> addXRPTransactionValidationError( + error = validationError, + ) null -> minAdaValue?.let { add( NotificationUM.Cardano.MinAdaValueCharged( @@ -406,6 +414,12 @@ object NotificationsFactory { } } + private fun MutableList.addXRPTransactionValidationError(error: BlockchainSdkError.XRP) { + when (error) { + is BlockchainSdkError.XRP.DestinationMemoRequired -> addRequireDestinationFlagErrorNotification() + } + } + fun MutableList.addRentExemptionNotification(rentWarning: CryptoCurrencyWarning.Rent?) { if (rentWarning == null) return add(NotificationUM.Solana.RentInfo(rentWarning)) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 87b25f2824..6254300fa0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -74,6 +74,7 @@ internal class SendNotificationFactory( val sendState = state.sendState ?: return@map persistentListOf() val feeState = state.getFeeState(isEditState) ?: return@map persistentListOf() val amountState = state.getAmountState(isEditState) as? AmountState.Data ?: return@map persistentListOf() + val recipientState = state.getRecipientState(isEditState) val amountValue = amountState.amountTextField.cryptoAmount.value.orZero() val feeValue = feeState.fee?.amount?.value.orZero() @@ -118,6 +119,7 @@ internal class SendNotificationFactory( ) addWarningNotifications( amountState = amountState, + recipientState = recipientState, feeState = feeState, sendState = sendState, sendingAmount = sendingAmount, @@ -233,6 +235,7 @@ internal class SendNotificationFactory( private suspend fun MutableList.addWarningNotifications( amountState: AmountState.Data, + recipientState: SendStates.RecipientState?, feeState: SendStates.FeeState, sendState: SendStates.SendState, sendingAmount: BigDecimal, @@ -247,8 +250,8 @@ internal class SendNotificationFactory( userWalletId = userWalletId, amount = amountValue.convertToSdkAmount(cryptoCurrencyStatus.currency), fee = feeState.fee, - memo = null, - destination = "", + memo = recipientState?.memoTextField?.value.orEmpty(), + destination = recipientState?.addressTextField?.value.orEmpty(), network = cryptoCurrencyStatus.currency.network, ).leftOrNull() } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 1e84ff3ed9..fc082393ac 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,5 +1,5 @@ [versions] -tangemBlockchainSdk = "develop-988" +tangemBlockchainSdk = "develop-989" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-444" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From b0a97f53d96128b3f8ab1554d37472771ae45b65 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 24 Mar 2025 18:29:12 +0500 Subject: [PATCH 030/138] Updated on 2026-08-14 --- .../configs/feature_toggles_config.json | 2 +- core/res/src/main/res/values-de/strings.xml | 1 + core/res/src/main/res/values-es/strings.xml | 1 + core/res/src/main/res/values-fr/strings.xml | 1 + core/res/src/main/res/values-ja/strings.xml | 1 + core/res/src/main/res/values-ru/strings.xml | 1 + .../src/main/res/values-uk-rUA/strings.xml | 1 + .../src/main/res/values-zh-rTW/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 1 + features/onboarding-v2/impl/build.gradle.kts | 1 + .../model/OnboardingNoteCreateWalletModel.kt | 1 - .../topup/model/OnboardingNoteTopUpModel.kt | 41 +++++++++++++++---- .../topup/ui/OnboardingNoteTopUpHeader.kt | 15 ++++++- .../topup/ui/OnboardingNoteTopUpScreen.kt | 10 ++++- .../topup/ui/state/OnboardingNoteTopUpUM.kt | 2 +- .../impl/model/OnboardingNoteCommonState.kt | 2 +- 16 files changed, 68 insertions(+), 14 deletions(-) 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 a3e83e6903..810a9dceb5 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 @@ -65,7 +65,7 @@ }, { "name": "NOTE_REFACTORING_ENABLED", - "version": "undefined" + "version": "5.23.0" }, { "name": "NEW_ATTESTATION_ENABLED", diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 1c51e92eb7..4ff77a5e42 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -587,6 +587,7 @@ Um zu beginnen, lade die Wallet einfach mit mehr als %1$s %2$s auf. Krypto kaufen Adresse der Wallet anzeigen + Um zu beginnen, lade die Wallet einfach mit mehr als %1$s auf. Aktivieren einer Wallet Der Twinning-Prozess ist teilweise abgeschlossen. Du kannst ihn jetzt nicht mehr beenden. Wenn der Prozess der Erstellung der Brieftasche in irgendeiner Weise unterbrochen wird, musst du diesen von vorne beginnen diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index bd31c5f775..7446797c14 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -578,6 +578,7 @@ Para empezar, simplemente recargue la billetera con más de %1$s %2$s Comprar cripto Mostrar la dirección de la billetera + Para empezar, simplemente recargue la billetera con más de %1$s Activar una billetera El proceso de emparejamiento está parcialmente completo. No puede salir ahora. Si el proceso de creación del wallet se interrumpe de alguna manera, tendrá que empezar de nuevo diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 83acd44b09..db80887fdb 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -580,6 +580,7 @@ Pour commencer, rechargez simplement le portefeuille avec plus de %1$s %2$s Acheter des crypto-monnaies Afficher l\'adresse du portefeuille + Pour commencer, rechargez simplement le portefeuille avec plus de %1$s Activer un portefeuille Le processus de jumelage est partiellement terminé. Vous ne pouvez pas le quitter maintenant. Si le processus de création du portefeuille est interrompu de quelque manière que ce soit, vous devrez recommencer diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 9c9b68a323..d0474aa52e 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -588,6 +588,7 @@ 始めるには、ウォレットに%1$s %2$s以上入金するだけです 暗号資産を購入する ウォレットのアドレスを表示する + 始めるには、ウォレットに%1$s 以上入金するだけです ウォレットを有効化する ツイン化プロセスは部分的に完了しています。現在終了することはできません。 ウォレットの作成プロセスが何らかの理由で中断された場合は、最初からやり直す必要があります。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index b46553bb3b..9e98a332d6 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -594,6 +594,7 @@ Пополните кошелек более чем на %1$s %2$s, чтобы начать пользоваться картой Купить криптовалюту Показать адрес кошелька + Пополните кошелек более чем на %1$s, чтобы начать пользоваться картой Активация кошелька Процесс связывания карт частично завершен. Вы не можете выйти из него сейчас. Если процесc создания кошелька каким-либо образом прервется, вам придется начинать сначала diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 1d760fbe95..595d9519f2 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -594,6 +594,7 @@ Щоб почати, просто поповніть гаманець більше, ніж на %1$s %2$s Купити криптовалюту Показати адресу гаманця + Щоб почати, просто поповніть гаманець більше, ніж на %1$s Активація гаманця Процес поєднання карток частково завершено. Ви не можете вийти з нього зараз. Якщо процес створення гаманця буде перервано будь-яким чином, вам доведеться почати все спочатку diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index d4ab1bc4e2..81c9546a39 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -205,6 +205,7 @@ 要開始,只需為錢包充值超過 %1$s %2$s 購買加密貨幣 顯示錢包地址 + 要開始,只需為錢包充值超過 %1$s 啟動錢包 結對過程已部分完成。你現在不能退出 如果創建錢包的過程以任何方式中斷,您將得重新開始 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 453cfe5df9..6761a8065b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -598,6 +598,7 @@ To get started, simply top up the wallet with more than %1$s %2$s Buy crypto Show the wallet\'s address + To get started, simply top up the wallet with more than %1$s Activate a wallet The twinning process is partly complete. You can\'t exit it now. If the process of creating the wallet gets interrupted in any way, you\'ll have to start over diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index 00be841eed..3c11edf62d 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -66,6 +66,7 @@ dependencies { implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) implementation(deps.compose.coil) + implementation(deps.compose.shimmer) implementation(deps.lottie.compose) implementation(deps.decompose.ext.compose) implementation(deps.androidx.activity.compose) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt index b8ca1d76a2..59f7b2e7ab 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt @@ -76,7 +76,6 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor( modelScope.launch { val userWallet = createUserWallet(scanResponse) saveWalletUseCase(userWallet, canOverride = true).onRight { - cardRepository.finishCardActivation(scanResponse.card.cardId) params.onWalletCreated(userWallet) } _uiState.update { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt index 2d7cbc4c04..28a0eda74b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBotto import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase @@ -23,10 +24,12 @@ import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.isPositive import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -45,12 +48,14 @@ internal class OnboardingNoteTopUpModel @Inject constructor( private val clipboardManager: ClipboardManager, private val shareManager: ShareManager, private val rampStateManager: RampStateManager, + private val cardRepository: CardRepository, + private val saveWalletUseCase: SaveWalletUseCase, ) : Model() { private val params = paramsContainer.require() private val commonState = params.childParams.commonState private val scanResponse = params.childParams.commonState.value.scanResponse - private var userWallet: UserWallet? = null + private var userWallet = params.childParams.commonState.value.userWallet private val _uiState = MutableStateFlow( OnboardingNoteTopUpUM( @@ -117,7 +122,7 @@ internal class OnboardingNoteTopUpModel @Inject constructor( return } val commonState = params.childParams.commonState.value - userWallet = commonState.userWallet ?: createUserWallet(scanResponse) + userWallet = commonState.userWallet ?: createAndSaveUserWallet(scanResponse) } private fun observeArtwork() { @@ -140,14 +145,34 @@ internal class OnboardingNoteTopUpModel @Inject constructor( if (commonState.value.cryptoCurrencyStatus == null) { loadAvailableForBuy(status) } + commonState.update { it.copy(cryptoCurrencyStatus = status) } + val amount = when (status.value) { + is CryptoCurrencyStatus.Loaded -> status.value.amount + is CryptoCurrencyStatus.NoAccount -> status.value.amount + is CryptoCurrencyStatus.NoQuote -> status.value.amount + else -> null + } + val hasCurrentNetworkTransactions = when (status.value) { + is CryptoCurrencyStatus.Loaded -> status.value.hasCurrentNetworkTransactions + is CryptoCurrencyStatus.NoAccount -> status.value.hasCurrentNetworkTransactions + else -> false + } + val amountToCreateAccount = (status.value as? CryptoCurrencyStatus.NoAccount)?.amountToCreateAccount + + if (amount?.isPositive() == true || hasCurrentNetworkTransactions) { + modelScope.launch { + cardRepository.finishCardActivation(scanResponse.card.cardId) + params.onDone() + } + } + _uiState.update { it.copy( - amountToCreateAccount = (status.value as? CryptoCurrencyStatus.NoAccount) - ?.amountToCreateAccount + amountToCreateAccount = amountToCreateAccount ?.format( { crypto( @@ -156,7 +181,7 @@ internal class OnboardingNoteTopUpModel @Inject constructor( ) }, ), - balance = (status.value as? CryptoCurrencyStatus.Loaded)?.amount?.format( + balance = amount?.format( { crypto( symbol = status.currency.symbol, @@ -217,10 +242,12 @@ internal class OnboardingNoteTopUpModel @Inject constructor( ), ) - private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet { - return requireNotNull( + private suspend fun createAndSaveUserWallet(scanResponse: ScanResponse): UserWallet { + val wallet = requireNotNull( value = userWalletBuilderFactory.create(scanResponse = scanResponse).build(), lazyMessage = { "User wallet not created" }, ) + saveWalletUseCase(wallet, false) + return wallet } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt index 572a257158..381b360e79 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt @@ -2,21 +2,25 @@ package com.tangem.features.onboarding.v2.note.impl.child.topup.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalTangemShimmer import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.common.ui.RefreshButton import com.tangem.features.onboarding.v2.common.ui.WalletCard import com.tangem.features.onboarding.v2.impl.R +import com.valentinilk.shimmer.shimmer @Composable fun OnboardingNoteTopUpHeader( @@ -58,10 +62,17 @@ fun OnboardingNoteTopUpHeader( ) SpacerH8() Text( - text = balance, + modifier = if (balance.isEmpty()) { + Modifier + .width(120.dp) + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius3)) + .shimmer(LocalTangemShimmer.current) + } else { + Modifier + }, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, + text = balance, ) SpacerHMax() RefreshButton( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt index 8fbe707ec3..e8dbf64055 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt @@ -57,8 +57,16 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif modifier = Modifier.padding(top = 16.dp), ) + val text = if (state.amountToCreateAccount != null) { + stringResourceSafe( + R.string.onboarding_top_up_min_create_account_amount, + state.amountToCreateAccount, + ) + } else { + stringResourceSafe(R.string.onboarding_top_up_body) + } Text( - text = stringResourceSafe(R.string.onboarding_top_up_body), + text = text, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt index f61ef91a50..327de0d586 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt @@ -6,7 +6,7 @@ data class OnboardingNoteTopUpUM( val cardArtworkUrl: String? = null, val availableForBuy: Boolean = false, val availableForBuyLoading: Boolean = true, - val balance: String = "–", + val balance: String = "", val isRefreshing: Boolean = false, val isTopUpDataLoading: Boolean = true, val amountToCreateAccount: String? = null, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt index 52ff268627..8ebe5bcd8b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt @@ -6,7 +6,7 @@ import com.tangem.domain.wallets.models.UserWallet internal data class OnboardingNoteCommonState( val scanResponse: ScanResponse, - val cardArtworkUrl: String? = null, val userWallet: UserWallet? = null, + val cardArtworkUrl: String? = null, val cryptoCurrencyStatus: CryptoCurrencyStatus? = null, ) \ No newline at end of file From a4340e40a89b1c694faa34e4ad05766d9af4b913 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Mar 2025 13:34:15 +0500 Subject: [PATCH 031/138] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../walletconnect/WalletConnectMiddleware.kt | 7 +- .../dialogs/ClipboardOrScanQrDialog.kt | 2 +- .../tangem/tap/routing/utils/ChildFactory.kt | 96 +++++++++++++------ .../com/tangem/common/routing/AppRoute.kt | 11 ++- .../configs/feature_toggles_config.json | 4 + features/send-v2/api/.gitignore | 1 + features/send-v2/api/build.gradle.kts | 19 ++++ .../send/v2/api/SendFeatureToggles.kt | 5 + features/send-v2/impl/.gitignore | 1 + features/send-v2/impl/build.gradle.kts | 70 ++++++++++++++ .../send/v2/DefaultSendFeatureToggles.kt | 11 +++ .../features/send/v2/di/SendFeatureModule.kt | 32 +++++++ .../send/impl/navigation/DefaultSendRouter.kt | 3 +- settings.gradle.kts | 12 ++- 15 files changed, 236 insertions(+), 40 deletions(-) create mode 100644 features/send-v2/api/.gitignore create mode 100644 features/send-v2/api/build.gradle.kts create mode 100644 features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt create mode 100644 features/send-v2/impl/.gitignore create mode 100644 features/send-v2/impl/build.gradle.kts create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendFeatureModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e5a1e6a705..8bee4d929a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -169,6 +169,8 @@ dependencies { implementation(projects.features.manageTokens.impl) implementation(projects.features.send.api) implementation(projects.features.send.impl) + implementation(projects.features.sendV2.api) + implementation(projects.features.sendV2.impl) implementation(projects.features.qrScanning.api) implementation(projects.features.qrScanning.impl) implementation(projects.features.staking.api) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index 8535bc008f..5239a2cbd9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -3,7 +3,6 @@ package com.tangem.tap.features.details.redux.walletconnect import com.tangem.blockchain.common.WalletManager import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.common.routing.AppRoute -import com.tangem.domain.qrscanning.models.SourceType import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.inject @@ -61,7 +60,11 @@ class WalletConnectMiddleware { if (uri != null && isWalletConnectUri(uri)) { store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri)) } else { - store.dispatchNavigationAction { push(AppRoute.QrScanning(SourceType.WALLET_CONNECT)) } + store.dispatchNavigationAction { + push( + AppRoute.QrScanning(AppRoute.QrScanning.Source.WALLET_CONNECT), + ) + } } } is WalletConnectAction.ShowClipboardOrScanQrDialog -> { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt index a6ac03c6c7..3a1c1dd7ea 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt @@ -21,7 +21,7 @@ object ClipboardOrScanQrDialog { } setNegativeButton(context.getText(R.string.wallet_connect_scan_new_code)) { _, _ -> store.dispatchNavigationAction { - push(AppRoute.QrScanning(source = SourceType.WALLET_CONNECT)) + push(AppRoute.QrScanning(source = AppRoute.QrScanning.Source.WALLET_CONNECT)) } } setOnDismissListener { diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 0ff58d9f5f..2d9bd2853d 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -2,6 +2,7 @@ package com.tangem.tap.routing.utils import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.domain.qrscanning.models.SourceType import com.tangem.feature.qrscanning.QrScanningComponent import com.tangem.feature.referral.api.ReferralComponent import com.tangem.feature.stories.api.StoriesComponent @@ -15,8 +16,9 @@ import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent import com.tangem.features.onramp.component.* import com.tangem.features.pushnotifications.api.PushNotificationsComponent import com.tangem.features.send.api.SendComponent -import com.tangem.features.swap.SwapComponent +import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.staking.api.StakingComponent +import com.tangem.features.swap.SwapComponent import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.wallet.WalletEntryComponent @@ -74,6 +76,8 @@ internal class ChildFactory @Inject constructor( private val referralComponentFactory: ReferralComponent.Factory, private val pushNotificationsComponentFactory: PushNotificationsComponent.Factory, private val walletComponentFactory: WalletEntryComponent.Factory, + private val sendComponentFactoryV2: com.tangem.features.send.v2.api.SendComponent.Factory, + private val sendFeatureToggles: SendFeatureToggles, private val testerRouter: TesterRouter, private val routingFeatureToggles: RoutingFeatureToggles, ) { @@ -256,18 +260,33 @@ internal class ChildFactory @Inject constructor( ) } is AppRoute.Send -> { - createComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = SendComponent.Params( - userWalletId = route.userWalletId, - currency = route.currency, - transactionId = route.transactionId, - amount = route.amount, - tag = route.tag, - destinationAddress = route.destinationAddress, - ), - componentFactory = sendComponentFactory, - ) + if (sendFeatureToggles.isSendV2Enabled) { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = com.tangem.features.send.v2.api.SendComponent.Params( + userWalletId = route.userWalletId, + currency = route.currency, + transactionId = route.transactionId, + amount = route.amount, + tag = route.tag, + destinationAddress = route.destinationAddress, + ), + componentFactory = sendComponentFactoryV2, + ) + } else { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = SendComponent.Params( + userWalletId = route.userWalletId, + currency = route.currency, + transactionId = route.transactionId, + amount = route.amount, + tag = route.tag, + destinationAddress = route.destinationAddress, + ), + componentFactory = sendComponentFactory, + ) + } } is AppRoute.Home -> { createComponentChild( @@ -284,10 +303,14 @@ internal class ChildFactory @Inject constructor( ) } is AppRoute.QrScanning -> { + val source = when (route.source) { + AppRoute.QrScanning.Source.SEND -> SourceType.SEND + AppRoute.QrScanning.Source.WALLET_CONNECT -> SourceType.WALLET_CONNECT + } createComponentChild( contextProvider = contextProvider(route, contextFactory), params = QrScanningComponent.Params( - source = route.source, + source = source, networkName = route.networkName, ), componentFactory = qrScanningComponentFactory, @@ -400,18 +423,33 @@ internal class ChildFactory @Inject constructor( route.asFragmentChild(Provider { SaveWalletBottomSheetFragment() }) } is AppRoute.Send -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = SendComponent.Params( - userWalletId = route.userWalletId, - currency = route.currency, - transactionId = route.transactionId, - amount = route.amount, - tag = route.tag, - destinationAddress = route.destinationAddress, - ), - componentFactory = sendComponentFactory, - ) + if (sendFeatureToggles.isSendV2Enabled) { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = com.tangem.features.send.v2.api.SendComponent.Params( + userWalletId = route.userWalletId, + currency = route.currency, + transactionId = route.transactionId, + amount = route.amount, + tag = route.tag, + destinationAddress = route.destinationAddress, + ), + componentFactory = sendComponentFactoryV2, + ) + } else { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = SendComponent.Params( + userWalletId = route.userWalletId, + currency = route.currency, + transactionId = route.transactionId, + amount = route.amount, + tag = route.tag, + destinationAddress = route.destinationAddress, + ), + componentFactory = sendComponentFactory, + ) + } } is AppRoute.AppSettings -> { route.asComponentChild( @@ -483,10 +521,14 @@ internal class ChildFactory @Inject constructor( route.asFragmentChild(Provider { OnboardingWalletFragment() }) } is AppRoute.QrScanning -> { + val source = when (route.source) { + AppRoute.QrScanning.Source.SEND -> SourceType.SEND + AppRoute.QrScanning.Source.WALLET_CONNECT -> SourceType.WALLET_CONNECT + } route.asComponentChild( contextProvider = contextProvider(route, contextFactory), params = QrScanningComponent.Params( - source = route.source, + source = source, networkName = route.networkName, ), componentFactory = qrScanningComponentFactory, diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index f033d76c1e..f7e8d9ea7b 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -9,7 +9,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.onramp.model.OnrampSource -import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import kotlinx.serialization.Serializable @@ -147,9 +146,15 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class QrScanning( - val source: SourceType, + val source: Source, val networkName: String? = null, - ) : AppRoute(path = "/$source/qr_scanning${if (networkName != null) "/$networkName" else ""}") + ) : AppRoute(path = "/$source/qr_scanning${if (networkName != null) "/$networkName" else ""}") { + + enum class Source { + WALLET_CONNECT, + SEND, + } + } @Serializable data class ReferralProgram( 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 810a9dceb5..6a2792b3a9 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 @@ -74,5 +74,9 @@ { "name": "TWIN_REFACTORING_ENABLED", "version": "undefined" + }, + { + "name": "SEND_V2_ENABLED", + "version": "undefined" } ] diff --git a/features/send-v2/api/.gitignore b/features/send-v2/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/send-v2/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/send-v2/api/build.gradle.kts b/features/send-v2/api/build.gradle.kts new file mode 100644 index 0000000000..7bf806b77c --- /dev/null +++ b/features/send-v2/api/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.send.v2.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Domain models */ + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) +} \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt new file mode 100644 index 0000000000..14b404270d --- /dev/null +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.send.v2.api + +interface SendFeatureToggles { + val isSendV2Enabled: Boolean +} \ No newline at end of file diff --git a/features/send-v2/impl/.gitignore b/features/send-v2/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/send-v2/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/send-v2/impl/build.gradle.kts b/features/send-v2/impl/build.gradle.kts new file mode 100644 index 0000000000..245cf3a903 --- /dev/null +++ b/features/send-v2/impl/build.gradle.kts @@ -0,0 +1,70 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.send.v2.impl" +} + +dependencies { + /** Api */ + implementation(projects.features.sendV2.api) + implementation(projects.features.txhistory.api) + + /** Libs */ + implementation(projects.libs.crypto) + + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.analytics) + implementation(projects.core.configToggles) + implementation(projects.core.navigation) + + /** Tangem SDK */ + implementation(tangemDeps.blockchain) + + /** Common */ + implementation(projects.common.ui) + implementation(projects.common.routing) + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.legacy) + implementation(projects.domain.tokens.models) + implementation(projects.domain.tokens) + implementation(projects.domain.wallets.models) + implementation(projects.domain.wallets) + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.appCurrency) + implementation(projects.domain.transaction.models) + implementation(projects.domain.transaction) + implementation(projects.domain.txhistory.models) + implementation(projects.domain.txhistory) + implementation(projects.domain.qrScanning.models) + implementation(projects.domain.qrScanning) + implementation(projects.domain.settings) + implementation(projects.domain.feedback) + implementation(projects.domain.txhistory) + + /** Compose libraries */ + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.material3) + implementation(deps.decompose.ext.compose) + implementation(deps.androidx.activity.compose) + + /** Other dependencies */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt new file mode 100644 index 0000000000..21da5a54eb --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.features.send.v2 + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.send.v2.api.SendFeatureToggles + +class DefaultSendFeatureToggles( + private val featureToggles: FeatureTogglesManager, +) : SendFeatureToggles { + override val isSendV2Enabled: Boolean + get() = featureToggles.isFeatureEnabled("SEND_V2_ENABLED") +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendFeatureModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendFeatureModule.kt new file mode 100644 index 0000000000..e8d18db28b --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendFeatureModule.kt @@ -0,0 +1,32 @@ +package com.tangem.features.send.v2.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.send.v2.DefaultSendFeatureToggles +import com.tangem.features.send.v2.api.SendComponent +import com.tangem.features.send.v2.api.SendFeatureToggles +import com.tangem.features.send.v2.send.DefaultSendComponent +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +internal object SendFeatureModule { + + @Provides + fun provideSendFeatureToggles(featureTogglesManager: FeatureTogglesManager): SendFeatureToggles { + return DefaultSendFeatureToggles(featureTogglesManager) + } +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface SendFeatureModuleBinds { + + @Binds + @Singleton + fun provideSendComponentFactory(impl: DefaultSendComponent.Factory): SendComponent.Factory +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt index 788ea93ba2..0a1397271b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt @@ -4,7 +4,6 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.navigation.url.UrlOpener -import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import javax.inject.Inject @@ -35,7 +34,7 @@ internal class DefaultSendRouter @Inject constructor( override fun openQrCodeScanner(network: String) { router.push( AppRoute.QrScanning( - source = SourceType.SEND, + source = AppRoute.QrScanning.Source.SEND, networkName = network, ), ) diff --git a/settings.gradle.kts b/settings.gradle.kts index 6bda05b59e..c8b22d2a48 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -134,9 +134,6 @@ dependencyResolutionManagement { enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include(":app") -include(":common") -include(":common:ui-charts") -include(":common:routing") // region Core modules include(":core:analytics") @@ -154,6 +151,9 @@ include(":core:pagination") // endregion Core modules // region Common modules +include(":common") +include(":common:ui-charts") +include(":common:routing") include(":common:ui") include(":common:google") // endregion @@ -194,6 +194,8 @@ include(":features:tokendetails:impl") include(":features:send:api") include(":features:send:impl") +include(":features:send-v2:api") +include(":features:send-v2:impl") include(":features:manage-tokens:api") include(":features:manage-tokens:impl") @@ -275,6 +277,7 @@ include(":domain:manage-tokens:models") include(":domain:onramp") include(":domain:onramp:models") include(":domain:promo") +include(":domain:promo:models") include(":domain:nft") include(":domain:nft:models") // endregion Domain modules @@ -302,5 +305,4 @@ include(":data:markets") include(":data:manage-tokens") include(":data:nft") include(":data:onramp") -// endregion Data modules -include(":domain:promo:models") \ No newline at end of file +// endregion Data modules \ No newline at end of file From d7c4218f92a2fbd2108b615367033752a009e962 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Mar 2025 13:35:51 +0500 Subject: [PATCH 032/138] Updated on 2026-08-14 --- .../tangem/utils/transformer/Transformer.kt | 11 ++++++++ domain/transaction/build.gradle.kts | 1 + .../transaction/error/ValidateAddressError.kt | 19 ++++++++++--- .../transaction/error/ValidateMemoError.kt | 6 +++++ .../usecase/ValidateWalletAddressUseCase.kt | 20 +++++++++----- .../usecase/ValidateWalletMemoUseCase.kt | 16 +++++++++-- .../send/impl/presentation/model/SendModel.kt | 11 ++++---- .../state/recipient/RecipientSendFactory.kt | 27 ++++++------------- 8 files changed, 74 insertions(+), 37 deletions(-) create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateMemoError.kt diff --git a/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt b/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt index 9c271d0920..773218b96d 100644 --- a/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt +++ b/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt @@ -1,8 +1,19 @@ package com.tangem.utils.transformer +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + /** * Transforms state to updated state. */ interface Transformer { fun transform(prevState: S): S +} + +fun MutableStateFlow.update(transformer: Transformer) { + update { transformer.transform(this.value) } +} + +fun MutableStateFlow.updateAll(vararg transformers: Transformer) { + transformers.forEach { transformer -> update { transformer.transform(this.value) } } } \ No newline at end of file diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index f798f98dc8..154228ce40 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.legacy) implementation(projects.libs.blockchainSdk) + implementation(projects.libs.crypto) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt index 75262c24e2..8fc2ee7870 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt @@ -1,7 +1,18 @@ package com.tangem.domain.transaction.error -sealed class ValidateAddressError { - data object AddressInWallet : ValidateAddressError() - data object InvalidAddress : ValidateAddressError() - data class DataError(val throwable: Throwable) : ValidateAddressError() +import arrow.core.Either + +typealias AddressValidationResult = Either + +sealed class AddressValidation { + sealed class Success : AddressValidation() { + data object Valid : Success() + data object ValidXAddress : Success() + } + + sealed class Error : AddressValidation() { + data object AddressInWallet : Error() + data object InvalidAddress : Error() + data class DataError(val throwable: Throwable) : Error() + } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateMemoError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateMemoError.kt new file mode 100644 index 0000000000..d5d51e0ed2 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateMemoError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.transaction.error + +sealed class ValidateMemoError { + data object InvalidMemo : ValidateMemoError() + data class DataError(val throwable: Throwable) : ValidateMemoError() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt index 7db3a825f7..1452cbea68 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt @@ -1,14 +1,15 @@ package com.tangem.domain.transaction.usecase -import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.transaction.WalletAddressServiceRepository -import com.tangem.domain.transaction.error.ValidateAddressError +import com.tangem.domain.transaction.error.AddressValidation +import com.tangem.domain.transaction.error.AddressValidationResult import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.lib.crypto.BlockchainUtils /** * Use case for validating wallet address. @@ -23,18 +24,23 @@ class ValidateWalletAddressUseCase( network: Network, address: String, currencyAddress: Set?, - ): Either { + ): AddressValidationResult { val isUtxoConsolidationAvailable = walletManagersFacade.checkUtxoConsolidationAvailability(userWalletId, network) val isCurrentAddress = currencyAddress?.any { it.value == address } ?: true val isForbidSelfSend = isCurrentAddress && !isUtxoConsolidationAvailable - val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, address) + + val decodedXAddress = BlockchainUtils.decodeRippleXAddress(address, network.id.value) + val addressToValidate = decodedXAddress?.address ?: address + + val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, addressToValidate) return when { - !isValidAddress -> ValidateAddressError.InvalidAddress.left() - isForbidSelfSend -> ValidateAddressError.AddressInWallet.left() - else -> Unit.right() + !isValidAddress -> AddressValidation.Error.InvalidAddress.left() + isForbidSelfSend -> AddressValidation.Error.AddressInWallet.left() + decodedXAddress != null -> AddressValidation.Success.ValidXAddress.right() + else -> AddressValidation.Success.Valid.right() } } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt index d4d5ab22e8..dc7ae1a340 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt @@ -1,8 +1,11 @@ package com.tangem.domain.transaction.usecase import arrow.core.Either +import arrow.core.left +import arrow.core.right import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.WalletAddressServiceRepository +import com.tangem.domain.transaction.error.ValidateMemoError /** * Use case for validating wallet memo. @@ -11,7 +14,16 @@ class ValidateWalletMemoUseCase( private val walletAddressServiceRepository: WalletAddressServiceRepository, ) { - operator fun invoke(network: Network, memo: String): Either = Either.catch { - walletAddressServiceRepository.validateMemo(network, memo) + operator fun invoke(network: Network, memo: String): Either { + return try { + val isValidMemo = walletAddressServiceRepository.validateMemo(network, memo) + if (isValidMemo) { + Unit.right() + } else { + ValidateMemoError.InvalidMemo.left() + } + } catch (ex: Throwable) { + ValidateMemoError.DataError(ex).left() + } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt index a316c282f7..2c8301e565 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt @@ -36,8 +36,9 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.AddressValidation +import com.tangem.domain.transaction.error.AddressValidationResult import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.domain.transaction.error.ValidateAddressError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase @@ -116,7 +117,7 @@ internal class SendModel @Inject constructor( private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter, @DelayedWork private val coroutineScope: CoroutineScope, private val innerRouter: InnerSendRouter, - private val appRouter: AppRouter, + appRouter: AppRouter, paramsContainer: ParamsContainer, validateTransactionUseCase: ValidateTransactionUseCase, getCurrencyCheckUseCase: GetCurrencyCheckUseCase, @@ -693,7 +694,7 @@ internal class SendModel @Inject constructor( }.saveIn(memoValidationJobHolder) } - private suspend fun validateAddress(value: String): Either = runCatching { + private suspend fun validateAddress(value: String): AddressValidationResult = runCatching { val maybeValidAddress = validateWalletAddressUseCase( userWalletId = userWalletId, network = cryptoCurrency.network, @@ -702,10 +703,10 @@ internal class SendModel @Inject constructor( ) onEnteredValidAddress(maybeValidAddress.isLeft()) maybeValidAddress - }.getOrElse { ValidateAddressError.DataError(it).left() } + }.getOrElse { AddressValidation.Error.DataError(it).left() } private fun validateMemo(value: String?): Boolean { - return value?.let { validateWalletMemoUseCase(cryptoCurrency.network, it).getOrElse { false } } ?: true + return value?.let { validateWalletMemoUseCase(cryptoCurrency.network, it).isRight() } != false } private suspend fun checkIfXrpAddressValue(value: String): Boolean { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt index c3fdbfd71b..3b0b10aaad 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt @@ -1,20 +1,18 @@ package com.tangem.features.send.impl.presentation.state.recipient -import arrow.core.Either -import arrow.core.getOrElse import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.txhistory.models.TxHistoryItem -import com.tangem.domain.transaction.error.ValidateAddressError +import com.tangem.domain.transaction.error.AddressValidation +import com.tangem.domain.transaction.error.AddressValidationResult import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase +import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.Provider import kotlinx.collections.immutable.toPersistentList -import timber.log.Timber internal class RecipientSendFactory( private val stateRouterProvider: Provider, @@ -66,10 +64,7 @@ internal class RecipientSendFactory( ) } - fun getOnRecipientAddressValidState( - value: String, - maybeValidAddress: Either, - ): SendUiState { + fun getOnRecipientAddressValidState(value: String, maybeValidAddress: AddressValidationResult): SendUiState { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val state = currentStateProvider() val isEditState = stateRouterProvider().isEditState @@ -78,10 +73,7 @@ internal class RecipientSendFactory( val isValidMemo = validateWalletMemoUseCase( memo = recipientState.memoTextField?.value.orEmpty(), network = cryptoCurrencyStatus.currency.network, - ).getOrElse { - Timber.e("Failed to validateWalletMemoUseCase: $it") - false - } + ).isRight() return state.copyWrapped( isEditState = isEditState, @@ -92,10 +84,10 @@ internal class RecipientSendFactory( error = maybeValidAddress.fold( ifLeft = { when (it) { - ValidateAddressError.InvalidAddress -> resourceReference( + AddressValidation.Error.InvalidAddress -> resourceReference( R.string.send_recipient_address_error, ) - ValidateAddressError.AddressInWallet -> resourceReference( + AddressValidation.Error.AddressInWallet -> resourceReference( R.string.send_error_address_same_as_wallet, ) else -> null @@ -143,10 +135,7 @@ internal class RecipientSendFactory( val isValidMemo = validateWalletMemoUseCase( memo = value, network = cryptoCurrencyStatus.currency.network, - ).getOrElse { - Timber.e("Failed to validateWalletMemoUseCase: $it") - false - } + ).isRight() return state.copyWrapped( isEditState = isEditState, From 5efb1956c7a97e6abda48e4d9eb5c9fa6eb61a44 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Mar 2025 13:37:16 +0500 Subject: [PATCH 033/138] Updated on 2026-08-14 --- .../tangem/utils/transformer/Transformer.kt | 4 - .../features/send/v2/api/SendComponent.kt | 20 ++ .../send/v2/DefaultSendFeatureToggles.kt | 2 +- .../features/send/v2/di/SendModelModule.kt | 20 ++ .../send/v2/send/DefaultSendComponent.kt | 105 ++++++ .../tangem/features/send/v2/send/SendRoute.kt | 35 ++ .../v2/send/analytics/SendAnalyticEvents.kt | 43 +++ .../features/send/v2/send/model/SendModel.kt | 40 +++ .../features/send/v2/send/ui/state/SendUM.kt | 9 + .../destination/SendDestinationComponent.kt | 44 +++ .../analytics/EnterAddressSource.kt | 12 + .../SendDestinationAnalyticEvents.kt | 37 +++ .../model/SendDestinationClickIntents.kt | 12 + .../destination/model/SendDestinationModel.kt | 306 ++++++++++++++++++ .../SendRecipientHistoryListConverter.kt | 92 ++++++ .../SendRecipientWalletListConverter.kt | 67 ++++ .../model/transformers/RecentListUtils.kt | 32 ++ .../SendDestinationAddressTransformer.kt | 18 ++ .../SendDestinationInitialStateTransformer.kt | 62 ++++ .../SendDestinationMemoTransformer.kt | 16 + .../SendDestinationRecentListTransformer.kt | 30 ++ ...dDestinationValidationResultTransformer.kt | 49 +++ ...DestinationValidationStartedTransformer.kt | 12 + .../destination/ui/ListItemWithIcon.kt | 245 ++++++++++++++ .../destination/ui/SendDestinationContent.kt | 248 ++++++++++++++ .../destination/ui/TextFieldWithPaste.kt | 103 ++++++ .../ui/state/DestinationRecipientListUM.kt | 22 ++ .../ui/state/DestinationTextFieldUM.kt | 42 +++ .../destination/ui/state/DestinationUM.kt | 25 ++ .../ui/state/DestinationWalletUM.kt | 21 ++ 30 files changed, 1768 insertions(+), 5 deletions(-) create mode 100644 features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/SendDestinationAnalyticEvents.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationClickIntents.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientHistoryListConverter.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/RecentListUtils.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationAddressTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationMemoTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationRecentListTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationStartedTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/ListItemWithIcon.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/TextFieldWithPaste.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationRecipientListUM.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationWalletUM.kt diff --git a/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt b/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt index 773218b96d..51b347095e 100644 --- a/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt +++ b/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt @@ -12,8 +12,4 @@ interface Transformer { fun MutableStateFlow.update(transformer: Transformer) { update { transformer.transform(this.value) } -} - -fun MutableStateFlow.updateAll(vararg transformers: Transformer) { - transformers.forEach { transformer -> update { transformer.transform(this.value) } } } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendComponent.kt new file mode 100644 index 0000000000..f416d89a0f --- /dev/null +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendComponent.kt @@ -0,0 +1,20 @@ +package com.tangem.features.send.v2.api + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +interface SendComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + val currency: CryptoCurrency, + val transactionId: String? = null, + val amount: String? = null, + val tag: String? = null, + val destinationAddress: String? = null, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt index 21da5a54eb..a36e1cfa6a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt @@ -3,7 +3,7 @@ package com.tangem.features.send.v2 import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.features.send.v2.api.SendFeatureToggles -class DefaultSendFeatureToggles( +internal class DefaultSendFeatureToggles( private val featureToggles: FeatureTogglesManager, ) : SendFeatureToggles { override val isSendV2Enabled: Boolean diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt new file mode 100644 index 0000000000..4ed9eb6da8 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.send.v2.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface SendModelModule { + + @Binds + @IntoMap + @ClassKey(SendDestinationModel::class) + fun provideSendDestinationModel(model: SendDestinationModel): Model +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt new file mode 100644 index 0000000000..1886f84b29 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -0,0 +1,105 @@ +package com.tangem.features.send.v2.send + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.api.SendComponent +import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents +import com.tangem.features.send.v2.send.model.SendModel +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterIsInstance + +internal class DefaultSendComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: SendComponent.Params, +) : SendComponent, AppComponentContext by appComponentContext { + + private val stackNavigation = StackNavigation() + + private val innerRouter = InnerRouter( + stackNavigation = stackNavigation, + popCallback = { onChildBack() }, + ) + + private val initialRoute = SendRoute.Empty + private val currentRoute = MutableStateFlow(initialRoute) + + private val model: SendModel = getOrCreateModel(params = params, router = innerRouter) + + private val childStack = childStack( + key = "sendInnerStack", + source = stackNavigation, + serializer = null, + initialConfiguration = initialRoute, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + createChild( + configuration, + childByContext( + componentContext = factoryContext, + router = innerRouter, + ), + ) + }, + ) + + @Composable + override fun Content(modifier: Modifier) { + BackHandler(onBack = ::onChildBack) + } + + private fun createChild(route: SendRoute, factoryContext: AppComponentContext) = when (route) { + SendRoute.Empty -> getStubComponent() + is SendRoute.Destination -> getDestinationComponent(factoryContext, route) + is SendRoute.Amount -> getAmountComponent() + is SendRoute.Fee -> getFeeComponent() + SendRoute.Confirm -> getConfirmComponent() + } + + private fun getDestinationComponent(factoryContext: AppComponentContext, route: SendRoute) = + SendDestinationComponent( + appComponentContext = factoryContext, + params = SendDestinationComponent.Params( + state = model.uiState.value.destinationUM, + currentRoute = currentRoute.filterIsInstance(), + analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, + userWallet = model.userWallet, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + callback = model, + isEditMode = route.isEditMode, + ), + ) + + private fun getAmountComponent() = getStubComponent() // todo + + private fun getFeeComponent() = getStubComponent() // todo + + private fun getConfirmComponent() = getStubComponent() // todo + + private fun getStubComponent() = ComposableContentComponent { } + + private fun onChildBack() { + if (childStack.value.active.configuration == SendRoute.Empty || childStack.value.backStack.isEmpty()) { + router.pop() + } else { + stackNavigation.pop() + } + } + + @AssistedFactory + interface Factory : SendComponent.Factory { + override fun create(context: AppComponentContext, params: SendComponent.Params): DefaultSendComponent + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt new file mode 100644 index 0000000000..078366292e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt @@ -0,0 +1,35 @@ +package com.tangem.features.send.v2.send + +import com.tangem.core.decompose.navigation.Route +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class SendRoute : Route { + + abstract val isEditMode: Boolean + + @Serializable + data object Empty : SendRoute() { + override val isEditMode = false + } + + @Serializable + data object Confirm : SendRoute() { + override val isEditMode: Boolean = false + } + + @Serializable + data class Destination( + override val isEditMode: Boolean, + ) : SendRoute() + + @Serializable + data class Amount( + override val isEditMode: Boolean, + ) : SendRoute() + + @Serializable + data class Fee( + override val isEditMode: Boolean = true, + ) : SendRoute() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt new file mode 100644 index 0000000000..07d650a412 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt @@ -0,0 +1,43 @@ +package com.tangem.features.send.v2.send.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM + +/** + * Send screen analytics + */ +internal sealed class SendAnalyticEvents( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = SEND_CATEGORY, event = event, params = params) { + + /** Recipient address screen opened */ + data object AddressScreenOpened : SendAnalyticEvents(event = "Address Screen Opened") + + /** Amount screen opened */ + data object AmountScreenOpened : SendAnalyticEvents(event = "Amount Screen Opened") + + /** Fee screen opened */ + data object FeeScreenOpened : SendAnalyticEvents(event = "Fee Screen Opened") + + /** Confirmation screen opened */ + data object ConfirmationScreenOpened : SendAnalyticEvents(event = "Confirm Screen Opened") + + /** If transaction delays notification is present */ + data class NoticeTransactionDelays( + val token: String, + ) : SendAnalyticEvents( + event = "Notice - Transaction Delays Are Possible", + params = mapOf(TOKEN_PARAM to token), + ) + + /** If error occurs during send transactions */ + data class TransactionError(val token: String) : SendAnalyticEvents( + event = "Error - Transaction Rejected", + params = mapOf(TOKEN_PARAM to token), + ) + + companion object { + const val SEND_CATEGORY = "Token / Send" + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt new file mode 100644 index 0000000000..11db3bfa86 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -0,0 +1,40 @@ +package com.tangem.features.send.v2.send.model + +import androidx.compose.runtime.Stable +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.send.ui.state.SendUM +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import kotlin.properties.Delegates + +@Stable +@ModelScoped +@Suppress("LongParameterList") +internal class SendModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, +) : Model(), SendDestinationComponent.ModelCallback { + + private val _uiState = MutableStateFlow(initialState()) + val uiState = _uiState.asStateFlow() + + var userWallet: UserWallet by Delegates.notNull() + var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + + private fun initialState(): SendUM = SendUM( + amountState = AmountState.Empty(), + destinationUM = DestinationUM.Empty(), + ) + + override fun onDestinationResult(destinationUM: DestinationUM) { + _uiState.update { it.copy(destinationUM = destinationUM) } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt new file mode 100644 index 0000000000..a65eb9fa49 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt @@ -0,0 +1,9 @@ +package com.tangem.features.send.v2.send.ui.state + +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM + +internal data class SendUM( + val amountState: AmountState, + val destinationUM: DestinationUM, +) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt new file mode 100644 index 0000000000..3110982216 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt @@ -0,0 +1,44 @@ +package com.tangem.features.send.v2.subcomponents.destination + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel +import com.tangem.features.send.v2.subcomponents.destination.ui.SendDestinationContent +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import kotlinx.coroutines.flow.Flow + +internal class SendDestinationComponent( + appComponentContext: AppComponentContext, + private val params: Params, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: SendDestinationModel = getOrCreateModel(params = params, router = router) + + @Composable + override fun Content(modifier: Modifier) { + val state = model.uiState.collectAsStateWithLifecycle() + + SendDestinationContent(state = state.value, model, isBalanceHidden = false) + } + + data class Params( + val state: DestinationUM, + val analyticsCategoryName: String, + val userWallet: UserWallet, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val callback: ModelCallback, + val currentRoute: Flow, + val isEditMode: Boolean, + ) + + interface ModelCallback { + fun onDestinationResult(destinationUM: DestinationUM) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt new file mode 100644 index 0000000000..1c61574dfa --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt @@ -0,0 +1,12 @@ +package com.tangem.features.send.v2.subcomponents.destination.analytics + +internal enum class EnterAddressSource { + QRCode, + PasteButton, + RecentAddress, + InputField, + ; + + val isPasted: Boolean + get() = this != InputField +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/SendDestinationAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/SendDestinationAnalyticEvents.kt new file mode 100644 index 0000000000..84c52b4907 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/SendDestinationAnalyticEvents.kt @@ -0,0 +1,37 @@ +package com.tangem.features.send.v2.subcomponents.destination.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE +import com.tangem.core.analytics.models.AnalyticsParam.Key.VALIDATION + +internal sealed class SendDestinationAnalyticEvents( + category: String, + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = category, event = event, params = params) { + + abstract val categoryName: String + + /** Address to send entered */ + data class AddressEntered( + override val categoryName: String, + val source: EnterAddressSource, + val isValid: Boolean, + ) : SendDestinationAnalyticEvents( + category = categoryName, + event = "Address Entered", + params = mapOf( + SOURCE to source.name, + VALIDATION to if (isValid) "Success" else "Fail", + ), + ) + + /** Qr Code button clicked */ + data class QrCodeButtonClicked( + override val categoryName: String, + ) : + SendDestinationAnalyticEvents( + category = categoryName, + event = "Button - QR Code", + ) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationClickIntents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationClickIntents.kt new file mode 100644 index 0000000000..73a1ce762b --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationClickIntents.kt @@ -0,0 +1,12 @@ +package com.tangem.features.send.v2.subcomponents.destination.model + +import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource + +internal interface SendDestinationClickIntents { + + fun onRecipientAddressValueChange(value: String, type: EnterAddressSource) + + fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean = false) + + fun onQrCodeScanClick() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt new file mode 100644 index 0000000000..adb93ac9b2 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -0,0 +1,306 @@ +package com.tangem.features.send.v2.subcomponents.destination.model + +import androidx.compose.runtime.Stable +import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.api.AnalyticsEventHandler +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.navigation.Router +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyUseCase +import com.tangem.domain.tokens.GetNetworkAddressesUseCase +import com.tangem.domain.transaction.usecase.IsUtxoConsolidationAvailableUseCase +import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase +import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase +import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent +import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource +import com.tangem.features.send.v2.subcomponents.destination.analytics.SendDestinationAnalyticEvents +import com.tangem.features.send.v2.subcomponents.destination.model.transformers.* +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.transformer.update +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +@Suppress("LongParameterList") +internal class SendDestinationModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, + private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, + private val getWalletsUseCase: GetWalletsUseCase, + private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, + private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase, + private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, + private val isUtxoConsolidationAvailableUseCase: IsUtxoConsolidationAvailableUseCase, + private val listenToQrScanningUseCase: ListenToQrScanningUseCase, + private val parseQrCodeUseCase: ParseQrCodeUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, +) : Model(), SendDestinationClickIntents { + private val params: SendDestinationComponent.Params = paramsContainer.require() + + private val _uiState = MutableStateFlow(params.state) + val uiState = _uiState.asStateFlow() + + private val analyticsCategoryName = params.analyticsCategoryName + private val userWallet = params.userWallet + private val cryptoCurrencyStatus = params.cryptoCurrencyStatus + private val cryptoCurrency = cryptoCurrencyStatus.currency + + private var validationJobHolder = JobHolder() + + init { + configDestinationNavigation() + initialState() + getWalletsAndRecent() + subscribeOnQRScannerResult() + } + + private fun initialState() { + if (uiState.value is DestinationUM.Empty) { + _uiState.update( + SendDestinationInitialStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + onAddressChange = ::onRecipientAddressValueChange, + onMemoChange = ::onRecipientMemoValueChange, + ), + ) + } + } + + fun updateState(state: DestinationUM) { + if (state !is DestinationUM.Empty) { + _uiState.value = state + } + } + + override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource) { + _uiState.update( + SendDestinationAddressTransformer( + address = value, + isPasted = type.isPasted, + ), + ) + val memo = (uiState.value as? DestinationUM.Content)?.memoTextField?.value + validate(address = value, memo = memo, type) + } + + override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) { + _uiState.update( + SendDestinationMemoTransformer( + memo = value, + isPasted = isValuePasted, + ), + ) + val address = (uiState.value as? DestinationUM.Content)?.addressTextField?.value.orEmpty() + validate(address = address, memo = value) + } + + override fun onQrCodeScanClick() { + analyticsEventHandler.send(SendDestinationAnalyticEvents.QrCodeButtonClicked(analyticsCategoryName)) + router.push( + AppRoute.QrScanning( + source = AppRoute.QrScanning.Source.SEND, + networkName = cryptoCurrency.network.name, + ), + ) + } + + private fun subscribeOnQRScannerResult() { + listenToQrScanningUseCase(SourceType.SEND) + .getOrElse { emptyFlow() } + .onEach(::onQrCodeScanned) + .launchIn(modelScope) + } + + private fun onQrCodeScanned(address: String) { + parseQrCodeUseCase(address, cryptoCurrency).fold( + ifRight = { parsedCode -> + onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode) + parsedCode.memo?.let { onRecipientMemoValueChange(it) } + }, + ifLeft = { + onRecipientAddressValueChange(address, EnterAddressSource.QRCode) + }, + ) + } + + private fun getWalletsAndRecent() { + combine( + flow = getWalletsUseCase().conflate().map { + it.toAvailableWallets() + }, + flow2 = getFixedTxHistoryItemsUseCase( + userWalletId = userWallet.walletId, + currency = cryptoCurrency, + pageSize = RECENT_TX_SIZE, + ).getOrElse { flowOf(emptyList()) }.conflate(), + ) { destinationWalletList, txHistoryList -> + val isUtxoConsolidationAvailable = isUtxoConsolidationAvailableUseCase.invokeSync( + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ) + _uiState.update( + SendDestinationRecentListTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + isUtxoConsolidationAvailable = isUtxoConsolidationAvailable, + destinationWalletList = destinationWalletList, + txHistoryList, + ), + ) + }.launchIn(modelScope) + } + + private suspend fun List.toAvailableWallets(): List { + return coroutineScope { + val cryptoCurrencyNetwork = cryptoCurrency.network + + return@coroutineScope filterNot { it.isLocked } + .map { wallet -> + async { + val addresses = if (!wallet.isMultiCurrency) { + getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let { + if (it.network.id == cryptoCurrencyNetwork.id) { + getNetworkAddressesUseCase.invokeSync(wallet.walletId, it.network) + } else { + null + } + } + } else { + getNetworkAddressesUseCase.invokeSync(wallet.walletId, cryptoCurrencyNetwork) + } + wallet to addresses + } + }.awaitAll() + .asSequence() + .mapNotNull { (wallet, addresses) -> + addresses?.map { (cryptoCurrency, address) -> + DestinationWalletUM( + name = wallet.name, + address = address, + cryptoCurrency = cryptoCurrency, + userWalletId = wallet.walletId, + ) + } + }.flatten() + .toList() + } + } + + private fun validate(address: String, memo: String?, type: EnterAddressSource? = null) { + modelScope.launch { + _uiState.update(SendDestinationValidationStartedTransformer) + + val addressValidationResult = validateWalletAddressUseCase( + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + address = address, + currencyAddress = cryptoCurrencyStatus.value.networkAddress?.availableAddresses, + ) + val memoValidationResult = validateWalletMemoUseCase( + memo = memo.orEmpty(), + network = cryptoCurrency.network, + ) + + type?.let { + analyticsEventHandler.send( + SendDestinationAnalyticEvents.AddressEntered( + categoryName = analyticsCategoryName, + source = it, + isValid = addressValidationResult.isRight(), + ), + ) + } + _uiState.update(SendDestinationValidationResultTransformer(addressValidationResult, memoValidationResult)) + autoNextFromRecipient(type, addressValidationResult.isRight(), memoValidationResult.isRight()) + }.saveIn(validationJobHolder) + } + + private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean, isValidMemo: Boolean) { + val isRecent = type == EnterAddressSource.RecentAddress + if (isRecent && isValidAddress && isValidMemo) onNextClick() + } + + private fun saveResult() { + params.callback.onDestinationResult(uiState.value) + } + + private fun onNextClick() { + saveResult() + if (params.isEditMode) { + router.pop() + } else { + router.push(SendRoute.Amount(isEditMode = false)) + } + } + + private fun configDestinationNavigation() { + combine( + flow = uiState, + flow2 = params.currentRoute, + transform = { state, route -> state to route }, + ).onEach { + // todo + // params.callback.onNavigationResult( + // NavigationUM.Content( + // title = resourceReference(R.string.send_recipient_label), + // subtitle = null, + // backIconRes = if (route.isEditMode) { + // R.drawable.ic_back_24 + // } else { + // R.drawable.ic_close_24 + // }, + // backIconClick = { + // if (route.isEditMode) { + // router.pop() + // } else { + // appRouter.pop() + // } + // }, + // additionalIconRes = R.drawable.ic_qrcode_scan_24, + // additionalIconClick = { + // router.push( + // AppRoute.QrScanning( + // source = AppRoute.QrScanning.Source.SEND, + // networkName = cryptoCurrency.network.name, + // ), + // ) + // }, + // primaryButton = ButtonsUM.PrimaryButtonUM( + // text = if (route.isEditMode) { + // resourceReference(R.string.common_continue) + // } else { + // resourceReference(R.string.common_next) + // }, + // isEnabled = state.isPrimaryButtonEnabled, + // onClick = ::onNextClick, + // ), + // prevButton = null, + // secondaryPairButtonsUM = null, + // ), + // ) + }.launchIn(modelScope) + } + + private companion object { + const val RECENT_TX_SIZE = 100 + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientHistoryListConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientHistoryListConverter.kt new file mode 100644 index 0000000000..3c77cd1b51 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientHistoryListConverter.kt @@ -0,0 +1,92 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.converters + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM +import com.tangem.features.send.v2.subcomponents.destination.model.transformers.RECENT_DEFAULT_COUNT +import com.tangem.features.send.v2.subcomponents.destination.model.transformers.RECENT_KEY_TAG +import com.tangem.features.send.v2.subcomponents.destination.model.transformers.emptyListState +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isZero +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList + +internal class SendRecipientHistoryListConverter( + private val cryptoCurrency: CryptoCurrency, +) : Converter, ImmutableList> { + + override fun convert(value: List): ImmutableList { + return value.filterRecipients(cryptoCurrency).ifEmpty { + emptyListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT) + } + } + + private fun List.filterRecipients(cryptoCurrency: CryptoCurrency) = this.filter { item -> + val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer + val isNotContract = item.interactionAddressType is TxHistoryItem.InteractionAddressType.User + val isSingleAddress = if (item.isOutgoing) { + item.destinationType is TxHistoryItem.DestinationType.Single + } else { + item.sourceType is TxHistoryItem.SourceType.Single + } + val notZero = !item.amount.isZero() + isTransfer && isSingleAddress && isNotContract && item.isOutgoing && notZero + } + .take(RECENT_LIST_SIZE) + .mapIndexed { index, tx -> + DestinationRecipientListUM( + id = "${RECENT_KEY_TAG}$index", + title = tx.extractAddress(), + subtitle = stringReference(tx.getAmount(cryptoCurrency).trim()), + timestamp = tx.extractTimestamp(), + subtitleEndOffset = cryptoCurrency.symbol.length, + subtitleIconRes = tx.extractIconRes(), + ) + }.toPersistentList() + + private fun TxHistoryItem.extractAddress(): TextReference = if (isOutgoing) { + when (val destination = destinationType) { + is TxHistoryItem.DestinationType.Multiple -> resourceReference( + R.string.transaction_history_multiple_addresses, + ) + is TxHistoryItem.DestinationType.Single -> stringReference(destination.addressType.address) + } + } else { + when (val source = sourceType) { + is TxHistoryItem.SourceType.Multiple -> resourceReference(R.string.transaction_history_multiple_addresses) + is TxHistoryItem.SourceType.Single -> stringReference(source.address) + } + } + + private fun TxHistoryItem.extractIconRes() = if (isOutgoing) { + R.drawable.ic_arrow_up_24 + } else { + R.drawable.ic_arrow_down_24 + } + + private fun TxHistoryItem.getAmount(cryptoCurrency: CryptoCurrency): String { + return amount.format { crypto(cryptoCurrency) } + } + + private fun TxHistoryItem.extractTimestamp(): TextReference { + val date = timestampInMillis.toDateFormatWithTodayYesterday( + formatter = DateTimeFormatters.dateDDMMYYYY, + ) + val time = timestampInMillis.toTimeFormat() + return TextReference.Res(R.string.send_date_format, wrappedList(date, time)) + } + + companion object { + private const val RECENT_LIST_SIZE = 10 + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt new file mode 100644 index 0000000000..674921ad63 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt @@ -0,0 +1,67 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.converters + +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM +import com.tangem.features.send.v2.subcomponents.destination.model.transformers.WALLET_DEFAULT_COUNT +import com.tangem.features.send.v2.subcomponents.destination.model.transformers.WALLET_KEY_TAG +import com.tangem.features.send.v2.subcomponents.destination.model.transformers.emptyListState +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList + +internal class SendRecipientWalletListConverter( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val isUtxoConsolidationAvailable: Boolean, +) : + Converter, PersistentList> { + override fun convert(value: List): PersistentList { + return value.filterWallets().ifEmpty { + emptyListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT) + } + } + + private fun List.filterWallets(): PersistentList { + var walletsCounter = 0 + val currentAddress: String = runCatching { + cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + }.getOrNull().orEmpty() + + return this.filterNotNull() + .filter { + val isCoin = it.cryptoCurrency is CryptoCurrency.Coin + val isNotSameAddress = it.address != currentAddress + val isNotBlankAddress = it.address.isNotBlank() + + isNotBlankAddress && isCoin && (isNotSameAddress || isUtxoConsolidationAvailable) + } + .groupBy { item -> item.name } + .values.map { wallets -> + val groupedByWallet = wallets.groupBy { it.userWalletId } + var i = 0 + groupedByWallet + .flatMap { item -> + item.value.map { wallet -> + val name = if (groupedByWallet.size > 1) { + "${wallet.name} ${++i}" + } else { + wallet.name + } + + DestinationRecipientListUM( + id = "${WALLET_KEY_TAG}${walletsCounter++}", + title = stringReference(wallet.address), + subtitle = stringReference(name), + address = wallet.address, + userWalletId = wallet.userWalletId, + network = wallet.cryptoCurrency.network, + ) + } + } + } + .flatten() + .toPersistentList() + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/RecentListUtils.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/RecentListUtils.kt new file mode 100644 index 0000000000..0083e68d7d --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/RecentListUtils.kt @@ -0,0 +1,32 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.transformers + +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM +import kotlinx.collections.immutable.toPersistentList + +internal const val WALLET_DEFAULT_COUNT = 1 +internal const val RECENT_DEFAULT_COUNT = 3 +internal const val WALLET_KEY_TAG = "wallet" +internal const val RECENT_KEY_TAG = "recent" + +internal fun loadingListState(tag: String, count: Int) = buildList { + repeat(count) { + add( + DestinationRecipientListUM( + id = "$tag$it", + isLoading = true, + ), + ) + } +}.toPersistentList() + +internal fun emptyListState(tag: String, count: Int) = buildList { + repeat(count) { + add( + DestinationRecipientListUM( + id = "$tag$it", + isLoading = false, + isVisible = false, + ), + ) + } +}.toPersistentList() \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationAddressTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationAddressTransformer.kt new file mode 100644 index 0000000000..e717f65218 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationAddressTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.transformers + +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.utils.transformer.Transformer + +internal class SendDestinationAddressTransformer( + private val address: String, + private val isPasted: Boolean, +) : Transformer { + + override fun transform(prevState: DestinationUM): DestinationUM { + val state = prevState as? DestinationUM.Content ?: return prevState + + return state.copy( + addressTextField = state.addressTextField.copy(value = address, isValuePasted = isPasted), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt new file mode 100644 index 0000000000..fdd0e7f59e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt @@ -0,0 +1,62 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.transformers + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.Network +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.utils.transformer.Transformer + +internal class SendDestinationInitialStateTransformer( + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val onAddressChange: (String, EnterAddressSource) -> Unit, + val onMemoChange: (String, Boolean) -> Unit, +) : Transformer { + override fun transform(prevState: DestinationUM): DestinationUM { + val memoType = when (cryptoCurrencyStatus.currency.network.transactionExtrasType) { + Network.TransactionExtrasType.NONE -> null + Network.TransactionExtrasType.MEMO -> R.string.send_extras_hint_memo + Network.TransactionExtrasType.DESTINATION_TAG -> R.string.send_destination_tag_field + } + return DestinationUM.Content( + isPrimaryButtonEnabled = false, + addressTextField = DestinationTextFieldUM.RecipientAddress( + value = "", + onValueChange = { onAddressChange(it, EnterAddressSource.InputField) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Text, + ), + error = null, + placeholder = resourceReference(R.string.send_enter_address_field), + label = resourceReference(R.string.send_recipient), + isValuePasted = false, + ), + memoTextField = memoType?.let { + DestinationTextFieldUM.RecipientMemo( + value = "", + onValueChange = { onMemoChange(it, false) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Text, + ), + placeholder = resourceReference(R.string.send_optional_field), + label = resourceReference(memoType), + error = resourceReference(R.string.send_memo_destination_tag_error), + disabledText = resourceReference(R.string.send_additional_field_already_included), + isEnabled = true, + isValuePasted = false, + ) + }, + wallets = loadingListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT), + recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT), + networkName = cryptoCurrencyStatus.currency.network.name, + isValidating = false, + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationMemoTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationMemoTransformer.kt new file mode 100644 index 0000000000..7a7883fcd8 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationMemoTransformer.kt @@ -0,0 +1,16 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.transformers + +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.utils.transformer.Transformer + +internal class SendDestinationMemoTransformer( + private val memo: String, + private val isPasted: Boolean, +) : Transformer { + override fun transform(prevState: DestinationUM): DestinationUM { + val state = prevState as? DestinationUM.Content ?: return prevState + return state.copy( + memoTextField = state.memoTextField?.copy(value = memo, isValuePasted = isPasted), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationRecentListTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationRecentListTransformer.kt new file mode 100644 index 0000000000..f1a3f20621 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationRecentListTransformer.kt @@ -0,0 +1,30 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.transformers + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.destination.model.converters.SendRecipientHistoryListConverter +import com.tangem.features.send.v2.subcomponents.destination.model.converters.SendRecipientWalletListConverter +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM +import com.tangem.utils.transformer.Transformer + +internal class SendDestinationRecentListTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val isUtxoConsolidationAvailable: Boolean, + private val destinationWalletList: List, + private val txHistoryList: List, +) : Transformer { + override fun transform(prevState: DestinationUM): DestinationUM { + val state = prevState as? DestinationUM.Content ?: return prevState + + return state.copy( + wallets = SendRecipientWalletListConverter( + cryptoCurrencyStatus, + isUtxoConsolidationAvailable, + ).convert(destinationWalletList), + recent = SendRecipientHistoryListConverter( + cryptoCurrency = cryptoCurrencyStatus.currency, + ).convert(txHistoryList), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt new file mode 100644 index 0000000000..8830a8ece8 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt @@ -0,0 +1,49 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.transformers + +import arrow.core.Either +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.transaction.error.AddressValidation +import com.tangem.domain.transaction.error.AddressValidationResult +import com.tangem.domain.transaction.error.ValidateMemoError +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.utils.transformer.Transformer + +internal class SendDestinationValidationResultTransformer( + private val addressValidationResult: AddressValidationResult, + private val memoValidationResult: Either, +) : Transformer { + override fun transform(prevState: DestinationUM): DestinationUM { + val state = prevState as? DestinationUM.Content ?: return prevState + + val isValidAddress = addressValidationResult.isRight() + val isValidMemo = memoValidationResult.isRight() + + val addressErrorText = addressValidationResult.mapLeft { + when (it) { + is AddressValidation.Error.DataError, + AddressValidation.Error.InvalidAddress, + -> R.string.send_recipient_address_error + AddressValidation.Error.AddressInWallet -> R.string.send_error_address_same_as_wallet + } + }.leftOrNull() + + return state.copy( + isValidating = false, + isPrimaryButtonEnabled = isValidAddress && isValidMemo, + addressTextField = state.addressTextField.copy( + error = addressErrorText?.let(::resourceReference), + isError = state.addressTextField.value.isNotEmpty() && !isValidAddress, + ), + memoTextField = state.memoTextField?.copy( + isError = state.memoTextField.value.isNotEmpty() && !isValidMemo, + isEnabled = !shouldDisableMemo(), + ), + ) + } + + /** Ripple X-Address contains memo, so memo field is unnecessary */ + private fun shouldDisableMemo(): Boolean { + return addressValidationResult.isRight { it == AddressValidation.Success.ValidXAddress } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationStartedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationStartedTransformer.kt new file mode 100644 index 0000000000..bc2354f57f --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationStartedTransformer.kt @@ -0,0 +1,12 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.transformers + +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.utils.transformer.Transformer + +internal object SendDestinationValidationStartedTransformer : Transformer { + override fun transform(prevState: DestinationUM): DestinationUM { + val state = prevState as? DestinationUM.Content ?: return prevState + + return state.copy(isValidating = true) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/ListItemWithIcon.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/ListItemWithIcon.kt new file mode 100644 index 0000000000..458fa9d397 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/ListItemWithIcon.kt @@ -0,0 +1,245 @@ +package com.tangem.features.send.v2.subcomponents.destination.ui + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +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.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.components.icons.identicon.IdentIcon +import com.tangem.core.ui.extensions.rememberHapticFeedback +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.send.v2.impl.R + +/** + * Row item with title and subtitle + * + * @param title title + * @param subtitle subtitle + * @param onClick click listener + * @param modifier modifier + * @param info info + * @param subtitleEndOffset offset for subtitle ellipsis + * @param subtitleIconRes icon + */ +@Composable +fun ListItemWithIcon( + title: String, + subtitle: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + info: String? = null, + subtitleEndOffset: Int = 0, + @DrawableRes subtitleIconRes: Int? = null, + isLoading: Boolean = false, +) { + AnimatedContent( + targetState = isLoading, + label = "Recent List Content Animation", + transitionSpec = { fadeIn().togetherWith(fadeOut()) }, + ) { isLoadingState -> + if (isLoadingState) { + ListItemLoading(modifier = modifier) + } else { + ListItemWithIcon( + title = title, + subtitle = subtitle, + onClick = onClick, + info = info, + subtitleEndOffset = subtitleEndOffset, + subtitleIconRes = subtitleIconRes, + modifier = modifier, + ) + } + } +} + +@Composable +private fun ListItemWithIcon( + title: String, + subtitle: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + info: String? = null, + subtitleEndOffset: Int = 0, + @DrawableRes subtitleIconRes: Int? = null, +) { + val hapticFeedback = rememberHapticFeedback(state = title, onAction = onClick) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .clickable { hapticFeedback() } + .padding(horizontal = 12.dp), + ) { + IdentIcon( + address = title, + modifier = Modifier + .padding(vertical = 8.dp) + .size(40.dp) + .clip(RoundedCornerShape(20.dp)), + ) + Column( + modifier = Modifier + .height(36.dp) + .padding(start = 12.dp), + verticalArrangement = Arrangement.SpaceBetween, + ) { + EllipsisText( + text = title, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Justify, + ellipsis = TextEllipsis.Middle, + modifier = Modifier, + ) + Row { + if (subtitleIconRes != null) { + Icon( + painter = painterResource(id = subtitleIconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier + .padding(end = 2.dp) + .size(16.dp) + .background(TangemTheme.colors.background.tertiary, CircleShape) + .padding(2.dp), + ) + } + val (text, offset) = remember(subtitle, info) { + if (info != null) { + val suffix = ", $info" + subtitle + suffix to suffix.length + subtitleEndOffset + } else { + subtitle to 0 + } + } + EllipsisText( + text = text, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ellipsis = TextEllipsis.OffsetEnd(offsetEnd = offset), + ) + } + } + } +} + +@Composable +private fun ListItemLoading(modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 12.dp), + ) { + CircleShimmer( + modifier = Modifier + .padding(vertical = 8.dp) + .size(40.dp), + ) + Column( + modifier = Modifier + .height(36.dp) + .padding(start = 12.dp), + verticalArrangement = Arrangement.SpaceBetween, + ) { + RectangleShimmer( + radius = 3.dp, + modifier = Modifier.size( + width = 70.dp, + height = 12.dp, + ), + ) + RectangleShimmer( + radius = 3.dp, + modifier = Modifier.size( + width = 52.dp, + height = 12.dp, + ), + ) + } + } +} + +// region preview +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ListItemWithIconPreview( + @PreviewParameter(ListItemWithIconPreviewProvider::class) config: ListItemWithIconPreviewConfig, +) { + TangemThemePreview { + ListItemWithIcon( + title = config.title, + subtitle = config.subtitle, + subtitleEndOffset = config.subtitleEndOffset, + subtitleIconRes = config.iconRes, + onClick = {}, + isLoading = config.isLoading, + ) + } +} + +private data class ListItemWithIconPreviewConfig( + val title: String, + val subtitle: String, + val info: String? = null, + val subtitleEndOffset: Int = 0, + val iconRes: Int? = null, + val isLoading: Boolean = false, +) + +private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvider( + collection = listOf( + ListItemWithIconPreviewConfig( + title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + subtitle = "0.000000000000000000000000000000 BTC", + info = "0.0.0000 at 00:00", + subtitleEndOffset = "BTC".length, + iconRes = R.drawable.ic_arrow_down_24, + ), + ListItemWithIconPreviewConfig( + title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + subtitle = "1 BTC", + info = "0.0.0000 at 00:00", + subtitleEndOffset = "BTC".length, + iconRes = R.drawable.ic_arrow_down_24, + ), + ListItemWithIconPreviewConfig( + title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + subtitle = "Wallet", + ), + ListItemWithIconPreviewConfig( + title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + subtitle = "0.000000000000000000000000000000 BTC", + info = "0.0.0000 at 00:00", + subtitleEndOffset = "BTC".length, + iconRes = R.drawable.ic_arrow_down_24, + isLoading = true, + ), + ), +) +//endregion \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt new file mode 100644 index 0000000000..967f088d14 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt @@ -0,0 +1,248 @@ +package com.tangem.features.send.v2.subcomponents.destination.ui + +import androidx.annotation.StringRes +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.containers.FooterContainer +import com.tangem.core.ui.components.inputrow.InputRowRecipient +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource +import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationClickIntents +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import kotlinx.collections.immutable.ImmutableList + +private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY" +private const val MEMO_FIELD_KEY = "MEMO_FIELD_KEY" + +@Composable +internal fun SendDestinationContent( + state: DestinationUM, + clickIntents: SendDestinationClickIntents, + isBalanceHidden: Boolean, +) { + if (state !is DestinationUM.Content) return + val recipients = state.recent + val wallets = state.wallets + val memoField = state.memoTextField + val address = state.addressTextField + val isValidating by remember(state.isValidating) { derivedStateOf { state.isValidating } } + val isError by remember(address.isError) { derivedStateOf { address.isError } } + LazyColumn( + modifier = Modifier // Do not put fillMaxSize() in here + .background(TangemTheme.colors.background.tertiary) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) { + addressItem( + address = address, + networkName = state.networkName, + isError = isError, + isValidating = isValidating, + onAddressChange = clickIntents::onRecipientAddressValueChange, + ) + memoField( + memoField = memoField, + onMemoChange = clickIntents::onRecipientMemoValueChange, + ) + listHeaderItem( + titleRes = R.string.send_recipient_wallets_title, + isVisible = wallets.isNotEmpty() && wallets.first().isVisible, + isFirst = true, + ) + listItem( + list = wallets, + clickIntents = clickIntents, + isLast = recipients.any { !it.isVisible }, + isBalanceHidden = isBalanceHidden, + ) + listHeaderItem( + titleRes = R.string.send_recent_transactions, + isVisible = recipients.isNotEmpty() && recipients.first().isVisible, + isFirst = wallets.any { !it.isVisible }, + ) + listItem( + list = recipients, + clickIntents = clickIntents, + isLast = true, + isBalanceHidden = isBalanceHidden, + ) + } +} + +private fun LazyListScope.addressItem( + address: DestinationTextFieldUM.RecipientAddress, + networkName: String, + isError: Boolean, + isValidating: Boolean, + onAddressChange: (String, EnterAddressSource) -> Unit, +) { + item(key = ADDRESS_FIELD_KEY) { + FooterContainer( + footer = resourceReference(R.string.send_recipient_address_footer, wrappedList(networkName)), + ) { + InputRowRecipient( + value = address.value, + title = address.label, + placeholder = address.placeholder, + onValueChange = { onAddressChange(it, EnterAddressSource.InputField) }, + onPasteClick = { onAddressChange(it, EnterAddressSource.PasteButton) }, + isError = isError, + isLoading = isValidating, + error = address.error, + isValuePasted = address.isValuePasted, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } + } +} + +private fun LazyListScope.memoField( + memoField: DestinationTextFieldUM.RecipientMemo?, + onMemoChange: (String, Boolean) -> Unit, +) { + if (memoField != null) { + item(key = MEMO_FIELD_KEY) { + val placeholder = if (memoField.isEnabled) memoField.placeholder else memoField.disabledText + TextFieldWithPaste( + value = memoField.value, + label = memoField.label, + placeholder = placeholder, + footer = resourceReference(R.string.send_recipient_memo_footer), + onValueChange = { onMemoChange(it, false) }, + onPasteClick = { onMemoChange(it, true) }, + modifier = Modifier.padding(top = 20.dp), + labelStyle = TangemTheme.typography.subtitle2, + isError = memoField.isError, + error = memoField.error, + isReadOnly = !memoField.isEnabled, + isValuePasted = memoField.isValuePasted, + ) + } + } +} + +private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Boolean, isFirst: Boolean) { + item(key = titleRes) { + AnimateRecentAppearance(isVisible) { + val (topPadding, paddingFromTop) = if (isFirst) { + 20.dp to 12.dp + } else { + 0.dp to 8.dp + } + val topRadius = if (isFirst) 16.dp else 0.dp + Text( + text = stringResourceSafe(titleRes), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .fillMaxWidth() + .padding(top = topPadding) + .clip( + RoundedCornerShape( + topEnd = topRadius, + topStart = topRadius, + ), + ) + .background(TangemTheme.colors.background.action) + .padding( + top = paddingFromTop, + bottom = 12.dp, + start = 12.dp, + end = 12.dp, + ), + ) + } + } +} + +private fun LazyListScope.listItem( + list: ImmutableList, + clickIntents: SendDestinationClickIntents, + isLast: Boolean, + isBalanceHidden: Boolean, +) { + items( + count = list.size, + key = { list[it].id }, + contentType = { list[it]::class.java }, + ) { index -> + val item = list[index] + val title = item.title.resolveReference() + + if (item.isVisible) { + ListItemWithIcon( + title = title, + subtitle = item.subtitle.orMaskWithStars(isBalanceHidden).resolveReference(), + info = item.timestamp?.resolveReference(), + subtitleEndOffset = item.subtitleEndOffset, + subtitleIconRes = item.subtitleIconRes, + onClick = { + clickIntents.onRecipientAddressValueChange( + title, + EnterAddressSource.RecentAddress, + ) + }, + isLoading = item.isLoading, + modifier = Modifier + .animateItem() + .then( + if (isLast && index == list.lastIndex) { + Modifier + .clip( + shape = RoundedCornerShape( + bottomStart = 16.dp, + bottomEnd = 16.dp, + ), + ) + } else { + Modifier + }, + ) + .background(TangemTheme.colors.background.action), + ) + } + } +} + +@Composable +private fun AnimateRecentAppearance(isVisible: Boolean, content: @Composable () -> Unit) { + AnimatedContent( + targetState = isVisible, + label = "Item Appearance Animation", + transitionSpec = { + (slideInHorizontally() + fadeIn()) + .togetherWith(slideOutVertically() + fadeOut()) + }, + ) { + if (it) { + content() + } else { + Box(modifier = Modifier.fillMaxWidth()) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/TextFieldWithPaste.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/TextFieldWithPaste.kt new file mode 100644 index 0000000000..162ab6bf7b --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/TextFieldWithPaste.kt @@ -0,0 +1,103 @@ +package com.tangem.features.send.v2.subcomponents.destination.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment.Companion.CenterEnd +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.fields.SimpleTextField +import com.tangem.core.ui.components.inputrow.inner.CrossIcon +import com.tangem.core.ui.components.inputrow.inner.PasteButton +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.components.containers.FooterContainer + +@Composable +internal fun TextFieldWithPaste( + value: String, + placeholder: TextReference, + label: TextReference, + onValueChange: (String) -> Unit, + onPasteClick: (String) -> Unit, + modifier: Modifier = Modifier, + footer: TextReference? = null, + labelStyle: TextStyle = TangemTheme.typography.body2, + error: TextReference? = null, + isError: Boolean = false, + isReadOnly: Boolean = false, + isValuePasted: Boolean = false, +) { + val (title, color) = when { + isError && error != null -> error to TangemTheme.colors.text.warning + isReadOnly -> label to TangemTheme.colors.text.tertiary + else -> label to TangemTheme.colors.text.secondary + } + val placeholderColor = if (isReadOnly) TangemTheme.colors.text.tertiary else TangemTheme.colors.text.disabled + FooterContainer(modifier, footer) { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding(end = 12.dp), + ) { + Row { + Column( + modifier = Modifier + .weight(1f) + .padding(12.dp), + ) { + Text( + text = title.resolveReference(), + style = labelStyle, + color = color, + ) + SimpleTextField( + value = value, + placeholder = placeholder, + placeholderColor = placeholderColor, + onValueChange = onValueChange, + readOnly = isReadOnly, + isValuePasted = isValuePasted, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + ) + } + AnimatedVisibility( + visible = !isReadOnly, + label = "Animate read only status change", + enter = fadeIn(), + exit = fadeOut(), + modifier = Modifier + .align(CenterVertically), + ) { + CrossIcon( + onClick = onPasteClick, + ) + } + } + AnimatedVisibility( + visible = !isReadOnly, + label = "Animate read only status change", + enter = fadeIn(), + exit = fadeOut(), + modifier = Modifier.align(CenterEnd), + ) { + PasteButton( + isPasteButtonVisible = value.isBlank(), + onClick = onPasteClick, + ) + } + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationRecipientListUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationRecipientListUM.kt new file mode 100644 index 0000000000..679928b142 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationRecipientListUM.kt @@ -0,0 +1,22 @@ +package com.tangem.features.send.v2.subcomponents.destination.ui.state + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +@Immutable +internal data class DestinationRecipientListUM( + val id: String, + val title: TextReference = TextReference.Companion.EMPTY, + val subtitle: TextReference = TextReference.Companion.EMPTY, + val timestamp: TextReference? = null, + val subtitleEndOffset: Int = 0, + @DrawableRes val subtitleIconRes: Int? = null, + val isVisible: Boolean = true, + val isLoading: Boolean = false, + val userWalletId: UserWalletId? = null, + val network: Network? = null, + val address: String? = null, +) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt new file mode 100644 index 0000000000..b2c3ac8b6e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt @@ -0,0 +1,42 @@ +package com.tangem.features.send.v2.subcomponents.destination.ui.state + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed class DestinationTextFieldUM { + + /** Current value */ + abstract val value: String + + /** Lambda be invoked when value is been changed */ + abstract val onValueChange: (String) -> Unit + + /** Keyboard options */ + abstract val keyboardOptions: KeyboardOptions + + data class RecipientAddress( + override val value: String, + override val onValueChange: (String) -> Unit, + override val keyboardOptions: KeyboardOptions, + val placeholder: TextReference, + val label: TextReference, + val isError: Boolean = false, + val error: TextReference? = null, + val isValuePasted: Boolean, + ) : DestinationTextFieldUM() + + data class RecipientMemo( + override val value: String, + override val onValueChange: (String) -> Unit, + override val keyboardOptions: KeyboardOptions, + val placeholder: TextReference, + val label: TextReference, + val isError: Boolean = false, + val error: TextReference? = null, + val disabledText: TextReference, + val isEnabled: Boolean, + val isValuePasted: Boolean, + ) : DestinationTextFieldUM() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt new file mode 100644 index 0000000000..7be35bbc1b --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt @@ -0,0 +1,25 @@ +package com.tangem.features.send.v2.subcomponents.destination.ui.state + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class DestinationUM { + + abstract val isPrimaryButtonEnabled: Boolean + + data class Content( + override val isPrimaryButtonEnabled: Boolean, + val addressTextField: DestinationTextFieldUM.RecipientAddress, + val memoTextField: DestinationTextFieldUM.RecipientMemo?, + val recent: ImmutableList, + val wallets: ImmutableList, + val networkName: String, + val isValidating: Boolean = false, + val isEditingDisabled: Boolean = false, + ) : DestinationUM() + + data class Empty( + override val isPrimaryButtonEnabled: Boolean = false, + ) : DestinationUM() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationWalletUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationWalletUM.kt new file mode 100644 index 0000000000..cc7629e559 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationWalletUM.kt @@ -0,0 +1,21 @@ +package com.tangem.features.send.v2.subcomponents.destination.ui.state + +import androidx.compose.runtime.Immutable +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Available wallet to send + * + * @property name wallet name + * @property userWalletId wallet id + * @property address blockchain address + * @property cryptoCurrency selected crypto currency + */ +@Immutable +data class DestinationWalletUM( + val name: String, + val userWalletId: UserWalletId, + val address: String, + val cryptoCurrency: CryptoCurrency, +) \ No newline at end of file From 12126ea31aa9e8dcd4859c00d416b38f883f539d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 24 Mar 2025 15:31:50 +0000 Subject: [PATCH 034/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 58cb7cc642..fc082393ac 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "release-app_5.22-987" +tangemBlockchainSdk = "develop-989" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.22-440" +tangemCardSdk = "develop-444" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From fc8fa8ff8db9673c0ebf021da109d097e429db35 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Mar 2025 16:29:04 +0500 Subject: [PATCH 035/138] Updated on 2026-08-14 --- .../tangem/domain/walletmanager/DefaultWalletManagersFacade.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 4beba64f1c..5dc33bd1cd 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -684,7 +684,7 @@ class DefaultWalletManagersFacade( override suspend fun isAccountInitialized(userWalletId: UserWalletId, network: Network): Boolean { val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network) - val initializableAccountWalletManger = walletManager as? InitializableAccount ?: return false + val initializableAccountWalletManger = walletManager as? InitializableAccount ?: return true return initializableAccountWalletManger.accountInitializationState == InitializableAccount.State.INITIALIZED } From bcc957e249b50a4a2fb96632e705a5035b8c17a2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 7 Mar 2025 18:18:27 +0700 Subject: [PATCH 036/138] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../DefaultLegacyWalletConnectRepository.kt | 11 +- data/wallet-connect/build.gradle.kts | 6 + .../walletconnect/DefaultWcSessionsManager.kt | 38 +++ .../initialize/DefaultWcInitializeUseCase.kt | 115 ++++++++++ .../pair/DefaultWcPairUseCase.kt | 216 ++++++++++++++++++ .../request/DefaultWcRequestService.kt | 48 ++++ .../respond/DefaultWcRespondService.kt | 53 +++++ .../data/walletconnect/utils/ToOurModel.kt | 22 ++ .../data/walletconnect/utils/WcSdkObserver.kt | 38 +++ domain/wallet-connect/build.gradle.kts | 4 + domain/wallet-connect/models/.gitignore | 1 + domain/wallet-connect/models/build.gradle.kts | 10 + .../domain/walletconnect/model/WcMethod.kt | 5 + .../domain/walletconnect/model/WcRequest.kt | 9 + .../domain/walletconnect/model/WcSession.kt | 9 + .../walletconnect/model/WcSessionProposal.kt | 15 ++ .../model/sdkcopy/WcSdkSession.kt | 8 + .../model/sdkcopy/WcSdkSessionProposal.kt | 11 + .../model/sdkcopy/WcSdkSessionRequest.kt | 17 ++ .../repository/WcSessionsManager.kt | 12 + .../walletconnect/request/WcRequestHandler.kt | 18 ++ .../walletconnect/request/WcRequestService.kt | 8 + .../walletconnect/respond/WcRespondService.kt | 9 + .../usecase/WcSimpleSignUseCase.kt | 25 ++ .../domain/walletconnect/usecase/WcUseCase.kt | 3 + .../usecase/WcUseCasesFlowProvider.kt | 7 + .../ethereum/EthPersonalSignUseCase.kt | 72 ++++++ .../usecase/ethereum/WcEthChain.kt | 53 +++++ .../usecase/ethereum/WcEthMethod.kt | 10 + .../usecase/initialize/WcInitializeUseCase.kt | 5 + .../walletconnect/usecase/pair/WcPairState.kt | 24 ++ .../usecase/pair/WcPairUseCase.kt | 20 ++ settings.gradle.kts | 1 + 34 files changed, 898 insertions(+), 6 deletions(-) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWcSessionsManager.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt create mode 100644 domain/wallet-connect/models/.gitignore create mode 100644 domain/wallet-connect/models/build.gradle.kts create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethod.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequest.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionProposal.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionRequest.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestHandler.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestService.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/respond/WcRespondService.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSimpleSignUseCase.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCasesFlowProvider.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/EthPersonalSignUseCase.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthChain.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMethod.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8bee4d929a..b7a7f549c4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -100,6 +100,7 @@ dependencies { implementation(projects.domain.qrScanning.models) implementation(projects.domain.staking) implementation(projects.domain.walletConnect) + implementation(projects.domain.walletConnect.models) implementation(projects.domain.markets) implementation(projects.domain.manageTokens) implementation(projects.domain.nft) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 0f673b9ba6..982bcd4438 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -8,9 +8,13 @@ import com.reown.android.relay.ConnectionType import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.data.walletconnect.pair.unsupportedDApps import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper -import com.tangem.tap.domain.walletconnect2.domain.* +import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.WcJrpcMethods +import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer +import com.tangem.tap.domain.walletconnect2.domain.WcRequest import com.tangem.tap.domain.walletconnect2.domain.models.* import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType @@ -555,9 +559,4 @@ internal class DefaultLegacyWalletConnectRepository( val userChains = userNamespaces.flatMap { it.value.map { account -> account.chainId } } return wcProvidedChains.intersect(userChains.toSet()) } - - private companion object { - - val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox") - } } \ No newline at end of file diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts index 5dc33697df..7dbf3dc44a 100644 --- a/data/wallet-connect/build.gradle.kts +++ b/data/wallet-connect/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { /* Project - Domain */ implementation(projects.domain.walletConnect) + implementation(projects.domain.walletConnect.models) implementation(projects.domain.wallets.models) /* Project - Data */ @@ -25,6 +26,11 @@ dependencies { implementation(deps.hilt.core) kapt(deps.hilt.kapt) + /* Reown - WalletConnect */ + implementation(deps.reownCore) + implementation(deps.reownWeb3) + /* Other */ implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWcSessionsManager.kt new file mode 100644 index 0000000000..388c5c30a1 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWcSessionsManager.kt @@ -0,0 +1,38 @@ +package com.tangem.data.walletconnect + +import com.reown.walletkit.client.Wallet +import com.tangem.data.walletconnect.utils.WcSdkObserver +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.repository.WcSessionsManager +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import timber.log.Timber + +internal class DefaultWcSessionsManager : WcSessionsManager, WcSdkObserver { + + private val _sessions = MutableSharedFlow>>( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + override val sessions get() = _sessions.distinctUntilChanged() + + override suspend fun saveSessions(userWalletId: UserWalletId, session: WcSession) { + TODO("Not yet implemented") + } + + override suspend fun removeSessions(userWalletId: UserWalletId, session: WcSession) { + TODO("Not yet implemented") + } + + override suspend fun findSessionByTopic(topic: String): WcSession? { + TODO("Not yet implemented") + } + + override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { + // Triggered when the session is deleted by the peer + Timber.i("onSessionDelete: $sessionDelete") + // todo (wc) find session, delete and update sessions Flow + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt new file mode 100644 index 0000000000..c7e39cd519 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt @@ -0,0 +1,115 @@ +package com.tangem.data.walletconnect.initialize + +import android.app.Application +import com.reown.android.Core +import com.reown.android.CoreClient +import com.reown.android.relay.ConnectionType +import com.reown.walletkit.client.Wallet +import com.reown.walletkit.client.WalletKit +import com.tangem.data.walletconnect.DefaultWcSessionsManager +import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase +import com.tangem.data.walletconnect.request.DefaultWcRequestService +import com.tangem.data.walletconnect.utils.WcSdkObserver +import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase +import timber.log.Timber + +internal class DefaultWcInitializeUseCase( + private val application: Application, + private val sessionsManager: DefaultWcSessionsManager, + private val networkService: DefaultWcRequestService, + private val wcPairFlow: DefaultWcPairUseCase, +) : WcInitializeUseCase { + + private val wcSdkObservers = mutableSetOf( + sessionsManager, + networkService, + wcPairFlow, + ) + + override fun init(projectId: String) { + val relayUrl = "relay.walletconnect.com" + val serverUrl = "wss://$relayUrl?projectId=$projectId" + val connectionType = ConnectionType.AUTOMATIC + + val appMetaData = Core.Model.AppMetaData( + name = "Tangem", + description = "Tangem Wallet", + url = "tangem.com", + icons = listOf( + "https://user-images.githubusercontent.com/24321494/124071202-72a00900-da58-11eb-935a-dcdab21de52b.png", + ), + redirect = "kotlin-wallet-wc:/request", // Custom Redirect URI + ) + + CoreClient.initialize( + relayServerUrl = serverUrl, + connectionType = connectionType, + application = application, + metaData = appMetaData, + ) { error -> + Timber.e("Error while initializing client: $error") + } + + WalletKit.initialize( + Wallet.Params.Init(core = CoreClient), + onSuccess = { + val walletDelegate = defineWalletDelegate() + WalletKit.setWalletDelegate(walletDelegate) + }, + onError = { error -> + Timber.e("Error while initializing Web3Wallet: $error") + }, + ) + } + + private fun defineWalletDelegate() = object : WalletKit.WalletDelegate { + override val onSessionAuthenticate: ((Wallet.Model.SessionAuthenticate, Wallet.Model.VerifyContext) -> Unit)? + get() = super.onSessionAuthenticate + + override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) { + wcSdkObservers.forEach { it.onConnectionStateChange(state) } + } + + override fun onError(error: Wallet.Model.Error) { + wcSdkObservers.forEach { it.onError(error) } + } + + override fun onProposalExpired(proposal: Wallet.Model.ExpiredProposal) { + wcSdkObservers.forEach { it.onProposalExpired(proposal) } + } + + override fun onRequestExpired(request: Wallet.Model.ExpiredRequest) { + wcSdkObservers.forEach { it.onRequestExpired(request) } + } + + override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { + wcSdkObservers.forEach { it.onSessionDelete(sessionDelete) } + } + + override fun onSessionExtend(session: Wallet.Model.Session) { + wcSdkObservers.forEach { it.onSessionExtend(session) } + } + + override fun onSessionProposal( + sessionProposal: Wallet.Model.SessionProposal, + verifyContext: Wallet.Model.VerifyContext, + ) { + wcSdkObservers.forEach { it.onSessionProposal(sessionProposal, verifyContext) } + } + + override fun onSessionRequest( + sessionRequest: Wallet.Model.SessionRequest, + verifyContext: Wallet.Model.VerifyContext, + ) { + wcSdkObservers.forEach { it.onSessionRequest(sessionRequest, verifyContext) } + } + + override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { + wcSdkObservers.forEach { it.onSessionSettleResponse(settleSessionResponse) } + } + + override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) { + wcSdkObservers.forEach { it.onSessionUpdateResponse(sessionUpdateResponse) } + } + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt new file mode 100644 index 0000000000..89099dc766 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -0,0 +1,216 @@ +package com.tangem.data.walletconnect.pair + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.reown.walletkit.client.Wallet +import com.reown.walletkit.client.WalletKit +import com.tangem.data.walletconnect.utils.WcSdkObserver +import com.tangem.data.walletconnect.utils.toOurModel +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionProposal +import com.tangem.domain.walletconnect.repository.WcSessionsManager +import com.tangem.domain.walletconnect.usecase.pair.WcPairState +import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase +import com.tangem.domain.wallets.models.UserWallet +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.suspendCancellableCoroutine +import timber.log.Timber +import kotlin.coroutines.resume + +val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox") + +@Suppress("UnusedPrivateMember") // todo(wc) remove after add mapping +internal class DefaultWcPairUseCase( + private val sessionsManager: WcSessionsManager, +) : WcPairUseCase, WcSdkObserver { + + private val onWalletSelect = Channel() + private val onAccountSelect = Channel() + private val onCallTerminalAction = Channel() + private val onSessionProposal = + Channel>(Channel.BUFFERED) + private val onSessionSettleResponse = Channel(Channel.BUFFERED) + + override fun pairFlow(uri: String, source: WcPairUseCase.Source, selectedWallet: UserWallet): Flow = + flow { + emit(WcPairState.Loading) + + // call sdk.pair and wait result, finish flow on error + walletKitPair(uri).onLeft { throwable -> + emit(WcPairState.Error(throwable)) + return@flow + } + // wait for sdk onSessionProposal callback + val (sdkSessionProposal, verifyContext) = onSessionProposal.receiveAsFlow().map { (fist, second) -> + val ourCopy = WcSdkSessionProposal( + name = fist.name, + description = fist.description, + url = fist.url, + proposerPublicKey = fist.proposerPublicKey, + ) + ourCopy to second + }.first { (sessionProposal, verifyContext) -> + true // todo(wc) check verifyContext? compare uri? + } + + // check unsupported dApps, just local constant for now, finish if unsupported + if (sdkSessionProposal.name in unsupportedDApps) { + Timber.i("Unsupported DApp") + val error = WcPairState + .Error(RuntimeException("todo(wc) use domain exception")) // todo(wc) WalletConnectError.UnsupportedDApp + emit(error) + return@flow + } + + // flow that collect terminal and all middle actions + val actionsFlow = channelFlow { + val userWallet = onWalletSelect.receiveAsFlow() + .stateIn(scope = this, started = SharingStarted.Lazily, initialValue = selectedWallet) + val accountSelect = onAccountSelect.receiveAsFlow() + .stateIn(scope = this, started = SharingStarted.Lazily, initialValue = Any()) + // combine all middle variables and emit new ProposalState to main FlowCollector + combine(accountSelect, userWallet) { account, selectedWallet -> + buildProposalState(sdkSessionProposal, verifyContext, account, selectedWallet) + }.onEach { newProposalState -> this@flow.emit(newProposalState) } + .launchIn(this) + + // collect terminal action and emit to this inner channelFlow, unlike combine above + onCallTerminalAction.receiveAsFlow() + .onEach { this.channel.send(it) } + .launchIn(this) + } + + // wait first terminal action and continue WC pair flow + val sessionForApprove: WcSessionProposal = when (val terminalAction = actionsFlow.first()) { + is TerminalAction.Approve -> terminalAction.sessionForApprove + TerminalAction.Reject -> { + // non suspending WalletKit.rejectSession call + rejectSession(sdkSessionProposal) + return@flow + } + } + + // start flow of approving in wc sdk + emit(WcPairState.Approving.Loading(sessionForApprove)) + fun mapResult(either: Either) = + WcPairState.Approving.Result(sessionForApprove, either) + // call sdk approve and wait result + walletKitApproveSession(sessionForApprove).onLeft { emit(mapResult(it.left())) }.onRight { + // wait for sdk callback + val result = when (val settledSession = onSessionSettleResponse.receiveAsFlow().first()) { + is Wallet.Model.SettledSessionResponse.Error -> mapResult( + // WalletConnectError.ExternalApprovalError(settledSession.errorMessage) todo(wc) + RuntimeException("todo(wc) use domain exception").left(), + ) + + is Wallet.Model.SettledSessionResponse.Result -> { + val newSession = settledSession.session.toDomain(sessionForApprove.wallet) + sessionsManager.saveSessions(sessionForApprove.wallet.walletId, newSession) + mapResult(newSession.right()) + } + } + emit(result) + } + } + + override fun onWalletSelect(selectedWallet: UserWallet) { + onWalletSelect.trySend(selectedWallet) + } + + override fun onAccountSelect(account: Any) { + onAccountSelect.trySend(account) + } + + override fun approve(sessionForApprove: WcSessionProposal) { + onCallTerminalAction.trySend(TerminalAction.Approve(sessionForApprove)) + } + + override fun reject() { + onCallTerminalAction.trySend(TerminalAction.Reject) + } + + override fun onSessionProposal( + sessionProposal: Wallet.Model.SessionProposal, + verifyContext: Wallet.Model.VerifyContext, + ) { + // Triggered when wallet receives the session proposal sent by a Dapp + Timber.i("sessionProposal: $sessionProposal") + onSessionProposal.trySend(sessionProposal to verifyContext) + } + + override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { + // Triggered when wallet receives the session settlement response from Dapp + Timber.i("onSessionSettleResponse: $settleSessionResponse") + onSessionSettleResponse.trySend(settleSessionResponse) + } + + private suspend fun walletKitPair(uri: String): Either = + suspendCancellableCoroutine { continuation -> + WalletKit.pair( + params = Wallet.Params.Pair(uri), + onSuccess = { + Timber.i("Paired successfully: $it") + continuation.resume(Unit.right()) + }, + onError = { + Timber.e("Error while pairing: $it") + continuation.resume(it.throwable.left()) + }, + ) + } + + private suspend fun walletKitApproveSession(sessionForApprove: WcSessionProposal): Either = + suspendCancellableCoroutine { continuation -> + WalletKit.approveSession( + params = TODO("wc sdk model"), + onSuccess = { + Timber.i("Approved successfully: $it") + continuation.resume(Unit.right()) + }, + onError = { + Timber.e("Error while approving: $it") + continuation.resume(it.throwable.left()) + }, + ) + } + + private fun rejectSession(sessionProposal: WcSdkSessionProposal) { + WalletKit.rejectSession( + params = Wallet.Params.SessionReject( + proposerPublicKey = sessionProposal.proposerPublicKey, + reason = "", + ), + onSuccess = { + Timber.i("Rejected successfully: $it") + }, + onError = { + Timber.e("Error while rejecting: $it") + }, + ) + } + + private fun buildProposalState( + sessionProposal: WcSdkSessionProposal, + verifyContext: Wallet.Model.VerifyContext, + selectedAccount: Any, + userWallet: UserWallet, + ): WcPairState.Proposal { + // todo(wc) a lot of mapping from Wallet.Model.SessionProposal to our Blockchain, Network, ect + // todo(wc) check security status by Wallet.Model.VerifyContext or Blockaid, don't know for now + val mock: WcSessionProposal = TODO() + return WcPairState.Proposal(mock, Any()) + } + + private fun Wallet.Model.Session.toDomain(userWallet: UserWallet): WcSession = WcSession( + userWalletId = userWallet.walletId, + sdkModel = this.toOurModel(), + ) + + private sealed interface TerminalAction { + data class Approve(val sessionForApprove: WcSessionProposal) : TerminalAction + data object Reject : TerminalAction + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt new file mode 100644 index 0000000000..c36cfbdb75 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt @@ -0,0 +1,48 @@ +package com.tangem.data.walletconnect.request + +import com.reown.walletkit.client.Wallet +import com.tangem.data.walletconnect.utils.WcSdkObserver +import com.tangem.data.walletconnect.utils.toOurModel +import com.tangem.domain.walletconnect.model.WcMethod +import com.tangem.domain.walletconnect.model.WcRequest +import com.tangem.domain.walletconnect.repository.WcSessionsManager +import com.tangem.domain.walletconnect.request.WcRequestHandler +import com.tangem.domain.walletconnect.request.WcRequestService +import com.tangem.domain.walletconnect.respond.WcRespondService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch + +internal class DefaultWcRequestService( + private val sessionsManager: WcSessionsManager, + private val respondService: WcRespondService, + private val requestAdapters: List>, + private val scope: CoroutineScope, // todo(wc) is ok inject scope? featureScope like Decompose Model scope +) : WcRequestService, WcSdkObserver { + + override val requests: MutableSharedFlow> = MutableSharedFlow() + + override fun onSessionRequest( + sessionRequest: Wallet.Model.SessionRequest, + verifyContext: Wallet.Model.VerifyContext, + ) { + // Triggered when a Dapp sends SessionRequest to sign a transaction or a message + val sr = sessionRequest.toOurModel() + val method = sr.request.method + val params = sr.request.params + scope.launch { + val session = sessionsManager.findSessionByTopic(sr.topic) + val handler: WcRequestHandler? = requestAdapters.firstOrNull { it.canHandle(method) } + + val deserialized: WcMethod? = handler?.deserialize(method, params) + if (handler == null || deserialized == null || session == null) { + respondService.rejectRequest(sr, "UnsupportedMethod") // todo(wc) use our domain error + return@launch + } + + val wcRequest = WcRequest(sr, session, deserialized) + handler.handle(wcRequest) + requests.emit(wcRequest) + } + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt new file mode 100644 index 0000000000..7381e9b548 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt @@ -0,0 +1,53 @@ +package com.tangem.data.walletconnect.respond + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.reown.walletkit.client.Wallet +import com.reown.walletkit.client.WalletKit +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.domain.walletconnect.respond.WcRespondService +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +internal class DefaultWcRespondService : WcRespondService { + + override suspend fun respond(request: WcSdkSessionRequest, response: String): Either = + suspendCancellableCoroutine { continuation -> + WalletKit.respondSessionRequest( + params = Wallet.Params.SessionRequestResponse( + sessionTopic = request.topic, + jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult( + id = request.request.id, + result = response, + ), + ), + onSuccess = { + continuation.resume(Unit.right()) + }, + onError = { + continuation.resume(it.throwable.left()) + }, + ) + } + + override suspend fun rejectRequest(request: WcSdkSessionRequest, message: String) = + suspendCancellableCoroutine { continuation -> + WalletKit.respondSessionRequest( + params = Wallet.Params.SessionRequestResponse( + sessionTopic = request.topic, + jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcError( + id = request.request.id, + code = 0, + message = message, + ), + ), + onSuccess = { + continuation.resume(Unit.right()) + }, + onError = { + continuation.resume(it.throwable.left()) + }, + ) + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt new file mode 100644 index 0000000000..5da50ba3c4 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt @@ -0,0 +1,22 @@ +package com.tangem.data.walletconnect.utils + +import com.reown.walletkit.client.Wallet +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest.JSONRPCRequest + +internal fun Wallet.Model.Session.toOurModel(): WcSdkSession = WcSdkSession( + topic = topic, +) + +internal fun Wallet.Model.SessionRequest.toOurModel(): WcSdkSessionRequest = WcSdkSessionRequest( + topic = this.topic, + chainId = this.chainId, + request = this.request.toOurModel(), +) + +internal fun Wallet.Model.SessionRequest.JSONRPCRequest.toOurModel(): JSONRPCRequest = JSONRPCRequest( + id = this.id, + method = this.method, + params = this.params, +) \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt new file mode 100644 index 0000000000..5c15ea1b80 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt @@ -0,0 +1,38 @@ +package com.tangem.data.walletconnect.utils + +import com.reown.walletkit.client.Wallet +import com.reown.walletkit.client.WalletKit + +internal interface WcSdkObserver : WalletKit.WalletDelegate { + + override val onSessionAuthenticate: ((Wallet.Model.SessionAuthenticate, Wallet.Model.VerifyContext) -> Unit)? + get() = super.onSessionAuthenticate + + override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) {} + + override fun onError(error: Wallet.Model.Error) {} + + override fun onProposalExpired(proposal: Wallet.Model.ExpiredProposal) {} + + override fun onRequestExpired(request: Wallet.Model.ExpiredRequest) {} + + override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) {} + + override fun onSessionExtend(session: Wallet.Model.Session) {} + + override fun onSessionProposal( + sessionProposal: Wallet.Model.SessionProposal, + verifyContext: Wallet.Model.VerifyContext, + ) { + } + + override fun onSessionRequest( + sessionRequest: Wallet.Model.SessionRequest, + verifyContext: Wallet.Model.VerifyContext, + ) { + } + + override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) {} + + override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) {} +} \ No newline at end of file diff --git a/domain/wallet-connect/build.gradle.kts b/domain/wallet-connect/build.gradle.kts index af72640fab..9b0cbaa2e3 100644 --- a/domain/wallet-connect/build.gradle.kts +++ b/domain/wallet-connect/build.gradle.kts @@ -7,4 +7,8 @@ dependencies { /* Project - Domain */ implementation(projects.domain.core) implementation(projects.domain.wallets.models) + implementation(projects.domain.walletConnect.models) + + /* Other */ + implementation(deps.moshi.adapters) } \ No newline at end of file diff --git a/domain/wallet-connect/models/.gitignore b/domain/wallet-connect/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/wallet-connect/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/wallet-connect/models/build.gradle.kts b/domain/wallet-connect/models/build.gradle.kts new file mode 100644 index 0000000000..47fbf86d0b --- /dev/null +++ b/domain/wallet-connect/models/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} +dependencies { + + /* Domain */ + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) +} \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethod.kt new file mode 100644 index 0000000000..05c3df4d98 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethod.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.walletconnect.model + +interface WcMethod { + data object Unsupported : WcMethod +} \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequest.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequest.kt new file mode 100644 index 0000000000..256aa09ce1 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequest.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.walletconnect.model + +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest + +data class WcRequest( + val rawSdkRequest: WcSdkSessionRequest, + val session: WcSession, + val method: M, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt new file mode 100644 index 0000000000..92501d8bba --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.walletconnect.model + +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession +import com.tangem.domain.wallets.models.UserWalletId + +data class WcSession( + val userWalletId: UserWalletId, + val sdkModel: WcSdkSession, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt new file mode 100644 index 0000000000..45c189cf20 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.walletconnect.model + +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionProposal +import com.tangem.domain.wallets.models.UserWallet + +data class WcSessionProposal( + val dAppInfo: WcSdkSessionProposal, + + // todo(wc) should map for all user wallets? + val wallet: UserWallet, + val missingChains: List, + val availableChains: List, + val notAddedChains: List, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt new file mode 100644 index 0000000000..bd88f8ea98 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.walletconnect.model.sdkcopy + +/** + * copy of [com.reown.walletkit.client.Wallet.Model.Session] + */ +data class WcSdkSession( + val topic: String, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionProposal.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionProposal.kt new file mode 100644 index 0000000000..93a3734601 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionProposal.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.walletconnect.model.sdkcopy + +/** + * copy of [com.reown.walletkit.client.Wallet.Model.SessionProposal] + */ +data class WcSdkSessionProposal( + val name: String, + val description: String, + val url: String, + val proposerPublicKey: String, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionRequest.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionRequest.kt new file mode 100644 index 0000000000..7b2ed2ff37 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionRequest.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.walletconnect.model.sdkcopy + +/** + * copy of [com.reown.walletkit.client.Wallet.Model.SessionRequest] + */ +data class WcSdkSessionRequest( + val topic: String, + val chainId: String?, + val request: JSONRPCRequest, +) { + + data class JSONRPCRequest( + val id: Long, + val method: String, + val params: String, + ) +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt new file mode 100644 index 0000000000..4d2d837da0 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.walletconnect.repository + +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +interface WcSessionsManager { + val sessions: Flow>> + suspend fun saveSessions(userWalletId: UserWalletId, session: WcSession) + suspend fun removeSessions(userWalletId: UserWalletId, session: WcSession) + suspend fun findSessionByTopic(topic: String): WcSession? +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestHandler.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestHandler.kt new file mode 100644 index 0000000000..410815ae03 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestHandler.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.walletconnect.request + +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import com.tangem.domain.walletconnect.model.WcMethod +import com.tangem.domain.walletconnect.model.WcRequest + +interface WcRequestHandler { + fun canHandle(methodName: String): Boolean + fun deserialize(methodName: String, params: String): M? + fun handle(wcRequest: WcRequest) + + companion object { + @OptIn(ExperimentalStdlibApi::class) + inline fun fromJson(params: String, moshi: Moshi): T? = + runCatching { moshi.adapter().fromJsonValue(params) }.getOrNull() + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestService.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestService.kt new file mode 100644 index 0000000000..5918accfd9 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestService.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.walletconnect.request + +import com.tangem.domain.walletconnect.model.WcRequest +import kotlinx.coroutines.flow.Flow + +interface WcRequestService { + val requests: Flow> +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/respond/WcRespondService.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/respond/WcRespondService.kt new file mode 100644 index 0000000000..c74ce0e017 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/respond/WcRespondService.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.walletconnect.respond + +import arrow.core.Either +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest + +interface WcRespondService { + suspend fun respond(request: WcSdkSessionRequest, response: String): Either + suspend fun rejectRequest(request: WcSdkSessionRequest, message: String = ""): Either +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSimpleSignUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSimpleSignUseCase.kt new file mode 100644 index 0000000000..e0bb5239a4 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSimpleSignUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.walletconnect.usecase + +import arrow.core.Either +import kotlinx.coroutines.flow.Flow + +interface WcSimpleSignUseCase : WcUseCase { + + fun signFlow(initModel: SignModel): Flow> + + fun action(action: MiddleAction) + + fun cancel() + fun sign(toSign: SignModel) + + sealed interface State { + val model: SignModel + + data class PreSign(override val model: SignModel) : State + data class Signing(override val model: SignModel) : State + data class Result( + val result: Either, + override val model: SignModel, + ) : State + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt new file mode 100644 index 0000000000..a2efe639cd --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt @@ -0,0 +1,3 @@ +package com.tangem.domain.walletconnect.usecase + +interface WcUseCase \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCasesFlowProvider.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCasesFlowProvider.kt new file mode 100644 index 0000000000..700cf1865c --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCasesFlowProvider.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.walletconnect.usecase + +import kotlinx.coroutines.flow.Flow + +interface WcUseCasesFlowProvider { + val flow: Flow +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/EthPersonalSignUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/EthPersonalSignUseCase.kt new file mode 100644 index 0000000000..5e86f84006 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/EthPersonalSignUseCase.kt @@ -0,0 +1,72 @@ +package com.tangem.domain.walletconnect.usecase.ethereum + +import arrow.core.Either +import com.tangem.domain.walletconnect.model.WcRequest +import com.tangem.domain.walletconnect.respond.WcRespondService +import com.tangem.domain.walletconnect.usecase.WcUseCase +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.* + +class EthPersonalSignUseCase( + private val wcRequest: WcRequest, + private val respondService: WcRespondService, +) : WcUseCase { + + private val onCallTerminalAction = Channel() + + fun signFlow(): Flow = flow { + val model = SignModel(wcRequest.method.raw) + emit(State.PreSign(model)) + + when (val action = onCallTerminalAction.receiveAsFlow().first()) { + TerminalAction.Cancel -> { + val result = respondService.rejectRequest(wcRequest.rawSdkRequest) + emit(State.Result(result, model)) + } + is TerminalAction.Sign -> { + emit(State.Signing(action.toSign)) + val signed = signAndPrepareForSend() + val result = + if (signed != null) { + respondService.respond(wcRequest.rawSdkRequest, signed) + } else { + respondService.rejectRequest(wcRequest.rawSdkRequest) + } + emit(State.Result(result, model)) + } + } + } + + @Suppress("FunctionOnlyReturningConstant") // todo(wc) remove later + private suspend fun signAndPrepareForSend(): String? { + return null // todo(wc) + } + + fun sign(toSign: SignModel) { + onCallTerminalAction.trySend(TerminalAction.Sign(toSign)) + } + + fun cancel() { + onCallTerminalAction.trySend(TerminalAction.Cancel) + } + + sealed interface TerminalAction { + data class Sign(val toSign: SignModel) : TerminalAction + data object Cancel : TerminalAction + } + + sealed interface State { + val model: SignModel + + data class PreSign(override val model: SignModel) : State + data class Signing(override val model: SignModel) : State + data class Result( + val result: Either, + override val model: SignModel, + ) : State + } + + data class SignModel( + val raw: List, + ) +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthChain.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthChain.kt new file mode 100644 index 0000000000..ab18902b6e --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthChain.kt @@ -0,0 +1,53 @@ +package com.tangem.domain.walletconnect.usecase.ethereum + +import com.squareup.moshi.Moshi +import com.tangem.domain.walletconnect.model.WcRequest +import com.tangem.domain.walletconnect.request.WcRequestHandler +import com.tangem.domain.walletconnect.respond.WcRespondService +import com.tangem.domain.walletconnect.usecase.WcUseCase +import com.tangem.domain.walletconnect.usecase.WcUseCasesFlowProvider +import com.tangem.domain.walletconnect.usecase.ethereum.WcEthMethod.SignMessage +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow + +@Suppress("UnusedPrivateClass") // todo(wc) remove later +private class WcEthChain( + val moshi: Moshi, + private val respondService: WcRespondService, +) : WcRequestHandler, WcUseCasesFlowProvider { + + private val _flow: Channel = Channel(Channel.BUFFERED) + override val flow = _flow.receiveAsFlow() + + override fun canHandle(methodName: String): Boolean { + return Name.entries.find { it.raw == methodName } != null + } + + override fun deserialize(methodName: String, params: String): WcEthMethod? { + val name = Name.entries.find { it.raw == methodName } ?: return null + return when (name) { + Name.Sign -> TODO() + Name.PersonalSign -> WcRequestHandler.fromJson(params, moshi) + Name.SignTypeData -> TODO() + Name.SignTypeDataV4 -> TODO() + Name.SignTransaction -> TODO() + Name.SendTransaction -> TODO() + } + } + + override fun handle(wcRequest: WcRequest) { + val useCase = when (wcRequest.method) { + is SignMessage -> EthPersonalSignUseCase(wcRequest as WcRequest, respondService) + } + _flow.trySend(useCase) + } + + enum class Name(val raw: String) { + Sign("eth_sign"), + PersonalSign("personal_sign"), + SignTypeData("eth_signTypedData"), + SignTypeDataV4("eth_signTypedData_v4"), + SignTransaction("eth_signTransaction"), + SendTransaction("eth_sendTransaction"), + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMethod.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMethod.kt new file mode 100644 index 0000000000..cf40981044 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMethod.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.walletconnect.usecase.ethereum + +import com.tangem.domain.walletconnect.model.WcMethod + +sealed interface WcEthMethod : WcMethod { + + data class SignMessage( + val raw: List, + ) : WcEthMethod +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt new file mode 100644 index 0000000000..71f465a43f --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.walletconnect.usecase.initialize + +interface WcInitializeUseCase { + fun init(projectId: String) +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt new file mode 100644 index 0000000000..1472db789e --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.walletconnect.usecase.pair + +import arrow.core.Either +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.WcSessionProposal + +sealed interface WcPairState { + data object Loading : WcPairState + data class Error(val throwable: Throwable) : WcPairState + data class Proposal( + val dAppSession: WcSessionProposal, + val securityStatus: Any, + ) : WcPairState + + sealed interface Approving : WcPairState { + val session: WcSessionProposal + + data class Loading(override val session: WcSessionProposal) : Approving + data class Result( + override val session: WcSessionProposal, + val result: Either, + ) : Approving + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt new file mode 100644 index 0000000000..4bf3e9b047 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.walletconnect.usecase.pair + +import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.domain.wallets.models.UserWallet +import kotlinx.coroutines.flow.Flow + +interface WcPairUseCase { + + fun pairFlow(uri: String, source: Source, selectedWallet: UserWallet): Flow + + // todo(wc) should map for all user wallets without this action in domain layer? + fun onWalletSelect(selectedWallet: UserWallet) + // todo(wc) accounts in future? + fun onAccountSelect(account: Any) + + fun approve(sessionForApprove: WcSessionProposal) + fun reject() + + enum class Source { QR, DEEPLINK, ETC } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index c8b22d2a48..cda72ef1f7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -270,6 +270,7 @@ include(":domain:qr-scanning:models") include(":domain:staking") include(":domain:staking:models") include(":domain:wallet-connect") +include(":domain:wallet-connect:models") include(":domain:markets") include(":domain:markets:models") include(":domain:manage-tokens") From 9957e986e1c534b7e62ede58c97801bab4e4f9e8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Mar 2025 14:01:44 +0200 Subject: [PATCH 037/138] Updated on 2026-08-14 --- .../markets/models/response/TokenMarketListResponse.kt | 8 ++++---- .../data/markets/converters/TokenMarketListConverter.kt | 6 +++--- .../kotlin/com/tangem/domain/markets/TokenQuotesShort.kt | 6 +++--- .../impl/model/converters/MarketsTokenItemConverter.kt | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt index 3dbf406f16..b2af512cda 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt @@ -20,7 +20,7 @@ data class TokenMarketListResponse( @Json(name = "name") val name: String, @Json(name = "symbol") val symbol: String, @Json(name = "current_price") val currentPrice: BigDecimal, - @Json(name = "price_change_percentage") val priceChangePercentage: PriceChangePercentage, + @Json(name = "price_change_percentage") val priceChangePercentage: PriceChangePercentage?, @Json(name = "market_rating") val marketRating: Int?, @Json(name = "market_cap") val marketCap: BigDecimal?, @Json(name = "is_under_market_cap_limit") val isUnderMarketCapLimit: Boolean?, @@ -28,9 +28,9 @@ data class TokenMarketListResponse( @JsonClass(generateAdapter = true) data class PriceChangePercentage( - @Json(name = "24h") val h24: BigDecimal, - @Json(name = "1w") val week1: BigDecimal, - @Json(name = "30d") val day30: BigDecimal, + @Json(name = "24h") val h24: BigDecimal?, + @Json(name = "1w") val week1: BigDecimal?, + @Json(name = "30d") val day30: BigDecimal?, ) } } \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt index 38d39f1843..2091fcf399 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -28,9 +28,9 @@ internal object TokenMarketListConverter : Converter tokenQuotesShort.weekChangePercent TrendInterval.M1 -> tokenQuotesShort.monthChangePercent } - val scaled = percent.setScale(4, RoundingMode.HALF_UP) + val scaled = percent?.setScale(4, RoundingMode.HALF_UP) return when { scaled == null -> PriceChangeType.NEUTRAL scaled > BigDecimal.ZERO -> PriceChangeType.UP From 53504be151723bf73e207fb07134cdfd2ff96058 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Mar 2025 15:16:11 +0000 Subject: [PATCH 038/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 58cb7cc642..fc082393ac 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "release-app_5.22-987" +tangemBlockchainSdk = "develop-989" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.22-440" +tangemCardSdk = "develop-444" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From a774347c704b8eeb4cb3bcce0956444a8578f946 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 26 Mar 2025 13:10:44 +0500 Subject: [PATCH 039/138] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../toggles/WalletConnectFeatureToggles.kt | 3 + .../tangem/tap/routing/utils/ChildFactory.kt | 40 ++- .../configs/feature_toggles_config.json | 4 + core/res/src/main/res/values/strings.xml | 8 + .../dropdownmenu/TangemDropdownItem.kt | 47 +++ .../dropdownmenu/TangemDropdownMenu.kt | 29 +- .../dropdownmenu/TangemDropdownMenuItem.kt | 11 + .../res/drawable/img_wallet_connect_76.xml | 15 + .../tokendetails/TokenDetailsPreviewData.kt | 3 +- .../state/TokenDetailsAppBarMenuConfig.kt | 12 +- .../TokenDetailsSkeletonStateConverter.kt | 11 +- .../state/factory/TokenDetailsStateFactory.kt | 5 +- .../ui/components/TokenDetailsTopAppBar.kt | 47 +-- features/walletconnect/api/.gitignore | 1 + features/walletconnect/api/build.gradle.kts | 18 ++ .../components/WalletConnectEntryComponent.kt | 8 + features/walletconnect/impl/.gitignore | 1 + features/walletconnect/impl/build.gradle.kts | 45 +++ .../components/ConnectionsComponent.kt | 24 ++ .../DefaultWalletConnectEntryComponent.kt | 58 ++++ .../connections/entity/WcConnectionsState.kt | 32 ++ .../connections/model/WcConnectionsModel.kt | 54 ++++ .../routes/ConnectionsInnerRoute.kt | 12 + .../connections/ui/WcConnectionsContent.kt | 299 ++++++++++++++++++ .../ui/preview/WcConnectionsPreviewData.kt | 141 +++++++++ .../di/WalletConnectFeatureModule.kt | 20 ++ .../di/WalletConnectModelModule.kt | 20 ++ settings.gradle.kts | 3 + 29 files changed, 887 insertions(+), 86 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownItem.kt rename features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt => core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt (90%) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenuItem.kt create mode 100644 core/ui/src/main/res/drawable/img_wallet_connect_76.xml create mode 100644 features/walletconnect/api/.gitignore create mode 100644 features/walletconnect/api/build.gradle.kts create mode 100644 features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectEntryComponent.kt create mode 100644 features/walletconnect/impl/.gitignore create mode 100644 features/walletconnect/impl/build.gradle.kts create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectionsState.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/ConnectionsInnerRoute.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectFeatureModule.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b7a7f549c4..3312184dff 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -198,6 +198,8 @@ dependencies { implementation(projects.features.biometry.impl) implementation(projects.features.nft.api) implementation(projects.features.nft.impl) + implementation(projects.features.walletconnect.api) + implementation(projects.features.walletconnect.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt index a6da04800a..e0f2e4a722 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt @@ -8,4 +8,7 @@ internal class WalletConnectFeatureToggles( val isSolanaTxSignEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "WC_SOLANA_TX_SIGN_ENABLED") + + val isRedesignedWalletConnectEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("WALLET_CONNECT_REDESIGN_ENABLED") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 2d9bd2853d..d4cd6dd17c 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -22,6 +22,7 @@ import com.tangem.features.swap.SwapComponent import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.wallet.WalletEntryComponent +import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent @@ -43,6 +44,7 @@ import com.tangem.utils.Provider import dagger.hilt.android.scopes.ActivityScoped import java.util.WeakHashMap import javax.inject.Inject +import com.tangem.features.walletconnect.components.WalletConnectEntryComponent as RedisegnedWalletConnectComponent @ActivityScoped @Suppress("LongParameterList", "LargeClass") @@ -78,8 +80,10 @@ internal class ChildFactory @Inject constructor( private val walletComponentFactory: WalletEntryComponent.Factory, private val sendComponentFactoryV2: com.tangem.features.send.v2.api.SendComponent.Factory, private val sendFeatureToggles: SendFeatureToggles, + private val redesignedWalletConnectComponentFactory: RedisegnedWalletConnectComponent.Factory, private val testerRouter: TesterRouter, private val routingFeatureToggles: RoutingFeatureToggles, + private val walletConnectFeatureToggles: WalletConnectFeatureToggles, ) { fun createChild(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child { @@ -296,11 +300,19 @@ internal class ChildFactory @Inject constructor( ) } is AppRoute.WalletConnectSessions -> { - createComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = Unit, - componentFactory = walletConnectComponentFactory, - ) + if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = Unit, + componentFactory = redesignedWalletConnectComponentFactory, + ) + } else { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = Unit, + componentFactory = walletConnectComponentFactory, + ) + } } is AppRoute.QrScanning -> { val source = when (route.source) { @@ -574,11 +586,19 @@ internal class ChildFactory @Inject constructor( ) } is AppRoute.WalletConnectSessions -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = Unit, - componentFactory = walletConnectComponentFactory, - ) + if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = Unit, + componentFactory = redesignedWalletConnectComponentFactory, + ) + } else { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = Unit, + componentFactory = walletConnectComponentFactory, + ) + } } is AppRoute.CurrencyDetails -> { route.asComponentChild( 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 6a2792b3a9..df8f975239 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 @@ -78,5 +78,9 @@ { "name": "SEND_V2_ENABLED", "version": "undefined" + }, + { + "name": "WALLET_CONNECT_REDESIGN_ENABLED", + "version": "undefined" } ] diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 6761a8065b..d9ab14d913 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -126,6 +126,7 @@ Delete Disabled + Disconnect Done Enable Enabled @@ -1213,6 +1214,13 @@ This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes. For testing purposes only Balance may be outdated. Refresh the page. + Connections + Disconnect all + Text about discnected all dApps + Disconect All dApps + New connection + Connect your wallet to a different dApps + No sessions Discard You have an interrupted backup. Do you want to resume? Yes, resume diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownItem.kt new file mode 100644 index 0000000000..b29403e17d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownItem.kt @@ -0,0 +1,47 @@ +package com.tangem.core.ui.components.dropdownmenu + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Suppress("ComposableEventParameterNaming") +@Composable +fun TangemDropdownItem(item: TangemDropdownMenuItem, dismissParent: () -> Unit, modifier: Modifier = Modifier) { + Text( + modifier = modifier + .clickable { + dismissParent() + item.onClick() + } + .padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16), + text = item.title.resolveReference(), + style = TangemTheme.typography.button.copy(color = item.textColorProvider()), + ) +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_TokenDetailsAppBarDropdownItem() { + TangemThemePreview { + TangemDropdownItem( + modifier = Modifier.background(TangemTheme.colors.background.primary), + dismissParent = {}, + item = TangemDropdownMenuItem( + title = TextReference.Res(id = R.string.token_details_hide_token), + textColorProvider = { TangemTheme.colors.text.warning }, + onClick = { }, + ), + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt similarity index 90% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt index fa6ff8aca3..6974bc2d7a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt @@ -1,11 +1,17 @@ @file:Suppress("TopLevelPropertyNaming") -package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components + +package com.tangem.core.ui.components.dropdownmenu import androidx.compose.animation.core.* -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DropdownMenu import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.TransformOrigin @@ -15,16 +21,19 @@ import androidx.compose.ui.unit.* import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupPositionProvider import androidx.compose.ui.window.PopupProperties +import com.tangem.core.ui.res.TangemTheme +import kotlin.math.max +import kotlin.math.min /** * Just copy paste [DropdownMenu] from material3 with deleting vertical paddings. */ @Composable -internal fun TangemDropdownMenu( +fun TangemDropdownMenu( expanded: Boolean, onDismissRequest: () -> Unit, modifier: Modifier = Modifier, - offset: DpOffset = DpOffset(0.dp, 0.dp), + offset: DpOffset = DpOffset(x = TangemTheme.dimens.spacing20, y = TangemTheme.dimens.spacing10.times(-1)), properties: PopupProperties = PopupProperties(focusable = true), content: @Composable ColumnScope.() -> Unit, ) { @@ -142,10 +151,7 @@ private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect) menuBounds.width == 0 -> 0f else -> { val intersectionCenter = - ( - kotlin.math.max(parentBounds.left, menuBounds.left) + - kotlin.math.min(parentBounds.right, menuBounds.right) - ) / 2 + (max(parentBounds.left, menuBounds.left) + min(parentBounds.right, menuBounds.right)) / 2 (intersectionCenter - menuBounds.left).toFloat() / menuBounds.width } } @@ -155,10 +161,7 @@ private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect) menuBounds.height == 0 -> 0f else -> { val intersectionCenter = - ( - kotlin.math.max(parentBounds.top, menuBounds.top) + - kotlin.math.min(parentBounds.bottom, menuBounds.bottom) - ) / 2 + (max(parentBounds.top, menuBounds.top) + min(parentBounds.bottom, menuBounds.bottom)) / 2 (intersectionCenter - menuBounds.top).toFloat() / menuBounds.height } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenuItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenuItem.kt new file mode 100644 index 0000000000..7d239636dd --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenuItem.kt @@ -0,0 +1,11 @@ +package com.tangem.core.ui.components.dropdownmenu + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.extensions.TextReference + +data class TangemDropdownMenuItem( + val title: TextReference, + val textColorProvider: @Composable () -> Color, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/img_wallet_connect_76.xml b/core/ui/src/main/res/drawable/img_wallet_connect_76.xml new file mode 100644 index 0000000000..a24b3060f6 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_wallet_connect_76.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 01d0aec7ba..7ce2014bc1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails import androidx.compose.ui.graphics.Color import androidx.paging.PagingData import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType @@ -27,7 +28,7 @@ internal object TokenDetailsPreviewData { onBackClick = {}, tokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig( persistentListOf( - TokenDetailsAppBarMenuConfig.MenuItem( + TangemDropdownMenuItem( title = TextReference.Res(id = R.string.token_details_hide_token), textColorProvider = { TangemTheme.colors.text.warning }, onClick = { }, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt index 4926ba0b4a..c5299ec1d5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt @@ -1,14 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import kotlinx.collections.immutable.ImmutableList -internal data class TokenDetailsAppBarMenuConfig(val items: ImmutableList) { - data class MenuItem( - val title: TextReference, - val textColorProvider: @Composable () -> Color, - val onClick: () -> Unit, - ) -} \ No newline at end of file +internal data class TokenDetailsAppBarMenuConfig(val items: ImmutableList) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index d4f23daa2d..124447b0a9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import arrow.core.getOrElse import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference @@ -14,9 +15,9 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton -import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.converter.Converter @@ -86,7 +87,7 @@ internal class TokenDetailsSkeletonStateConverter( private fun createMenu(cryptoCurrency: CryptoCurrency): TokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig( items = buildList { addGenerateXPubMenuItem(cryptoCurrency) - TokenDetailsAppBarMenuConfig.MenuItem( + TangemDropdownMenuItem( title = TextReference.Res(id = R.string.token_details_hide_token), textColorProvider = { TangemTheme.colors.text.warning }, onClick = clickIntents::onHideClick, @@ -94,9 +95,7 @@ internal class TokenDetailsSkeletonStateConverter( }.toImmutableList(), ) - private fun MutableList.addGenerateXPubMenuItem( - cryptoCurrency: CryptoCurrency, - ) { + private fun MutableList.addGenerateXPubMenuItem(cryptoCurrency: CryptoCurrency) { val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return val isBitcoin = isBitcoin(cryptoCurrency.network.id.value) @@ -107,7 +106,7 @@ internal class TokenDetailsSkeletonStateConverter( if (isBitcoin && hasDerivations) { add( - TokenDetailsAppBarMenuConfig.MenuItem( + TangemDropdownMenuItem( title = resourceReference(R.string.token_details_generate_xpub), textColorProvider = { TangemTheme.colors.text.primary1 }, onClick = clickIntents::onGenerateExtendedKey, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index d360a8883f..b69153ce42 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference @@ -376,13 +377,13 @@ internal class TokenDetailsStateFactory( return copy( items = buildList { if (isSupported && hasDerivations) { - TokenDetailsAppBarMenuConfig.MenuItem( + TangemDropdownMenuItem( title = resourceReference(R.string.token_details_generate_xpub), textColorProvider = { TangemTheme.colors.text.primary1 }, onClick = clickIntents::onGenerateExtendedKey, ).let(::add) } - TokenDetailsAppBarMenuConfig.MenuItem( + TangemDropdownMenuItem( title = TextReference.Res(id = R.string.token_details_hide_token), textColorProvider = { TangemTheme.colors.text.warning }, onClick = clickIntents::onHideClick, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt index 3ea60b1140..648e7aee48 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt @@ -3,8 +3,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.padding import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -14,14 +12,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.util.fastForEach -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig import com.tangem.features.tokendetails.impl.R @@ -58,10 +54,9 @@ internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) { expanded = showDropdownMenu, modifier = Modifier.background(TangemTheme.colors.background.primary), onDismissRequest = { showDropdownMenu = false }, - offset = DpOffset(x = TangemTheme.dimens.spacing20, y = TangemTheme.dimens.spacing10.times(-1)), content = { config.tokenDetailsAppBarMenuConfig?.items?.fastForEach { - AppBarDropdownItem( + TangemDropdownItem( item = it, dismissParent = { showDropdownMenu = false }, ) @@ -78,42 +73,6 @@ internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) { ) } -@Suppress("ComposableEventParameterNaming") -@Composable -private fun AppBarDropdownItem( - item: TokenDetailsAppBarMenuConfig.MenuItem, - dismissParent: () -> Unit, - modifier: Modifier = Modifier, -) { - Text( - modifier = modifier - .clickable { - dismissParent() - item.onClick() - } - .padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16), - text = item.title.resolveReference(), - style = TangemTheme.typography.body1.copy(color = item.textColorProvider()), - ) -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_TokenDetailsAppBarDropdownItem() { - TangemThemePreview { - AppBarDropdownItem( - modifier = Modifier.background(TangemTheme.colors.background.primary), - dismissParent = {}, - item = TokenDetailsAppBarMenuConfig.MenuItem( - title = TextReference.Res(id = R.string.token_details_hide_token), - textColorProvider = { TangemTheme.colors.text.warning }, - onClick = { }, - ), - ) - } -} - @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/features/walletconnect/api/.gitignore b/features/walletconnect/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/walletconnect/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/walletconnect/api/build.gradle.kts b/features/walletconnect/api/build.gradle.kts new file mode 100644 index 0000000000..9eea21d4c0 --- /dev/null +++ b/features/walletconnect/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.walletconnect.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Domain models */ + implementation(projects.domain.wallets.models) +} \ No newline at end of file diff --git a/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectEntryComponent.kt b/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectEntryComponent.kt new file mode 100644 index 0000000000..c7d60d1091 --- /dev/null +++ b/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectEntryComponent.kt @@ -0,0 +1,8 @@ +package com.tangem.features.walletconnect.components + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface WalletConnectEntryComponent : ComposableContentComponent { + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/walletconnect/impl/.gitignore b/features/walletconnect/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/walletconnect/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts new file mode 100644 index 0000000000..f3822c54db --- /dev/null +++ b/features/walletconnect/impl/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.walletconnect.impl" +} + +dependencies { + implementation(projects.features.walletconnect.api) + + /** Core */ + implementation(projects.core.configToggles) + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Domain models */ + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + /** AndroidX */ + implementation(deps.androidx.activity.compose) + + /** Compose */ + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.coil) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + + /** Other */ + implementation(deps.decompose.ext.compose) + implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt new file mode 100644 index 0000000000..09c4b03ff5 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt @@ -0,0 +1,24 @@ +package com.tangem.features.walletconnect.connections.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.walletconnect.connections.model.WcConnectionsModel +import com.tangem.features.walletconnect.connections.ui.WcConnectionsContent + +internal class ConnectionsComponent( + appComponentContext: AppComponentContext, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: WcConnectionsModel = getOrCreateModel() + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + WcConnectionsContent(modifier = modifier, state = state) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt new file mode 100644 index 0000000000..3f3ee812d0 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt @@ -0,0 +1,58 @@ +package com.tangem.features.walletconnect.connections.components + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.walletconnect.components.WalletConnectEntryComponent +import com.tangem.features.walletconnect.connections.routes.ConnectionsInnerRoute +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("UnusedPrivateMember") +internal class DefaultWalletConnectEntryComponent @AssistedInject constructor( + @Assisted private val appComponentContext: AppComponentContext, + @Assisted params: Unit, +) : AppComponentContext by appComponentContext, WalletConnectEntryComponent { + + private val contentNavigation = StackNavigation() + private val contentStack = childStack( + source = contentNavigation, + serializer = ConnectionsInnerRoute.serializer(), + initialConfiguration = ConnectionsInnerRoute.Connections, + childFactory = ::screenChild, + ) + + @Composable + override fun Content(modifier: Modifier) { + val childStack by contentStack.subscribeAsState() + + BackHandler(onBack = router::pop) + Children(stack = childStack, animation = stackAnimation()) { child -> + child.instance.Content(modifier = modifier) + } + } + + private fun screenChild( + config: ConnectionsInnerRoute, + componentContext: ComponentContext, + ): ComposableContentComponent = when (config) { + ConnectionsInnerRoute.Connections -> ConnectionsComponent(childByContext(componentContext)) + ConnectionsInnerRoute.QrScan -> TODO("[REDACTED_JIRA]") + } + + @AssistedFactory + interface Factory : WalletConnectEntryComponent.Factory { + override fun create(context: AppComponentContext, params: Unit): DefaultWalletConnectEntryComponent + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectionsState.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectionsState.kt new file mode 100644 index 0000000000..62474e35b9 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectionsState.kt @@ -0,0 +1,32 @@ +package com.tangem.features.walletconnect.connections.entity + +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import kotlinx.collections.immutable.ImmutableList + +internal data class WcConnectionsState( + val topAppBarConfig: WcConnectionsTopAppBarConfig, + val connections: ImmutableList, + val onNewConnectionClick: () -> Unit, +) + +internal data class WcConnectionsTopAppBarConfig( + val startButtonUM: TopAppBarButtonUM, + val endActions: ImmutableList, +) + +internal data class WcConnectionsUM( + val userWalletId: String, + val walletName: String, + val connectedApps: ImmutableList, +) + +internal data class WcConnectedAppInfo( + val name: String, + val iconUrl: String, + val subtitle: TextReference = stringReference("Connected App"), + val isVerified: Boolean = false, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt new file mode 100644 index 0000000000..0d6c65c6d2 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -0,0 +1,54 @@ +package com.tangem.features.walletconnect.connections.model + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.features.walletconnect.connections.entity.WcConnectionsState +import com.tangem.features.walletconnect.connections.ui.preview.WcConnectionsPreviewData +import com.tangem.features.walletconnect.impl.R +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@ModelScoped +internal class WcConnectionsModel @Inject constructor( + private val router: Router, + private val uiMessageSender: UiMessageSender, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + val uiState: StateFlow = MutableStateFlow( + WcConnectionsPreviewData.stateForModel( + onBackClicked = router::pop, + openQrScanScreen = ::openQrScanScreen, + disconnectAll = ::disconnectAll, + ), + ).asStateFlow() + + private fun openQrScanScreen() { + // TODO("[REDACTED_JIRA]") + } + + private fun disconnectAll() { + val message = DialogMessage( + title = resourceReference(R.string.wc_disconnect_all_alert_title), + message = resourceReference(R.string.wc_disconnect_all_alert_desc), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_disconnect), + onClick = { + // TODO(wc): disconnect all + }, + ) + }, + secondActionBuilder = { cancelAction() }, + ) + uiMessageSender.send(message) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/ConnectionsInnerRoute.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/ConnectionsInnerRoute.kt new file mode 100644 index 0000000000..a07346591c --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/ConnectionsInnerRoute.kt @@ -0,0 +1,12 @@ +package com.tangem.features.walletconnect.connections.routes + +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class ConnectionsInnerRoute { + @Serializable + data object Connections : ConnectionsInnerRoute() + + @Serializable + data object QrScan : ConnectionsInnerRoute() +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt new file mode 100644 index 0000000000..005ff5c7a4 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt @@ -0,0 +1,299 @@ +package com.tangem.features.walletconnect.connections.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import coil.compose.AsyncImage +import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.walletconnect.connections.entity.WcConnectedAppInfo +import com.tangem.features.walletconnect.connections.entity.WcConnectionsState +import com.tangem.features.walletconnect.connections.entity.WcConnectionsTopAppBarConfig +import com.tangem.features.walletconnect.connections.entity.WcConnectionsUM +import com.tangem.features.walletconnect.connections.ui.preview.WcConnectionsPreviewData +import com.tangem.features.walletconnect.impl.R +import kotlinx.collections.immutable.ImmutableList + +@Composable +internal fun WcConnectionsContent(state: WcConnectionsState, modifier: Modifier = Modifier) { + Scaffold( + modifier = modifier, + containerColor = TangemTheme.colors.background.secondary, + topBar = { + ConnectionsTopBar( + modifier = Modifier.statusBarsPadding(), + config = state.topAppBarConfig, + showEndButton = state.connections.isNotEmpty(), + ) + }, + floatingActionButton = { + if (state.connections.isNotEmpty()) { + NewConnectionButton( + onClick = state.onNewConnectionClick, + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + ) + } + }, + floatingActionButtonPosition = FabPosition.Center, + content = { innerPadding -> + Content( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + connections = state.connections, + onNewConnectionClick = state.onNewConnectionClick, + ) + }, + ) +} + +@Composable +private fun Content( + connections: ImmutableList, + onNewConnectionClick: () -> Unit, + modifier: Modifier = Modifier, +) { + if (connections.isEmpty()) { + EmptyConnectionsBlock(modifier = modifier, onNewConnectionClick = onNewConnectionClick) + } else { + ConnectionsBlock(modifier = modifier, connections = connections) + } +} + +@Composable +private fun EmptyConnectionsBlock(onNewConnectionClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(R.drawable.img_wallet_connect_76), + contentDescription = "Wallet Connect", + modifier = Modifier.size(76.dp), + ) + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing24), + text = stringResourceSafe(R.string.wc_no_sessions_title), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h3, + ) + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + text = stringResourceSafe(R.string.wc_no_sessions_desc), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body1, + ) + NewConnectionButton( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing48), + onClick = onNewConnectionClick, + ) + } +} + +@Composable +private fun ConnectionsBlock(connections: ImmutableList, modifier: Modifier = Modifier) { + Box(modifier = modifier) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing76 + bottomBarHeight, + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { + items( + items = connections, + key = { it.userWalletId }, + itemContent = { + ConnectionItem( + modifier = Modifier + .fillMaxWidth() + .background( + color = TangemTheme.colors.background.primary, + shape = RoundedCornerShape(TangemTheme.dimens.radius14), + ) + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)), + connection = it, + ) + }, + ) + } + BottomFade(modifier = Modifier.align(Alignment.BottomCenter)) + } +} + +@Composable +private fun ConnectionItem(connection: WcConnectionsUM, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + key("${connection.userWalletId}_${connection.walletName}") { + Text( + modifier = Modifier.padding(top = 12.dp, bottom = 4.dp, start = 12.dp, end = 12.dp), + text = connection.walletName, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + ) + } + connection.connectedApps.fastForEach { appInfo -> + key("${connection.userWalletId}_${connection.walletName}_${appInfo.name}_${appInfo.iconUrl}") { + AppInfoItem( + appInfo = appInfo, + modifier = Modifier + .clickable(onClick = appInfo.onClick) + .padding(all = 12.dp), + ) + } + } + } +} + +@Composable +private fun AppInfoItem(appInfo: WcConnectedAppInfo, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + AsyncImage( + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(TangemTheme.dimens.radius8)), + model = appInfo.iconUrl, + contentDescription = appInfo.name, + ) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + Text( + text = appInfo.name, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + ) + Text( + text = appInfo.subtitle.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ConnectionsTopBar( + config: WcConnectionsTopAppBarConfig, + showEndButton: Boolean, + modifier: Modifier = Modifier, +) { + var showDropdownMenu by rememberSaveable { mutableStateOf(false) } + // TangemTopAppBar is not friendly with dropdown menu + TopAppBar( + modifier = modifier, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = TangemTheme.colors.background.secondary, + titleContentColor = TangemTheme.colors.icon.primary1, + actionIconContentColor = TangemTheme.colors.icon.primary1, + ), + navigationIcon = { + IconButton(onClick = config.startButtonUM.onIconClicked) { + Icon( + painter = painterResource(id = config.startButtonUM.iconRes), + tint = TangemTheme.colors.icon.primary1, + contentDescription = "Back", + ) + } + }, + title = { + Text( + text = stringResourceSafe(R.string.wc_connections), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + actions = { + AnimatedVisibility(showEndButton) { + IconButton(onClick = { showDropdownMenu = true }) { + Icon( + painter = painterResource(id = R.drawable.ic_more_vertical_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = "More", + ) + } + } + + TangemDropdownMenu( + expanded = showDropdownMenu, + modifier = Modifier.background(TangemTheme.colors.background.primary), + onDismissRequest = { showDropdownMenu = false }, + content = { + config.endActions.fastForEach { + TangemDropdownItem( + item = it, + dismissParent = { showDropdownMenu = false }, + ) + } + }, + ) + }, + ) +} + +@Composable +private fun NewConnectionButton(onClick: () -> Unit, modifier: Modifier = Modifier) { + PrimaryButton( + modifier = modifier, + text = stringResourceSafe(R.string.wc_new_connection), + onClick = onClick, + ) +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WcConnectionsContentPreview( + @PreviewParameter(WcConnectionsStateProvider::class) params: WcConnectionsState, +) { + TangemThemePreview { + WcConnectionsContent(state = params) + } +} + +private class WcConnectionsStateProvider : CollectionPreviewParameterProvider( + listOf( + WcConnectionsPreviewData.stateWithEmptyConnections, + WcConnectionsPreviewData.fullState, + ), +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt new file mode 100644 index 0000000000..57ce635a39 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt @@ -0,0 +1,141 @@ +package com.tangem.features.walletconnect.connections.ui.preview + +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.walletconnect.connections.entity.WcConnectedAppInfo +import com.tangem.features.walletconnect.connections.entity.WcConnectionsState +import com.tangem.features.walletconnect.connections.entity.WcConnectionsTopAppBarConfig +import com.tangem.features.walletconnect.connections.entity.WcConnectionsUM +import com.tangem.features.walletconnect.impl.R +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import timber.log.Timber + +internal object WcConnectionsPreviewData { + private const val BASE_URL = "https://raw.githubusercontent.com/TaranVH/LOGOS/refs/heads/master/Crypto" + private val connections = persistentListOf( + WcConnectionsUM( + userWalletId = "wallet_id_1", + walletName = "Wallet 2.0", + connectedApps = persistentListOf( + WcConnectedAppInfo( + name = "Moralis", + iconUrl = "$BASE_URL/satoshi.png", + isVerified = true, + onClick = { Timber.d("Moralis clicked") }, + ), + WcConnectedAppInfo( + name = "website.com", + iconUrl = "$BASE_URL/doge_again_lol.jpg", + onClick = { Timber.d("website.com clicked") }, + ), + ), + ), + WcConnectionsUM( + userWalletId = "wallet_id_2", + walletName = "Bitcoin", + connectedApps = persistentListOf( + WcConnectedAppInfo( + name = "React app", + iconUrl = "$BASE_URL/LiteCoin.png", + isVerified = true, + onClick = { Timber.d("React app clicked") }, + ), + ), + ), + WcConnectionsUM( + userWalletId = "wallet_id_3", + walletName = "Wallet 2.0", + connectedApps = persistentListOf( + WcConnectedAppInfo( + name = "Moralis", + iconUrl = "$BASE_URL/satoshi.png", + isVerified = true, + onClick = { Timber.d("Moralis clicked") }, + ), + WcConnectedAppInfo( + name = "website.com", + iconUrl = "$BASE_URL/doge_again_lol.jpg", + onClick = { Timber.d("website.com clicked") }, + ), + ), + ), + WcConnectionsUM( + userWalletId = "wallet_id_4", + walletName = "Bitcoin", + connectedApps = persistentListOf( + WcConnectedAppInfo( + name = "React app", + iconUrl = "$BASE_URL/LiteCoin.png", + isVerified = true, + onClick = { Timber.d("React app clicked") }, + ), + ), + ), + WcConnectionsUM( + userWalletId = "wallet_id_5", + walletName = "Wallet 2.0", + connectedApps = persistentListOf( + WcConnectedAppInfo( + name = "Moralis", + iconUrl = "$BASE_URL/satoshi.png", + isVerified = true, + onClick = { Timber.d("Moralis clicked") }, + ), + WcConnectedAppInfo( + name = "website.com", + iconUrl = "$BASE_URL/doge_again_lol.jpg", + onClick = { Timber.d("website.com clicked") }, + ), + ), + ), + WcConnectionsUM( + userWalletId = "wallet_id_6", + walletName = "Bitcoin", + connectedApps = persistentListOf( + WcConnectedAppInfo( + name = "React app", + iconUrl = "$BASE_URL/LiteCoin.png", + isVerified = true, + onClick = { Timber.d("React app clicked") }, + ), + ), + ), + ) + val stateWithEmptyConnections = WcConnectionsState( + topAppBarConfig = WcConnectionsTopAppBarConfig( + startButtonUM = TopAppBarButtonUM( + iconRes = R.drawable.ic_back_24, + onIconClicked = {}, + enabled = true, + ), + endActions = persistentListOf( + TangemDropdownMenuItem( + title = resourceReference(R.string.wc_disconnect_all), + textColorProvider = { TangemTheme.colors.text.warning }, + onClick = { }, + ), + ), + ), + connections = persistentListOf(), + onNewConnectionClick = {}, + ) + val fullState = stateWithEmptyConnections.copy(connections = connections) + fun stateForModel( + onBackClicked: () -> Unit, + openQrScanScreen: () -> Unit, + disconnectAll: () -> Unit, + ): WcConnectionsState { + return fullState.copy( + topAppBarConfig = fullState.topAppBarConfig.copy( + startButtonUM = fullState.topAppBarConfig.startButtonUM.copy(onIconClicked = onBackClicked), + endActions = fullState.topAppBarConfig.endActions + .map { it.copy(onClick = disconnectAll) } + .toImmutableList(), + ), + onNewConnectionClick = openQrScanScreen, + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectFeatureModule.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectFeatureModule.kt new file mode 100644 index 0000000000..e8a98073d4 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectFeatureModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.walletconnect.di + +import com.tangem.features.walletconnect.components.WalletConnectEntryComponent +import com.tangem.features.walletconnect.connections.components.DefaultWalletConnectEntryComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface WalletConnectFeatureModule { + + @Binds + @Singleton + fun bindWalletConnectEntryComponentFactory( + factory: DefaultWalletConnectEntryComponent.Factory, + ): WalletConnectEntryComponent.Factory +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt new file mode 100644 index 0000000000..9b8d6d4f4f --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.walletconnect.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.walletconnect.connections.model.WcConnectionsModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface WalletConnectModelModule { + + @Binds + @IntoMap + @ClassKey(WcConnectionsModel::class) + fun bindWcConnectionsModel(model: WcConnectionsModel): Model +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index cda72ef1f7..0cf1c30675 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -235,6 +235,9 @@ include(":features:biometry:impl") include(":features:nft:api") include(":features:nft:impl") + +include(":features:walletconnect:api") +include(":features:walletconnect:impl") // endregion Feature modules // region Domain modules From a71e17561fdf0616d68658af41cf1f24bbb6f0be Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 24 Mar 2025 22:18:50 +0300 Subject: [PATCH 040/138] Updated on 2026-08-14 --- .../datasource/local/logs/AppLogsStore.kt | 60 ++++++++++++++++--- .../feedback/DefaultFeedbackRepository.kt | 2 + .../feedback/SendFeedbackEmailUseCase.kt | 2 +- .../feedback/repository/FeedbackRepository.kt | 2 + 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt index ac6d80ef03..60e30ca5e7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt @@ -4,18 +4,15 @@ import android.content.Context import com.tangem.datasource.BuildConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.joda.time.DateTime import org.joda.time.format.DateTimeFormatterBuilder import timber.log.Timber -import java.io.BufferedWriter -import java.io.File -import java.io.FileWriter +import java.io.* +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream import javax.inject.Inject import javax.inject.Singleton @@ -30,7 +27,7 @@ import javax.inject.Singleton @Singleton class AppLogsStore @Inject constructor( @ApplicationContext private val applicationContext: Context, - dispatchers: CoroutineDispatcherProvider, + private val dispatchers: CoroutineDispatcherProvider, ) { private val scope = CoroutineScope( @@ -38,8 +35,10 @@ class AppLogsStore @Inject constructor( CoroutineExceptionHandler { _, error -> Timber.e("AppLogsStore.scope is failed $error") }, ) private val mutex = Mutex() + private val zipMutex = Mutex() - private val file = File(applicationContext.filesDir, NEW_LOG_FILE_NAME) + private val file = File(applicationContext.filesDir, PERMITTED_FILE_NAME) + private val fileZip = File(applicationContext.filesDir, PERMITTED_FILE_NAME_ZIP) private val formatter = DateTimeFormatterBuilder() .appendDayOfMonth(2) @@ -58,6 +57,16 @@ class AppLogsStore @Inject constructor( /** Get log file */ fun getFile(): File? = if (file.exists()) file else null + suspend fun getZipFile(): File? { + return zipMutex.withLock { + if (file.exists()) { + zip(listOf(file), fileZip) + } else { + null + } + } + } + /** Save log [message] */ fun saveLogMessage(tag: String, message: String) { // Temporally logs are not saved in prod environment @@ -133,8 +142,41 @@ class AppLogsStore @Inject constructor( } } + @Suppress("NestedBlockDepth") + private suspend fun zip(filesToCompress: List, outputZipFile: File): File? { + return withContext(dispatchers.io) { + if (outputZipFile.exists() && !outputZipFile.delete()) { + return@withContext null + } + + val buffer = ByteArray(BUFFER_SIZE) + + FileOutputStream(outputZipFile).use { fos -> + ZipOutputStream(fos).use { zos -> + filesToCompress.forEach { file -> + FileInputStream(file).use { inStream -> + val ze = ZipEntry(file.name) + zos.putNextEntry(ze) + var len: Int + while (inStream.read(buffer).also { len = it } > 0) { + zos.write(buffer, 0, len) + } + } + } + zos.finish() // Ensures the zip output is finalized + } + } + outputZipFile + } + } + private companion object { + const val BUFFER_SIZE = 1024 + const val LOG_FILE_NAME = "logs.txt" const val NEW_LOG_FILE_NAME = "app_logs.txt" + // the only name that we allow to send as email to company addresses + const val PERMITTED_FILE_NAME = "log.txt" + const val PERMITTED_FILE_NAME_ZIP = "log.zip" } } \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index 17f8dca358..028f1697e1 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -94,6 +94,8 @@ internal class DefaultFeedbackRepository( override fun getLogFile(): File? = appLogsStore.getFile() + override suspend fun getZipLogFile(): File? = appLogsStore.getZipFile() + override fun sendEmail(feedbackEmail: FeedbackEmail) { emailSender.send( EmailSender.Email( diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt index 570f0f9b56..7bb6e53a9b 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt @@ -31,7 +31,7 @@ class SendFeedbackEmailUseCase( subject = emailSubjectResolver.resolve(type), message = createMessage(type), // Temporally user data is not sent - file = null, // feedbackRepository.getLogFile(), + file = null, // feedbackRepository.getZipLogFile(), ) feedbackRepository.sendEmail(email) diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt index ae50655518..802c36dc7d 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt @@ -27,5 +27,7 @@ interface FeedbackRepository { fun getLogFile(): File? + suspend fun getZipLogFile(): File? + fun sendEmail(feedbackEmail: FeedbackEmail) } \ No newline at end of file From 86cd2e3e5380f4fff1bdc3ab4482449077acc1e2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Mar 2025 20:25:03 +0300 Subject: [PATCH 041/138] Updated on 2026-08-14 --- .../wallet/domain/Wallet2CobrandImage.kt | 6 ++++++ .../res/drawable/ill_keiro_card2_120_106.webp | Bin 0 -> 31224 bytes .../res/drawable/ill_keiro_card3_120_106.webp | Bin 0 -> 43620 bytes 3 files changed, 6 insertions(+) create mode 100644 features/wallet/impl/src/main/res/drawable/ill_keiro_card2_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_keiro_card3_120_106.webp diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt index 67d80b72bb..eecab67075 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -169,6 +169,12 @@ internal enum class Wallet2CobrandImage( batchIds = setOf("AF95"), ), + Keiro( + cards2ResId = R.drawable.ill_keiro_card2_120_106, + cards3ResId = R.drawable.ill_keiro_card3_120_106, + batchIds = setOf("BB000017"), + ), + KishuInu( cards2ResId = R.drawable.ill_kishu_inu_card2_120_106, cards3ResId = R.drawable.ill_kishu_inu_card3_120_106, diff --git a/features/wallet/impl/src/main/res/drawable/ill_keiro_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_keiro_card2_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..58ddd441a0377676fd8405d48899006459b3dde1 GIT binary patch literal 31224 zcmV)GK)%0HNk&HSc>n-cMM6+kP&il$0000G0002r0RX1~06|PpNaF?o01X^9Z5uEx zf8_g)h@=GyA5j7Tz-%B*oQ8FqG>w{6%d_u(wr%5U-#u--Y`n~`ZQC~5CYf2In9lqn zf`QvcirVA5-ZJlgK$03sf+V?T&;Ng1;js5j($WhmA|OGM8@E}|s{#Zn_aT5Q{)N{* z|MB^c&wqUWjJ=RaOuV3?fYIAueG|NimS=RYJ=spS-FVaM|d^1l!Oz^Zwx z@W(R`eu60Qe{3#ai{eE+bFEQG$aiA2Q z%ph9i^>hfa zY37pQa=K;3Znq|9U}D_pfb$1bqUE(p&wW3k=+O4P{kI-~F>&ROnxof4O)9Sjm0*6x_^qar; z7lF5-MXeeJ{h-E=X2F%U`kO?pm5TK+>^2KYIy;@x!p6^&aT{7TD@`xzROK10MsM)? z$%v)i6c$fBoO&09gdv{@elEKnZ}$ItHF~+U78L&g+N>z#6vOZnuk4f^d?3G3ojZnv`s}<%Jb~w0@bJxXoGj z-j>XHZOM(p*wu$ah@qNCvW`VS$&CW9Q3n5{x4U5>%_i;N7v1{l+|%-!{x&ZSYQ0qoLP?GM9sj;4*@}N&xhcA z(3*C)?|M2KAV3=2^ewEny%`{gMJw!e`XU0P!o2nkrnL;O)}mI=Ya-$qh3meLU!M{U zx8I5UDgtCg?@O%Cl&rc7;+Cx-KvHzDc*Bai8!c(M`X~eBMg1RywfeYd6g;QrwG;@5 zja-JUX;&pXa^iBCfy6g~4L@flR}B_~ewqJgwGK_Xgwok++cpCcQLY7mIDvWK{_N(fK)3Af1pw6^*wX;zvgea5 zAcF(!f+!cz z?pO6Ue2H#&9WKUnX?8$!ynt)L>o^-ged{U`-M)s5HypkvR9?$E-7wytTGI{B1`xcE z_!8eR-v2EBjpmrc7j&I9JIJhWN_@EC8%A_t;nlT0K3Y6CtZyY(NB1;4fB>(nua5m5 zzJ19HrkgO{6u?W9M7J+#zJGP;BJK`m2UzvhwMpgGJ&jGbuMxbk_?C9~_MP92x~B{F zPnE}Xe<`QDx;DPE2C*J$CLiMOxe?)05w(LIQ-E{5)Lw(y_j;r1o> z;B>d_Er;*7c731QbG`$v^V`CuPWO1h{__xCvZ}K$kHWWa#TRtL!Asf!8cgNsOVTB4 z3xh6+E-bv$vgqD;i|)%~xLBd9ds66n0*~Cn^@VGio@U9Hb1*_aeh6|@W zx|q3OWWo;J#P;^B@>6yCRsrnXuzm5mLk?hFOn2-MR&4;*EtV%tm$u-`)v0^W7M$)- z>Z@y;9Z-4hyB)ffyEX0AJxT8m%EJ4r%iYn%3|=Q&aN0|$?{fnbzZ!LIYuXF#A*DMP zx?z|_w=OEb8+C1biEm{VFC4nl*4!_0f1jIIo^^eL_qc^KtG!N)x36)!xMPIZUeEh3 z#pTsK#oZa3n28tNaUQ;jcTBek_{O_CcENNp!P~bYta}3dolSSByw63kgh__&x8}JqVQ~S63zwkff|7L!%dPV)u{DAlm{Wtul z?T@=B{jb{JcYoFY=KBMGJpUj6!`jc)m;A4&-|lDcpSNG`f5SKSfB8SUKLY>JKgECO z_5lC!?WOz!h`;>b_HW5Qd*3zv_x!(x|Brnq`2X$Sw;$uZyT8@)8R`Gif6f0d>VNVN zw%?ro>HD|++xLI&m-T)V{L}wu)Fbl$>L2QVz<%xji1m%z$8taH`i1-7_Ye4g<3DzP z#(H;ux&Cw3PtU)?f8GB%`D6A4{HOXy`w#9vkl!r-%JFOa|NMX4e}G@+pWJ`y|Aqf= z`)%~;{ty0-^*`(XbUobtOaF`g55Q;hkM2M6zr}x^|Lgv%|NqxF|4(#(y+8c@(?4v# z{Fm}$yls-&>60DfY?jwdnC}~8wz_1;c-tkl(`J^HDw>CHEr6TCwS?0KMaVy*?hgoEC1 z@$$=KjN;rKVM%a&w1(=Y>KQM-X?AvV*v$Lz6Pr+hIyksNq<#?Bp08rB^mkR#fG(|` z#~>5fnYa@&eq0$s%{pl&pViKEd+lpazE!o+p!i+Y4W%=W-`CV1`8_ zbmbp`k-95n$G6=s*m1yA@;au59*G3q>OaooHi`cWm|o74Jln<^;`NV#-wZNycZq~> ze=0&M6JBklc?u<|Ar-Wy!DA!r82JH&BM=3f&YEVH_^!KwQ}$-j23Q0KyCjB*dfaOQ0z%CV+RcC$*%YPN~Ck`9!glT5Jz5VPEc`XuQOi+7(Un{pwCy$}`CmXXzOmw{>&w^rVs z{d0u_8=-68cKlqqZE(1`e?ENTFUZM7j7m1lH8CTb)cU>&0uqUFw(K@?KfNj+J95c|AHzg>OUfjzZt(Yow0nU_zHqej-o)hz zF@f|@_BvydQdf1uc}lj)#+fROBnr*3@#P4}0Kj~RVdxkB@l}1(CLPsD*AkjAFF0LE zJ2YMK+f%!&E1iDH12(Bu(|uP_Z}z!5s!kczoKJx(Sss-UM#km;5or}y#}CWs7{e!Z53tQyuwfw9t*O99!W{<%&P%)}@h=If z39gpMJ?xZ^a4bop>Jx~ z=ii&{~P5aUY*^1lAN~IiB-qk zU9=nSPRHJ5sa_cPpjPFNEi(BQ)S=na)M1U2aiE3&Rzi;!)c-Sfe^YI{ABt7bQ_J_U z($rr=EtBF9P7(5$1eccQLSsQca=VXo$&Q-C=S?v*`lMykMMb`wFUHa97yTSvDV2-G z?vUqv`OznZq|Ql1?`LzbAkjEXP^E%&eAsGJQia;=#-@Hzn6ypWg8IjH0$6YkKpSw5 zz3}`_$cq!7^&$IRFPN8eyT}sG#wD@g$&%lD9MNe1a-0Y}FLI}ZOumf&&$HtWvVF_< z6~@&5(?B8?$p!93T*_%>)X8d}{R8xXn_avIN2I$dFlO*Rl0X?l#&@9Vqcx%~7UcaT zJl;nx?n(vgsLl(RMJT?e@Z0=PwG!v0Xh2W0K6~aMXB>FLjzlJqtXq=$e3^EpK})yd z(c_TVP@-na;GcBKl8e~^g0i3rZ0FR|w|ibX!9V!CQ&hv-TXYX!lv(;e+Q;GD_?YmJ zGU6+vfgy5lJ3dfj1b&3&w=MM{TU+Ew<9ef5WY0Qm`vzN=G(^`pYy(&giM>s3ZytK? zNVX5fm+t;LiYWHNn7%*|PirRBa#@U)`qO0ojxKF|(X)5o`Jt?1Q`S}F~eOa)J6diFBcvo`;dR0PFY`ykT)w!hEQ1zuk<@4TN zYord`U??Cx;;L3)6Oj_@asvtMm6nStI*o~?v=;I)(kX5?NX59sr-0{bW+#c_a$t-+ zO~Bl742cMYc&U*5_2&!IoT!S%fc9nd4uLnx1;h(N;NeLceV>{z6}d_LxoCM<;jO_D zvY5HPMh=AwFbpL?6&Bu`X(xIioZPhbZ3E)Q!04AwxmASj+JG&o0K$tIWPm)PD@_Rv zB>nI}ie{)EzoH(dEc8*yq^x`kt8{7W;=v#qj%mC8(tQs>F$yE6aw*~LC^{;E2Da%m z>+Cg{<@>u0+U{Au7%Z+9{VF?+uJF;P3o5b6UE9L|lhTsaa?NyZAw}b#z0;&zUfL0D zi9dbIpX@mFp_yE&b1^X#+R@D5W95cD<@u$=?5jCni{QFZsHZ7yPZHvmt zjLVb1OslzF%7FwFT}v7s=+(k9{>+ zL?yt{p_JpA$n|PwF?jYJ*IAgpa!lZQQ)2&ft4piO?kH1u(Y{g;1v@FA0RI2sg}?v+ z0000000000001NXrix#o6e{716!lx8!QOOOK^=5H72`uM5C16%uSL6LmNr`ixQfdE z+%)`#zjkvLo3gzGx|)Q}w$e0grao}fv7DuA11WZe+3sJgRg_dG8gNY{b-Myjp*3gf zKOe)d$Zc+MczFlREj!ENr*gPN=nVTPB~(SxT$EPSXjcd9lw$rhHUtc|*vvzlpMHv= zpA6k<8ry|7?U(}wTEgC9c#;7$zJ)9Z8tPyE{9L^1Wl!2e@ee;t2M*F2l%D)sPLZ2b zdMxk;5A>&Yn2r}`W4U~u+j9_g${)U=7%;U`KqZZej=Z|{T3f&TSIV>N)$S@BB5;n2 z0;%c+ABg$_ME{+$$wQR-7N+l6i+Ll~s5hoiy+H5WJsv_YB>UccSvsGjnfyrEIlj*kN79?{+pq?%jdzHwINf4Q#<`K z91t9lV86u#Pg^kYb-F{Xg}OW2la#vY*vj4kg=r-Ty_XmzqC*x$q19N}lUUEO8+9bEHe3IsW&GIm&y{L7Lt9ujSKUMSMtLyauFhY))W z_CCS>RUCv&Nnd<1IT3$mB2|Wll;%8X&gxKc@b$&)wkv7GG#q2I@%y+lf9@E^AE=7F zLdJpKK07)Dcpm$|E>_2>)O^M6Tr<9RH<%H+I{f?h))nd@_%qIwf?^1T;%-uBE-J+& zL|;f^EEwrjvhH~A;h_2c1Tj8uEMz2Bn|?bj<=TU@(y;;6`YhC2hZkk@J5|gw@~oN0 zDPxRby`n58d0AA11A>{v%u6C+JQ)ky+2OQVo zn!|aF_kHKyev{KCtkwqk0=s9ldjRVAk z;r=|tA$*kK)`}ma0ltNWo??zQyAH1Csg!Np50Fey|N|t9%fFeH+XByFI*vJ_jA9_YPkkp zb{W}srx@7L#fdxdOrZbzDS9wO_s|7E*)Upst?ZHR0 zz_3v1oDzD6+V=4KC>n2%C2o0NH=%fZ>#s?%7xMGL!z7EdM}iD;P8E>SOImmmAF&=+ znh@5e=sF>rsfP^5Qj%5F*m_3?^WiF6cW+}_Hf6Doxurza{Qnu|BgaHjyic z6(2bgDD+q$=RQPgR6*y}Y(5m*k%1x;aF9!V@wPq_FpJBKQ0`&mN*|rN!_0V%(%gU< zlr5Vk(;eP5Jh$I2dc;KsK!1l!Q%qH+PE|juA`wQgCj4vP!1qQYOCr2v{of4q-w5CD zxqcBg4~gflj<}81FRWD5!S!DaxDfhRO?eaP++g!1JqDtZ@j}vMocgpHrs{uVCi8rx z6_9c>CfX3!xKF|i1J=>Xcaek;t5=#Nxw~Q^QIIu2biP#dVyedSsK>uZN(DD2Z3Ac| zqV|{&D1aLqBE}!d!KBSI-w5%FiPE|rfkO;Fr-~C;J|ieM6R%#vldZ@gri=~B%f^f& zbrVNJ<*}H_ak#~yph>x2nW3GKlKK>BwSox;1Qh(7-OG=mM5@@doQ|cK*V^L;_(qfM zK?*`ifi8YYe{j|7p=vKIKwKof<~Y?qNS@~tlTERwS)WE z0Wx|jWNS050O(g{YA?l%&y)L9wVs#Dm7HD3jfe%y2pDR)6d^8#Qu@KjLb_Va9IB>% zNX!mfFLT1vg=c&_aGlP0Q8y#g3M+GkN(fRx+2Y`z;Ln|0539Sxc|we2c?G{_*k=9G zi)nr=G8_yQTI`!h>xq)s5#q9It~0sRfZABJOg+*n3xb%c^5q20X-;MHB<3!7e=(O!#_>oN5QUx*OdY z!DjWND=z$Jl`#yv!qC@UwDo-&#xh&;Xkq3^G7Zw&^Bz}7vgCntBgoy*6i~Tu6pQOO z2g}pa1Axq_6P^dP!HTb>;;w=5Ir2(PvRwGxZIX7`+D7MIzhd^W-l%q@wPMz?W|NO# z#Pv6KhWhZ_Ia4={&$%h0c@Wr4MoyNY9{ry8g01eIu7+gVp6{f~2DNx&6L$vMW^qNs z`l|FMyRnv98+dJLt56FaHLTMiGX_D}ZMq8%$D)fC&nAH}8go((JlqUR8~UP9BzXnU znQ79N?eV-xb^)OZk&J)+fI^J=73KpB`lCa(e?RfKL4nJDjXM`SAO_D4!)1z)->(}_ z7T!ePGNzIt+^Vy)W!CCm?+(?aD+Om@<%qsD3y>l-h)*e-DA|Y2n`5SX)Gva7|L4f* zeJSA&qyQ%3-N(=)F-A=7Pbe{g?RSS~#_E|@fpO8^7a0%#zhlRha>-^=e|=1@&b|^r zOFqLbWGKBiJC9%4{2|a1bglCCa7!Qxqqci(1 zQeNF`wLbJc)YspuaaLRW{6d^?pPWX;xo}%=bp)(9%vQAq^rNBee^?RRqF=V*4DR7! z1hJNNM;;Ly?^+}yc-oJrTu{xU=?$o`JK1P@;!{qm+UT&@J&>yB8BGOiDxJfdt&Rw9 z241;c`Fr{sn$%-AYo!sTNG~?cbnI@{y!j?U-a;e|)1a2~7q+jV_0ZK~K}>{pGPl8y z2nwIL&-?pyzn{1;2Z;h+r?y@&5N?QWL`W552v2L~r6zxZ-|il>mKgqA)L_LeWf_9X z6LaMf3Qyx(uP}=&)@I@U=HL9?*$Msp2Jinu`O92p0%1F81pbonjj)MKtTpsoue7Vf zcL*0dD^CmZl7Uz!kz#1nDgqicqkHGUpOIgT+Ot=Yo=5aBx@;K)`MT#q+ZRX0tUipE z9@AK~Bc*v_*C`WECq?HzQ+Z0o{93J#Fk;pkrH7@JP)A^&_M3@wc%a>E?nh@X3mukog;wF_Oi%U`rAEmCsrqur`|8KnJoD<=WwaSsId83xzyj*tN~(m z06Sr2Iwf$xI?ryV?cukas3kvSxN)_*mgLF!Al=UHrw&!egIe`ipvd%E6fX2Pq?aa&`UW&@42HPT95}VyP;*i@i$*W5Bq8-Nd6JZ|}+m0}#nPKm&*b z@k;s4W|L*e;u|wBd$U0hJfk@QS%Ve1ZXyialOO8Fmc3W@fZM3}E=7+6<(fCP5}JIa z$*1x&=H})k1LET>87%+CD*o2xm&dD}U`*>=HjK$C!b9d_7B|co*cgtz=*5pszjKmqX+cSJ+wK{v{?gx2JO(Kd8W_JUOxgc!Oi;=+a)1|3 z%Q&X=rzht)?2TPpTvU3o4fvCASQRvze$k?q|jdS{?wv2orzVBmBMB8@vI zfXguN%LEtK|F4v~B{h(Y8+2c5mmQ<}Noos3YXCovTYCX>Z2tteiu{*>tEdJ1s#arP z6aVklf;a*E%nk8w^j@l-1rq2;=lO(sCJi`h=PBqhK9adc&#Nj`V7pH!ghgAN!#qVM zhWieVD4JO})V4iR@EJ*(fsS15LJ@{IJbFeizaQtdig_bmnRC_p4Eakl8M(d(Vl7rH z5lZ8mUhGY8%+^jdbFZkDQ!V&uk!a~vPQiv@7rD>}N2?_t4p|bsc07SfHyj@&Gmi}n zpMP8N22C_3*{;pkrSXMVitm;go;4tOhk&N1!YNGd(%F&z@7p|r{pFa(eAeazCCSrJS&$PV+IC&q z&Eir320$|NOS-mDj1Vdh1KIyuWWy*b+eQ%jZ~(58HR{NofWIE7u78ZQ2uSr$p-LOK z=ofc&b}iws44aQmOW1V!WpO8lmsvW^N?Vv0m7AB9r;J+EZq4;{t`^cnvTi!UXU g&V&?UM4-TC#RDC;lTbsl!cQ9b4#ZQ(3#yJr%yntX{lC;C4y*!AtS|&M25><|rY0Ti{}w#fz6gphc^HS*p27++2(MkU&b) zKil8HXkZ0qi3lxT8CQ21IJLB{<7?|cj5Eg)TiCT_^jMRE`gam4ONL$|@&r+b?IlOLHJ)2Dku>>CyZ4FrPW?bgfAMb2HA1maf_bid0u2$H-(E^ty6! zc*x!61H;<~V!WK1#%E9qOJ2qw_3Y-s;V!EV&3I)W z&1!`aglMh9%tNxxTdgw-jo4MWa`a z7T3a1iqt<__(fuZ`+*r6LORTB{J0*_Lw4G3JlOqlb}vSy6_?n7eWmPDsUdlY6K4A# z3~JcL@1+*sd%o{jx=wHBAD;XLPLOGm(wkjXG0=MQ+(BX1m^#d=B?)tcK24A)8VwWx zEor1Ch-?+q{e2A$loZqrT~@h{S@fK2!-+UWpZM%e^TsKg;#1BN z+Q9KD3SOjTv3bOSlyd8H8ffiFq`-e=ag8c|6AcNz=i8de`7EKhEHh7W=7SjsPT%0V z77uJZ3D?b0AA(sDQ2IQu+;g7GK$JThvZ@x?vKHAlML){s#VJ4syIKzNh{!w; z-V!GfV3!molld+S@B6QVBmi*Snh-Km+-gnbAS5yJyw$CQP!-r`_Tuck)tLvsLCfGu z1K#Hbx*tRE6;HC8+?~hN)IY1E=OVmOxd!$TfK?>G5Unx*FfU+EfEwG3Ku<9}Bf{8M zM8dICYu3F*7yB=C@pcufqb$WSMH>No7J_j|e&q5ZFp zbGRD7<(u-mjJ&4wv$M@`W8;ku|6*a)he`Tlqm}br4VOud&M#+2xNTegZQ+QT_zovQ z@x&ohrcn})=Ax>V6wqD~Q$E^?u!pMELoGJAWE^nc?id9HEAa{O>ge+(U>6tP&+Be8 z_0R}Dhx`Bs4+btgPbRyEC__?!kpkC0i{sY4i5N-Fc2<($1a>u9kNLy8ojs0xz@ZK3 zUbUKQ@4_Am)eetdk$iTHT@2WX5-2}=ugs=q)-6X%sv0DhqG&bJoz`(5RC8ezE{#H>$C_;_a+P!K)P z2yW|78F{k@Dkme54UV8Y68-6FR0}8H5mpF*I7B`~<$}nsQ9d@ny3@?zM2((kIA`nB z!bU(Wa4-Siv-7wGn#vW8>hzvd(ch2tgHxkFGVu0$@<#u)l3Ceg@f?6dxz8IqbAM^y zVf@!To^@r2R7-prN=~G~uM;XV#fz$~awRISA^055(Mnw}4(Cv#jc8JQIX3$E-{)0)7uJGaWvCZcZElkdH*-1vwPcSL zm@g#HN*D$^D+{rpP)-ak!r|~m{(j8uB)%O0w-95otJ3Fq029X*KsC0;h0|a3SGg+! ze&b;GotByCha|d~Jt>f4Y7oKnaPt!FQmZN#S|#WbkJD2-d|)Zghu&mk_4mJqC!rGG zkS1OyL6Q9$i3ONHGhl~|Axrx`G5Cj!az>QVTRd6)eoyAiAKaNaZDIN=mFI3LCM72q zbgyOhD{aaO_BKJx{sMS+c_JbrmptHNV;rO7{?Gl z=`1^S^RlKc^M5daUzz&qEEI|3_Twq>%Gh?w5OJpGJaWJpfC#60>rWH*B+x-Nv)rcc z|JlFUdKgOLq`_Os?BOvrIs%r1K$#D699>C;c@Mwq%Rw2vx>64N7tv*tW>Z8V@QJ2{ zx8mma47K{h4{GaQ=SYpE?)T06iB-S_Btw#LdLA^s3eFdCM;nIGa z=&rvxztnXA9Ec=ez<9pu_R+1}_=@8u!Ix?R{0y{Sz9Dvi2&{GS#tH|fSo-M-_OcSD z_F#Q@=C**)pBg33e>KuOR%{CV5OtX+a`?&4f%Qxnt_31rFHWAI;-WL*g5hrg(ZIlxtEu+ zZpPrlEBj$cEOff9{&R&i^M>o|-Np>_5-brqV7Bv9Kn(vb8BQsMca14r%|xOipqWI4 zyJH(AxI@M6s*s>V-A_%I$2U3z`JK67^$2_Z9#6t4Hb9LyQ=41cP~}M z@GLa!S|2-7nlVOH8RW-=XTVUFxmCi{oSi>lb&DHsvv%rlc~?Rke4s?oti?*dC`NTt;R?y;eo{5N&{bUgoi zqc2ULcMLICxy0po-kXCbHlKj}j2}vkqXE042DZCI|Cj|`| zqM|FWa5!-u$(tO3+sW1QkiufF4fG27=yFG{s#r8qaP>m%+soi$6ScTY6RI;J#=g*1 zbXQhW5+`T!B;-Mn?JI0M>f*y=w+2FgN+W|ujCSQmrkn8vn$!uEPWQXwAz!+aP*L85 z&xBG8i2hU}d@Z9dAD_@a$ZfGDpqK4HAPI+~B{LHC{^xQ9>6;uM)x`QX-E#$yJ#hkP z)rbJ7zXq30`os4#w1Y%lbEqaxLqf4j{Tk#k1?bxi6#8C0IMwH~1RERz!75v~FTqs5 z9Q4nH6awNUbLTP{plr`V_S$l<8lO$pa9G;Wxxgx|>VN#+?f0A$n{JJgv1n5QFd0Qm zrsE3j>=H=NQv>~0R#KVr47M%t?6&cr!LD67pOHMl=>%NCA!I|ioHgB_@bGL2yTz$~$$g=v+4#k2INZ&i!N zyo$9aCR*6@VVe*^!}izublzFNM!JyI)eso#BR&FXV4EFtoU;H!C4qs)1DB4RcRAmU<{Gks?Aa3!9;de74Ln%>ZfhR|BWI2&gloYDP;ZwD>EYG zEfi2{yf7a;)k8W0!wi6CeL+;UP zdt}6S%`gGaA7hri+B4!C!dRM=7r>aG{EFSw8JQ(ni55-Vd}|`l`s*2=f=K|8UCfZ) z4nbx@4bsC^6CVlGcxZuqs6l>??0#-=H@rO-!~o|<=JmNawenQMQ82-1k)l1`aaemo zE@|_Ro$?<{`6gi@rhboT?vQH0NHq)Jnj!ky8ne!Sknz?RyDO>(DA;RYXjZz3+qW(Q zN{N{aU}OC;*%uoA{I80hX^{xd6FO-)yhEab90n}4tnVJ|PgvcxrE=%nv;gYA&xCA% zSOa+>sKrP8wBX)ok4Xhwt8W^=#y4GEsfj$c5PQ{PUsfa)I2V}G4e0P;M*GkgN535E zDrW_JC;o8?w6eg*QL%21aV>7l6glx#3khG^khZ@&O~*C*C2|rv6V+LA_*Q$Vav|2R zyEyO=nOkg`YfuVlj;w6gKjDzMvsBFfm}e^)=aM!DS{p_9l_D*K>^ildFZHlB>#QA> zAf30<+8=qjFzacmW%$uY6)lIhS7FXeY4V%8$vm6rto;k)PKC?h_M{i2f+x)F4dhnP z31I)lRekc^$vOt)TGjfadS~Blh;aMUwzC6X*&~{>rQY{4>=# zMk$OU&yGb|BBgI`o%35h?0s)KTB}aSSV&ueq_jRi2%JsN*v>)s3e+L%30iy6?ptq* zwzG-t`wRG*e7b#%jqL9%l|-g_qJ1~R;~wo&usp`mW_ zb_xUW96XjThib>7B2XL)6G?5>81)#d9hv4E4lgxb?aRgpm5>vP3Px&54tXwsxL0`n zDq28BCW{Ux>6Nz);1f9U*eR{)&QBQ1IVE@gH~cAvpkmAXYm=|x8)BzV4luwVs9$?= zo~K0&A0f#&&v5fyRvh*Ez!8^R!8XhzP}3yb688o6XIAmkLNdE(k00cA|9xGL;h_lSR%e_+FNvYG`=aYfVdA z`20Kqtf*tKJUo2^w@=hb`9|3^ODAvaT^!yRPzKT~8s)B1faFThvQZ2kL@PsIkXPkk z6Gi%QA_G!GYGB8^Xn5aPmkyYr*TV(>oa{~R;ZkG>#5&wn7m}H!o`_0D3@NV zV|fIBS^Vs#nSKD52^IxDgc~z*Z5|B?jv&X5df5!eAqz%AkglqD9C~F$XFA+xBk~i&$YOP(;L~QO zANS9VL1W5^?PBw(F6vcqqD|bZmFYCLiAu1gho`cQ#D*Fi?cf*La$?pGEUvHiz=V5omESjag#0sI^~&8I=bQDE>+IPLxtJd|#);pH zHxh>VbOABls;hb8p7${xV2A!J`F0POgs!zSh)2l^dYJT|1n=i zxiwxmiaW0lniniQ+X*yXidWcuefL~5F-+#a21axu^<95FpO5JuE`cqIjqI(r?kpB# z^^3TZgbpqS5DepXJuXlS8DSmF^Ew(DEoI3XqFS!yjUr7hkME*;R_)O22dZ9;5H7$hA z5dX(XOS9Y^5IyYA8?)m=i9VyKn%DG`T=>Lb(#aq>PmG*%K}YI!coCR8<8tp`>hYZ` zGNPlsnX{MjR<9yFwHPN%K2(?zW!~Ii0Pb5iBIH=HK*G;7km%n_ID|RFE#TzYoE77W zN+t*3oCV;i!5%K~yo>8ve7>)riy4k~Ul=T%<(Br}V-bcYLz2?H?ayM2spiN7D0v9j zf5+;KdcO14Crx^fHI{rXva)6<`T1OSxo<`Li)Nrj`wk;*CeQo32wMl{P-EPs%mV+D zDo>A^9Wm+4#s9M}yhVZ<6%R?eIR;f?YK~3zyQU|#VTX~>ev=AZzGZ(1YkhgI^}-p`@|JaQ3KCnLz(lBh=$G^%lXu!a zV8g5xGIk|iFT5*)O7!Hgh@Oea=BWk!Gmpi_4gO`ps{S>bR_XNvUhI{MI&JBSOmL#5 zQ(?#Ngk^`77n-Aw*-WLd&kiqgO$Y}kn;Z#<5Tdx@C!e(perDmVNWu7!#Y$!Dz<XiP+FO$RiA^B;d%HBaKGP1Zw%RFxj?-#l0&#)Od#<~EXq!>8(pfdz$+xQLokOK1g zVp?mRBmCr}OuCNhNKzP#N7FOOVa!Z8WwheMfDUnziE7Yc#+nX7$1UGAaR{}X86c`e zJp^-YGhwP^mHPt+*87S>uL-!FDJ8t)2OgWLbOkTZcWYE~D^JTZ&2L=dAkaJVAMj4p z>GdP@sytZjpFGSoq#Ca?YU55)?o%PLHQ{zh$h%C}XLQjIK4Is>XDdCq#bMYjp!n7- zaK4DPW(-|C_kIlG)^cXcK_DIa@|AHP)^S+8}G%erfydpJAs0 zJJ1A=Q+J{pO~o8rqU9R${a zr6ud~0iDSM!dxlSw3UHp(gNy-{x6cB9eiU2r`cnvn(&C}6m0K~Uokv1pL*9!LG556 zMekt-7efu4o<*b@wd^a;mNu=j?Z+{O$M*#9c+^|7HR~gJD>6MB|Gn^xp$OqBoblq~ ze6JVw9UP?mhv?)aQAnd^c!;jqbB61k7T!Z>(867%wW}>R4vYirt1SS$;Pu19fw0pU zX5uLdb>3^m?EHAXz0@t%tO2i#%bnOn#9o8o=IJb~7dSJ1;i?7-=ij58+Vp+QbgVLH zx8*0Ahfgp|%ha6um5`3oPbiRJ_jYVG-*gf=Rzub1)qrdO$pWF_O6s<=PpqJA%1lX5 z^C?Qa!W_^HT62*o(+}Ln}4QR|w1G-hEJEC)wu=ZTL2l!u64yN~` zvM|xB?6|DNg98R9HUh;H_tedaNv42+brV0VM{3T%(<+c9de22@afj4A*R}}#t+#ES z=uutrb3FG+It>DN2pvw^T?a7h!cDOaMP1Vte=bQvg%EK4AiHx?(`Dt$RGJVkmJ8NsiPF&N$Du=qHkSuVHte7@?@nbk=~%(bno#yAJCCj zljFi^^(c)=1HM5r98_7})$deSPJ7gkqxW5alkGhwoVA1_*!h(k=aM(yev%`_Rw3@k z{Ca=ae+5>ylgtw{RVsB?f)oOm=rvi-g+jo5WeiE?1S;oh>p(ZkJlVP9E_M{NXfC-moBI+ieZAM^Rcg<6iOIHk@#FnP7+aBaNt$+W?<> zB^D^U_K!4zJX{LqI7>%{JL>$PZ-H!LhY^^htLFqg)xGzq6bg&NoQAj+7}cMxgtK*w zZk{kMjQzJ<=wrk~(sV$f7o*#;&C6Sy%Q7_`|OiHWx?b+gV+Xm18w4Pmo6gpAF0 zH|8ILPHeTFJ0z`tDdt$&)`VjhG6?j2Z~}WpJ!Ks2WM@l8Q3I~?5Lo2$(v%+s+&g3` zHtFm+{aFmT0}*xIXqtvBy9Y==0Pv6+0k=6w#>!+&>omDDw_4JvEcMBbV;X({%|p;D zw~147{y^$AL{tsoN&R2C;ry#pjHG|DLs`}`k+#H!%Zwsj>?KE`gRc4rrVF&IU3z;S zF+Eb|G|bm`LdYn{sK6!XKvamwq_um~A3Uzr%E1}b#2^5sdjBhjydr@}qVtfenr72> zKnNn|Qw~VwK=#uKUas7a`30^y-XAH=-*1#t6Qbh`k?m~%LJhb&2P?WGMig{C|6 zUNYH?$9aKb9{MKhei8h3(UeqK*d;cxP6W||Z)S%S0Yrd-P8$wwd}%#mT%-2+ley34 znS?_9C(3@JcY6W{Zwc=pZyEtT@A_>{GlreCor49#x#_eHl7N zUOd86$`Ng2UxuAZzrDYaf=a@V*7J)}8xV*II|QOQU!~erTlyYW?3`4iyWh^ztB6^!E?G7Ny7gRb#Yc`F1Plt zAeg2tH5uvEELki)Ofbw>RpzQ?~$@aDM{ zZ!*DB>FwR|h!r6pR?7W$Gb!t3D&;Jt>ri{$3&@1c+Ds zo?zQo(T;px*NjoDWLPw99)IERm0y3dxw&dXw}Ozp#CI625dg-P%HNLg$hsl%)Mg7l zU)LV5ZkP`&BG;=j>_u%j*A7USXYJ@T6&N2}dt%xX3jc?$afJ1w!$ubo^SG#DA$M`_ z5KtN5Uah{0YO!TcT8p3Sut#_F9;Q9s0C%uP`58pOSwriY3wLK1oxAezdI}HEph}q> zJD*0S=IBx&PJx{Ed)l?Iry@QmX+ldcNW^qw9xs{RCKXnBp_p z`&c)t(H5inebBjEQ?%U^YFKj`JQwgz=Qe&?Qgv3db|cvM1SQ=o=Kc@e1&;${Fb%M! z@Qv1NZt^~3kQ8TLozsjpn>@oU8oHBKP`FU#-5ZMp$mS)V>PZ>76}3Fq8NS4)=+)e# znrMc>Y5u3)6&eW@X=V|c`1PH%A334IYBtUHUHD@f{n#gsKyWF#*&|40t1Qe*>tA1M zlnQ){pWcKJYlK`voy=ANA@6>iS7-Ufd501XDzO}He+SR4H*{yZG!)iH+vYRdd&m0o z&VJo0N{Vz3u6RmNuwT0;p=%1fn_4+JPjAvKfNn#}`2-PWkpxqEz=Lrw=o!0W=!ZWK zQJ%)*CE$?xQPPQrz)}wn;IttdDQP|ubU%WtO>h|ClDY?QZS#%{QRCYeQ ze`FUi*WJ$U>uTVx%_+U0hB|}eN*>KWxU7?WOvx#ce7Hp0Kd9VV3|e06B_?oQ*f;AT zTFQ73pjBD^(VZ81)+GWq=fJ+6keWZfmm-9UqaY2jm%&W9XE4!jlr1%~kv`1ZL4w@Y z8*~$qdfc5%h0!G;1ivL$>o>Z`7@#X>2NPb=W1Ry4nLn+Qlg`G8#U|^sVcD-%--f$b z_74;e#I-@kXd^$L6N!JwDvHFGWi8`SP07_il4nX9SU_j-2OH&%Zw*F-Jlr!RTFS60 z#<+%ZL2ULUwNV4Hq2>#?&st|qj>y}&@BzY<0n%UDS_vg!Hid8#m@8*bZpYXBz?M5t zu8M)VZzJY5@i|=FjPDdF&YP9KC)C`H`F%bq9EX>P)eVjfLw2w7q$t9&)Tz6g^jey^ zn5v3LFcq5_F~>()clFh=`Bv!LuE;#w@-n+y<@zf1v!VHN-gT(KOrfHsBu z8@pzf6>*p_vS=TfyHYltvHxQEP&M+RCfFPKoE{Q4xsiZP=badX+;;>IJaK=41ACV2 zMC7RgPC8QDN{HJI%cJj3xFv85zn@)Iz!aOZ2aueuyJLF@G>$SZ4|#}_`_95~_OV(ls2ziVm?~)+Nxme_rSK zhCHg^%@jM+6nkPdca9pOic_b@FX36ZnXIdTzT+bCvndXnR;q$J7lhUkR(#g02(m&% z#k~jDa{W#G(hh7)ZFpKB@lYwVwh+gju{xs+f?;Um>IJGGSEKaCH6F&f6d1ng_PPEG z*<~U$(9z?YK6a^E%$Ws7@(#IUkpDB^OAF|690uosdMcy0jD|OplaTyNV0qn3D`Vg z3{$-6c=BhGeHgDub3Qxo;{ylqjfuSL_lbWP z%le;BVC&J=r=bfEyF@(0(>!wmy#3APwoGZk+b(ufR(t6=_$YuZ9wYdbXhN|3p*+ zQK&|qx}O23XW0Y~YPvdr*Q#RltqJp$yd8d~Fo{zY^>6j%#8f^ESSg&f){c?iXx(u*4#PXAfNd#1UcdN^D z^jCG(0t=JvG|EvYQ8dzqGcO;u%ZB$MCn)TT=<-v>>;qv#XK_Di4`hDYz=YI!KACle z=QMJ=zlDR+nqAeIh_>qs=jQ=lJ3b0zQlsw+hb6(!cuDs{UuO#7%~A7?02Mmw@L4f3 z{Ob0s|4pb=tsCbFI&HscG&zyOJAIbAKDTu)xbHZ})ht3vB>W=u1sjXjosqRT`YsZ6 z)O2&=dAYs2Z!KUnJiJu{O3c~K$qjj{?Q4o+jh<_L7u^{y-Pa^jjxQyli-FYcug=-n zqU(>nEN$F75eooehN*a38AjkRU@;4Tci%bpr0MacX_r{F|6ktZGL{}}v_s>?IO{qB zxM$fPgUHL>YJO&zw=qDz07vu2-EvL;n(F&&7jr{>enh2e7NcMT`P2CL8m+$Hj z-;@}{#APh!7x^v#jsCg37W<3b-s>785!#zle!^}FE>l!1h1mpFO{_zJfN9YHT_;Xc z$koCIG`fUc|6I(_zpkgFi!)Eoze&a)gtrHXDekhp=oKTYMJN(O1Px+U%E?6Dch79w zzwmE7Gw04nHL98wP1@}jYO7K)bILoCBaiLQcfbcuntPavXvBr%POv0DVo(>k4X`T< z%6+9QVNqh{p-#?VN375fL2#qq%KyM-5rqUfIeoV+E`y2nA*FYEF&aKw#`~Z4!jG(H z8~pG+{kM=?w4feDx?KWkwDgoojQww_*d}pF<+<~+OquAVa-=Sj0Xr1q1s64@_90Zp7@TvJQTdTsTR9sGqnI1Y(>=vv zbLk^jx^@Emc8mr!b6mQUXOdY#zbcP3@#zO7ds$%J-?>RmyT)cgGl-68HVkcGH%NG% z4GC>wlbkw=CB&r&Avld|w#D2Pef;Wd$zc|m_<%CP^cA_?0t219k3W>^QifYw-`QyW zvd;4*AsNlNy24B$I)B`YAD|6#U14iAvUaxy{WqFl9Sx&mH- z+KWOUNx)~x3(wE(#|0Zx(&oX$)7hNNEjQTk7yw$Tex5F%LobjcuZdt~A;A3jDder* z6vyQ+^!5o_l(jafbwmX!xB^@}y>N5oFoK>J;{_(UIFS!fn4P#9nb-g{Havc=JRD^M+X+N055ujP{|m2G^5w4G+1i#3M0(++=m8Ro zG#x>h8n{%zM-#8T4B|nI*RBLjgs4?!7f3QE4<+AMEOtw&Sg~bEy0AgAakA#^^bL2j zXL~9Im|IC-%UUx03NQLY-pS>W(R1ob3@kZIB?GPsF&sW!>`RpqFNvUhOTkcJUZ9HB zta$Y0xYNGCFCBo$upqDmE;&g@qorI6TmT(!(JQ;{7&mE_39TSeR}h~Xz;-Q?un8y_ zS0cBx*OQ-)_&ArL1Kc!@gZTxM1%?GNU6Xe;-qwF{+ z7D17%(zMr=*;Vo3AgMsUm=Zyt%X35!J5bGM?Pf`XGrEP}MX1f%CGJwa7q0wT8b)kB zFi^BD6QHY@Gw&H79fUVZXeyS*A4Idmo5`?+_=E0Q-nmY(HL0!GCZ)c>eHo|l9@C{1_d8|%8Apvjtwhfq6p(jIkPa}I0CmN8dXi<2 zh(&Laq33|_VCLm}QtIldEi`$bnd1Np_&MdbH-Jsw>^%z3Tr3_Hp~>f>vufE44#EMK zJffHxc|BSK8QGSp+Jf|dKSJ{DMqCmHwRol4(SsOf$LC$(QDFMcOL@gmau|t)>^Pmx zNOGD{ssg^mKTxAezL%A9Kx3$h6-6t)Vfkw2GimJ^j#cJCn`J$|571vRv46OsoGz8B z!9v~l;;_s+fM~l4ubBkuY`x^q)WMgf$dln2WS(ZbG=oWq# zHd+jH*qwH$X)_?4H2!oO9-`O{+gowvnisBo#Ca}D!9@7~8>bgv~rCgOQB5PcHH>!M0P4!*l zT508B1X(0n;AjT1r@$3}5S~Gmfz_ofVoA2%p4 zPJIZhpwPKi-y?GFlEimCcOjr~OsKN(RQ_c7@E!0ZiZzm3Z^WuO@>dC33MI*u_iamX z&=exKF>8NS%QPlI{}IH#=KU7*&$U-%BfC+(1A15q3%x>Q`eClqke*@B1*yu-=ry*x zPkk{J7Z*@&u^S+YFpsP$817hV0H=w!K|7|drW37EiRn+2KOz2POs&R_B462v{TYn& zYXLS}+rcsJUbthRWWpCnWk;3By~RKOKGE0X_kRLhk6g=B+Y~>QtSCK=Wo3y$8{?lB z=F|f-${^JL0Z-mtX{6lSFW7qAjryuwVuJRlIS$Bb~m7LhZpzBuGNZ zN>#_VxX7pfdjF)N03N5X^;^!TeP3lKC#>;+IXF%9Smcldy*lLX6VypVG36R#wX{lY zOHT`%EfF#@h*LXnBd2I$R=Q?ENd-=6z34cc=U)Y&JP@O;K0?Yo?w)9xJMhL69{_ea z)R*{!<|tp{T;C8IuT~z14ka*;q&upSSr;loTz^EHDh`@lX0Sjyhm0a5NDMcP+9L$jf-GTy*sTvJ#uN!V;3h*28uXn4jTyl`}a#SX&V>Zbl(HB zwRQp9utiqTwShg@pC2WC?v$!(W)5W}$O`nze0ati-YDyYh%aN65E>Ym7%qhLJBI9n zRcX|3*gCcVmBJw`PGX4;Oj@1YV`XG*t5ZXEbgS|OzT;S|rf6sugPu&dgH%MBHvWk6 zNrq>T_Fq5!bJXYJ6=%qiE^b?hYR-N6Curm1&B`7HkYfdCIi53|e7UFl`E0-EU^ZrK zGJ2L2>!Jm7>}iZJ>{;BkJLpi*6C=s{>oVHfG+De*VQ2rOIG7-!41^22RrWVvO^W(~ zv^X2Q0_Ird=Cs(BP_((icNJe32wxENWycaQ10Z{Yj?W{dCYA`bDx&5_A9Re}eFuWy!^$tuu-c0ype_ZrnnGkj?{No=PSNk9as472 zJ>Gz(oukZlyqAdiYSRU9&T*YYGFN*bZth1voPmwXv(|{dfGe)&zSdHv)(j2d&~Bs5 zZUdd4U?ec0QZ}vb54eZ?iY)5ro!jF3P+q2z!;{wN{e zKyv4Jf)OvImltIg7Ph#+mpR_3p9Ap}S3zdtp6E^Ny3(!fkQ{mTF$M^?-k>~>h$qb$ z;vPTNB)Q5V<{eyIb6?tXT&+oaK`kGm!Yz4@1=R@S;1YMHe&|V z=&DL2gBr%i!3&BSu(e|OvyU&m@>D@_^yeiLMA9RNhcCdbBhU0T1bIXshWYO-3Y;w7 zJ&TwI3hs(?rMEASCnVF$L3NH-kdIDr5+f5cm_rrQDopTMss`KXwARZ+7FZBGJZ*pd z2@$wK%Le3l&*vw*HK=hA%}xce$1UIUr`kMt5La1f7v_F87xc*hHXPPU_ANIz89pv+vkBN>lhghD zWPO4L$PC!_2{e314TsX$$Sa?=h4I>fX&VL~X#l<@y}`+}6o=?H&4-&(TozKX@V*vN}hHP%4Ks_wYbKnM+2t2Dd)*E!BJUr;Qu9N^Hl1szt1Gbgm z;e+MeyRS#%I5{22GAlK}cETLEot#*-HMM1(HB9JXw4x;$GmHCmkfR~Q<}MZ`BH+aV z2A(7(p4q+}WnW_Cu55KCNE-WZgDg%4qcidIWY21+>@97DI7x`n^o+wT&)0cy9_2&V z4^?1Rao?+%hI?%WK&9yjd5zgt{ztRcGEN&OLK}uFqYvSMLRR&zd-6<&qG=Kc)#E&3 zK4(NR`e6=^=JZbiLf$2!$7usz_Ge~_j?S<@Tb(3KIxO(;5CWEhmAwFi&$tWX58Z7d zgOcFQEm_n29cyQik1N^_vJ`F+-$4jHk@4j9|W$%N(K^GsOwyQnJ7RS!I z3D+8{TOFCtQ9+1)AKeWq&4M5R&>{E+_Tl^sm8tL)HU%$|`X}<}V+}%_FkO0V?y&2o z7J^NV;P7mXZgyb1$p-|O#-d>7eXu5F?dlpZpc2X_m#==UMS9v`+(au8muFGG5LJ<- z4fW{T>7aKOaqT}Ud$4SjD!L~4XYsw3N02XUcpJ+?hW~$ZYt<>5-}S<3#8P17+pz2e zvYtL4#N$gUJ)Ppyk6E*g3suBq(;DYO3Dn;v1hOS`{b~V?FAF*2`|ed5FY+UN z@L~eWj_(4A?sUDy|AZCqZ7bhzUoMR^wPU~dT+tTfenn2OKt-GD5u_m2_7^QXOI}$t zecr>SuEaUUmdS8*;72tmM_<-;LBqN1#;w}z*5}$-pvBFUPM|jCIaw!8e2|#87qs!I zi2hzSARt%yRgy{(f{%;PUEbaGEwQf8m?%x;QuhH8&cO}; zPQh2YBF|Vk}g8LV?_NLkxSoabmJM4v$+?r8EBCB^>~> zEC5J&hwBJTtN8S#BJ~{R373STAJ%#e=9C1-zAAOoW}33e#D!NFF0Sd$4tm7!R5s<#<-0b`Av z%>1rK(w>E+98i%buQeL*ps}qBG+G@L0RLKI)y55D%7ZF~C74jjzVx8#cs{>QtF5?M z6OSU}>Y5lEdzAaezq2U9V6PaS3FzT@2YvSQDL3EyQt$bJp#fc0>=v1m6gdm)53iin z@I=bBphlb>SPa=1@?;RB1mnb2mn47hrty8b#0_Qj!ZVO%e)G_Mg#tFP^)i=e8p#vB zz2DJrWn&xxDlp_77TE$aQ+_?Sg1h!Aok1S*O~$f%dho53BnxOMb?3rT_=n%fY0(oy zuFP7v1eF!!TlQc)-G#9S1W;O3PPyh6x4PPe4uET8)T17$g+Il-t#5Ye^8M!yvvETJ zi>B!WlNU|`W{|Ql*(|(cSdt4^zyj1P7+!%5K05`hfuyN1>yM1!*NUDw;s`H{VXnLD(1wR$M!#ytb;6U6Ek z_b7h~vkezXyNnX;<8*QJCd{GYLMDGql+v7I3m`MWoDxbx!u25#FS}{PA zU`*{ulSD+)C_ht?-_ryxy)PebGfTJ33d7J)Y^DRa;|EWSqW6lx64OudtTK>~pWi=z zo3ak+i;N)5f%){L?eBu?GaMtnLcmjf^4n6?R+R{D67)VKrk@q9$SQ0mw98U_c(0>C z$e}WdPD-{Art?u{dM?!hUOripzR?`D86}P4O65;?;LzN?yrkSR<}dJX zMUl{v-*&bwm;nrK?~hFOZiq%80UDlf{lAzpsIO?N{G#)L=_4{Egs65bBes9~XdW_+ zoS(!%!v;BtZGra{Rl}nT+Ay4+Out|MI@YgH6nL^xFU_$#NWtiM^4_xXquQx&>+$oO zZ+wRDT|}BoKHt28iV)W~P3M`~U9^aU^%J#!V9GjLM*Vx#a!RqE^7{}0Ut~M<^>H@p zdU~JZeoB#HD_^LLnO)uK<)_|xIvrStHJQQVxfl;-f+ zcGn02A?>%BlvdY-iIpOWcm3biLM#=4-WM~iT`UZ>SFDcK#(Sst3Jp*ZpiHhFfexi^ zhyKa0sBr#Ez`{}!e-d<@&9ZS-MK(M{c7t5QB+YYYPEgF?<6I{BO#USoub;Yk~d(a@?CnMCi!(4gdka};v*yNFHfpXt`- zMCo!nHX9M_0)Yd1dcUR^;=uPO!nFSacv<;2_H~BWTOrxlOGMdB2j%UZkc0)m2{Pj( zXcg1o4?)bYtmdaTVFlRvpY=t0Gc%mD`Vb&ZPc$9rn?-$ILR3Wx>m7|f#yMNM=gB4& zLQ|=z$YyQK|Ladd9Lfg*ffifHvY=nXTMR#jG}Tz~onuwDaQO$nVf0C*U~5GAKQ^Z1 z_d6l&9|V16r&t~gH$7h=m+8C`_Z%DURP%3bD;a|;+2pvlM+FstNCCt3DT$4IgO7-% z8is#EkaiStPHI%mb~E^tO!Az^B{2dES@ff0`Cbd{Htqg$x55a6w9?{{ z{g*qt&RLTMlbsSvO!HrvwcL*{%*Tw1NE9{#|wYAbqupU5;SwAtWt+f4PE{h{HgO8IL z!~}5fzB^Ci(9>I!cp=C zXqC}k&r%YNAvk5LhLHpKY1kw=!8 zK*PDtjg`yviOdtWpZ|lS1nr+c)2UMnaj>ll-5)Bq^TzkH6kz=?SEf0PEcK4)Z4rG% zK%lw6MKbf-MfhvV{#5#sXa-uyHvp`FBw9_`4Zw>+T!V1gs7jINT=G+Qq+YxZ_2aoGMnx+zllw}AgrMAEozi*jrsveDrM&@THDai>8#)GJ+H;q6_t!D~) z%a1It4IH{QB!sO4;8M^sayv< zV>6PB7jg^(SWsYVv2g-g79`NX|1T5VQqLFJHf)7&ppOHfXl;ydK7a@tO9g?`xbjcQ zll0ImmJp-C?smz>)e@-ma=$o{9{ck_|!Zd!?5n_UM`Q zr7Kf{Iy79u`(Ot_p}@I;U9dWrz-6KTs-M^lx_V4#*;p58x;nI#nXdzJ)qTxp{qp?b z!%tzXd5kuZyA8OuA`Lpo{=Wp19^I!5K6%@$O5BY`{H^3a-=EZdeDqR54HvaXF^!C# zgN_lXlLFi+0DGd@vplq9wnNJx9l8*3k-EyTaOOl#0(%2?0?_ET_-KJE$0Oymdmqd! z_WqU1T}R&{vqnI?_kMVL z4B1)*n_5)ZCHGJ&CAu)`qRdbv0R4ShaWa@Vo0SE+Z)yaC1J9KScX~dYZ-)iiJkv2F z%Zf7F`|ogT)=tbnPxXof<|Wiu{;vxDGw?#gz=NHlWh(8%Wv;K+N$~UZ?ID3e4?somj-}c#hx~oFj%h0ER!IMI|s>Z%v`3{H- zIlj6a(ks9sexyqN(Jyl4^Tj$Rj~k+O)UNi6n0n&=S6zx+Qrdc9oPaX01B>?@047Fn z#+3zhkjpW@!tsRFmhj%*3cjcTCf+%^;o?N!u*9BG6Wii$xQS!V(Qze{$)LSf$x({R zvF?R{d(CMOLWLEty6H2>);D4!bjluj`Xoz{HhF382O_7m$Q&uI^IPy0HyM3cnJ=-{|uU}mK!-*c=$!cdW=yy z$&ZtgT>R$BHaaW$u|RXPxNrZp_U#?F$za5s(lv)DsCE_r%4@ou(%8DIx&jPYRb2@g z5?n;mT{_uLa=sC{+x%5zPTaC*CU!1`g1TpM4HKso!o;R%A%3y*JJMbB)c%uXl?%jZ zv+M5Mpe`ox00v%9M%ExL_DdtM3mXlZd-+#zhal5+ciZ<5Ng;_V;!s%@ zx-oSRa3})3v$YBG0@H^vpbWqw{Xx-RuBFH;+=1);Gde1Q-W!3OmSF5g_|N9_$1Qn{ zrm*dCR{90K&)Xh1{k&w!Ka>B{U0&xk{)>ic?SZb{q@8F8zlC@0vqv%YT17LP>Z+xF zeT?~ghYFsO;-IH&tT5_OuOBJjsj2v=AX4zDKUntR8m2C~or zKbHMc_GN44W?;g8c0$B<<~Z}zNX&qsv2@Zj`}zpmm`$jccdRWx4fM4@#rKyBzVFS> z@y!driC4}|R;%S4eA7;vScB6QVraH&`rQWrh%7*Hr=7=vpGw=yjlQ$Ic$GlumElAs zp_7E9t$?clg1?4VIMuNVV4MX zyd0s@_#ywkP^+KSy*+NE!%WYB9P3F=TcJYheL;a+oG_mZ=gt3!hFmCZuuV=2%-=aW z%Tk_)WkBzY?I9Y8-KO;-rkxPgGp@o_jvti5X~REjOoM}@C#C49lOx<(K7Y}qMHYx$ z5NB8gy8u*t>l;Z%QE+X#q*(#87O8^Kgh7Z$3NvhkP#~1A^eDBTc}OcOHlQ2-kB3!P z{IGM>P3_|opapeCTv*62U=%N7dUd?+i$yu4Sh$?adc7qB(wV_J*+>%UEZXs1#VJXB z6#;OB0OPgWk)={EZuGoaHy zbgk=8U!vo{lB_LR_7k2BVF&38V9>?GiJN8qrW|kgjygNJJQ0bO9=%BXKgcoS&E2a% z)pd9a^k1pzdir7(ZU~*E9SmF@?l$wLz}~Awa?x|*2)(r$aLYvu`loI2^O*q9CD#v?#0(6zd9DIf3okou& zBvV_Yxf2Q)45LzG>`N-9LhUznT+YA2!&m3*6o-MdXY6mqn#l8A065Vsp(?blL!n=xlHo4v-FJu^w zws&8Rqk2Xn%vaH^lZD*lA0#v%u>&L}NMrCq4u+9#%{pI-1} zXYnFtD1xYM@w|gG(Yky|3$^q5y`k9npnVt*QD%8_^-MQIb2SI*@=?4jNPUHy!6P9M zXd(6^mH(P$eEv18bP95G;7jw`U#_V~47JGUuOgsBGs2UY?KjO}vENUcuM6o@4;9L0 z=@5bJuXMe30e;!~TITJ=edkCT>>Y_jKB9$>Y#koVUPY`)#`cX<%m*kjVZ4+Ik@H@z zes=GkEN1HTB`Apm$izYkXd4DQ2|`+r0l)g6b>2k0mrHm0=Lcsla!hi)yt6s7p)^C& z&*+*uvQN->?G~KS&0^O6G=IF;Vs86D4iWX1ee8x}m8(!kAD%m@A{Keuq*sHFFCX=- zQIXg3V<)Gb za~HzC@B%0wCJ}SiN-#e$xq$&Yx+>|gK>h)T``N%#v?MxtrfGnc6b;S~W&xWcHA$Yo zztNco3Ei=rTXGyALWOK@wG+^xNejx)3b3+^-i_{%``xBst2w|0mB_Uypg0MvTh4{D zV#}JQEf?jes>PzlO5upespGIsEzsJ+(SsMPYMga}a~OY@Lo{pb<|i-(52T`x&9k+9 z6rfg67di5`{FMv4@_OW$-HK`TTIp1cvPo##7CdgJ4%;|-q?PA91|AAwzG&teJgmQK z)}5PoosDf}71Gb`|IyP-hr~^VNcxT-zl1&V+Egu&GORnxyMGl8AQv7F(qP-ao-<;S z=yJ9{+&kT7SC$|>?cCbjQ6V^+ro#*_)U>?4N)2hMdYaoP_f~3a)+=s;{go$)kNgLE z&re-@YB`Q`Ox(tr9aX~54f-yPD-)!QFOBor{Di;A#bJ7h?P0h>PVWh$WUU=4DXa9f zc2mpi0ts@uv8Pf-&^3P?R5%gWApRH4Ab^>*Qxd2!c1NtfXqcgNu2G!a!^(L-20p5; z&?@*V)KiN6(!fmpT-uIo-{a%89BfI&!?Zi~@(z;d7XZ1Jp};hTzH_@x)3wu;0i^31ZFD32K z<)q?w`iu^=01ww-B#&sa%|(Apd=o<|ZW6abMvtZ`SKcoNMpBH1fn(S7dWq=+Ak0&k zt_EKV$(j^DM`rf5w*jb0K1UlFqk-l;X3Z3IxsJTU-LIz`{$vuDoMo2!B literal 0 HcmV?d00001 diff --git a/features/wallet/impl/src/main/res/drawable/ill_keiro_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_keiro_card3_120_106.webp new file mode 100644 index 0000000000000000000000000000000000000000..371231a672934184f6b524504948327564447251 GIT binary patch literal 43620 zcmV)KK)SzDNk&FossI32MM6+kP&il$0000G0002r0RX1~06|PpNDT-801X@>0TTHC zTTSMONEQOVCA0M2C7v$bmWa;-6}ec##UKijr#`|h`G+qP}n_Em3fd(L;YRolOa zaNxF$BgK1twg=<)8<_zad4}S6UId(x;z!%$5 zIN=FDkzgNH9h@Q*PR>P7Z=l4}M+z(gdyYpQs0270p{CZfbPw)l9a$6ky zPXmQ-t+x7er@jy4$GR!JthL*w!3QMK&0f{md2>_%Ike_1z9m~NT8 z;WRtNN##rHoiSdYG^bf^t<8*MQd5qpZXX9cdIX_(!=7)JK>#Kg^^b#bgkKrSvYd*F z24mF0uci zFba_r#h2=~Q_IbPh+#?U-Wit}R?J3Q7lLV5KSyoeF$H`8qp7l6UKGrzv6+I}u?!CD zPsQkwvG|APj8A-DiYUKwI z#33+)xpCwQ1}Kp+1aJ%{s`4NfW75Gi$nn z&H1AR_Le#1stY6kC7Rb;ZM^_{0gPL=jcc7$f2jh_4ZKA(LIoGQ0Uwn!q}{=H8it8 zdms^Et(${={jlIv$I;7p4}#=`#a|ke?~Rg@-dxBP1CY4DQ=je2I$@c$=QBZ4`=7Yh z>+^`)Z45zzgZk0*%In=OAlacj>W543e}jZ4>@So1^L_|lr2M&M`Dd|>V;>tJBXz~* zULib48&2@_wIs5`u_p;_@#qCZFmF3=1vierk%V^2<3X2KO}BU{xarV+igauiR!Ntu zKPgh1xm9%2xu(l$(Tx-TVCyvw<0VA5uaYF89?Nj*!2W$9x?Uq}S`6$d@b#gWUF2na-XYozg`z?4Ub^tDH8r;2=f4!tj zy;TQx-yxUwRkF4xzTwh+i_+>$w0pfq2lXV;B|!c@$KFrlyYC4k?)AugAqPR&6ht@~OAtdm&ffH&W?RI_NScaH+TI;CmsyY1Qs~C*NdW zD)}b9-iq(JTXgw*(7oQO($-#*z!xXDlSEM3yE*vj4leg92o8`I{5Bu!@Ir?yzB1Xp4{uL z1hU3EjJN0++#5FSdxz+fh5h@&rc>I!7wU;&zUStx!rm&H`h6oE?kyya(EXE5_wyvS zW7NL4dsF$>TXFxsNQ{ZE*RZv>s-(LwrZKL(v$*?cy04#XynVB9sh2G5NG>hf*50a; zFMRi<0`^vLlLXjnm^3x_zG&0+8p*e?DaHLffpDqUko@a4CX#un2)w=$uh#(Vt!(Ng z!4JFixh>7N@h!S#G<9z^GU$5AaPWNv?hU?uZ@%Z|(d~<-PrW2xtpPYu|9q(Qsh4yw z(#*amk49rB%ckdUaW8D@ty=DlXu4!!_s$V;QzhQ@c<^Nv?t2S+%Az|Q&3B4Ry`=CK z(cCWtcwdZ7y%l#aX;ZIZ-@eM2=(iZyJFmilcS4jMK?vS z&tAi)pAWvqrTf~-zWdr5Zz}u#o-esK8gCIzeTq8p{yxWB;HAT*2yfpT@bXJ^pKDpz zi)fn;FWdXtvN*ne_&es%m$ z{jc($mY+Sp(tqavLGNSx*ZQCPe&Qe1f4Tp~{=fbk_KWLR{6AOUy8fa+rvIV+s`p<1 zv-_*>2l_AlA7F3kf8~Gp`&#@>(l$U@YVgB{-^D4z`yLT{x4t;{$AJL zz(4$S`}u$QU+q4Ueqa0l_|MF5@cx?qNA$nqKfHgF|9k!$+xPu%^PXkBPx-(4U*`Qw z{zLa8^1t1`$^X><0sGPScl&?*ALx9Q`O*Hzs7K{L)c@B1f&I$=AN$kF32(pj|DE;@ z_$%(;@juJ{@BgFp*8fBO7x=Ge|3Lo@|3mz@%X}ENqoEiBgQ-LAMrnH zJ^{a*|8oDI{zLuW?Z3(|{NL%n-G1nM!upr~|N77M{~_PbzqNnK{|Www|A+cN|Nr_v z{C$)D?SKFOyYQ9!HUIq&TB=?E01cv`02@U>05*z%0Bscj0NN@50kl*A18ArK2GLLe z4Wgg`8%00>Hi~-K1`P8NrgVcdLcC4ZQGHs~=mlon936y1W6`ZuF8}}r(NF*lpy$P! zY3R~@|IiLY*qw6@?u;s7Z2PFhErmWvBM~G~4H@{krM3!IiEiAq*02+aM^@J7m`}KS z=aUD@K@DG#8g|6NP4M*V8!>w>%n_3t5N4sx>U`xjtyM0Nq!Rj{MPKjg7xBK|UDrY2 znc_KX$v;#stFmsSA7iv~yFfbj7U!yLDp->~0-IZR|8I)vy0+3~E14j!ebgTu?& zSILf5R=)uXhjx9*!~-9{l5v=VY`~y%J*<$nOcq%~lhYi{&Sd26E2tIY-%}O>G=kQ| zcxQFbWbtav5aaowgU?HAwNu2QA;;x@ess}0mU@nkC}!qcP2Hd7i}rYe_k^5JQO)w1 zeBBV2XvYxMwjc$he^>&_Nnn+%)g@q6(*zR(2$@V`s*gDU#nceh6hL6+xoV!OfBdPA z-Rz8*W65%J9+}z)0!Eq4T!U`hIGJ|w_$4z#u5us$gA1s!#GYP1cEX0DkRFlpYZ~&UDQI+4o7P z+4ef->sS9m6h%so*aeRq5NasbJ{&?eE)=Rb&z3K7q<2BODRx@vN&#zll_4v10&{YH zaTK*KI?n*|TAmc!EP8pqzsrJ-Cr(7gLCs%qv9oxO7Z(>bJiThmb=@Q|8>*JZY1*ow z^KhD65U&~1x%J(feGjx@e5mQY$b9Y1$rZD-hLCXg6Fz5YAp15OB3H+|aHXDsq|wFk-Sf*AW}AEUM9?I@ezJ8{Z#7~O&fc)V>VkYWBpMZKOL zmO_-RwNY^E5)E-KvvcZ!e>Bk*`);hyhY7B@PySpqK!o;)E^+j0UF0}s@*MKff<4_B zkg3BY@1#%TmN%VdPrdJ6Co%fY`eq7J-~9&Obtc?pi)OBIdI-n7Z!qF_&YCHE47e)0 z&fi`q`JZ*8$qyfmsTx_I1~hQUg_sONV( z=mYPW`GFwM-`lWP3Z~HTi(k(GPgWTYg-sE6%5&+3CV&Qoi3$5D-G;vLmGk}=En}6# zspB0^H{$u#OUNtlUABFte081oJx34Gk;HdLlDl=&eBdt*G==VjB9FSUpRX-Yfl4^) z%J`#-(GU)|vo2D|;Yk3RvZ_mA+Z8n83*rEg;JrGXxgt^Hx+bEhz3fyKC*O+E)kL~K zB=6_*lPe5Cy^!Rp z^P+&st|F6*zCsvc=C~K|JnI^1GSlwiZ)Dl4>WZ{nSXV`tbtI_iDy3NFE*xX%B~Amt zbzK-JG=6@I6%}8?(w#(M#6rj=BU{_S2B2U*f3Nj(Qo0(MRB7yFGrA8w&8^4oyvU+= z3Dth0CV>10Z4H|J9iNt(;-`G`9Rf|qlvR*k)Wrf34EMtjIqcMVnT~m*hPvC2w|7t0 zXq}>Tumy(}#qpKFrdZw;?8_)669GZj0ZccWW`u-**DF$%sb6{8128CLFDj)KZwLL@ zQ?WJA%XqgdCZ$fz?N*%zBKRErrI4;RSC|hXk#k~y@;e+u$6;DpJbs=NMmrKlTm-K} zHuGBTtslI~LBhs8XAbjy%1_XBvIxqa1Dwu3R28`HZnQxRj{Zn)?3}lS$kgI(e0$oxH^)lE!!+eeIp9TYD0m&qO<)Z$;@$$9$wnrQej$B zAwB@S^syM$TajOCA=!0gHkj{y2z)11F$~>Kr-vh_+SkFQiDQoj=Baxll#?Zph;E^h z-7Iupz{2O$!_DI4^|@o-I=qe2+@OYL@}1OL28xfRCUZF@wPL&*-gmPo0bE51-SKvZx5d^&lzl4CIA>_+3a5{`krqtHR8|_{kjedtJXIrF*`>ND*Uu9qYOsH%84Wiu9xg}6uSUj8SO_VtN^ds8GVrSLt1P>|1rqK%4mnUr~ zS_ySu2NgSz^$G_tx>BY7;S`DQG2e%HT9sF1=cdaw4brC?i^#U(uR|SQdBM7doA;QQ zQkdpiO|uG4mF(?j_PB#n2ds^Cy>+4MQa~N9qQye8Lj3DlUA%Zx3^C` z;l?fnIe!yQdbcBRiV~>66=<9O118w|B@6OF@H-5mb-3VR;P7<-JGfv%E94I}*idR{_ z{4ok_99@nCGnf{SR-|-K`S7ps4fkT3?=T5@)FV;6O&HsI#9&R&`mORwGwF;ye&Rw+ zdeNFhxt=icGS8S;n0^_KX=J`s4$*(|GeVY30~D77^(NRwfJR88P%AnsIC>m=>;jL9 z;f`D<*TWgDr(>hXVH>KoIu;FXOQs`O*r+ttGFc$+6FP1iP?ClO#ozW-wgjHPS49b6 z3Gbf`SGk*&dw*QI*ZuN$Eup|UfXF_fWI^ZOU3PE_t?cX{TX;`bLSF(6S`{NlFeSH> z_&-eD3I@Sdk`IscCdkpK<4#5cwFEGCI3A+*##o1~HoaUlChVP~WT<>TDi7fzOUKgr zpLWYJO-r_~H_tyjTf26W66LRVc+MVuL-MlN?}&ILLvr6TPnc>-zX{q9Q$ACuiJd4e z(o2mw#^?y+Man}bF`gX!K1SoC!0QE}BT*nKY!X+Ig(S|@{9a?l=bOZb{8m+R0^2F! zML)RY93!gcGBA{M+NckFW9EX#sKymw{?i*R*ab*Z_Iv0u(-yHoR2i-)Wx0HnHj8Do z`{gu?uhF%R$_a$S zb`TFtx!c;2-@j99x%PF~xE=D}4L@go2S1l8i6 zHh4Vl%ePa$&WyARanF8<{uJQagd1`oe6?Y=Qs=_(0nx6VN0Aw4>ONA6!%Jv)=KY( zw5dzzN4^+bQ_1VHb5-*i!1^|h4BiXs-JcDCVLupX+N?zW`|;NK^pf;WxW%((5HOZe z;E}`n-8Qzk`0!crjiW`wXV3sC?D7|C za99>;LM?Z9V0oR|ea}Gynhq00000000000000000nK7ADwg@0XEpWCCr0Q3dmDfz#_B75%wf$v9d!KxtD3p zIhn!MhcqzBgPV{4hv{v7u{%Fw$OMuL%4~-MJK5ASx$RxG0T;d=pP*oW(ZWQsViZ=M zss*c~WIx~gmHGOys#Z`L1au&lll=1Pfv9Tr8Bj{( z9>bukiF&!PuDclgcW|^y1pB6tZ|0wza(4fao|wz%cu0(O$QrYcH9aqmang#Lba7Hx@Q#$WBf{sc+NzgK?45#Z0K$*=+mOFRV0Ok7c%jiIOUQ|qV+ zgj^&|3^oBM00000t|U+ybppS!A85oZZAOL$67FAVi=Se^bE0%}fqL)4;tt4mfC(5g zM~f_M?H`DY_nD%f7t>bx)W7@P9?hN z`J4JLRZ)ZU?ZHbl+L)Wgvo5B3=|7!=Z#8-?+il2()d4DvWg3K1Yegz&Ue%JA(f`hB zMumvd&vZCcP#18$NSg=V@(_7R-BoMpBO~d)@y0&QUY8jkE(?^fkwSJ>?^k))O9)agE_KgF;BaCA@aFUx_xOsCE|>4woS3h zt58RWtRNuftK3tFBNGL3mSLt9LINGh9KJ}m3GQ*Lq=soCk6^{sg}z3sHuzcpI*Vhp zIB#nQ#(~*~YClwy;bAn=eDs# zj`CuB>;by>N%P~fn#vOyJ{jCwc1HgTiZa!FMXV>uMq*er zy&o;|YE`4o9RU?zP0fRMMSX9PEIu40QQIRa9?S5pld4Fy?;kiPHigOl>Z@uO5()oe z$Z&5QLy?g1HyJ!SB&bia`%T^bC#N$_`%G>|w&m~O$N(xm$rs{(oXMxQ#SzNt%hp!W z_SZ|f#4=Gl9CVR**taFEAA=DtW7k1c1ke^qr?Ck@aT|g^Wfl>Lmp&**4-UjJP8|>~ zT7T80fjV+ts&Q89rzB79TWahSXbAKoOmi!;Ep9eP%={z`Pd=ikVvfK9IPk#2EpSDi z3;dtA`6x(MQnu;5;i6CS+TI%TvwO{DYX=Kmnx10Cq+GMj5{HIMeVWZ%A26$pwF&_rkNP0U{b&;s6W$O5=U9 zei&u?F@~u9{>h!V*SV-1{w2c7wYJADB74Dn`l@SLSnRDr_J<(D!p8PZgZ+Q`lpr6w zB8`(rLT*eI=YeNq6$zytQh_n@YF_EGx09Y31Lm<$VP0%!Ix3NvS6N)I<(MmR&06%z zD}N5Q(XrIYm9JX`qoevTEIvYi)*{&xQMF7{m0U};n${E*D%yf|b2}ZFvARTrA9$bo zQXCqX7?w}V2e{7lV?+p!{JAX)h#>8~h{K?vw)P(w@3y1uDugnZEYtqj&80X3@Vxlr+3o{LQ#-T8#Uc}@VRMvvoNoxTmP!^rov;jm# z%$T@Z8^wfFK~HzWlXR=*O`$Y|)X7~RcTWQPnK7g%0ZMLAa<7`TYs9$h)^YksFccpRZQ5s9i4U(k}j`N*+f69&&cYe5pi64+SIP-R_Dj^B zeoqn{W7(Uf$5dWbeO-8vJ0xw__&bn};->{SDP=k7L&Ce%k!w=8F@`FhsKwXdbO_b_ zy1pk2jWhT^knQ^+p-qvE(4%XeD8$s8d~26jFUacWgm4KH+txu_E*{+vbn=^%ksgGf zwM-ZI;LMNb8#1w=O}@g6wx073Tyo1(0#Bl)*?J}bow!(WzydKmx9Ut;&*i~*jbmg7 z)sF!+3p+@I%!#^W(?!R^tuaVL?MwU3Ask$`@^^o|i)VdhZz^z4sE)!dOya=045pW4 zq#uM}l2OMP7HI6Gz_bpx`-mO!6hMEc`#tMHN{1>El8An9s(rFGF%ifxYU|P5A`>oZ z3!w{JG<`$D$V(Q{yV>ih9XR*m*$;29wqTJz6eJ`C6}261FINnMS23i zxYwRqnPuqE_6l#n3KhPbzG1s0$*cAz&N=Z|38r3rDj(C(H+4-l-j3Qp?HorOqgp{& z`o?}wfe519YD+42UJAV!;2vDxEh)3xo=?z^qfT^?E~eR}JwL3ohjIP z(h8ny0LC<0igb)s%T=1UxM$krXkh&J;yq}#EhVd>HtaUQ)2yK#z~ufajHN z^3xd$H~jxc5p&Knad|;X0o+4Jl;GkkG67U54R^tl30;_YV2CPe3a%vji?#fOn;@(n zl5$7RFs4-Ouk-Eu5jN7FfM57d$yy`rwUx|Y75>k)S7msGEGpmYO;;mKJt2r6peCw8L0AtR|&76r4 zV{M`4q^~nGZm|lPg%$iE?#jtuQ|(}RXP~ze8Doo zE9bE#L@8Wi(j0+h>+qE+;_|YN;nu@np++ct&B))AwIoEDMlSLXIc?KX|3C6H^>jogx1Cl`K-1m|DroszH`ST#1qw^Baed zC#n^-$BpU>@Qg)q<3-i@6+yP_G!5zeBgue8FByJX?pr{c2+?u{@g|IDS{Ww2y7@B_pSe*xxUvd~ z`~PTjYzDhIf=PX&_wb48F}`01K~fHB9z49~ALX0gv_>M`bah<&jmG!81?j$h=XSM} znUIh=*OAmLiy-$Mh%}+g7)q)Pz|Svc#(Ngk`2w{hyT_^YKc}tOKi&SS5)n)`EBYQ4 z*W@VJCd_xNxdd5jSs3(oe-OP_E&7v@^kxr0WX7_(Wp8wTS;9oOEIRlDB9tyrUvo1N zapd{7@X0=))WajNnhCt77sPszESwL$z-dUlt&%7aINnN{k^}qq>?qvdGz$ML1CRCM zb?pXvFPqsr%`eXP1Q_fX0DZ}tY8b5!?NjZ;K`E#Fr^*9nF zz4R>9HJ1VlRn4?Y@lMWPgq(i~w!Su5$#JBA$I!^`%kqf$TW!k1Y@H6+42InHAfZBD zcKmy?rO)ZK?TM7S7me}PjJvQTY_o*rhoXw8n<^j(mpt5#8H+`7h~9KnlkPJA z!2)=*JFF9-H0B0Nug7+`borx0l`N3yk+1dC2iaG8ixl&=KYfL5>+;>ea!}gJ4q9`2 z3Qb6~q&|E~%Lz{aP?(AA-aIupmMik^>QwdX&|jq{lyFrGR%Axt2w1Bem%a{VhNS61~(+6b&hu6uPoX}^+)_5FCf_*=SNI2wjYhzANB5> z!`24V-0UKZtqh_*@ICnK{*Y{|8m+aUIIB4hrMQVT@{=?i^RpnpL&!0b#I^r65B?%P zqv4bL5(p@@)T@x3_xqdR#2n2xePlh=3HhWdR||`!dtWm>8O+w~D?7M(qH|f#2+wmx zAKlxHj;O0sQJoO#NY2sVoOF!upHn4_T~lT=Y=rDAoe}>Fc zg}@*O71Of%{R&Ex=gCiz(ew0B=gLHVHURC^Xv0EvsjN6zbbNFhllGRosHIfFLwKb( z9IVz|_X&CcM2Jkrkkv6w(^WMCW%||r2wpj zFVMnYsd?^CP?D3lnt8-_^F&DzVD7{{nEv6phvz#iZ@7HVj z#4FGZimVRxFSz)5S>wLsF?X|>J%qvUlUK)*(cAAjwa`?0X?$PrVYyExiZ7j;*j4e3 zZiBHkMvw3LDLRHSUG2QOdm*|nWSOFv6h9UOgEC-WGN;>8e>Df$x6NS#s#XoQp5Svx z!Lq7R67b+%40rLpiz(mh4_6ubgj1Wq8bI7_eQ4L~BjLh)w}XZFau$n9p;qxB;p`Hf zWLd*)eJy=MtygwOYas;9KvfeZQQUAKXww zGgl@)(Pb62<$RybT55x_-~)6n7~1n}OWzU8M&%_3v9PfF*`INM5gy4@f3tP?R{qkQ zP56^vC1+w@rM#6exN48`pPRtClP6d**zV+Lw+KE3d`@ADn%8LSj|?Mn#3d6`SI((YKdPsU2;=lz~I= z%twTToZ1V-zAq|&6(r&-G$9ITGb!nS3l&3{ZyE96Ao3wG5ez|B^R7ij#=(ec_u27s zg9pot#)pZ$+lUEL$QUj=onTb5D&2i7utXR}BT0rw^Wai25!iqv|4tIdnU7ogaJVS{ z$)Ij6*Tx_^?lEX-o_n|wkZxSIQmz(6zxRvsUX^0=$vAkEK-XW-i%2O+;s@(KhoBP! z$Mt(|e$21f@t_?5SAR=NH3s1w6=jbC6-iUKNgqr+yiN9fHzSV%JM^x5Vo0Nu6|3+y6yg5xPFF9x~mum6?DBkVND&AWF*rm(KPQzw75V_P7$Q`{Tmrg+E(^1wyA!GJnm#P?{j78VA zVlTi)?Y96c?Ea+=;fGsorzblD*FrOc{l{nZ3(mdtK`I{(13PsmC>$LO(FC@clpv%y zVeVkb^n#5zbQF3W2C@zjmCrZI^BjE4VEL@=B7^eH^K)4)l5hv|f9XpR4)5Y5=k|}9 zdvuM_91N~*4{lom@q#CEepjBuP}M}tj`FWpjZf-HgpLIM zY0>cOK7h>g?JBBmh`W8C?V7(TBD4$S40)_SbM;^3Xtu6At{op7s$O+9V=KHeWz2Ib znIj5vl!GW6Uzi4BtAR*$f_kuY{9zLyUWUyXuX;s51SSF7qP zA(a-Bx2s1TMNuBw4?OjT?b`)t_?p>tV1k;`I1)M_4^+ILV^)VvD+nSt-*VtXE0R=+EnbQKoGF|qLk7v=w!`@`k)hDhnwJFn%C=XUgJ0D%K^HWh&wm(r}LnC-Y7`J-#OW)qKp)zvDoXszl23 zkmyf`^9=SdbAWpfhGt>xFp4bg8LHclRH+?3V7sf^Oy-EiPjvv_*7(!x{Md&Bk z1Fu-f8&&0m?qK_PO|MbFK~zpkkeO_%RT1)VE4D}Q1omLb4cQ9){&3*G2t?@Y+qtBu0#?p~ zV;NhRK|8;?L)^`G7?I6u2^or8n5Q3SS4#uY8^*$SLCG-i2F70X(fJn8)O=Q&v1AiL zEIKnQ>2`EFBiT4vAg!C=LL{%%R_yS5wr-2-MBEWWG#o*as*u!5o3L)F52;paxlr{f z?%XQDrySntog_(ksV@o$v~=re zNJ+Q3W}18=OFtRgJED9_f(AV}lc__K9D?dE9t#v)y%MIm!o{v-wDt5)p;8ETz44zP+vsNkubN0=2ek92gP{T=OPW1UvWwK&DHfV0y-R z#0F$O0nD!kXHAJ;@3^PMsG;Z)@}OLeLz}WhN)>T-mKu~&oMS$rOG)xN)0gzQ>yC!6 z9Zw6UJ9nzOq16KFx+ILSVI$2lb_u|6cQ%FciDs(pcRMFPuL zHqH3aEKnf{EAS~4#X5CN{F+ccUM=l>I{v-rDbuyTA>R`6Hp(EY@#Jp;b8>^&0W zk}s&6@_#N6-hAn57iSHn(VV(b^M&_gM*5B=Ft*LI^N7D9jlV@7(~@2QcWoWISC6^P zk{|fm?B+Aw_i=oz4&9IHj!+W3wvoYts99D3*3YpGLV^yKE$2+5en}7?C%7 zGViga?U~_7$S0I%zT+#Ztf@}A5|iZF@?o0w;MNr?LjT1qbhv4p$6SM zWREZ|fC(!eQC=G?ry?je%!Q;J=oH{8Ls;sRX*88%WTVrn$)1Lt5(z+BM5jm;0t*`5%B*1NWV#Q{L-F<=5AxA+<(v=as8@Bo_q z9o2MzV3)mHU!8lzm-X(=PL+S`h(s_HJI;ppUN%l4q!8EeD!SNXUTHh`Vu+Vth2)$`QrKk8jkj$<8C`@E^6 zzaa99LZ>T+Hs4yRk}k&#GDABEPh4WXun42tp9i4q4d=lZ)0l_a!PzhB$Y}+c{tl@YiZnz4fT{mr&CdyT zDe8x!pEQa-B+boYl(4Am@zCK&28DNuOhYyyKYAAh>)sYlG3Cecd}QpKbZb>iEQ!x% zg5i`%N8v#L@`N2+ZftJ>5#?`+zxGYdcKo~G?fZ<)jffyy2Qeu>g*IKnWx8|N_e9^I zPh#OlXNd;=nRmy?d>^|egO_(d&doiR>*sm@Uz6iCvtZJTUU$26zrlz`zG!yS8wm=f zPTAbyF6DAAT^@#98!!!fq>JUq3H`q_pvaC*COjpyTJqR1c( zj7F#+bjIGmi$D;v>4r=`1u^tVF_thfMUN;TGAN?FNI${miIxN4(gmCYg-$fmF>p_uUz19LnuGR=hgrQ{{i8T9&drv43Zao zXc$QexRX$5VyiKqSEl@dZb_`hbRt}+u-l8`*99*f-4uMdLdAwYo|10?RU~1=Co8m` z`4L$V#$(pkNJ)VlW31H3EXK`sdro1 z61_Aw*V`+EwqWux0ghTgT6GXNf5Y6cjZuhtD+!>hjG^?}#=XMnm2ymb7#C4&fi($3 zrh2r{TYs>;m4qNZ<3MX%MM|r0dv1zvxO1hP^&WiuW>>6;ubW$`nIUv{xuzOuw=**< zDE8&TAg+}ZQTTVq5=5n_^F9rU!m=xl==WCbFzzTsdS{+mwFeO~J6c86nB&5@g&%~% zbBVh@A>wx}g(km1_D7ZuesahI4oclarn}v|gHxqRmly!`rec6N*YpKbWPi6|YQQ$g z+~G!++vP-*M~~6WfhxBg##C0|aHl+I&4%je5&!DQr8U3+$zwjb+M+ztv*Ic=%`x?f zXr@7WD|Z+wg+s$LEI+~CpfCBr@%c0IWdo!v3M@GWcV}=szB&t zqZgA*8~}grs6G8e^$*2X5pUT7ctwR7O}R||@ae)aQHnX}O1W$sjWAfGQ+zaeR5mFK}gAQUR|lS{1B{Ml{*! zzz%~tp*_QSN!&8V*oAeeq5H>(d&j)!JS)jVyi|SMs3&0mJ|zkEgK8si4hk)hc$*SX z>)k_KtxlF4^}af8X;GKW=%9EPz!{7~SDM)T?9Sslh#Wk->5>ve{^%!lg44LI*}$Gw zN?q$Z(8!=>6(I(9sILqLmia6zqc{wC@1l=t#Uco z$T;9lCt#5PvM9mQt9mzWjFuL9%@!srsxF0+y6i@G?#kRwtoah%dD z($11goQBvF;e4Um--*i_q(RU#t9xk|eksQu%P@1%a*>6l>`NXwJwgRJ6u6J!z)z!hF|80m| z>C)G4k3C$XieLudYyDORaI6s{A=ZGNhKN+Z(x+_m^zt#YQe$^65ItaQJ?G0--ITgT zRVhRrqo(v++dxjQ5T#cb@iTn$mixCnMtCK6iJ?otmf6j4+nsMNtuDYAd0ZN&r zvvYwKo%`3Ye9(%GRVg?Ox~T4P+fd;I0z9;5D=0L}9z|RTR+PS0i&MT%B*^RTBe%-S zFS7S5wre55mzpx$$7_xCNYX2}6WiI!&{U0ZhD_^MB|(l)MFO;0d0U3}x|??Y}ryfBjtAxV&tVdVS=kK!1u zGk9ZwTI{VyKx*?As$>Hr`IgURj~2Gp?AokdVI23=QP2kuhW~D zOlnsZQVay?r4e5si7p8wp^B6l^ zsXgMf!!z0oNgcJ?I`Fc+~xCv>gF!hpdFG|2%o%Y6V`izjuPp+&Ec@h;` zIJXHG_8s}LB-$~+^p*GHBHZM~SbT7iZ*1b*vBNlVKGlEK=!Vk6$<&s@&o8o{Zr>zW z-xZp*|8n;_Aer-*{n2cs#&c7oq>#Ui{GsI1-Je(12X}IR7Y5dvi(Y^QIMhWbkSkPZ zWmk9QkHLV0-f$9GN!pf83YK4Q{!y7;(&vox8cO``1!?$pM8hRFHvh$7e?H7Ob$~d- zZH=3pUzY4-{ifCo%%sgYYyReYCc!htcyS)Ea4TeAvLQP)BNtyh%{?u;!cVLQ_1kRn z9MP>r)KvO>ERl*22oqF(5z)Fk&h_&hoXm9p{Rngj1TS0CBz{HBxfLC>e3>T&n#Rfi|^NR*&LC0ECklmdALm@U|5c5K+h(gBCI zfz-035ycTxCO5W;i_su9hG|3m&;b8VkqZTz04qS$zYlS-kNgg1KkxHso0HT5h`Tx8 z99dd)5R`tpwC#?Es#2uZ-mJFp@H@f>RDAlF@9jsN|AvA0%2m6Y4c8OS+Q;?_lvwx$ zd=mmJNBX9aqOtWV$cFbov67p`_u0fK`&pp-YbNrsPBW)(l|0=ruMeM^4rw$1YAhj= zaS{eZ~nsF)Ei)6ex8R=dpFs1qsV3wr zPgwuI0g6d?nPzn9U~o^MNW5*sm|7~s0WzB*S_98PT zoG4kr=TFg9_P>Td3H+#?r2bHJUa(x)>RpzxrM09%r$iR@hup!5s2zZXC2j&|-1J(fLq10xbrZLi}IZ|J8fMg{t zM02rn1EVrRkwq6lDzDyzJ-=a;mg~nOR9NC|mO8mK78DWMb7UpP4d1}uwR(lqX6+9*c-qKP^oJTzGdDEjQKBF zm#7o$(5hP^>q=0!;jnYB+=n8=0w;*TSl~uO9F(=ASC1i;$cpJTkYHQYjg3T+T}Uqb zp6KT!Nda=QS#`Xm{-|Tk%Za#rXD10KsG{Gy%T&b5oEOXSdos3NS0!2btL}EIuwW<= zA!x~MKwzKlsZNWSZVHCK>Ypp&_ii;3*dF<3O;*CV(fkbnkG16hi#Nxg|z@0_;X@La-tGzMb*EvNx!AKcrm>Ao zbOa0u+XaFDmla`(?PJAi8k$jqO2`O*DW#Sffk8h*Z=)xmeTcvuVP>4#`4QhpRe=)Q ze}fBvMmIrG!ejQ#4N zvEobO<5TCpWX+iaI?Uf4e?`12O0L(|DuiZDm77|5&D#w9PPAKvbV$1{pW=jqoSx)7 z>ow4{VQ9#dW4^!GOb!7&MkUs)ZG6C^zjnHEfAZ=`%{z+bb6>b|zRceutoXbI?q1vnbm2- zbZW=lX{xj>69#FG4nhjpw5|!UaoT?r2>?q^gmj)H4(bgm#^o3GV=;D2gMW&a5 z8U-RwRakG9*279}F${_s1E0n;e-J%z5YKazs!RX9#y?yi*ZzII7yBfeUF+Q|z8Z8k z|2l+vJVk1O6-efpDAg5_&twb50r%74+xp|R;Fj(;L;VsrRw`$2+M4I`9sKZ~Xr|?+ zE{Pazl)s3X5J`{yVdn zSeX=!+O)P69B@^cO+!wE_EGKP3Zwg$1P|cnz5hcSuyYHrV6~7y3ke2{s&Md%ufsk z4O93OHrqr3?CDW3(-2k}ELl}Zd+utBwKYbu^K?Ua=wllpIp$pg;!VNE;R@82DW?=R z?mMiwJfnX;9fxX%)GL~E~VPdYb1YA+A&dG$`M6BmejR-Dzm zG$ruMV~$hX+PYp~q1S2wWFizWVDjDC7;D-9t~(;tmYT;%PYuB|qg`=xNIM=xBj662 z0;ojDoh6pS?Q^l0ppki0ajQ~5vZwIULdny{qqn54OgmB>_E0#(4J|l{n<|FrfqV<> zS$OT7NN4lN)i1ltRk8zK_H5!5{Nx!e+XQ81%}!g3F`9!+_%2QUW<(Y#s+V-jICWbT ze%4M4ny}0yiP=9SA$%!kdxBo}2E~q4Zr|@*>9+2%T!Gaowkn71=qzkT$oE*L<)x%f zfF3}F>CEQxmFABuR`wY*@)bI6cmzA7`vXcv4eOp@RK7D=g%n|r-jEN5YremD8*z(A z*i0A9olJ}j86}^Xqgn*qK`YlU6p20g*M^?QYk!c9f0;Rl<@mSna#w&!6Eu&Y41~$4 zr;x<>3%xAPX~e~x7NNoRqCKX(BQhs2^nY1+P{=^NvuZ@kTtq>1x24MpzSea&?JVv=n$yx4RSDb?MIW8`V8gKF7OD68^7|3YqACv#M1!S}Te4JbsL zQU+x8An2?!ante1Hkv!dL1{eX+RVDFYYe(@lT#4_xlvk!Wz4v}G~l=8zYjciCOZO+wH_G^_&*Hb@#a79IBJR~OLuaAZH1*gT-Ka9P*RV+NPU zY!S4VqldFZcPN`s>U*!uZC9Fc!BFi%e*Fs?Wqd<>cHi~z0`=+S#(Du$AX`9a&`Mag|HWOaKzr5U57>{X6!(z zeiaxbl?y07Ik^W)fE$p1xzl1V2JI}D7vg$%_QH$WeRyeQ58<2yPT`?P%`HH*W@1E2 zJnrNmf$eQ;Ai#SSF3JCHSlydn)y+R0!xEvIsmpLgy}XMebn5B0qzRs=1aGf&oN&5K z1ekr9L4FoQop1SMD^(sVL-kRZ<9~h208l-WE*J_}B}Vfx?SL7X*BUy7ck74MILZ<1pQq4rHdRR z>EJkt73qcw$6EO3u3TPOJtfy*K<9t$^dA!UZ`?ucYV#se8y33NqiQ#q@RB<10k(PY z{ZLo9*Usl|_G{(`!p=6kSAOq2vpTV2Ua4c`yL!(@T?rS_KpDgu3u*ey&pXa?ut4{o zFiAp>O>&E9t}VSHNwsycR7p{t!BgZ|BZ_tEf2mysy-9T(gz$NQ$vj?PkpH5nZarrb zw$@%A6lZSaDzD!1B-l{4z}&fp(bJ4L#9nxf|0zIm>s?#L4L-50h!%&ndnbWJcuJUh z?`xvx&ZZ9^EyMk_I~DAjy6!d6rnephkDQKB^yQ;)ma0-fya_lGHCi`LvxK{jP)vHJ zK`_1tW%-hE`C{_Mw$|j*-!?SE?0@b(YNrkp9RIcq3R4g;xsLc ze=O$^>UmpS$~vYGoLg0&4eH~4dqd*E=`~@?Y5Nqm+&T6fgnSvrofyH)Y9D( zV_u_n;`vU{hqi^&GRd%=>J?3T4LMU>%JpJq2NitP;)Qey_wnv_VE@2vKrK9kl(1>+ zISaJw96t+hIJC*l-_S>NBK;VHr{{_#$n3mRf@|{(cH)^TrTah zPA2CGibRp**dl7=k?Tbvm5ZA0d|0ReMck4(r+C&i__v;s*xsky@{dTQv2)aT;G0ZGq8JaVQHu%Zm zc_KvOpob@GcQ%6M#7@XF_a3MhT(=8N;IE@y`d~lDazzf_2o1E_b%DMuZ_Le>JFzgN z$^hc_czkqQNL&B8%6mE|r9}XRm!^kEhNszQ7q&S!!vBn?b)x!B?JLbT($Msd8m+#A znM3I38Kz9h?${{z-r>9JXNhM@b8-tUX%KBlP3Zzm^$Vb8j^-Nhq%X=)deY}*iB4h% zI)cB?RU}P9IByAQ|3IRXu=N6NFXpK$=o_ZX1U~sFW41G~e0cP6@Yt9n63 zRbwi&hTdS6gzmi0Xr;gxN*81bb%Z#Gp|)2VKqu& z=PoV5c)M7F7yr5HOC52VzRrUGG}KztK)REy3nW`tmH)z4a95p2)d znn6k?2eGjWF4V4Wh6K9SO_0X_5)pMuQ^5wjYT}C_(tug?<>C?{(o{3L?ONvAok3`Q z5GcgMg?6YJFHK{eT-6N00v@NyM=oS#6v!$~?)>duu0+rcKO;KO;4@}t{aD9In%GG)b4(K^MD!wmp zPZ0_#d=c^qIvxPJ)p{iP-p{tPIATZKb(u!5b(k)3xS~fe^tTbAHM+X_Z76h52<*97!}&q} z+4}nAi0K&PT1Qr?CZQ^e;05Y_E5m`BDX6lI6Ow9%^robq3c1mC4gQ{KJ1lTj{Z#~` zG(LUN2B}(c+ffd?u)6O{P&*2s*k^0C0CnwjXhH;cXgQ8yWBNHWB+np%)#KJw7V7}JdCv?}BfN~vRSgs48e-2D9 z5r6^Q(_q6B@#wKboi_c@9!-a4OviN1u+?2dmfm#v*o=287zyB6ogjt))h@j_OvB+U zI;IYe@*4`XxVP&Alw(97a?q};^WcFvM^MI)o!Wk}6h{EE7kpD{wQu1u8DS0Sx|zol zCd!o#@0M`0XZ)Zf^1}{l%BX#_px{O^vK#S?TI7Dsaz(%iKGPuc$;=)a0m)0Jt57eM zMK^c(KdrRdD8df-2Y#<8Uni6btOp;jiux$(j3h0l8Uu2$soApYwLJhb8lNz_v~G@D zD6y{-%M?`-QrgpgjY(#_R;{pk)QRV_^5uBmGEd+oUL)Yt)BowcEm_<>(39FKXyl@U zI>2FFse7ztMEnc6!x+eO)24L8QrKPBRGZTCedH0@upuk`cThq-U22mp{;9}*L-|e= z=I0K+WP&a@(>jU_X=#b6CT!)2anK!}yKHiqTymTDfEsM$#79U}js! zzdiSbMGjq`0-?bz)DwEyOpcbq8}kYHPccmq6}(_NDX>Tr1c4sZML$ajaw;lrt|fra z?uB)(dY9ix!;?LHIe3xANS$w&fy|$T<*E^CKCwp7x;^Pe-O>5kpN@^^Yen-C7M7$c zgh~y6>w?!wiW@9KF+y4BI@yB>S{2#u^{NU_eyu4#EL^~3#ReAI6*9iv(XAl$3(Mi0 zpE|B}wo@PB!7)_#{7Iky;QZ{r(pn4!ryG9hk?IkBCg;6mfj{A4R&Enwrota_eam3H zk$fYpWalVKucEN7s}rG>Cm>Hc0%NpI>oe_Bv}s>+mavC+j$3&s68 zsz|R;P&w`d-(GhqYMu=9>C5yFVeZN06dC4aEffhc+^3iTs1e4Q4RS$GKc6J7j~;yi zfIYRPW_h7J!)~Hb8vwA;SkY{vI_pf62P#vF0rf9hJ14k~JjEKJ7bkUd@)a7Suc~Uf4Lt63l|0ia>5i){x8f`@Nxb6uBUGfQyGk2@M34d0p^kL)8 zL;zNIfK6v8Vz&frOm*y_eky(eDMzH3e)kT~hlZJLuo|V80$G*@gL`M5_Iie%v_MM} z_JJ~U6rdvDV}n!Oo#pnBMph<#cntJ`YF)I2#y2MwHdN5_l+Vv&1lsx_IM~+0rm49k_gfCGo@V~D zO;$OUsG94`SJQO$m&Yr5Abj+}tmwIfgfeqm3qjj7eJP$uE}Cz5Eu!_vOC9B78CWb<1C?i2%AkrQ52BSCH|2Yx56!@EmPP2qP6P8 z=0+9Z>DM50ws*&m-BrjWehzK}gmhr9?GwkTfdk_u+d+hp>tbxaIaqrG%|mQnk58Q> zoU`8ehN%&2^=8?ZFNoAQ{yOCJ68OBF!aE`b%F zj%_i`(xbm|*qrVhE=178Ng}7UgplS{d3W-v7j(L2%=IYXE}1ZoPR)Qish>7h*G@0a zLbXnqHYB>20On|?>`0EnmZU7?`;t1&JcQk%MNWA;UD1;4n>>-`BH+v75yr<%>{CSJe|-X zpUSDMdzHh6<;g_kAMBoIS(MX@?OckS1tq%pmzrVOC%t*oEn9dsxf=0l;z$WsA{nckKBUMQC0zctRr%Xg2^c4!M~6 z7NMcP$1eTwi%McfSbUHKJ_}7)ckLTDv#VpwX`(T?d+?PzGOjeA!Qo1k{?%D_^g?yq zz~RiRgv#KuIefA)S`v+IhUTQ%TqIWOlZ4UNrFYI5@JfYik39A zvDf~E@BQ?5M&qKI9Vy#82l^x|RUg8pd-wEtnJn2e+x^rtQXL4R@xQm=}@ zRtu%#0%-yuNV%&=Roa=VbIVP`^PQG*cu{c88C6i*!@1w~{WwU?;_VH(1gu;S36~!? zmMk^tv*~NV(mR9U`g&?lbu2k@HKVkie*m`Gw25^1Y5KtaO8;t7dAKz>yqmmo=Q4{} zte@N({e3~>mK4wy3E5R0G!|W(V1?S;M;j}KMEH?uJL(ehzr9bS`+qAiGeB~>v+XPq z_n`qirfp<3b~7y{`w8Rw9j1AY65&YN*X$Y+aH>;VQ=)Z1Z3ycsGD?k@o)@TARc~Mm zK}isr%)Q=IXu02S%jdOvM%vJ)!**JxMTjx-LHMKkJbFbB8n$+l7@!hmA74Sb^>0yW*ckk1$G%pfV}CI zmUy(1Hbp64w_|UbkF_e-Z@LMm?hdl$dtoam3lLya{Eb_}(KeL)gn#tQX~kSJbF;s^ zS$Eu?@=ib|MADF$1oo7y?lK_K_9XdPcpk=SXdgxuaE`v4 zE177*d!=03>($MMR3I>#I%mEfI7?)H%RP2ahJUOZ9B$)-FQ(hCZV8eRtFrwTrUef6 z!CVqH7lnXW@L)u++w3I8*+vRrjtadqWE9?R5OInUP|@H(vUAV=Yv5Dl4s` z?Bdi*?xYwreyg5|Yb49@5pq9WJB?pB&mYGi*zYdoC!?kfCnVS}`_;w28yh)s0xqAC z0d|`zP0Z&B*ubf1e1PP|RR6ain;Q=73h3002MFA#C4DdP_&AeDaIS zUl!{{hU{BqSwhqET#jrxsw~u*;-#`!$*cdZcZy}>`?;ANG?mwxea*^sR}jxcC41d7 z=#4Rn`6={r;z%HXz$PowPG+D*TwsJAs;WJRwA`GTCXcUDIQ2E`keZ0Hk3e$4CbeIS zq3rDyfltOKa4KxBy`fHKQM4hzPMM~id9C+!^I*Gkv|xubaRYX~z{!?JbFN=!fEJ>Is zk%w57YcSPqsFb)TD#AT$0QJTSgj0yb0!jPLJo?|5G2^X1&nlK{zG;a1M{v&rF*Adu z$~9swcZTDP9fQ2hGiAajx^E!62uv>h7lJxhyO1(Z;LKp^k>B@2 zihh;DE7rmB!Rbf{ks#5(x(6w~i}?;V%UeI(`<6${>>UU>vU~!zNmCW=sN>)A(@p@7;K3*Buv~fElk<;^d zD$2J@%pP-nE)jhSrbTH;HsEA?Cyz9XEQ!f6<6EK>6*$%UZHsjG-?DQ7Fl1c+IwRic zzaTx^E_o50TITrA-hDb;()=`NQvT&9>FCi50 zoi!r??0QtM)UR}{N7#{?(6nOv_is;=EohZt2gW{HLHPCz@rLML}sBm3L@UGx#qxI13e2))MRaUjo1_vrz zL!}Bq+YYT3WiVYbF+7SU!@}aWvSs4`&0_ZRLz^wSN__|gD@1uLXp)mq-KukN+| z4yeLhbZJqVaFlN!>A*eI9G7q4MqwihCrklO$~hdCez9nBx`=k#96sT(y`K1?2@WLT zyJ!)K_q1&gm5L_EURP3W(iR`zoXR_-3~e5z7`b|{kCHno;4%lSO>?YWK;`RlfQ7wZ zp$WLeQIZGmL61`n|Bu}6)MIGU>>aM<2q;%y!rdw0$riDzyvGeglQbSRrt`~mMA&mh z?x_$b=25)s+Hq)H!zL@S3?xZ_xmAi;J$OT08m8gfL&Z~Su_t+Iowa(uPSwI0NnC;x z$LnbYw+XF#2DlLDgk)#Ur+MhX#(WTVj1pYuM|Ld%lO`oL{2f6(s#$p5%x^6JkMmMT z@lJAF&A`s5JZAB@y>EUkDcnB@(rG+F)bGGQ`$7WKoSyxv&-_Wa+XRm45fs}WXmtO3 z&!fyR#^9t3N?+_V;=)PDr^IPADt^$p1fant>H1_)P-elNzxDeIhgHK|Kg|%1wJS7Y z)OM6rVb99Evvn3@rb>+s&8jLF9{bbzY!Gyh|#%t~a`4L0H1RALEFqmc00?K#aZ<*nH4(vMIZ-&IJh)EIh#c1fI3-_~9yCl}`ANggcU#a1oklyc{C-%uC zNOEwH>Wc{mXcJ70E$n}0zEIdBl!TA|5+qCu00o4P6M$)9&D-@SPi z2sR1lzmrjz`NfxOz{=v@t-ARHhYXlq$le#RnCYDnQyaJTyTv4$M2SR5!s#b3IU3*} z-@i<&oTld7%gGRfLqxQ@|GMuy&Eso{U;AcUpH@t;*W6>ghmPm6z-9vCa6H9Mz$|J7 zQh@%%r`0m$@=rp=BiR*vBfMP1Tb=#CsKz_wqZDY-FV4S&nR~ad<-DCp{=?wogXy*~ zcxnRE2lO)$zdLm6WCk7ReSG@>bg^!BI{UG- zLei3ccWaA;%YuH?s;)E^>tFxQBMet|WUt2xjzTi9BTZ1zgb>?i(0vJChejZPpe`>LGgz6B_gq zwhzeQ)$GRX>y4p$%BL!KD2TKA*BIx_4dhf3vcA?_8pXXW2~Zx--2X_@9*Y>?JEU7x zWO5X4pqGJxQ;H#ytz+-r`;=N&!SRN{4Nn!x9*+8yB%S9;^?EfiPZ64R;k4_q*k)r5 z$ysggHUG0N4+ena$A+(A!;YUuPE2s>2^%!3FXW-Ckr5bzrMkV@9nqp}bK>!k-HrdN z@6hyX35}DHJbKXTFz>d5)yd%IA``bUcPVl@kQz6cwJ3k%p>n3#6dXGQcOg06%iabG zOlIE(cwe8xEJdIm763d&d6diCf0(S;nlBB6UxUkr6TV<-P2(N2txIiFvdp2?-D4Es zu9j&opHT%U8yrLAT|t=1TVR76Ozl2~UFvx``P<}+)lI=D@32t%_U%5U07E~!!=9@T zYO10^UxrR-4DQ@$Z&hD;+Ptw<`T@kiIW$|E6EL5~Nwz%#7!8nf>yM+AwFyd=@8p%R zqx&x1&Gw{LqjYdU$x0W&C@6ish6nc>%L&j(Wr&V$gOLTU)KW5+R@!saRr~cmOaF5E zL=XGX_c6?3?mx0&Z^N#yh~Bspv4$cj0Dxn_C%HI@9X2AJ0Z}Z1CC|{q*`@I%M7Cz} zz&0l8-Ew3?P;iY44B*qwwwA*y=eoP0(VHfltmk%JCx2M0++8a|?njm$7TC;q%@Y}A zHl)hdO!0SXzB&~oEt(5xaC94n-aY0=5VB-Dxd9HTVHGkAP<#|CffoPb&`vYHD_%zW z0;yFV#v5sN=!%-a??FmVOw|Haeh1#7sMg~n%gGJ{dj#6lWRQ;T&_sbBoWFKcKSE-6 z6sVjNLj8x?Iyu>lf1(ySMb(CEZAWH8G&yflD2HO-DN)RgI&G;Q#c0Wa9jn7E>-?d@ zI)|I(6KUlCQCpT_S@du@kgd=-PCS13%O!0-51R*y0omrIxNUH)nk-oi@qX_!jsft{ zI8RPSSi`ol4G;)el<$5Y4Uj&v*hh+Vk=;tH#?+)JUHiip|Fwo0Bs*d#HEB%=AH+gi zGb@s-*&k1m0(lwwuL6vnwRk#UN#ko2jW(}U5htqPMs1$00uVhHE?UxTphGD8!)Ez$ z2P#Eak23Q8Z*2l#0S<)}raoej%g#m1Vill#1@E;_m~I_}{yKZT`p>PphPy{$HpCA9 z2smNZ>Oaa&L(I##$ae$jx1PXOrv{^y3~}GnangpB+E_)RGFid()0Q51Q?RGitKfh7 zj;jrxsE(=XF}?hheXve6HCs1!FW6s1!axI+vzp>33n~CPQPJUiTfLX|m27#K-SbSx z{}%O^+0{oNzttmvWgfFV)=5!gzMz@bXM3(j$hn%j;S#a_iuIcmu~VZfbiS9U1yhrr z*LAnyNk)9ZbM=YAcs#5lxIt9Q|Fvm9l~;9{qQ7WG|=gur6NQg^#>g z0t6(W-i3P;OOvJG#v2uC!4C&eTyK_qTPL720j&NyuvgV#8f{ zU+t)=kp17qxX=%<0fP06;#g~RUid;De~-{L&Ay{;^QT_TKx@3Ab7zOuihT1Q&Hh0I z%NfzWH&!)7MAOK}gvXE?I(`npuWJLm za=t~7t-XrFwZ?S+rl!R!gX(pom(nYd0M$0z=JS#|)c{|JkPlXQcE_R^2A(gU7MncL zkCs^);(;bz^h4Uj`+ISF36nm)>EB^_fhU&-a8g%Tn;oT^pxZi93-(9iD__B#P2EwK zHhk#xDKq+N=#fj#`+Ua3MY=2%DvqF)(3`cLBx0f?-NM-j#bs*N$0jT^$l3INMnW{^jqd%X=)2y zb0vIbJcs<`92MFl0a(hi?@YelRok9f587QP38q`W2trTcJ0AqC;63}X{7^Lg!FD;m zL1rbEEXrNnGt}+%{(XW z%J#t^`I38)CJ-3xQlO0}N_qAM2L5N_Uci(Hqt8WDjn1~w0A5aNZ)J(?>W}$T@3gVz7Jj~Xm}4Fc zt_8YI!FXP^KC6JzEQmdt|{eIG7D;7LGK|GUmYpB=}L1P{~uIwnwgUHN{Gu zGt{d#{Aev$c0UB2`PFzba!QD*iU)e#nRkaDRbQeyDDR!ag%Hcq|BvjZZ**du+1j0O zdSa!QSX3dy_EBZefjX3~az42Y;e!VQc(1Vh*}q~p{9e9QL=_SSF8+_#rs){|NylI8 zwp3BLm+(dy!#}4D3)4!&MAq@^HUr>Q3t5v~=VhsAx97PhFU2HS`vswIklTAGYJ*IK$wPlt;Lu-wxvk+WvZ)t<49V6!_ z9w$2U)ON4CXI%l=;ZIuB@Hw{Gq9ipX(cTk+$FhmX42T@O{`*ac0zOskRr126vnb9^ zBqpJyC^Q6}@3i%Q)$@bk$xl_ya6ao3EEFQKybtc%AyB6{fcuVA&gusjefx-u5HWc} zGLi)tC+wEk?=xB$%0@cQ8$|d9 zwvNCrqNAZAY73S>ivp`Ux#l>Wt>VR#$nDmm;N=2!Ci(XhzYytwZAl9tm%}(uwzl}E zBxNAqTAYmy5i?4ivN(&aS1F=@8w(rk|2iDeqU{%u%lHC)$G`uGs)AW?!fAxCjwk(L ze8$MkN_S0EC<5R#ok^>zw6QlWvS`29iG@T$w6XsThJ(CK*$GT0wNYW-c?K48N4=Uz z8S-p)MwEBo-7;>})+4{P2QiEiZgfF65y{N*pv;jp>CrD7rv9R+)YB$S91DBlJAP8dA$uh#o@=hK1-p^X6$>i|6*nw1Qrw$+zF$y{)A>i~%caSc3n4uUsY_RGo*xDrn^YU-n$3s*)eou`6JNtgZ@lUqG8SO*9C71s0 z5h3-!=LzmHU$dZ`k($1i3F_rj%gl$Vx))p!Yz>jYOvz}t*>&dK3G%TpW7Q$&0{lv4 zJbS%?_-PC0g$AVcSJ6jmQaxv+Sg(BTSuW<10_`w+KaZ4G<;czjj$=n!mW17T zMjUBv;P{5r8%0R$zJ;AHV}Vd;C$?CNx6BB6v?PX}or<}-*O|TwEe-6A9L@kVNcdG6 zVKfZqA6Pq3mk}~0F6Oatg&4b#wl26!W_~2kPVzt0L0H74z+8b;(X0VoK!ZuCr|8!} z=Og8JpiR#xYq%rl$AsM{KPcN5{e|B7dHZ;C?4!Mq=H@x=I0fw%C3QrWKBtOcv5wRv zQV#J>pK}1)dqj~0-)v>*GOT;PeU6W#*&%uBi!4Pvpfw`5StVMlm+ReEtE3FxXBE7D zW7ssE!PFH=d)k3Ukwwajt**QUjS}X%j#Op(Z(&veXjj8}8{PXL92By`M?kNj@N%g# z@?#dUGq|iDsdpR|p>w!GdQ#XkTM$`;A}6LP+{|hFMD3HsGA9<5uwRYabsCT<2-(Fv zU21N3fRJaCcgmF!sU;O%yb|8NMhJ`)V&CXb#e|OAXaA!EC!n zc31i;06FFyu_|T4WX(eW${!tFvc;q<=O$qvZ-OrST-20cme^gc(2PIP?WY=C-vp=dU=@7Q7(87Y z%PkX2;&(Xy0lSb_Op-Se64M|-vqpQ4DiAM!g{?f>d(VI6Mv)$*c|dh7MPmtIepnc! zna6P<);IbF@DzR1=9|N5r#{1rR|_tPnOY-*m7E=!DZ#$e_Lp1Kyu*rpGo{+!)^VsT zmJXI?#t%1G@tlDfk17rDh>(=sM&$n7c6YuI4y;|?pf^&>gA9#I0M|qM1>FTL0B8r1 zAuB2D=nRre*2M#9XaQHp;4Ac|)~QyZBm%#4QvITsRXvB_lza`U;<{aL8(xz})E?AN z3=w9f=^dIs5W+G(CMdOp-U9A9V6z^UC^WyZYmR*Mu=KZ%*-v^tBnR8mP;q`D{&7}} z-L@QsP)1Krm02(n+RvhG#Cy_VpNdb~6UPKMw$MiTUKiAEa=IcTFKsr4aPeoA_$9K0 zQ>7v26a1|Mw@UHBv{5Y+eaM)sKG!9q-{8A&9p(}#@NA3~;~?hccSe>ublAU`1Y-ZpULhSbV8~^sjg1VTFkdN_ZvkDcS zI>&{ z4UB#Q5tQb9Bgb;LU2{BI?rO3&dH8hdS-x}(JeZ*1FeibpiQ0eR<;F+(08WfB_3EHF zxw!hTfz7-X^H_925j~`-zx>)5>9;(CJA@Hkx3EO6l{DO4rZYyxOSdYH93ts1;QTvGeqq_!wSDc6t}lr^VU*3wHjZ0)x~&kAWL6#+mDN?LHFB_#a2 zef)O+k<0>*7UFi(5&XUt*o>sW6OT*871IH{Z~#b#M&sZyaa-EUW{L0HHx{gpC*fjt zT!5LmJ?NH#lbJEO)te6gL4P%}BckHlXZtVgc)SZ%=Jl;-7-U}g)VdX0DEhxEPN&T1 z0lHxxToJVaxkM(*)2Y33FB=6WSZB&1cw(LGHOacIXP*mnUYJkWWNnRP(wXc_eJ@>P z6F)e~wjXA#B5(S_`u*?g&xfi5Us6pcPy@vax_bx&9_o(cQ!OD;41AA6+p9(M$Q?O5 zhJbQOP4&)?cId&~1BxuAVxY6>fX14a$T;5KtKr%1SK69V*n;yM4iSfN9$*b$&gh?4 z_V?%Rn-S_k4=_ODH_a!j_lKkVz>_0K$d<5uB`Pp?F2Qc2!}4c8f9n9-cE*w%B|3SFE_tA!&KlvZw&@{oTt*XiD+T%gC%~{_eUM1Dt+9 zrF-zalbQJ!s&~QGem7EIzNzY+NMayZ1$EUvloR}tvQc;+6P`e6@nvIh!_IR3L4j-g zhb*E}8z4`L#=uso&WmKI5BOpMBdJ$Z0YDHAf_*Wx+vl&D4U3O(A$oSV05 zg?`G04Av$gKlms?9i;F~d~04)%N)F<7}o3)&<0j}CnqqjUO7bGIL+vSt<>D>cP?vI zgjxcpE3Pc_X!7ZbeNDzF@sAZ|N+M*w08M`x(Fd%|=|$UV)sP+(>f!{%l)JU=xLSM6 z*|wA?lXnuz$xyR2;Vu-7%f~7r_!->A9AWTRjDCVs4L!W>F)76I*Z?|N zwoS04C$eP=R9op~xB%=L%I$ig#=0Oj9*0PqtyQOb#&4T-PK)$+X zsw=jbzHlhGpzrG=?B3^RqXwUS?AK#$_AFG(EcoxYhB#In|Uj7h$LgWH#$LukLQ-m6q4R z)~eN4euR{g$=bClr4B6(ZAow6%Q;rrbI+cAP_Y08zS%VoR_VOEQdvZQHX3Kzj}J8= z$U@CSVff2a)xA`VR1k%72do>I)@e}FU~lAbeE6iO9SS{S4<9A|#L5^0O9S3u)JCdI zKw(5l5OF+M@6dEM5d7j{gr{!%@(Qge@1#~|Lch7{|Ps zg$IifNIw%>D+qYsNBo>09hp=;CReASM^+AB0rV*g)~Zlqr&WZ&<=?Bx$S#yCtzaQJ z5wJ>)FEf&Xk>K7|5Ly4*7W$|HGijKFCBQj^D}~f!wBYZ<8vfT(TN=$hiz1ZQB9%ua zwaE~Rn+yCrZy8s#VYM3>3dwanPO??(W~B5hu~k&Fs#3PS$nm*QN=!KOxWYJ)LUrgsQQJ)V;6 z#z?+jYw6{^0pHnlDZ;?m^^2dT7Q{8^C12MWwPmJ_rT^asqB+LF96%fbAsuZ}_(lkM zyFKCZ(+?{Q$Z$i;zsOuMM)5u^``_xa)5Uz2wG|0&0yAAKL#QU`ldlxuCSt;s4)f@+ zvy!%fLX2GqD2tPo*Xxr3F>~jrmJ9vB|AWt;r6l0$dw7bmbo?xK<%` z`&NBL2nbLFg$CkdHa?fA_N*lDtr6G=CHRjfc%T*4rEe^uixmqJ)AMd$c^nObJg`Fx zmh(aGf5Rtz;k*P&PpHoUjl0s-ymSzw#g3(4jEW4RYs8O1*L5Ls>XODn7Q0BW4%Tc= z)1AOwyrUEE0v3%N^8^9o>rnQ+k^v66x6*(u=g~@fq8Lk-rk9Q6*bcUAu=|pNoK<2v zd0aJ%t~m(GO{@VQqgWTU7E0g&+4I;RdGpqnn=fxb0v)t zftR^5(T%Y!?K>Fyuk0h*%Q*S96Ll=S^}^}d2I0f6cC=m z(ASU_v?XK~0jc=t>4-}giKzPWgsj#nUO>@ucs{(2IRpNTi#9z@pDHvj+IwQ00Nxcq zI+1_iy1g4YWocdJ7z4>dflFSY;jh8>z<*Lkl?HpJVnXiB@wd96|1uTYr%7+-$SSp` zFgM<9J`SiHlvaE#^4*kO<$0mM)b+Xnh=zvhkM=bibn+^_q+a^=*uMZeDHn!>4)d+@ zJH*iww%MJ6IQ06j1{#Rl&XJI>8ApdsbaCunyi`*K9_77v^VRGo(bYo^(2(_g59bY}o z{*T@JVtx)l9)Eu2&sNp`kR1JyXkvdPNbiQaP(3$DJO~!ki_+6(O2R^!_$iWY*z7!q zhw*(-FUI4GKVBmwVi(_jR@W$gvAnx>%tam3WxD5B$5*dl%fpdYvm4&0$ zye*(T&NhGYiWy(HG?Kwa{B$g^~E5z6bizwu+_ zsiJeN&C8lcJDT+)vhwRA>^~`oo+@Ys-$zXC_;^Kl>-l}>v^W(E1;ovn5Atx! zrzPml4d`M)Q*s41^LGI0KV#hjsv_gMtJRVU?;fX) zWvn1bp~BcTi}P;empO*i|6*8_0_}qJTR23>jeK}uM_i`CJ)%gqO^vQ`ik4j2>;Z8$ zhVkV+I^;jQ)E$a#8l8o(OV*vaiemp3>uPZX{k;gB*RS-l80cIVxiP}6LU*`i-}Xca zezx8oQtN)UcELFPhkdR}FpA_gVo6p1*s+dCY`*mY7_#0P>F?f=<)(j&-6*U zuIp`b5Bop49_MW>ZLkg#_JV5RI*&+q0nuKvE(q|c!!;yV3gm4PaK!uCiqS}N`78Ft>dLaT-GsEmS~nSqyiz00k$#{J8>7oXN>VfA78J! z5CVHos%WsCu#B>H$S~6$Ak{h9rh;`zRW#**d)KXAPomDF=Ie`$jxkoR$Yu~J;_JF#F(#mN}k<=|$dQUrQ_ zgnjbpRnrYz4?a>pXR9}&i@+z+$l1+73*O10~)3D)y1 z1%85gC}BR#ybU!S?9B@hP%e0cuj0tCDG_jmR{Qvl_xEwpxz_Kjt@wX9W{rw8KIbM& z26P;A`AuY{S5X5TayvW2vh9wlC(`QMdLV+b@_u?;t4mng?PgO3@q=o@t&D`}W3d4s z7dV9A)z^6y{pAxX0T76FU31!aD()6)MK4VVy-6IF-@6IS6~iil`72t`qH~s zZ;`5>k1ZKoJh|(;NocKbo6J~SCOVS|WcU_wq8dxCkfMYBh)v@nsejf~QVYAL5vo^; z`uEhY)j@u`yKZJ!s?icOL5MGsZELrs7yKyGF!ny!4bYo?aW zORz3Y)y^2GzHha}lG!nE+D}%X4jwq(((Wm~=vX z$jt#Y9%tcE90)bo6$i0r)>E8Hb345Q@3BU8*&*8q66fRG?u1D7G#UAS!GK_d%G&qK z=tWoEy(YH_TiX1vIpI!&wdcqh25rwuaN{G1=GdB!Y$Z#UlI8zzn|ZPa1^v!mHqQr=~EJ~ z6%NEP5C3v#$}a`Z80ne608Ey&JWW;7kLr7lKAc1t1VL&zltof z+I8u;{t8>(Bq){ebk7rGx|!-7@^VK))Y*IcXJu$sh|-zSKx94yi4H{n!`r$)NP;5Ph&Z%r(lh?$YS`uf#Xn~n|_f-KK6Qod|o7;e3b~Ls%>fAd7 zd!AmMI=Oaa=D)c@sO)q5Li3(jCxl~z{Zu$}KUKaR*FrCzE)bQ4Tzg$tdf{!NVrMY_ zthYHga%|Qj({;;JG=>xGPSG6}ar#dQ6(<2Y*lFSNxQd@b(_NAEjcWHwSm~!%_GSVc z+yWvr#wOnbZ@t(Ju9I>3me5DzUf&An>Z*Om_LGmvj^##)1q^?G6W8jKM&_1o zPxRM}v8%KxsuGZmc=J)jNYN@BoIseYm1HTg$kP$IT&S4m_^Ak&0q_xi!OD+}Cbpju zgH&Op1Os+U3O~hIpV)&(a#923e9G}@HM$mPGKMfjBq4kvQ#46Lcm|qT$Yw9Zl&J3xqabDxrc(puX;(Hp?0=Bt|XVD zvrt;_q0+B4$pRy{8=*#qU)Dhw z`qj?jwJDLQo2qh?ir8~dF+-Xw8ex0ghgQYfGS_weieti>BqL|!+-a8aL@!pmzH6=bv?_D zLYnYpLhaYFCeS(VgE|q@)lzp(QDie07`}XDAr9jHDM4@9gClK;-bC2|9diUT{AL4H zn289RzVtc%psNdafz3T1Cp%bT4<`3kAGYwGXAV^sZwY~XrZUJIbD06` zV(&P!0RZ>t;t!J^-NFB!XoSg$+&qFw4uqDMpKd_dNhK$iz=G1l6`Gv2|Fzjm@cM%3 zvgJ!GM)`f}``a1+O%x5fR44Oke-N=sG=>%oI z$3BR4Uu;%`t`xFvhA=w9q`nI#6l5@{m$k(FSo2sJ_v15jobuA^Ru0HLN(jIE3#HH6 zZwxN!@>)8~hzTD6d9Arr>_T$u?->(?;tN=@!VhC*28c*-;}{v7Hq0d&g5>h0cD zALmG7is*^H|Bi98M?_L=lO3>xpcn2l)%eTG@2pG;i4zR{UtwuIlHwdP;- z|DV#Qe_?9~s<5pe5!>Ez3eM{=#~xdon@h?Ta(K>QW>LBRpr&nlAt|#0Vbc7yH0^yC zMp7}X$zmL+vL~2;88TTTu$-fnvCM1vE?4km1bryQoBsG>E{*I!T;CDg@3CWsr3h~A zhHoxG9pc~#!|`GT%23vYa&W+@iKd!#@-{4Rrf6XhnoukuTb`$edI z@oOb22^J?Vt(rKyQXrVgiHA#33FZo%eIK&tItY3X*XXIX2@(fP0@C9{$G$! zT=`%N(l?oVj)zSA)6u^ji1N}C{|ogEd2jZZ{7wlBMqYX6Yu|YK@aT;FCYgrfV9~rf z%yLDQpWYC(_yVA^VdzN8b=PCtE_BxHC*Q{-0lLuA?Jtl$BiI5bylb37l&?l3Y(_x# zR+kM;SXzUhP?0vE{MO@jL&MW_bz00EL<+DaMF~G|nC5U&Z%v=T zxZc!~Jg9T>YZ2iFfdKDN`F+3upp+?uvQJsytNQO^jDT1g2!Y;o>UHoF2WQ3s)gCvw z?x7m=mE+i9{+T_Fj2pglz+E2NuxyLbnZGp`jNP83+V;$hkb%U7F(>GB&VE)bYbLtR zlUuGBW%B`P0eap2MEh`*$Mv^>#Fm7N2B}@XW{dk{0*Sf=>xzC*_4X6#OYu<=CZtHm z8CaYQwu|aF`4ztkzNuy)SO6PKy{Ld5 zaur8bXwbzaC6d&r1ZbU=YDS`GXw+8CuTVGZXg&v{ViZ2zT~U%d!z&;PvIZ>!ip$%* zC|>CM|CTm>2c!T_6sj$&$QW4TaM;Pdo41$+>T3z9?!X%c9-W6o_RuuK z+&YPIrt&ACnu`V~HLpNVcYC$>NLuD!*U*lO7&Y0se$Y4>Un}qI)=30iUX%YJeJi4C zYULbw1YhTExdh8bY#NxoAW5e``6=^=US)fM=R#*RyD!W-CLaKTaJK;9y|$C5&an}j z-wCRH^KvXLiGIp6%NQ5WN}M7wMntWa#D5tAv<=tLO>V|;P?Z==;DH__BwQ{)?};RB zO+9@d%b=h$Q{VIibi@h^E&!3Ig2o8sK*9QbgM3T0w3J2>rF{u?GtgcTbZ5AE3F=Y& zk84!Vsd7`);2fz)bYMxmJL-;-@p0Q?3bS6k_uAlGP#GghA?AR}1xA`E2I|HY$jLz> z;NP;1fZYkU02jdiH|%PSKthHsGJIcBHJN+4{+vH$`>)nGTlT8cE-_be7VHqM{(}g2jM;HrjQgc`Ng>zakm*wKf2JOr$v-sDj*XQRd+ogC zxpyzbZo%Qr$6UXiwr_*}8#15@Q#eR|#rmb5z}x(sITm+QptRrt@s z^Zz|hy41x{8#Eh0^Y$RuV?8AH_tcXP{oI;7y<9wtJURIqoqVW_@R?<3l5b;KD(m9+h`x(nfRcH{1+gs z!o4HBWjY0;?s1&QS3!?T+|_t@g?(z5-`p+uyommfPxs-$x{N3V3b2Yo{GRh=_nxx# zuH{CzEfR*w4fwxJmshca4OeY|PV^5k-S3z~EB|fg6DedHE<4{YoZST2(l4VNaa0tyhY;O^c(jgCKw=2k=HPiYeVv%4-|(d?@M>3z=qSF&k}0 zQE79XRP9f7`nw7vzhDgo(lR@RA**Z+@oOuJ{RiJoh8`}0j*zTo%|v@fL&A_+6tS>6 zoibL=$O@`B>tpM3OAtTcI?D!piGN4`fbiAQhjjScnn9j*JzuL(TS!%mti+!;>4bT8 z%9U<;o%jRH{`OmTK|ys}h;p&B5J^7lvE>`U9kGA8xz-9eI*KgvL@}|aB;+9&YI_^e z%3>H;dJcs4?Kz(O=$@;WojV+;sj`oix9U4GNLU^m4{YNBdz&l6C_ZDRJ zEe7;44KX6z4ltkSl^2q-4i^yYGPo3s6Cbs4YHMv2v|P#+lUaJ4n))586l$Q78LiR6}LMf}YRohO!^#s8JeX z=X$Oayv4G&V?fLmUQs zA>y<}ed1w_edhWe9+v#6t@R5Oc4rj$D9ohvu-Y{MAc{Pbg!K;)Ro02@+;FZ|U7A0G z<#|`O6Vt3ID)~^i_fI!R9Bs1|&@t$~9dJW~!kd5gs*fkhwU+l5=jZMuRNAw1f;*4Gd1V2{zn9Wung~HlVAwV7#bvZ-YcR-tM9w zAC{4tR9zVRgcQNijA9c~hbHc_tZ)huCqcV!x)8#7$+h0elmZzL-kyvk&2r~a2%@i~ z@>`3jM<5Yp46g21Ctnd3;Rp_(q!t_{_tk6jRLKTQuZFR9k3u;$#ZRlWY(s7 z(3sH_D^O>%k4koam>X}7gI>;mgJAT^JzYGO4Xzs5e?6hcsK&+uvje@Z8pbnQ<~|bP z2r+8{Wl{z=Fg@Tj{se42zvCa&Y%95DNPRUWJFfrxtl>Onc0qexR|3zLyM%g!D!O(u z0nIOrpr5W#Qc&QI!kjz;qr8q_j3foGfxHxhNR%iiosCGJ`VA%s{V(|MA&yvdyg%YZkMCWP3%4I2yD|3k;*hho?b%9U zVL}LmrbgHzI%q9MN|6S2_(sP4y3C@Te7!<_}$b+iCs{nemgZ8v!U zYBUYZUAjj6Y_3cYJZ7L6w3srW4fh8}nLgJ1_}}8`RZXumE`o}eeZ7NO4}BX3&dPSl z-QkfMOHIYP;BCI9Nf#iKwK9zGhY3bp-Psx$h1#CC*}Mk)55|ZA5t2Rz7&j+ z-y8JC(~~!>z_kgW%3GMA_=_$z`Br4p3;|YFZ(c4~gALd_RcJT-2FWQ%!) zk9NMhigsT&f>TR8&cbmRjs;4Xzi$cdCFW%b(V0C=vBcJ!3G6}+mo6QA@`YmSSC99B z_3p0Qu)fH%(|l5nahNDQ>gw##3=|{XX<)=$lAXZBlh~EoB|2@rgVtCat2TJmis0dI z0KKQBNK6BzyZCsX+0pQAkjGh!?qsL;K@=Ni?s|B>X;#zJpD}EwGn4B*hHJj#@=i88 z+Fm|$!XG$25UCs^UAzeX*htp=z$n8YvPJwqA8QLT({L%v;3ts^AG^m35bLysn$W?GMT=IdM=AO993B2B6Hy86XYZ@a7NYAf892?M~_b4_rPnCL$@~Xnm-JG z#y*c-pkR&UK9HYIDI(Rw>Drk#aXh8ay7<-b+4Zg)>`LNrRWMam0>RSyqgzPC|%P&CDqswGPN!USJda!kKJ{{Q~G!`^EzK^aJm9 z;E=n0!JBLoum670l8u}4jtzTA%*%iLPpBnjifF#L>x0E#y%WHUsI(F_ZH z{syiXhBdOUb!wI~Did2Y;U}1ECX5(;^oai0=S^6VwVDe`Pf|>MDHX@!hlEae)e;oD z><&#%%F@`WMo+_c*-j8LiriV1b~Z>HD2ohi&ma~B_Y-{K!!@JlJCs;9`jpxMrsg>Y z2`#rr+<0-`*f%DHu&ju6x~`PeIxAWTSZhduX5k`acy92=^WP=yroBeN!4dNQ=>yc` zgy?4;8tjom5j^w^SmWlyV)L(t#2t!QqU{K{PkE|ed04;+J0RWFJi&a?vE(jQvn3nD zdzF)KuGSF)u0yOI|NKMd2hJgmS)ZqWkZUVZ*%&o3A7P9hm`6LFW~F|;+noToe1G@V z?bWQ)zL=Uk(ji z`J6WHpOQ5SW;z7(B&%i&+!Nbb*g1^b9T7G3Wx2M9Gzmm+F#tkl(7{S0R++*hi-Jy0 zoc@gs#`$F{c0g!@R;F49++dhdBsWoNH?ls@f43oRzXsf`-8yBvRz=i=eE*Egm{L5p zx1!7=K3@wMqi_RU%mmKPuk-I;0VRY%` zjs|@`u~8y3AM~|kAIQ zGeIe%jFKn>Wl_fYvmWCkNP4mdi;msRdZ}ZI=WhR^Y}=azCj^x7U&@z1JG_XKF4;=y ztWQu6ZaXCmP#RIjAuVe(RK5s=9!4h>MPbX1L;D9_A{k|Q5Nu*^t?SFj3A^#E%mpbM zY1?18*e>sZf>oX5lSM0H#)$T0i0kE;NR*Uu<#hhZ_~>K57j60IkQid9lhO=8Cyn;(qpnKAz9?)i$|lN)rJmlMd`d$NWZ*^?LtR$oCT5 z?ab|N6(OQ37puv_CK6MSQQr~;S^|17+lCDFb28oLXMe}CQ$pR^1{Gl_Nj?c6x_V1C zXRxH-L5&)E`1AQxJ04An1kXL9tiZt=!RT`Ug3F7g@Ou;FnSO^HqXmldDYL%)WQm&- zf=r!y!V@2$4u>d4<%4g+fK-<#Rx|vA;fQRHA-Bfd&i4`vChSY8SRhf&-Kdf{GLpF@ zg7#&;P7s^v#^*(2iWet4SB0u22O=avTh3UXETjq{I7f>XfcXaq3GS&OdpG%y`(|=t z&jN!l^IRYcuJw4vx=h$7ny}=oOBOQV16#fOMfoKUE6E~x2ZHH>1G!IX_67FT^Rp<2 z`k|CV!Qf-zBWnkF#n_0ttRXr&qpew$WPF>$+10J~Y5`IJ8GvQ7G^>7R`rUkdg3BFf z14}k^ld`uyOV`b=Zmy{%3v_A~iyZ0@*vYh&OF_x*>$Ako`1BdDJ+=Y%zr7Ry%|rKV z!DJ7vIHO7g;ju691nI*UJ(48_O4ZAzh!VA-ox2tj7=0@u;Sdr{EB6l~lWhN#ft!c$ z9J`80625rsLPAzWv5A_w5^qAsJB5D`>4xs~O^+^1t!-SNhqjW)(cueu(Fn_HyoKAc`8`8U{atg@~8 zY=;Y}e#*pfSk0Sg@&7M`i2Iqzl|#5CW&C{eq&jpOss(TE7~fE>ODs%vw)a$EkGqOz z)I)9gAbZFp(p*f90_EZ)$3g9hJEY(??;0i3j$#ltIC#ohG@PSxprVtq-y@(kd{%b^ z-^wrwy3nE103s>-!AzEfXfAhpZ59UN6?sSl%O24tSOp=-)*2bz$bXTZ!N{KM)AA<~ zL0;Wv%;rdupJ`%xe;0hm`6tnD*@V`YHU);8D=&l^5K#cjF%RoEzErT#$Mm{K`nCn* zqT0GR=S)X!%R2;fITGX>ZaQtHb!Huh-QXx}AE90jrS2SQ%U5CrZSz-s*>0~%r>k2B zl#hkW`pX3!gqz33!JF9V!m)rIH70&VwFGbFPe=`5oriv!MOSxf#fi;kIOU{|MRg@P zg7LBD^Y)-btguv*1hf2Dob5h&tjh)^GIC*jspXCH`F}Gq={eMeCQw&+yxGK=%wC-q zV=t>eTm}o5ohPT0zXhTH%K!VCLD63V@%?3`kn)C?oY3Pzfj<9~{3!jLotsf@#ME&m^ZtB6!Hu zfp|NRyI?lUH``Gc`t8d9m45;j_E;`Ae@w%UWD%}8GUCL54z=zhHo#Rng{ZoCq&VYC zh|ZaQu0awuw)!UJ^2$i;%6?m$UpK+H(L^0zG$67|dqowIOvtHsugj3cq9;Vc4T+|F M+0%jrykjr`0RE4P1poj5 literal 0 HcmV?d00001 From cbe8cb0b3c753c0706f221166aa8161e9ef48223 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 26 Mar 2025 20:19:23 +0700 Subject: [PATCH 042/138] Updated on 2026-08-14 --- data/wallet-connect/build.gradle.kts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts index 7dbf3dc44a..fab7b8d0e6 100644 --- a/data/wallet-connect/build.gradle.kts +++ b/data/wallet-connect/build.gradle.kts @@ -27,8 +27,12 @@ dependencies { kapt(deps.hilt.kapt) /* Reown - WalletConnect */ - implementation(deps.reownCore) - implementation(deps.reownWeb3) + implementation(deps.reownCore) { + exclude(group = "app.cash.sqldelight", module = "android-driver") + } + implementation(deps.reownWeb3) { + exclude(group = "app.cash.sqldelight", module = "android-driver") + } /* Other */ implementation(deps.kotlin.coroutines) From 4c7016557f2cf72158d26ae950c2eeae9a23207c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Mar 2025 15:14:57 +0500 Subject: [PATCH 043/138] Updated on 2026-08-14 --- .../ui/amountScreen/AmountScreenContent.kt | 7 +- .../AmountPastedTriggerDismissTransformer.kt | 2 +- .../converters/AmountStateConverter.kt | 61 +++++ .../field/AmountFieldChangeTransformer.kt | 14 +- .../converters/field/AmountFieldConverter.kt | 65 ++++- .../AmountFieldSetMaxAmountTransformer.kt | 10 +- .../ui/amountScreen/models/AmountState.kt | 5 + .../common/ui/amountScreen/ui/AmountField.kt | 68 ++++- .../amountScreen/ui/AmountFieldContainer.kt | 4 + .../features/send/v2/di/SendModelModule.kt | 12 + .../send/v2/send/DefaultSendComponent.kt | 19 +- .../features/send/v2/send/model/SendModel.kt | 22 +- .../features/send/v2/send/ui/state/SendUM.kt | 2 +- .../amount/SendAmountBlockComponent.kt | 46 ++++ .../amount/SendAmountComponent.kt | 41 +++ .../amount/SendAmountComponentParams.kt | 41 +++ .../amount/SendAmountReduceTrigger.kt | 40 +++ .../analytics/SendAmountAnalyticEvents.kt | 31 +++ .../amount/di/SendAmountModule.kt | 27 ++ .../amount/model/SendAmountModel.kt | 239 ++++++++++++++++++ .../destination/model/SendDestinationModel.kt | 2 +- ...SendAmountPastedTriggerDismissConverter.kt | 2 +- .../AmountPasteDismissStateTransformer.kt | 2 +- 23 files changed, 731 insertions(+), 31 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/analytics/SendAmountAnalyticEvents.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/di/SendAmountModule.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt index cfd16d8ee2..d8a18ff233 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt @@ -40,7 +40,12 @@ fun AmountScreenContent( bottom = TangemTheme.dimens.spacing16, ), ) { - amountField(amountState = amountState, isBalanceHidden = isBalanceHidden) + amountField( + amountState = amountState, + isBalanceHidden = isBalanceHidden, + onValueChange = clickIntents::onAmountValueChange, + onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, + ) buttons( segmentedButtonConfig = amountState.segmentedButtonConfig, clickIntents = clickIntents, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt index ebf69ed80d..ad6a224264 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt @@ -6,7 +6,7 @@ import com.tangem.utils.transformer.Transformer /** * Dismisses indication on pasted value */ -class AmountPastedTriggerDismissTransformer : Transformer { +object AmountPastedTriggerDismissTransformer : Transformer { override fun transform(prevState: AmountState): AmountState { if (prevState !is AmountState.Data) return prevState diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index f91822b7b7..94981c571b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -3,6 +3,7 @@ package com.tangem.common.ui.amountScreen.converters import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverter +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverterV2 import com.tangem.common.ui.amountScreen.models.AmountParameters import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig import com.tangem.common.ui.amountScreen.models.AmountState @@ -30,6 +31,7 @@ import kotlinx.collections.immutable.persistentListOf * @property cryptoCurrencyStatusProvider current cryptocurrency status provider * @property iconStateConverter currency icon converter */ +@Deprecated("Use AmountStateConverterV2") class AmountStateConverter( private val clickIntents: AmountScreenClickIntents, private val appCurrencyProvider: Provider, @@ -80,4 +82,63 @@ class AmountStateConverter( selectedButton = 0, ) } +} + +/** + * Converts initial [String] to [AmountState] + * + * @property clickIntents amount screen clicks + * @property appCurrency selected app currency + * @property maxEnterAmount max enter amount data + * @property cryptoCurrencyStatus current cryptocurrency status + * @property iconStateConverter currency icon converter + */ +class AmountStateConverterV2( + private val clickIntents: AmountScreenClickIntents, + private val appCurrency: AppCurrency, + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val maxEnterAmount: EnterAmountBoundary, + private val iconStateConverter: CryptoCurrencyToIconStateConverter, +) : Converter { + + private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) { + AmountFieldConverterV2( + clickIntents = clickIntents, + cryptoCurrencyStatus = cryptoCurrencyStatus, + appCurrency = appCurrency, + ) + } + + override fun convert(value: AmountParameters): AmountState { + val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) } + val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) } + val noFeeRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero() + + return AmountState.Data( + title = value.title, + availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)), + tokenIconState = iconStateConverter.convert(cryptoCurrencyStatus), + amountTextField = amountFieldConverter.convert(value.value), + isPrimaryButtonEnabled = false, + appCurrencyCode = appCurrency.code, + segmentedButtonConfig = persistentListOf( + AmountSegmentedButtonsConfig( + title = stringReference(cryptoCurrencyStatus.currency.symbol), + iconState = iconStateConverter.convertCustom( + value = cryptoCurrencyStatus, + forceGrayscale = noFeeRate, + showCustomTokenBadge = false, + ), + isFiat = false, + ), + AmountSegmentedButtonsConfig( + title = stringReference(appCurrency.code), + iconUrl = appCurrency.iconSmallUrl, + isFiat = true, + ), + ), + isSegmentedButtonsEnabled = !noFeeRate, + selectedButton = 0, + ) + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt index 392fa817ba..4094aba847 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt @@ -31,6 +31,7 @@ class AmountFieldChangeTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val maxEnterAmount: EnterAmountBoundary, private val minimumTransactionAmount: EnterAmountBoundary?, + private val reduceAmountBy: BigDecimal = BigDecimal.ZERO, private val value: String, ) : Transformer { @@ -58,7 +59,7 @@ class AmountFieldChangeTransformer( val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue val isExceedBalance = checkValue.checkExceedBalance(maxEnterAmount, amountTextField) - val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } ?: false + val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } == true val isZero = if (amountTextField.isFiatValue) { decimalFiatValue.isNullOrZero() } else { @@ -67,6 +68,7 @@ class AmountFieldChangeTransformer( val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided return prevState.copy( isPrimaryButtonEnabled = !isZero && !isCheckFailed, + reduceAmountBy = reduceAmountBy, amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, @@ -74,10 +76,11 @@ class AmountFieldChangeTransformer( error = when { isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance) isLessThanMinimumIfProvided -> { - val minimumAmount = minimumTransactionAmount - ?.amount - ?.format { crypto(cryptoCurrencyStatus.currency) } - .orEmpty() + val minimumAmount = + minimumTransactionAmount.amount.format { + crypto(cryptoCurrencyStatus.currency) + } + resourceReference( R.string.transfer_notification_invalid_minimum_transaction_amount_text, wrappedList(minimumAmount, minimumAmount), @@ -98,6 +101,7 @@ class AmountFieldChangeTransformer( private fun AmountState.Data.emptyState(): AmountState.Data { return copy( isPrimaryButtonEnabled = false, + reduceAmountBy = BigDecimal.ZERO, amountTextField = amountTextField.copy( value = "", fiatValue = "", diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt index d460c0ca5d..ce11f41fce 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt @@ -19,12 +19,13 @@ import com.tangem.utils.isNullOrZero import java.math.BigDecimal /** - * Converts initial [String] to [AmountField] + * Converts initial [String] to [AmountFieldModel] * * @property clickIntents amount screen clicks * @property appCurrencyProvider selected app currency provider * @property cryptoCurrencyStatusProvider current cryptocurrency status provider */ +@Deprecated("Use AmountFieldConverterV2") class AmountFieldConverter( private val clickIntents: AmountScreenClickIntents, private val cryptoCurrencyStatusProvider: Provider, @@ -76,6 +77,68 @@ class AmountFieldConverter( type = AmountType.FiatType(appCurrency.code), ) + private companion object { + private const val FIAT_DECIMALS = 2 + } +} + +/** + * Converts initial [String] to [AmountFieldModel] + * + * @property clickIntents amount screen clicks + * @property appCurrency selected app currency + * @property cryptoCurrencyStatus current cryptocurrency status + */ +class AmountFieldConverterV2( + private val clickIntents: AmountScreenClickIntents, + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val appCurrency: AppCurrency, +) : Converter { + + override fun convert(value: String): AmountFieldModel { + val cryptoDecimal = value.toBigDecimalOrNull() ?: BigDecimal.ZERO + val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency) + val fiatRate = cryptoCurrencyStatus.value.fiatRate + val (fiatValue, fiatDecimal) = when { + fiatRate.isNullOrZero() -> "" to null + value.isEmpty() -> "" to BigDecimal.ZERO + else -> { + val fiatDecimal = fiatRate?.multiply(cryptoDecimal) + val fiatValue = fiatDecimal?.parseBigDecimal(FIAT_DECIMALS).orEmpty() + fiatValue to fiatDecimal + } + } + val isDoneActionEnabled = !cryptoDecimal.isNullOrZero() + return AmountFieldModel( + value = value, + fiatValue = fiatValue, + onValueChange = clickIntents::onAmountValueChange, + keyboardOptions = KeyboardOptions( + imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, + keyboardType = KeyboardType.Number, + ), + keyboardActions = KeyboardActions( + onDone = { clickIntents.onAmountNext() }, + ), + isFiatValue = false, + cryptoAmount = cryptoAmount, + fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrency), + isError = false, + isWarning = false, + error = TextReference.EMPTY, + isFiatUnavailable = fiatRate == null, + isValuePasted = false, + onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, + ) + } + + private fun getAppCurrencyAmount(fiatValue: BigDecimal?, appCurrency: AppCurrency) = Amount( + currencySymbol = appCurrency.symbol, + value = fiatValue, + decimals = FIAT_DECIMALS, + type = AmountType.FiatType(appCurrency.code), + ) + private companion object { private const val FIAT_DECIMALS = 2 } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt index 990e027cdf..62280c0ebe 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt @@ -15,6 +15,7 @@ import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.extensions.isZero import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal import java.math.RoundingMode /** @@ -26,6 +27,7 @@ class AmountFieldSetMaxAmountTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val maxAmount: EnterAmountBoundary, private val minAmount: EnterAmountBoundary?, + private val reduceAmountBy: BigDecimal = BigDecimal.ZERO, ) : Transformer { override fun transform(prevState: AmountState): AmountState { @@ -42,9 +44,10 @@ class AmountFieldSetMaxAmountTransformer( val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals) val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals, roundingMode = RoundingMode.HALF_UP).orEmpty() - val isLessThanMinimumIfProvided = minAmount?.amount?.let { decimalCryptoValue < it } ?: false + val isLessThanMinimumIfProvided = minAmount?.amount?.let { decimalCryptoValue < it } == true return prevState.copy( isPrimaryButtonEnabled = !isLessThanMinimumIfProvided, + reduceAmountBy = reduceAmountBy, amountTextField = amountTextField.copy( isValuePasted = true, value = cryptoValue, @@ -52,10 +55,7 @@ class AmountFieldSetMaxAmountTransformer( isError = isLessThanMinimumIfProvided, error = when { isLessThanMinimumIfProvided -> { - val minimumAmount = minAmount - ?.amount - ?.format { crypto(cryptoCurrencyStatus.currency) } - .orEmpty() + val minimumAmount = minAmount.amount.format { crypto(cryptoCurrencyStatus.currency) } resourceReference( R.string.transfer_notification_invalid_minimum_transaction_amount_text, wrappedList(minimumAmount, minimumAmount), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt index 52758a02cc..3833714e62 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.PersistentList +import java.math.BigDecimal /** Model for amount state */ @Stable @@ -21,6 +22,8 @@ sealed class AmountState { * @param isSegmentedButtonsEnabled indicates if currency switches is enabled * @param amountTextField amount field state * @param appCurrencyCode app currency code + * @param isEditingDisabled indicated whether amount is editable + * @param reduceAmountBy reduces amount to be sent by specified value */ data class Data( override val isPrimaryButtonEnabled: Boolean, @@ -32,6 +35,8 @@ sealed class AmountState { val isSegmentedButtonsEnabled: Boolean, val amountTextField: AmountFieldModel, val appCurrencyCode: String, + val isEditingDisabled: Boolean = false, + val reduceAmountBy: BigDecimal = BigDecimal.ZERO, ) : AmountState() data class Empty( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt index f2f8e078f9..70d51c2503 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt @@ -25,12 +25,13 @@ import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTr import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.rememberDecimalFormat import kotlinx.coroutines.delay +@Deprecated("Use AmountField with clicks") @Composable internal fun AmountField(amountField: AmountFieldModel, appCurrencyCode: String) { val decimalFormat = rememberDecimalFormat() @@ -80,6 +81,60 @@ internal fun AmountField(amountField: AmountFieldModel, appCurrencyCode: String) AmountSecondary(amountField, appCurrencyCode) } +@Composable +internal fun AmountField( + amountField: AmountFieldModel, + appCurrencyCode: String, + onValueChange: (String) -> Unit, + onValuePastedTriggerDismiss: () -> Unit, +) { + val decimalFormat = rememberDecimalFormat() + val isFiatValue = amountField.isFiatValue + val currencyCode = if (isFiatValue) appCurrencyCode else null + val (primaryAmount, primaryValue) = if (isFiatValue) { + amountField.fiatAmount to amountField.fiatValue + } else { + amountField.cryptoAmount to amountField.value + } + val requester = remember { FocusRequester() } + + AmountTextField( + value = primaryValue, + decimals = primaryAmount.decimals, + visualTransformation = AmountVisualTransformation( + decimals = primaryAmount.decimals, + symbol = primaryAmount.currencySymbol, + currencyCode = currencyCode, + decimalFormat = decimalFormat, + ), + onValueChange = onValueChange, + keyboardOptions = amountField.keyboardOptions, + keyboardActions = amountField.keyboardActions, + textStyle = TangemTheme.typography.h2.copy( + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ), + isAutoResize = true, + isValuePasted = amountField.isValuePasted, + onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, + modifier = Modifier + .focusRequester(requester) + .padding( + top = TangemTheme.dimens.spacing24, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ) + .requiredHeightIn(min = TangemTheme.dimens.size32), + ) + + LaunchedEffect(key1 = Unit) { + delay(timeMillis = 200) + requester.requestFocus() + } + + AmountSecondary(amountField, appCurrencyCode) +} + @Composable private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: String) { val secondaryAmount = if (amountField.isFiatValue) amountField.cryptoAmount else amountField.fiatAmount @@ -96,11 +151,12 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri val text = if (amountField.isFiatValue) { secondaryAmount.value.format { crypto(secondaryAmount.currencySymbol, secondaryAmount.decimals) } } else { - BigDecimalFormatter.formatFiatAmount( - fiatAmount = secondaryAmount.value, - fiatCurrencySymbol = secondaryAmount.currencySymbol, - fiatCurrencyCode = appCurrencyCode, - ) + secondaryAmount.value.format { + fiat( + fiatCurrencySymbol = secondaryAmount.currencySymbol, + fiatCurrencyCode = appCurrencyCode, + ) + } } Text( text = text, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index 8b9904cc42..104f2f60ae 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -24,6 +24,8 @@ internal fun LazyListScope.amountField( amountState: AmountState.Data, isBalanceHidden: Boolean, modifier: Modifier = Modifier, + onValueChange: (String) -> Unit, + onValuePastedTriggerDismiss: () -> Unit, ) { item(key = AMOUNT_FIELD_KEY) { Column( @@ -63,6 +65,8 @@ internal fun LazyListScope.amountField( AmountField( amountField = amountState.amountTextField, appCurrencyCode = amountState.appCurrencyCode, + onValueChange = onValueChange, + onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, ) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt index 4ed9eb6da8..488de424c8 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt @@ -2,6 +2,8 @@ package com.tangem.features.send.v2.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model +import com.tangem.features.send.v2.send.model.SendModel +import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel import dagger.Binds import dagger.Module @@ -13,6 +15,16 @@ import dagger.multibindings.IntoMap @InstallIn(ModelComponent::class) internal interface SendModelModule { + @Binds + @IntoMap + @ClassKey(SendModel::class) + fun provideSendModel(model: SendModel): Model + + @Binds + @IntoMap + @ClassKey(SendAmountModel::class) + fun provideSendAmountModel(model: SendAmountModel): Model + @Binds @IntoMap @ClassKey(SendDestinationModel::class) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 1886f84b29..3a97b61648 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -14,6 +14,8 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents import com.tangem.features.send.v2.send.model.SendModel +import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent +import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -63,7 +65,7 @@ internal class DefaultSendComponent @AssistedInject constructor( private fun createChild(route: SendRoute, factoryContext: AppComponentContext) = when (route) { SendRoute.Empty -> getStubComponent() is SendRoute.Destination -> getDestinationComponent(factoryContext, route) - is SendRoute.Amount -> getAmountComponent() + is SendRoute.Amount -> getAmountComponent(factoryContext, route) is SendRoute.Fee -> getFeeComponent() SendRoute.Confirm -> getConfirmComponent() } @@ -82,7 +84,20 @@ internal class DefaultSendComponent @AssistedInject constructor( ), ) - private fun getAmountComponent() = getStubComponent() // todo + private fun getAmountComponent(factoryContext: AppComponentContext, route: SendRoute) = SendAmountComponent( + appComponentContext = factoryContext, + params = SendAmountComponentParams.AmountParams( + state = model.uiState.value.amountUM, + currentRoute = currentRoute.filterIsInstance(), + analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, + userWallet = model.userWallet, + appCurrency = model.appCurrency, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + callback = model, + isEditMode = route.isEditMode, + predefinedAmountValue = model.predefinedAmountValue, + ), + ) private fun getFeeComponent() = getStubComponent() // todo diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 11db3bfa86..5c9840758f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -4,9 +4,11 @@ import androidx.compose.runtime.Stable import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.send.v2.send.ui.state.SendUM +import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -21,20 +23,28 @@ import kotlin.properties.Delegates @Suppress("LongParameterList") internal class SendModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, -) : Model(), SendDestinationComponent.ModelCallback { +) : Model(), + SendDestinationComponent.ModelCallback, + SendAmountComponent.ModelCallback { private val _uiState = MutableStateFlow(initialState()) val uiState = _uiState.asStateFlow() var userWallet: UserWallet by Delegates.notNull() var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() - - private fun initialState(): SendUM = SendUM( - amountState = AmountState.Empty(), - destinationUM = DestinationUM.Empty(), - ) + var appCurrency: AppCurrency = AppCurrency.Default + var predefinedAmountValue: String? = null override fun onDestinationResult(destinationUM: DestinationUM) { _uiState.update { it.copy(destinationUM = destinationUM) } } + + override fun onAmountResult(state: AmountState) { + _uiState.update { it.copy(amountUM = state) } + } + + private fun initialState(): SendUM = SendUM( + amountUM = AmountState.Empty(), + destinationUM = DestinationUM.Empty(), + ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt index a65eb9fa49..41cfc2f614 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt @@ -4,6 +4,6 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM internal data class SendUM( - val amountState: AmountState, + val amountUM: AmountState, val destinationUM: DestinationUM, ) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt new file mode 100644 index 0000000000..01594d7266 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt @@ -0,0 +1,46 @@ +package com.tangem.features.send.v2.subcomponents.amount + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.ui.AmountBlock +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams.AmountBlockParams +import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +internal class SendAmountBlockComponent( + appComponentContext: AppComponentContext, + private val params: AmountBlockParams, + val onResult: (AmountState) -> Unit, + val onClick: () -> Unit, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: SendAmountModel = getOrCreateModel(params = params, router = router) + + init { + model.uiState.onEach { + onResult(it) + }.launchIn(componentScope) + } + + fun updateState(amountUM: AmountState) = model.updateState(amountUM) + + @Composable + override fun Content(modifier: Modifier) { + val state = model.uiState.collectAsStateWithLifecycle() + val isClickEnabled = params.blockClickEnableFlow.collectAsStateWithLifecycle() + val isEditingDisabled = params.blockEditDisabledFlow.collectAsStateWithLifecycle() + + AmountBlock( + amountState = state.value, + isClickDisabled = !isClickEnabled.value, + isEditingDisabled = isEditingDisabled.value, + onClick = onClick, + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt new file mode 100644 index 0000000000..9cb990495f --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt @@ -0,0 +1,41 @@ +package com.tangem.features.send.v2.subcomponents.amount + +import androidx.compose.foundation.background +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.ui.amountScreen.AmountScreenContent +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams.AmountParams +import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel + +internal class SendAmountComponent( + appComponentContext: AppComponentContext, + private val params: AmountParams, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: SendAmountModel = getOrCreateModel(params = params) + + fun updateState(amountUM: AmountState) = model.updateState(amountUM) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + AmountScreenContent( + amountState = state, + isBalanceHidden = false, + clickIntents = model, + modifier = Modifier.background(TangemTheme.colors.background.tertiary), + ) + } + + interface ModelCallback { + fun onAmountResult(amountUM: AmountState) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt new file mode 100644 index 0000000000..e3bafeb146 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt @@ -0,0 +1,41 @@ +package com.tangem.features.send.v2.subcomponents.amount + +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent.ModelCallback +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +internal sealed class SendAmountComponentParams { + + abstract val state: AmountState + abstract val analyticsCategoryName: String + abstract val userWallet: UserWallet + abstract val appCurrency: AppCurrency + abstract val cryptoCurrencyStatus: CryptoCurrencyStatus + + data class AmountParams( + override val state: AmountState, + override val analyticsCategoryName: String, + override val userWallet: UserWallet, + override val appCurrency: AppCurrency, + override val cryptoCurrencyStatus: CryptoCurrencyStatus, + val isEditMode: Boolean, + val callback: ModelCallback, + val currentRoute: Flow, + val predefinedAmountValue: String?, + ) : SendAmountComponentParams() + + data class AmountBlockParams( + override val state: AmountState, + override val analyticsCategoryName: String, + override val userWallet: UserWallet, + override val appCurrency: AppCurrency, + override val cryptoCurrencyStatus: CryptoCurrencyStatus, + val blockEditDisabledFlow: StateFlow, + val blockClickEnableFlow: StateFlow, + ) : SendAmountComponentParams() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt new file mode 100644 index 0000000000..4c4f1acb33 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt @@ -0,0 +1,40 @@ +package com.tangem.features.send.v2.subcomponents.amount + +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import java.math.BigDecimal +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Trigger for reducing amount from another component + */ +interface SendAmountReduceTrigger { + suspend fun triggerReduceBy(reduceBy: ReduceByData) + suspend fun triggerReduceTo(reduceTo: BigDecimal) +} + +/** + * Trigger for reducing amount from another component + */ +interface SendAmountReduceListener { + val reduceToTriggerFlow: Flow + val reduceByTriggerFlow: Flow +} + +@Singleton +internal class DefaultSendAmountReduceTrigger @Inject constructor() : + SendAmountReduceTrigger, + SendAmountReduceListener { + override val reduceToTriggerFlow = MutableSharedFlow() + override val reduceByTriggerFlow = MutableSharedFlow() + + override suspend fun triggerReduceBy(reduceBy: ReduceByData) { + reduceByTriggerFlow.emit(reduceBy) + } + + override suspend fun triggerReduceTo(reduceTo: BigDecimal) { + reduceToTriggerFlow.emit(reduceTo) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/analytics/SendAmountAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/analytics/SendAmountAnalyticEvents.kt new file mode 100644 index 0000000000..97d65a88e9 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/analytics/SendAmountAnalyticEvents.kt @@ -0,0 +1,31 @@ +package com.tangem.features.send.v2.subcomponents.amount.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE + +internal sealed class SendAmountAnalyticEvents( + category: String, + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = category, event = event, params = params) { + + /** Selected currency */ + data class SelectedCurrency( + val categoryName: String, + val type: SelectedCurrencyType, + ) : SendAmountAnalyticEvents( + category = categoryName, + event = "Selected Currency", + params = mapOf(TYPE to type.value), + ) + + /** Max amount button clicked */ + data class MaxAmountButtonClicked( + val categoryName: String, + ) : SendAmountAnalyticEvents(category = categoryName, event = "Max Amount Taped") + + internal enum class SelectedCurrencyType(val value: String) { + Token("Token"), + AppCurrency("App Currency"), + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/di/SendAmountModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/di/SendAmountModule.kt new file mode 100644 index 0000000000..5152941ae4 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/di/SendAmountModule.kt @@ -0,0 +1,27 @@ +package com.tangem.features.send.v2.subcomponents.amount.di + +import com.tangem.features.send.v2.subcomponents.amount.DefaultSendAmountReduceTrigger +import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener +import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceTrigger +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +internal object SendAmountModule { + + @Provides + @Singleton + fun provideSendAmountReduceTrigger(): SendAmountReduceTrigger { + return DefaultSendAmountReduceTrigger() + } + + @Provides + @Singleton + fun provideSendAmountReduceListener(): SendAmountReduceListener { + return DefaultSendAmountReduceTrigger() + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt new file mode 100644 index 0000000000..25fa331654 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -0,0 +1,239 @@ +package com.tangem.features.send.v2.subcomponents.amount.model + +import androidx.compose.runtime.Stable +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.common.ui.amountScreen.converters.* +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer +import com.tangem.common.ui.amountScreen.models.AmountParameters +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.core.analytics.api.AnalyticsEventHandler +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.navigation.Router +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase +import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams +import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener +import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents +import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents.SelectedCurrencyType +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.orZero +import com.tangem.utils.isNullOrZero +import com.tangem.utils.transformer.update +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +@Stable +@ModelScoped +internal class SendAmountModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, + private val sendAmountReduceListener: SendAmountReduceListener, + private val analyticsEventHandler: AnalyticsEventHandler, +) : Model(), AmountScreenClickIntents { + + private val params: SendAmountComponentParams = paramsContainer.require() + + private val _uiState = MutableStateFlow(params.state) + val uiState = _uiState.asStateFlow() + + private val analyticsCategoryName = params.analyticsCategoryName + private val userWallet = params.userWallet + private val cryptoCurrencyStatus = params.cryptoCurrencyStatus + + private var minAmountBoundary: EnterAmountBoundary? = null + private var maxAmountBoundary: EnterAmountBoundary = MaxEnterAmountConverter().convert(cryptoCurrencyStatus) + + init { + configAmountNavigation() + initMinBoundary() + subscribeOnAmountReduceByTriggerUpdates() + subscribeOnAmountReduceToTriggerUpdates() + } + + private fun initMinBoundary() { + modelScope.launch { + minAmountBoundary = getMinimumTransactionAmountSyncUseCase( + userWalletId = userWallet.walletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ).getOrNull()?.let { + EnterAmountBoundary( + amount = it, + fiatRate = cryptoCurrencyStatus.value.fiatRate.orZero(), + ) + } + + initialState() + } + } + + private fun initialState() { + val predefinedAmountValue = (params as? SendAmountComponentParams.AmountParams)?.predefinedAmountValue + if (uiState.value is AmountState.Empty) { + _uiState.update { + AmountStateConverterV2( + clickIntents = this, + appCurrency = params.appCurrency, + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxEnterAmount = maxAmountBoundary, + iconStateConverter = CryptoCurrencyToIconStateConverter(), + ).convert( + AmountParameters( + title = stringReference(userWallet.name), + value = "", + ), + ) + } + if (predefinedAmountValue != null) { + onAmountValueChange(predefinedAmountValue) + } + } + } + + fun updateState(amountUM: AmountState) { + if (amountUM !is AmountState.Empty) { + _uiState.value = amountUM + } + } + + override fun onCurrencyChangeClick(isFiat: Boolean) { + _uiState.update(AmountCurrencyTransformer(cryptoCurrencyStatus, isFiat)) + } + + override fun onAmountValueChange(value: String) { + _uiState.update( + AmountFieldChangeTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxEnterAmount = maxAmountBoundary, + minimumTransactionAmount = minAmountBoundary, + value = value, + ), + ) + } + + override fun onMaxValueClick() { + val decimalCryptoValue = cryptoCurrencyStatus.value.amount + if (decimalCryptoValue.isNullOrZero()) return + + _uiState.update( + AmountFieldSetMaxAmountTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxAmount = maxAmountBoundary, + minAmount = minAmountBoundary, + ), + ) + analyticsEventHandler.send( + SendAmountAnalyticEvents.MaxAmountButtonClicked(categoryName = analyticsCategoryName), + ) + } + + override fun onAmountPasteTriggerDismiss() { + _uiState.update(AmountPastedTriggerDismissTransformer) + } + + override fun onAmountNext() { + (uiState.value as? AmountState.Data)?.amountTextField?.isFiatValue?.let { isFiatSelected -> + analyticsEventHandler.send( + SendAmountAnalyticEvents.SelectedCurrency( + categoryName = analyticsCategoryName, + type = if (isFiatSelected) { + SelectedCurrencyType.AppCurrency + } else { + SelectedCurrencyType.Token + }, + ), + ) + } + saveResult() + if ((params as? SendAmountComponentParams.AmountParams)?.isEditMode == true) { + router.pop() + } else { + router.push(SendRoute.Confirm) + } + } + + private fun subscribeOnAmountReduceToTriggerUpdates() { + sendAmountReduceListener.reduceToTriggerFlow + .onEach { reduceTo -> + _uiState.update( + AmountReduceToTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minAmountBoundary, + value = reduceTo, + ), + ) + } + .launchIn(modelScope) + } + + private fun subscribeOnAmountReduceByTriggerUpdates() { + sendAmountReduceListener.reduceByTriggerFlow + .onEach { reduceByData -> + _uiState.update( + AmountReduceByTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minAmountBoundary, + value = reduceByData, + ), + ) + } + .launchIn(modelScope) + } + + private fun saveResult() { + val params = params as? SendAmountComponentParams.AmountParams ?: return + params.callback.onAmountResult(uiState.value) + } + + private fun configAmountNavigation() { + val params = params as? SendAmountComponentParams.AmountParams ?: return + combine( + flow = uiState, + flow2 = params.currentRoute, + transform = { state, route -> state to route }, + ).onEach { (_, _) -> + // todo + // params.callback.onNavigationResult( + // NavigationUM.Content( + // title = resourceReference(R.string.send_amount_label), + // subtitle = null, + // backIconRes = R.drawable.ic_back_24, + // backIconClick = { + // if (route.isEditMode) { + // saveResult() + // } + // router.pop() + // }, + // primaryButton = ButtonsUM.PrimaryButtonUM( + // text = if (route.isEditMode) { + // resourceReference(R.string.common_continue) + // } else { + // resourceReference(R.string.common_next) + // }, + // isEnabled = state.isPrimaryButtonEnabled, + // onClick = ::onAmountNext, + // ), + // prevButton = ButtonsUM.PrimaryButtonUM( + // text = TextReference.EMPTY, + // iconResId = R.drawable.ic_back_24, + // isEnabled = true, + // onClick = { + // saveResult() + // router.pop() + // }, + // ).takeIf { route.isEditMode.not() }, + // secondaryPairButtonsUM = null, + // ), + // ) + }.launchIn(modelScope) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index adb93ac9b2..51c0bc7f63 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -20,11 +20,11 @@ import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.send.v2.send.SendRoute -import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource import com.tangem.features.send.v2.subcomponents.destination.analytics.SendDestinationAnalyticEvents import com.tangem.features.send.v2.subcomponents.destination.model.transformers.* +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt index 3766d7c9c9..a1d6b22fb4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt @@ -19,7 +19,7 @@ internal class SendAmountPastedTriggerDismissConverter( return state.copyWrapped( isEditState = isEditState, - amountState = AmountPastedTriggerDismissTransformer().transform(amountState), + amountState = AmountPastedTriggerDismissTransformer.transform(amountState), ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountPasteDismissStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountPasteDismissStateTransformer.kt index b2d7752952..fa01ef2dec 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountPasteDismissStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountPasteDismissStateTransformer.kt @@ -8,7 +8,7 @@ internal class AmountPasteDismissStateTransformer : Transformer override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( - amountState = AmountPastedTriggerDismissTransformer().transform(prevState.amountState), + amountState = AmountPastedTriggerDismissTransformer.transform(prevState.amountState), ) } } \ No newline at end of file From d10f285f447189861fe762a32413e90753332c23 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Mar 2025 17:09:47 +0400 Subject: [PATCH 044/138] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 48 ++-- .../tokens/DefaultTokensFeatureToggles.kt | 4 +- .../configs/feature_toggles_config.json | 8 +- .../domain/tokens/TokensFeatureToggles.kt | 2 +- .../CachedCurrenciesStatusesOperations.kt | 3 + .../CurrenciesStatusesOperations.kt | 53 +---- .../LceCurrenciesStatusesOperations.kt | 224 ------------------ ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 7 +- 8 files changed, 33 insertions(+), 316 deletions(-) delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/LceCurrenciesStatusesOperations.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 73ecbf344e..6f4c7f7bcc 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -5,7 +5,9 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.promo.PromoRepository import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.operations.* +import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations +import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.features.swap.SwapFeatureToggles @@ -351,21 +353,13 @@ internal object TokensDomainModule { networksRepository: NetworksRepository, stakingRepository: StakingRepository, ): BaseCurrenciesStatusesOperations { - return if (tokensFeatureToggles.isBalancesCachingEnabled) { - CachedCurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - } else { - LceCurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - } + return CachedCurrenciesStatusesOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + stakingRepository = stakingRepository, + tokensFeatureToggles = tokensFeatureToggles, + ) } @Provides @@ -377,20 +371,12 @@ internal object TokensDomainModule { networksRepository: NetworksRepository, stakingRepository: StakingRepository, ): BaseCurrencyStatusOperations { - return if (tokensFeatureToggles.isBalancesCachingEnabled) { - CachedCurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - } else { - CurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - } + return CachedCurrenciesStatusesOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + stakingRepository = stakingRepository, + tokensFeatureToggles = tokensFeatureToggles, + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt index 0aae1ffb84..2927924f8c 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt @@ -7,6 +7,6 @@ internal class DefaultTokensFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : TokensFeatureToggles { - override val isBalancesCachingEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "BALANCES_CACHING_ENABLED") + override val isNetworksLoadingRefactoringEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "NETWORKS_LOADING_REFACTORING_ENABLED") } \ No newline at end of file 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 df8f975239..e051641afc 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 @@ -43,10 +43,6 @@ "name": "STAKING_TON_ENABLED", "version": "undefined" }, - { - "name": "BALANCES_CACHING_ENABLED", - "version": "5.21.0" - }, { "name": "NFT_ENABLED", "version": "undefined" @@ -82,5 +78,9 @@ { "name": "WALLET_CONNECT_REDESIGN_ENABLED", "version": "undefined" + }, + { + "name": "NETWORKS_LOADING_REFACTORING_ENABLED", + "version": "undefined" } ] diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt index faf7427071..b48aaa3c4d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt @@ -7,5 +7,5 @@ package com.tangem.domain.tokens */ interface TokensFeatureToggles { - val isBalancesCachingEnabled: Boolean + val isNetworksLoadingRefactoringEnabled: Boolean } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index a14f37e601..cab4aff110 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -12,6 +12,7 @@ import com.tangem.domain.core.utils.lceError import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error @@ -28,6 +29,7 @@ class CachedCurrenciesStatusesOperations( private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, + @Suppress("UnusedPrivateMember") private val tokensFeatureToggles: TokensFeatureToggles, ) : BaseCurrenciesStatusesOperations, BaseCurrencyStatusOperations(currenciesRepository, quotesRepository, networksRepository, stakingRepository) { @@ -258,6 +260,7 @@ class CachedCurrenciesStatusesOperations( true } .distinctUntilChanged() + .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } } private fun getNetworksStatuses( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index eeeb5b4e59..2679e1a047 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -1,57 +1,6 @@ package com.tangem.domain.tokens.operations -import arrow.core.Either -import arrow.core.left -import arrow.core.right -import com.tangem.domain.core.utils.EitherFlow -import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.model.Quote -import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations.Companion.RETRY_DELAY -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* - -class CurrenciesStatusesOperations( - currenciesRepository: CurrenciesRepository, - private val quotesRepository: QuotesRepository, - private val networksRepository: NetworksRepository, - stakingRepository: StakingRepository, -) : BaseCurrencyStatusOperations(currenciesRepository, quotesRepository, networksRepository, stakingRepository) { - - override fun getQuotes(id: CryptoCurrency.RawID): Flow>> { - return quotesRepository.getQuotesUpdatesLegacy(setOf(id)) - .map, Either>> { quotes -> - if (quotes.isEmpty()) Error.EmptyQuotes.left() else quotes.right() - } - .retryWhen { cause, _ -> - emit(Error.DataError(cause).left()) - delay(RETRY_DELAY) - true - } - } - - override fun getNetworksStatuses( - userWalletId: UserWalletId, - network: Network, - ): EitherFlow> { - return networksRepository.getNetworkStatusesUpdatesLegacy(userWalletId, setOf(network)) - .map, Either>> { it.right() } - .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } - } - - override suspend fun fetchComponents( - userWalletId: UserWalletId, - networks: Set, - currenciesIds: Set, - currencies: List, - ): Either = Unit.right() +class CurrenciesStatusesOperations { sealed class Error { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/LceCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/LceCurrenciesStatusesOperations.kt deleted file mode 100644 index 255d2f86b5..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/LceCurrenciesStatusesOperations.kt +++ /dev/null @@ -1,224 +0,0 @@ -package com.tangem.domain.tokens.operations - -import arrow.core.* -import arrow.core.raise.ensureNotNull -import arrow.core.raise.recover -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.lce.lce -import com.tangem.domain.core.lce.lceFlow -import com.tangem.domain.core.utils.EitherFlow -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.YieldBalanceList -import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.* -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* - -class LceCurrenciesStatusesOperations( - private val currenciesRepository: CurrenciesRepository, - private val quotesRepository: QuotesRepository, - private val networksRepository: NetworksRepository, - private val stakingRepository: StakingRepository, -) : BaseCurrenciesStatusesOperations { - - override fun getCurrenciesStatuses( - userWalletId: UserWalletId, - ): LceFlow> { - return transformToCurrenciesStatuses( - userWalletId = userWalletId, - currenciesFlow = getWalletCurrencies(userWalletId), - ) - } - - private fun transformToCurrenciesStatuses( - userWalletId: UserWalletId, - currenciesFlow: EitherFlow>, - ): LceFlow> = lceFlow { - currenciesFlow.collectLatest { maybeCurrencies -> - val nonEmptyCurrencies = maybeCurrencies.bind().toNonEmptyListOrNull() - ensureNotNull(nonEmptyCurrencies) { TokenListError.EmptyTokens } - - // This is only 'true' when the flow here is empty, such as during initial loading - if (isLoading.get()) { - val loadingCurrencies = createCurrenciesStatuses( - currencies = nonEmptyCurrencies, - maybeNetworkStatuses = null, - maybeQuotes = null, - maybeYieldBalances = null, - ) - send(loadingCurrencies) - } - - val (networks, currenciesIds) = getIds(nonEmptyCurrencies) - - fun createCurrenciesStatuses( - maybeQuotes: Either>?, - maybeNetworkStatuses: Either>?, - maybeYieldBalances: Either?, - ): Lce> = createCurrenciesStatuses( - currencies = nonEmptyCurrencies, - maybeQuotes = maybeQuotes, - maybeNetworkStatuses = maybeNetworkStatuses, - maybeYieldBalances = maybeYieldBalances, - ) - - combine( - getQuotes(currenciesIds), - getNetworksStatuses(userWalletId, networks), - getYieldBalances(userWalletId, nonEmptyCurrencies), - ::createCurrenciesStatuses, - ) - .distinctUntilChanged() - .collectLatest { maybeCurrenciesStatuses -> - send(maybeCurrenciesStatuses) - } - } - } - - private fun getWalletCurrencies(userWalletId: UserWalletId): EitherFlow> { - return currenciesRepository.getWalletCurrenciesUpdates(userWalletId) - .map, Either>> { it.right() } - .catch { emit(TokenListError.DataError(it).left()) } - .distinctUntilChanged() - } - - private fun createCurrenciesStatuses( - currencies: NonEmptyList, - maybeQuotes: Either>?, - maybeNetworkStatuses: Either>?, - maybeYieldBalances: Either?, - ): Lce> = lce { - isLoading.set(maybeNetworkStatuses == null || maybeYieldBalances == null) - - var quotesRetrievingFailed = false - - val networksStatuses = maybeNetworkStatuses?.bindEither()?.toNonEmptySetOrNull() - val yieldBalances = maybeYieldBalances?.bindEither() - val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) { - null - } - - if (quotes == null) { - quotesRetrievingFailed = true - } - - currencies.map { currency -> - val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } - val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val yieldBalance = findYieldBalanceOrNull(yieldBalances, currency, networkStatus) - - val currencyStatus = createCurrencyStatus( - currency = currency, - quote = quote, - networkStatus = networkStatus, - yieldBalance = yieldBalance, - ignoreQuote = quotesRetrievingFailed, - ) - - if (currencyStatus.value is CryptoCurrencyStatus.Loading) { - isLoading.set(true) - } - - currencyStatus - } - } - - private fun findYieldBalanceOrNull( - yieldBalances: YieldBalanceList?, - currency: CryptoCurrency, - networkStatus: NetworkStatus?, - ): YieldBalance? { - if (yieldBalances !is YieldBalanceList.Data) return null - - val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id) - - if (supportedIntegration.isNullOrBlank()) return null - - return yieldBalances.getBalance( - address = extractAddress(networkStatus), - integrationId = supportedIntegration, - ) - } - - private fun createCurrencyStatus( - currency: CryptoCurrency, - quote: Quote?, - networkStatus: NetworkStatus?, - yieldBalance: YieldBalance?, - ignoreQuote: Boolean, - ): CryptoCurrencyStatus { - val currencyStatusOperations = CurrencyStatusOperations( - currency = currency, - quote = quote, - networkStatus = networkStatus, - yieldBalance = yieldBalance, - ignoreQuote = ignoreQuote, - ) - - return currencyStatusOperations.createTokenStatus() - } - - private fun getQuotes(tokensIds: NonEmptySet): Flow>> { - return quotesRepository.getQuotesUpdatesLegacy(tokensIds.mapNotNull { it.rawCurrencyId }.toSet()) - .map, Either>> { it.right() } - .retryWhen { cause, _ -> - emit(TokenListError.DataError(cause).left()) - // adding delay before retry to avoid spam when flow restarted - delay(RETRY_QUOTES_DELAY) - true - } - .distinctUntilChanged() - } - - private fun getNetworksStatuses( - userWalletId: UserWalletId, - networks: NonEmptySet, - ): EitherFlow> { - return networksRepository.getNetworkStatusesUpdatesLegacy(userWalletId, networks) - .map, Either>> { it.right() } - .catch { emit(TokenListError.DataError(it).left()) } - .distinctUntilChanged() - } - - private fun getYieldBalances( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): EitherFlow { - return stakingRepository.getMultiYieldBalanceUpdatesLegacy(userWalletId, cryptoCurrencies) - .map> { it.right() } - .catch { emit(TokenListError.DataError(it).left()) } - .distinctUntilChanged() - } - - private fun getIds(currencies: List): Pair, NonEmptySet> { - val currencyIdToNetworkId = currencies.associate { currency -> - currency.id to currency.network - } - val currenciesIds = currencyIdToNetworkId.keys.toNonEmptySetOrNull() - val networks = currencyIdToNetworkId.values.toNonEmptySetOrNull() - - requireNotNull(currenciesIds) { "Currencies IDs cannot be empty" } - requireNotNull(networks) { "Networks IDs cannot be empty" } - - return networks to currenciesIds - } - - private fun extractAddress(networkStatus: NetworkStatus?): String? { - return when (val value = networkStatus?.value) { - is NetworkStatus.NoAccount -> value.address.defaultAddress.value - is NetworkStatus.Unreachable -> value.address?.defaultAddress?.value - is NetworkStatus.Verified -> value.address.defaultAddress.value - else -> null - } - } - - private companion object { - const val RETRY_QUOTES_DELAY = 2000L - } -} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index c8879e30a3..d86abc8442 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -10,7 +10,7 @@ import com.tangem.domain.tokens.mock.MockQuotes import com.tangem.domain.tokens.mock.MockTokens import com.tangem.domain.tokens.mock.MockTokensStates import com.tangem.domain.tokens.model.* -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations import com.tangem.domain.tokens.repository.MockCurrenciesRepository import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.domain.tokens.repository.MockQuotesRepository @@ -164,7 +164,7 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { quotes: Flow>> = flowOf(MockQuotes.quotes.right()), statuses: Flow>> = flowOf(MockNetworks.verifiedNetworksStatuses.right()), ) = GetPrimaryCurrencyStatusUpdatesUseCase( - currencyStatusOperations = CurrenciesStatusesOperations( + currencyStatusOperations = CachedCurrenciesStatusesOperations( currenciesRepository = MockCurrenciesRepository( sortTokensResult = Unit.right(), removeCurrencyResult = removeCurrencyResult, @@ -176,6 +176,9 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { quotesRepository = MockQuotesRepository(quotes), networksRepository = MockNetworksRepository(statuses), stakingRepository = MockStakingRepository(), + tokensFeatureToggles = object : TokensFeatureToggles { + override val isNetworksLoadingRefactoringEnabled: Boolean = false + }, ), dispatchers = dispatchers, ) From 0c949ebd42f5622d78a0b35433f3db2196136284 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Mar 2025 19:46:33 +0200 Subject: [PATCH 045/138] Updated on 2026-08-14 --- ...Event.kt => BlockchainApiExceptionEvent.kt} | 2 +- .../analytics/events/TangemSdkErrorEvent.kt | 18 ++++++++++++++++++ .../handlers/BlockchainExceptionHandler.kt | 4 ++-- .../amplitude/AmplitudeAnalyticsHandler.kt | 8 +------- .../handlers/firebase/FirebaseClient.kt | 1 - .../domain/scanCard/UseCaseScanProcessor.kt | 8 ++------ .../utils/ScanCardExceptionConverter.kt | 4 ++-- .../domain/sdk/impl/DefaultTangemSdkManager.kt | 9 ++------- .../ui/securitymode/model/SecurityModeModel.kt | 13 ++++--------- .../models/ExceptionAnalyticsEvent.kt | 4 +--- 10 files changed, 33 insertions(+), 38 deletions(-) rename app/src/main/java/com/tangem/tap/common/analytics/events/{BlockchainExceptionEvent.kt => BlockchainApiExceptionEvent.kt} (92%) create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/events/TangemSdkErrorEvent.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainExceptionEvent.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainApiExceptionEvent.kt similarity index 92% rename from app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainExceptionEvent.kt rename to app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainApiExceptionEvent.kt index 385ab3e517..3e687ae5fd 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainExceptionEvent.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainApiExceptionEvent.kt @@ -2,7 +2,7 @@ package com.tangem.tap.common.analytics.events import com.tangem.core.analytics.models.AnalyticsEvent -class BlockchainExceptionEvent( +class BlockchainApiExceptionEvent( selectedHost: String, exceptionHost: String, error: String, diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/TangemSdkErrorEvent.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/TangemSdkErrorEvent.kt new file mode 100644 index 0000000000..bfd7f4bcc9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/TangemSdkErrorEvent.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.common.analytics.events + +import com.tangem.common.core.TangemSdkError +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.tap.common.analytics.converters.AnalyticsErrorConverter + +class TangemSdkErrorEvent( + val exception: TangemSdkError, +) : AnalyticsEvent( + category = "TangemSdk", + event = "Tangem Sdk Error", + params = errorConverter.convert(exception), +) { + + companion object { + private val errorConverter = AnalyticsErrorConverter() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt index c6e47dcb6d..a1931b993e 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt @@ -2,7 +2,7 @@ package com.tangem.tap.common.analytics.handlers import com.tangem.blockchain.common.ExceptionHandlerOutput import com.tangem.core.analytics.api.AnalyticsErrorHandler -import com.tangem.tap.common.analytics.events.BlockchainExceptionEvent +import com.tangem.tap.common.analytics.events.BlockchainApiExceptionEvent import javax.inject.Inject class BlockchainExceptionHandler @Inject constructor( @@ -10,7 +10,7 @@ class BlockchainExceptionHandler @Inject constructor( ) : ExceptionHandlerOutput { override fun handleApiSwitch(currentHost: String, nextHost: String, message: String) { analyticsErrorHandler.sendErrorEvent( - BlockchainExceptionEvent( + BlockchainApiExceptionEvent( selectedHost = nextHost, exceptionHost = currentHost, error = message, diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt index a618ce78ab..2aa36c5413 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt @@ -1,13 +1,11 @@ package com.tangem.tap.common.analytics.handlers.amplitude -import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsHandler -import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder class AmplitudeAnalyticsHandler( private val client: AmplitudeAnalyticsClient, -) : AnalyticsHandler, AnalyticsErrorHandler { +) : AnalyticsHandler { override fun id(): String = ID @@ -15,10 +13,6 @@ class AmplitudeAnalyticsHandler( client.logEvent(eventId, params) } - override fun sendErrorEvent(event: AnalyticsEvent) { - send(event.id, event.params) - } - companion object { const val ID = "Amplitude" } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt index 47b97a7dbc..06ccf50709 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt @@ -28,7 +28,6 @@ internal class FirebaseClient : FirebaseAnalyticsClient { ) } - // TODO [REDACTED_TASK_KEY] fix recordException usages in the app override fun logException(error: Throwable, params: Map) { fbCrashlytics.recordException(error) { eventConverter.convertEventParams(params) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt index 772960fb03..abf3f1f23d 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt @@ -11,6 +11,7 @@ import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.domain.card.ScanCardException import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.StateDialog +import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.inject @@ -35,12 +36,7 @@ internal object UseCaseScanProcessor { ifLeft = { val error = scanCardExceptionConverter.convertBack(it) - Analytics.sendException( - ExceptionAnalyticsEvent( - exception = error, - params = mapOf("Event" to "Scan"), - ), - ) + Analytics.sendErrorEvent(TangemSdkErrorEvent(error)) CompletionResult.Failure(error) }, ifRight = { CompletionResult.Success(it) }, diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/utils/ScanCardExceptionConverter.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/utils/ScanCardExceptionConverter.kt index 53062e6918..add75d87d0 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/utils/ScanCardExceptionConverter.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/utils/ScanCardExceptionConverter.kt @@ -12,7 +12,7 @@ internal class ScanCardExceptionConverter : TwoWayConverter ScanCardException.UnknownException(value) } - override fun convertBack(value: ScanCardException): TangemError { + override fun convertBack(value: ScanCardException): TangemSdkError { return when (value) { is ScanCardException.UnknownException -> TangemSdkError.ExceptionError(value) is ScanCardException.UserCancelled -> TangemSdkError.UserCancelled() @@ -31,7 +31,7 @@ internal class ScanCardExceptionConverter : TwoWayConverter ScanCardException.UnknownException(value) } - private fun concertScanChainException(value: ScanCardException.ChainException): TangemError { + private fun concertScanChainException(value: ScanCardException.ChainException): TangemSdkError { return when (val e = value as? ScanChainException) { is ScanChainException.DisclaimerWasCanceled -> TangemSdkError.UserCancelled() is ScanChainException.OnboardingNeeded, diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index bc1616ef60..7ef7432c75 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -14,7 +14,6 @@ import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.services.secure.SecureStorage import com.tangem.common.usersCode.UserCodeRepository import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.res.getStringSafe import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.hdWallet.DerivationPath @@ -41,6 +40,7 @@ import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.visa.VisaCardActivationResponse import com.tangem.sdk.api.visa.VisaCardActivationTaskMode +import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent import com.tangem.tap.derivationsFinder import com.tangem.tap.domain.tasks.product.CreateProductWalletTask import com.tangem.tap.domain.tasks.product.ResetBackupCardTask @@ -224,12 +224,7 @@ internal class DefaultTangemSdkManager( private fun sendScanResultsToAnalytics(result: CompletionResult) { if (result is CompletionResult.Failure) { (result.error as? TangemSdkError)?.let { error -> - Analytics.sendException( - ExceptionAnalyticsEvent( - exception = error, - params = mapOf("Event" to "Scan"), - ), - ) + Analytics.sendErrorEvent(TangemSdkErrorEvent(error)) } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/model/SecurityModeModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/model/SecurityModeModel.kt index 6568f05a99..8f4cf86296 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/model/SecurityModeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/model/SecurityModeModel.kt @@ -4,15 +4,15 @@ import androidx.compose.runtime.Stable import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.analytics.api.AnalyticsExceptionHandler -import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.common.util.cardTypesResolver import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings +import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor @@ -33,7 +33,7 @@ internal class SecurityModeModel @Inject constructor( private val tangemSdkManager: TangemSdkManager, private val cardSettingsInteractor: CardSettingsInteractor, private val analyticsEventHandler: AnalyticsEventHandler, - private val analyticsExceptionHandler: AnalyticsExceptionHandler, + private val analyticsErrorHandler: AnalyticsErrorHandler, ) : Model() { private val scannedScanResponse = cardSettingsInteractor.scannedScanResponse.value @@ -96,12 +96,7 @@ internal class SecurityModeModel @Inject constructor( is CompletionResult.Failure -> { val error = result.error if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) { - analyticsExceptionHandler.sendException( - ExceptionAnalyticsEvent( - exception = error, - params = mapOf("Event" to "Security Mode Changed"), - ), - ) + analyticsErrorHandler.sendErrorEvent(TangemSdkErrorEvent(error)) } } else -> Unit diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/ExceptionAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/ExceptionAnalyticsEvent.kt index 82372912e5..c2af9763c7 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/ExceptionAnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/ExceptionAnalyticsEvent.kt @@ -2,7 +2,5 @@ package com.tangem.core.analytics.models class ExceptionAnalyticsEvent( val exception: Throwable, - // val category: String, - // val event: String, - var params: Map = mapOf(), + val params: Map = mapOf(), ) \ No newline at end of file From b0fc7b3057d1b22817076e7e27d8b5275ddd7216 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Mar 2025 17:09:48 +0300 Subject: [PATCH 046/138] Updated on 2026-08-14 --- .../com/tangem/datasource/api/common/config/TangemTech.kt | 4 ++-- .../api/common/config/managers/ProdApiConfigsManagerTest.kt | 2 +- gradle/tangem_dependencies.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt index dd567e31f4..e15e7ce4e3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt @@ -19,13 +19,13 @@ internal class TangemTech( private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, - baseUrl = "https://api.tangem-tech.com/v1/", + baseUrl = "https://api.tangem.org/v1/", headers = createHeaders(), ) private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.DEV, - baseUrl = "https://devapi.tangem-tech.com/v1/", + baseUrl = "[REDACTED_ENV_URL]", headers = createHeaders(), ) diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index a58ab9efa6..9097d47c30 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -152,7 +152,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { id = ApiConfig.ID.TangemTech, expected = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, - baseUrl = "https://api.tangem-tech.com/v1/", + baseUrl = "https://api.tangem.org/v1/", headers = mapOf( "card_id" to ProviderSuspend { APP_CARD_ID }, "card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY }, diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index fc082393ac..475ddc9d26 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] tangemBlockchainSdk = "develop-989" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-444" +tangemCardSdk = "develop-445" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 80d00dfb6bd38a4188f6307422367c2440f41986 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 26 Mar 2025 22:17:53 +0500 Subject: [PATCH 047/138] Updated on 2026-08-14 --- .../com/tangem/core/ui/components/Spacers.kt | 5 + .../core}/ui/components/UnableToLoadData.kt | 6 +- .../components/currency/icon/CurrencyIcon.kt | 4 +- .../currency/icon/CurrencyIconIconBadge.kt | 32 ++ .../{IconBadge.kt => CurrencyIconTopBadge.kt} | 24 +- .../core/ui/components/fields/SearchBar.kt | 43 ++- .../res/drawable/ic_nft_placeholder_76.xml | 16 + .../ui/components/MarketTokenDetailsChart.kt | 2 +- .../ui/components/TokenMarketDetailsBody.kt | 2 +- .../ui/components/MarketsListLazyColumn.kt | 1 + .../entity/NFTCollectionAssetUM.kt | 9 + .../entity/NFTCollectionAssetsListUM.kt | 16 + .../nft/collections/entity/NFTCollectionUM.kt | 14 + .../collections/entity/NFTCollectionsUM.kt | 29 ++ .../entity/NFTCollectionsWarningUM.kt | 5 + .../nft/collections/entity/NFTSalePriceUM.kt | 10 + .../nft/collections/ui/NFTCollection.kt | 178 ++++++++++ .../nft/collections/ui/NFTCollectionAsset.kt | 100 ++++++ .../ui/NFTCollectionAssetLoading.kt | 47 +++ .../collections/ui/NFTCollectionWarning.kt | 14 + .../collections/ui/NFTCollectionsContent.kt | 318 ++++++++++++++++++ .../nft/collections/ui/NFTCollectionsEmpty.kt | 74 ++++ .../collections/ui/NFTCollectionsFailed.kt | 54 +++ .../nft/collections/ui/NFTSalePrice.kt | 40 +++ .../onramp/tokenlist/ui/OnrampTokenList.kt | 14 +- .../wallet/ui/components/WalletNFTItem.kt | 2 +- 26 files changed, 1003 insertions(+), 56 deletions(-) rename {features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl => core/ui/src/main/java/com/tangem/core}/ui/components/UnableToLoadData.kt (88%) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconIconBadge.kt rename core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/{IconBadge.kt => CurrencyIconTopBadge.kt} (66%) create mode 100644 core/ui/src/main/res/drawable/ic_nft_placeholder_76.xml create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetUM.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetsListUM.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsUM.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsWarningUM.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTSalePriceUM.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAsset.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAssetLoading.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionWarning.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsFailed.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTSalePrice.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt index 99230c1853..131ca91e00 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt @@ -14,6 +14,11 @@ fun SpacerH(height: Dp, modifier: Modifier = Modifier) { Spacer(modifier = modifier.height(height)) } +@Composable +fun SpacerH2(modifier: Modifier = Modifier) { + SpacerH(2.dp, modifier) +} + @Composable fun SpacerH4(modifier: Modifier = Modifier) { SpacerH(4.dp, modifier) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/UnableToLoadData.kt b/core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt similarity index 88% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/UnableToLoadData.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt index c96d848bac..19f8e5fcbd 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/UnableToLoadData.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.tokenlist.impl.ui.components +package com.tangem.core.ui.components import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement @@ -8,16 +8,16 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R @Composable -internal fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { +fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { Column( modifier = modifier, verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt index a9a7677136..4727ba9759 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt @@ -109,7 +109,7 @@ private fun BoxScope.ContentIconContainer( ) if (icon.topBadgeIconResId != null && shouldShowTopBadge) { - TopBadge( + CurrencyIconTopBadge( modifier = Modifier .offset(x = networkBadgeOffset, y = -networkBadgeOffset) .align(Alignment.TopEnd), @@ -120,7 +120,7 @@ private fun BoxScope.ContentIconContainer( } if (icon.showCustomBadge) { - BottomBadge( + CurrencyIconBottomBadge( modifier = Modifier.align(Alignment.BottomEnd), ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconIconBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconIconBadge.kt new file mode 100644 index 0000000000..b074076629 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconIconBadge.kt @@ -0,0 +1,32 @@ +package com.tangem.core.ui.components.currency.icon + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun CurrencyIconBottomBadge(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size12) + .background( + color = TangemTheme.colors.background.primary, + shape = CircleShape, + ), + ) { + Box( + modifier = Modifier + .padding(all = TangemTheme.dimens.spacing2) + .matchParentSize() + .background( + color = TangemTheme.colors.icon.informative, + shape = CircleShape, + ), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/IconBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconTopBadge.kt similarity index 66% rename from core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/IconBadge.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconTopBadge.kt index 09e597f7a6..03057d495d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/IconBadge.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconTopBadge.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.res.painterResource import com.tangem.core.ui.res.TangemTheme @Composable -internal fun TopBadge( +fun CurrencyIconTopBadge( @DrawableRes iconResId: Int, alpha: Float, colorFilter: ColorFilter?, @@ -38,26 +38,4 @@ internal fun TopBadge( contentDescription = null, ) } -} - -@Composable -internal fun BottomBadge(modifier: Modifier = Modifier) { - Box( - modifier = modifier - .size(TangemTheme.dimens.size12) - .background( - color = TangemTheme.colors.background.primary, - shape = CircleShape, - ), - ) { - Box( - modifier = Modifier - .padding(all = TangemTheme.dimens.spacing2) - .matchParentSize() - .background( - color = TangemTheme.colors.icon.informative, - shape = CircleShape, - ), - ) - } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt index 9863c30ba0..d69616e5d6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusManager @@ -36,7 +37,11 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @Composable -fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFieldColors = TangemSearchBarColors) { +fun SearchBar( + state: SearchBarUM, + modifier: Modifier = Modifier, + colors: TextFieldColors = TangemSearchBarDefaults.defaultTextFieldColors, +) { val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current val interactionSource = remember { MutableInteractionSource() } @@ -161,18 +166,30 @@ private fun ClearButton( } } -private val TangemSearchBarColors: TextFieldColors - @Composable - get() = TextFieldDefaults.colors().copy( - focusedContainerColor = TangemTheme.colors.field.primary, - unfocusedContainerColor = TangemTheme.colors.field.primary, - focusedTextColor = TangemTheme.colors.text.primary1, - unfocusedTextColor = TangemTheme.colors.text.primary1, - cursorColor = TangemTheme.colors.icon.primary1, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - ) +object TangemSearchBarDefaults { + val defaultTextFieldColors: TextFieldColors + @Composable @Stable get() = TextFieldDefaults.colors().copy( + focusedContainerColor = TangemTheme.colors.field.primary, + unfocusedContainerColor = TangemTheme.colors.field.primary, + focusedTextColor = TangemTheme.colors.text.primary1, + unfocusedTextColor = TangemTheme.colors.text.primary1, + cursorColor = TangemTheme.colors.icon.primary1, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + ) + val secondaryTextFieldColors: TextFieldColors + @Composable @Stable get() = TextFieldDefaults.colors().copy( + focusedContainerColor = TangemTheme.colors.field.focused, + unfocusedContainerColor = TangemTheme.colors.field.focused, + focusedTextColor = TangemTheme.colors.text.primary1, + unfocusedTextColor = TangemTheme.colors.text.primary1, + cursorColor = TangemTheme.colors.icon.primary1, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + ) +} // region Previews @Preview(widthDp = 328) diff --git a/core/ui/src/main/res/drawable/ic_nft_placeholder_76.xml b/core/ui/src/main/res/drawable/ic_nft_placeholder_76.xml new file mode 100644 index 0000000000..6e6a840d1f --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_nft_placeholder_76.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt index 39970e9671..076b704bb8 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt @@ -16,7 +16,7 @@ import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.common.ui.charts.state.rememberMarketChartState import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData +import com.tangem.core.ui.components.UnableToLoadData @Composable internal fun MarketTokenDetailsChart( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt index 0d04d56bbe..b796c0f38a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData +import com.tangem.core.ui.components.UnableToLoadData internal fun LazyListScope.tokenMarketDetailsBody( state: MarketsTokenDetailsUM.Body, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt index 703e2eae67..6fcc3b1195 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity +import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.list.InfiniteListHandler diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetUM.kt new file mode 100644 index 0000000000..fc75d29651 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetUM.kt @@ -0,0 +1,9 @@ +package com.tangem.features.nft.collections.entity + +internal data class NFTCollectionAssetUM( + val id: String, + val name: String, + val imageUrl: String?, + val price: NFTSalePriceUM, + val onItemClick: () -> Unit, +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetsListUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetsListUM.kt new file mode 100644 index 0000000000..7b0147b03a --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetsListUM.kt @@ -0,0 +1,16 @@ +package com.tangem.features.nft.collections.entity + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class NFTCollectionAssetsListUM { + data object Collapsed : NFTCollectionAssetsListUM() + + @Immutable + sealed class Expanded : NFTCollectionAssetsListUM() { + data class Loading(val itemsCount: Int) : Expanded() + data class Failed(val onRetryClick: () -> Unit) : Expanded() + data class Content(val items: ImmutableList) : Expanded() + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt new file mode 100644 index 0000000000..8e3375906b --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt @@ -0,0 +1,14 @@ +package com.tangem.features.nft.collections.entity + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference + +internal data class NFTCollectionUM( + val id: String, + val name: String, + @DrawableRes val networkIconId: Int, + val logoUrl: String?, + val description: TextReference, + val assets: NFTCollectionAssetsListUM, + val onExpandClick: () -> Unit, +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsUM.kt new file mode 100644 index 0000000000..394144b65d --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsUM.kt @@ -0,0 +1,29 @@ +package com.tangem.features.nft.collections.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class NFTCollectionsUM { + + data class Empty( + val onReceiveClick: () -> Unit, + ) : NFTCollectionsUM() + + data class Loading( + val onReceiveClick: () -> Unit, + ) : NFTCollectionsUM() + + data class Failed( + val onRetryClick: () -> Unit, + val onReceiveClick: () -> Unit, + ) : NFTCollectionsUM() + + data class Content( + val search: SearchBarUM, + val collections: ImmutableList, + val warnings: ImmutableList, + val onReceiveClick: () -> Unit, + ) : NFTCollectionsUM() +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsWarningUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsWarningUM.kt new file mode 100644 index 0000000000..f0bdc22aaa --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsWarningUM.kt @@ -0,0 +1,5 @@ +package com.tangem.features.nft.collections.entity + +import com.tangem.core.ui.components.notifications.NotificationConfig + +internal data class NFTCollectionsWarningUM(val config: NotificationConfig) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTSalePriceUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTSalePriceUM.kt new file mode 100644 index 0000000000..8747a49f55 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTSalePriceUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.nft.collections.entity + +import androidx.compose.runtime.Immutable + +@Immutable +internal sealed class NFTSalePriceUM { + data object Loading : NFTSalePriceUM() + data object Failed : NFTSalePriceUM() + data class Content(val price: String) : NFTSalePriceUM() +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt new file mode 100644 index 0000000000..32f7159f78 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt @@ -0,0 +1,178 @@ +package com.tangem.features.nft.collections.ui + +import android.content.res.Configuration +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import coil.compose.SubcomposeAsyncImage +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.currency.icon.CurrencyIconTopBadge +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.collections.entity.NFTCollectionAssetsListUM +import com.tangem.features.nft.collections.entity.NFTCollectionUM +import com.tangem.features.nft.impl.R + +private const val CHEVRON_ROTATION_EXPANDED = 180f +private const val CHEVRON_ROTATION_COLLAPSED = 0f + +@Composable +internal fun NFTCollection(state: NFTCollectionUM, modifier: Modifier = Modifier) { + val isExpanded = state.assets is NFTCollectionAssetsListUM.Expanded + + Column( + modifier = modifier, + ) { + Row( + modifier = Modifier + .clickable(onClick = state.onExpandClick) + .padding( + vertical = TangemTheme.dimens.spacing16, + horizontal = TangemTheme.dimens.spacing12, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Logo(state) + + Text(state) + + val rotation by animateFloatAsState( + targetValue = if (isExpanded) { + CHEVRON_ROTATION_EXPANDED + } else { + CHEVRON_ROTATION_COLLAPSED + }, + label = "chevron_rotation", + ) + + Icon( + modifier = Modifier + .rotate(rotation) + .size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_chevron_24), + tint = if (isExpanded) { + TangemTheme.colors.icon.primary1 + } else { + TangemTheme.colors.icon.inactive + }, + contentDescription = null, + ) + } + } +} + +@Composable +private fun Logo(state: NFTCollectionUM) { + val networkBadgeOffset = TangemTheme.dimens.spacing6 + + Box( + modifier = Modifier, + ) { + SubcomposeAsyncImage( + modifier = Modifier + .align(Alignment.CenterStart) + .size(TangemTheme.dimens.size36), + model = state.logoUrl, + loading = { + RectangleShimmer(radius = TangemTheme.dimens.radius8) + }, + error = { + Box( + modifier = Modifier + .clip(shape = TangemTheme.shapes.roundedCorners8) + .background(TangemTheme.colors.field.primary), + ) + }, + contentDescription = null, + ) + CurrencyIconTopBadge( + modifier = Modifier + .offset(x = networkBadgeOffset, y = -networkBadgeOffset) + .align(Alignment.TopEnd), + iconResId = state.networkIconId, + alpha = 1f, + colorFilter = null, + ) + } +} + +@Composable +private fun RowScope.Text(state: NFTCollectionUM) { + Column( + modifier = Modifier + .weight(1f) + .padding( + horizontal = TangemTheme.dimens.spacing12, + ), + ) { + Text( + modifier = Modifier, + text = state.name, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing2), + text = state.description.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTCollection(@PreviewParameter(NFTCollectionProvider::class) item: NFTCollectionUM) { + TangemThemePreview { + NFTCollection( + state = item, + ) + } +} + +private class NFTCollectionProvider : CollectionPreviewParameterProvider( + collection = listOf( + NFTCollectionUM( + id = "item1", + name = "Nethers", + logoUrl = "", + networkIconId = R.drawable.img_eth_22, + description = TextReference.Str("3 items"), + assets = NFTCollectionAssetsListUM.Collapsed, + onExpandClick = { }, + ), + NFTCollectionUM( + id = "item2", + name = "Nethers", + logoUrl = "", + networkIconId = R.drawable.img_eth_22, + description = TextReference.Str("3 items"), + assets = NFTCollectionAssetsListUM.Expanded.Loading( + itemsCount = 3, + ), + onExpandClick = { }, + ), + ), +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAsset.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAsset.kt new file mode 100644 index 0000000000..9951cd16a7 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAsset.kt @@ -0,0 +1,100 @@ +package com.tangem.features.nft.collections.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import coil.compose.SubcomposeAsyncImage +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.SpacerH2 +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.collections.entity.NFTCollectionAssetUM +import com.tangem.features.nft.collections.entity.NFTSalePriceUM + +@Composable +internal fun NFTCollectionAsset(state: NFTCollectionAssetUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .clickable { state.onItemClick() }, + ) { + SubcomposeAsyncImage( + modifier = Modifier + .aspectRatio(1f), + model = state.imageUrl, + loading = { + RectangleShimmer(radius = TangemTheme.dimens.radius16) + }, + error = { + Box( + modifier = Modifier + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.field.primary), + ) + }, + contentDescription = null, + ) + SpacerH12() + Text( + text = state.name, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + SpacerH2() + NFTSalePrice( + state = state.price, + ) + } +} + +@Preview(widthDp = 180, showBackground = true) +@Preview(widthDp = 180, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTCollectionAsset( + @PreviewParameter(NFTCollectionAssetProvider::class) state: NFTCollectionAssetUM, +) { + TangemThemePreview { + NFTCollectionAsset( + state = state, + ) + } +} + +private class NFTCollectionAssetProvider : CollectionPreviewParameterProvider( + collection = listOf( + NFTCollectionAssetUM( + id = "item1", + name = "Nethers #0853", + imageUrl = "img", + price = NFTSalePriceUM.Loading, + onItemClick = { }, + ), + NFTCollectionAssetUM( + id = "item2", + name = "Nethers #0854", + imageUrl = "img", + price = NFTSalePriceUM.Failed, + onItemClick = { }, + ), + NFTCollectionAssetUM( + id = "item3", + name = "Nethers #0855", + imageUrl = "img", + price = NFTSalePriceUM.Content("0.05 ETH"), + onItemClick = { }, + ), + ), +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAssetLoading.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAssetLoading.kt new file mode 100644 index 0000000000..66bf216ad7 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAssetLoading.kt @@ -0,0 +1,47 @@ +package com.tangem.features.nft.collections.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +internal fun NFTCollectionAssetLoading(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + ) { + RectangleShimmer( + modifier = Modifier + .aspectRatio(1f), + ) + SpacerH12() + TextShimmer( + modifier = Modifier + .width(TangemTheme.dimens.size86), + style = TangemTheme.typography.subtitle2, + textSizeHeight = true, + ) + SpacerH2() + TextShimmer( + modifier = Modifier + .width(TangemTheme.dimens.size48), + style = TangemTheme.typography.caption2, + textSizeHeight = true, + ) + } +} + +@Preview(widthDp = 180, showBackground = true) +@Preview(widthDp = 180, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTCollectionAsset() { + TangemThemePreview { + NFTCollectionAssetLoading() + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionWarning.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionWarning.kt new file mode 100644 index 0000000000..337e675026 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionWarning.kt @@ -0,0 +1,14 @@ +package com.tangem.features.nft.collections.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.features.nft.collections.entity.NFTCollectionsWarningUM + +@Composable +internal fun NFTCollectionWarning(state: NFTCollectionsWarningUM, modifier: Modifier = Modifier) { + Notification( + modifier = modifier, + config = state.config, + ) +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt new file mode 100644 index 0000000000..6a1b002cf4 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt @@ -0,0 +1,318 @@ +package com.tangem.features.nft.collections.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.UnableToLoadData +import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.components.fields.TangemSearchBarDefaults +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.collections.entity.* +import com.tangem.features.nft.collections.entity.NFTCollectionAssetUM +import com.tangem.features.nft.collections.entity.NFTCollectionAssetsListUM +import com.tangem.features.nft.collections.entity.NFTCollectionUM +import com.tangem.features.nft.collections.entity.NFTCollectionsUM +import com.tangem.features.nft.collections.entity.NFTSalePriceUM +import com.tangem.features.nft.impl.R +import kotlinx.collections.immutable.persistentListOf + +@Suppress("LongMethod") +@Composable +internal fun NFTCollectionsContent(state: NFTCollectionsUM.Content, modifier: Modifier = Modifier) { + val listState = rememberLazyListState() + + Box( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.secondary) + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + Column( + modifier = Modifier, + ) { + SearchBar( + state = state.search, + colors = TangemSearchBarDefaults.secondaryTextFieldColors, + ) + state.warnings.fastForEach { + NFTCollectionWarning( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing16), + state = it, + ) + } + LazyColumn( + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing16, + ) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.primary), + state = listState, + ) { + state.collections.fastForEach { collection -> + item(key = collection.id) { + NFTCollection( + modifier = Modifier.fillMaxWidth(), + state = collection, + ) + } + when (val assets = collection.assets) { + is NFTCollectionAssetsListUM.Collapsed -> Unit + is NFTCollectionAssetsListUM.Expanded.Loading -> { + assetsListLoading( + collectionId = collection.id, + content = assets, + ) + } + is NFTCollectionAssetsListUM.Expanded.Failed -> { + assetsListFailed( + collectionId = collection.id, + content = assets, + ) + } + is NFTCollectionAssetsListUM.Expanded.Content -> { + assetsListContent( + collectionId = collection.id, + content = assets, + ) + } + } + } + } + } + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + text = stringResourceSafe(R.string.nft_collections_receive), + onClick = { }, + ) + } +} + +private fun LazyListScope.assetsListLoading( + collectionId: String, + content: NFTCollectionAssetsListUM.Expanded.Loading, +) { + val itemsCount = content.itemsCount + val rowCount = (itemsCount + 1) / 2 + repeat(rowCount) { rowIndex -> + item( + key = "loading_${collectionId}_$rowIndex", + ) { + val paddingValues = PaddingValues( + start = TangemTheme.dimens.spacing6, + top = TangemTheme.dimens.spacing6, + end = TangemTheme.dimens.spacing6, + bottom = TangemTheme.dimens.spacing20, + ) + Row( + modifier = Modifier + .padding(TangemTheme.dimens.spacing6) + .animateItem(), + ) { + NFTCollectionAssetLoading( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + ) + if (rowIndex == rowCount - 1 && itemsCount % 2 != 0) { + Box( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + ) + } else { + NFTCollectionAssetLoading( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + ) + } + } + } + } +} + +private fun LazyListScope.assetsListFailed(collectionId: String, content: NFTCollectionAssetsListUM.Expanded.Failed) { + item( + key = "failed_$collectionId", + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size142), + contentAlignment = Alignment.Center, + ) { + UnableToLoadData( + onRetryClick = content.onRetryClick, + ) + } + } +} + +private fun LazyListScope.assetsListContent( + collectionId: String, + content: NFTCollectionAssetsListUM.Expanded.Content, +) { + val items = content.items + val itemsCount = items.size + val rowCount = (itemsCount + 1) / 2 + repeat(rowCount) { rowIndex -> + val item1 = items[rowIndex * 2] + val item2 = items.getOrNull(rowIndex * 2 + 1) + item( + key = "content_${collectionId}_${item1.id}_${item2?.id}", + ) { + val paddingValues = PaddingValues( + start = TangemTheme.dimens.spacing6, + top = TangemTheme.dimens.spacing6, + end = TangemTheme.dimens.spacing6, + bottom = TangemTheme.dimens.spacing20, + ) + Row( + modifier = Modifier + .padding(TangemTheme.dimens.spacing6) + .animateItem(), + ) { + NFTCollectionAsset( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + state = item1, + ) + if (item2 == null) { + Box( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + ) + } else { + NFTCollectionAsset( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + state = item2, + ) + } + } + } + } +} + +@Suppress("LongMethod") +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTCollectionsContent() { + TangemThemePreview { + NFTCollectionsContent( + state = NFTCollectionsUM.Content( + search = SearchBarUM( + placeholderText = resourceReference(R.string.common_search), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = { }, + ), + collections = persistentListOf( + NFTCollectionUM( + id = "item1", + name = "Nethers", + logoUrl = "", + networkIconId = R.drawable.img_eth_22, + description = TextReference.Str("3 items"), + assets = NFTCollectionAssetsListUM.Collapsed, + onExpandClick = { }, + ), + NFTCollectionUM( + id = "item2", + name = "Nethers", + logoUrl = "", + networkIconId = R.drawable.img_eth_22, + description = TextReference.Str("3 items"), + assets = NFTCollectionAssetsListUM.Expanded.Loading( + itemsCount = 1, + ), + onExpandClick = { }, + ), + NFTCollectionUM( + id = "item3", + name = "Nethers", + logoUrl = "", + networkIconId = R.drawable.img_eth_22, + description = TextReference.Str("3 items"), + assets = NFTCollectionAssetsListUM.Expanded.Failed( + onRetryClick = { }, + ), + onExpandClick = { }, + ), + NFTCollectionUM( + id = "item4", + name = "Nethers", + logoUrl = "", + networkIconId = R.drawable.img_eth_22, + description = TextReference.Str("3 items"), + assets = NFTCollectionAssetsListUM.Expanded.Content( + items = persistentListOf( + NFTCollectionAssetUM( + id = "item1", + name = "Nethers #0854", + imageUrl = "img", + price = NFTSalePriceUM.Content("0.05 ETH"), + onItemClick = { }, + ), + NFTCollectionAssetUM( + id = "item2", + name = "Nethers #0855", + imageUrl = "img", + price = NFTSalePriceUM.Loading, + onItemClick = { }, + ), + NFTCollectionAssetUM( + id = "item3", + name = "Nethers #0856", + imageUrl = "img", + price = NFTSalePriceUM.Failed, + onItemClick = { }, + ), + ), + ), + onExpandClick = { }, + ), + ), + warnings = persistentListOf( + NFTCollectionsWarningUM( + config = NotificationConfig( + title = TextReference.Res(R.string.nft_collections_warning_title), + subtitle = TextReference.Res(R.string.nft_collections_warning_subtitle), + iconResId = R.drawable.ic_alert_triangle_20, + ), + ), + ), + onReceiveClick = { }, + ), + ) + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt new file mode 100644 index 0000000000..1376f56e72 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt @@ -0,0 +1,74 @@ +package com.tangem.features.nft.collections.ui + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.collections.entity.NFTCollectionsUM +import com.tangem.features.nft.impl.R + +@Composable +internal fun NFTCollectionsEmpty(state: NFTCollectionsUM.Empty, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .padding(TangemTheme.dimens.spacing16), + ) { + Column( + modifier = Modifier + .padding(TangemTheme.dimens.spacing16) + .align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(R.drawable.ic_nft_placeholder_76), + contentDescription = null, + ) + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing24), + text = stringResource(R.string.nft_collections_empty_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + text = stringResource(R.string.nft_collections_empty_description), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + PrimaryButton( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing48) + .wrapContentWidth(), + text = stringResourceSafe(R.string.nft_collections_receive), + onClick = state.onReceiveClick, + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTCollectionsEmpty() { + TangemThemePreview { + NFTCollectionsEmpty( + state = NFTCollectionsUM.Empty( + onReceiveClick = { }, + ), + ) + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsFailed.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsFailed.kt new file mode 100644 index 0000000000..f16d193537 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsFailed.kt @@ -0,0 +1,54 @@ +package com.tangem.features.nft.collections.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.UnableToLoadData +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.collections.entity.NFTCollectionsUM +import com.tangem.features.nft.impl.R + +@Composable +internal fun NFTCollectionsFailed(state: NFTCollectionsUM.Failed, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .padding(TangemTheme.dimens.spacing16), + ) { + UnableToLoadData( + modifier = Modifier + .align(Alignment.Center), + onRetryClick = state.onRetryClick, + ) + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + text = stringResourceSafe(R.string.nft_collections_receive), + onClick = { }, + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTCollectionsFailed() { + TangemThemePreview { + NFTCollectionsFailed( + state = NFTCollectionsUM.Failed( + onRetryClick = { }, + onReceiveClick = { }, + ), + ) + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTSalePrice.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTSalePrice.kt new file mode 100644 index 0000000000..ce4a1db2cf --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTSalePrice.kt @@ -0,0 +1,40 @@ +package com.tangem.features.nft.collections.ui + +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.nft.collections.entity.NFTSalePriceUM + +@Composable +internal fun NFTSalePrice(state: NFTSalePriceUM, modifier: Modifier = Modifier) { + when (state) { + is NFTSalePriceUM.Content -> { + Text( + modifier = modifier, + text = state.price, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + is NFTSalePriceUM.Failed -> { + Text( + modifier = modifier, + text = "", + style = TangemTheme.typography.caption2, + ) + } + is NFTSalePriceUM.Loading -> { + TextShimmer( + modifier = modifier + .width(TangemTheme.dimens.size48), + style = TangemTheme.typography.caption2, + ) + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index 679abe3866..c45da297d2 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -6,11 +6,9 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -18,6 +16,7 @@ import androidx.compose.ui.util.fastForEachIndexed import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.components.fields.TangemSearchBarDefaults import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.tokenlist.TokenListItem @@ -74,16 +73,7 @@ internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) { private fun SearchBar(searchBarUM: SearchBarUM) { SearchBar( state = searchBarUM, - colors = TextFieldDefaults.colors().copy( - focusedContainerColor = TangemTheme.colors.field.focused, - unfocusedContainerColor = TangemTheme.colors.field.focused, - focusedTextColor = TangemTheme.colors.text.primary1, - unfocusedTextColor = TangemTheme.colors.text.primary1, - cursorColor = TangemTheme.colors.icon.primary1, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - ), + colors = TangemSearchBarDefaults.secondaryTextFieldColors, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt index 850b0d78ab..7f2a8515b7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt @@ -394,7 +394,7 @@ private fun RowContentContainer( @Preview(widthDp = 360) @Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_WalletNFTItem_InLight(@PreviewParameter(WalletNFTItemProvider::class) state: WalletNFTItemUM) { +private fun Preview_WalletNFTItem(@PreviewParameter(WalletNFTItemProvider::class) state: WalletNFTItemUM) { TangemThemePreview { WalletNFTItem( state = state, From eee3d40e31ff359115804db297c353436aad09c9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Mar 2025 11:08:57 +0500 Subject: [PATCH 048/138] Updated on 2026-08-14 --- .../features/send/v2/di/SendModelModule.kt | 6 + .../send/v2/send/DefaultSendComponent.kt | 31 +- .../features/send/v2/send/model/SendModel.kt | 15 +- .../features/send/v2/send/ui/state/SendUM.kt | 2 + .../send/v2/subcomponents/Notifications.kt | 48 +++ .../destination/SendDestinationComponent.kt | 2 + .../fee/SendFeeBlockComponent.kt | 42 +++ .../v2/subcomponents/fee/SendFeeComponent.kt | 30 ++ .../fee/SendFeeComponentParams.kt | 47 +++ .../subcomponents/fee/SendFeeReloadTrigger.kt | 59 ++++ .../fee/analytics/SendFeeAnalyticEvents.kt | 39 +++ .../v2/subcomponents/fee/di/SendFeeModule.kt | 27 ++ .../subcomponents/fee/model/FeeCalculation.kt | 90 ++++++ .../fee/model/SendFeeAlertFactory.kt | 127 ++++++++ .../fee/model/SendFeeClickIntents.kt | 16 + .../subcomponents/fee/model/SendFeeModel.kt | 290 ++++++++++++++++++ .../fee/model/converters/FeeConverter.kt | 50 +++ .../converters/SendFeeCustomFieldConverter.kt | 112 +++++++ .../converters/custom/CustomFeeConverter.kt | 14 + .../bitcoin/BitcoinCustomFeeConverter.kt | 143 +++++++++ .../BaseEthereumCustomFeeConverter.kt | 25 ++ .../ethereum/EthereumCustomFeeConverter.kt | 126 ++++++++ .../ethereum/EthereumEIPCustomFeeConverter.kt | 204 ++++++++++++ .../EthereumLegacyCustomFeeConverter.kt | 181 +++++++++++ .../custom/kaspa/KaspaCustomFeeConverter.kt | 131 ++++++++ .../SendFeeCustomAutoFixTransformer.kt | 45 +++ .../SendFeeCustomValueChangeTransformer.kt | 39 +++ .../transformers/SendFeeFailedTransformer.kt | 20 ++ .../SendFeeInitialStateTransformer.kt | 34 ++ .../transformers/SendFeeLoadedTransformer.kt | 64 ++++ .../transformers/SendFeeLoadingTransformer.kt | 17 + .../SendFeeNotificationsTransformer.kt | 51 +++ .../transformers/SendFeeSelectTransformer.kt | 46 +++ .../send/v2/subcomponents/fee/ui/FeeBlock.kt | 132 ++++++++ .../v2/subcomponents/fee/ui/SendCustomFee.kt | 82 +++++ .../v2/subcomponents/fee/ui/SendFeeContent.kt | 79 +++++ .../subcomponents/fee/ui/SendSpeedSelector.kt | 102 ++++++ .../fee/ui/SendSpeedSelectorItem.kt | 152 +++++++++ .../fee/ui/state/CustomFeeFieldUM.kt | 20 ++ .../fee/ui/state/FeeSelectorUM.kt | 44 +++ .../v2/subcomponents/fee/ui/state/FeeUM.kt | 32 ++ 41 files changed, 2811 insertions(+), 5 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/Notifications.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeBlockComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeReloadTrigger.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/analytics/SendFeeAnalyticEvents.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/di/SendFeeModule.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/FeeCalculation.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeAlertFactory.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeClickIntents.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/CustomFeeConverter.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/BaseEthereumCustomFeeConverter.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomAutoFixTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomValueChangeTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeFailedTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeInitialStateTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadedTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadingTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeNotificationsTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeSelectTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendFeeContent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelector.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/CustomFeeFieldUM.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt index 488de424c8..60d45d9aa5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt @@ -5,6 +5,7 @@ import com.tangem.core.decompose.model.Model import com.tangem.features.send.v2.send.model.SendModel import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -29,4 +30,9 @@ internal interface SendModelModule { @IntoMap @ClassKey(SendDestinationModel::class) fun provideSendDestinationModel(model: SendDestinationModel): Model + + @Binds + @IntoMap + @ClassKey(SendFeeModel::class) + fun provideSendFeeModel(model: SendFeeModel): Model } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 3a97b61648..3683819133 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.pop +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel @@ -17,6 +18,9 @@ import com.tangem.features.send.v2.send.model.SendModel import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent +import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -66,7 +70,7 @@ internal class DefaultSendComponent @AssistedInject constructor( SendRoute.Empty -> getStubComponent() is SendRoute.Destination -> getDestinationComponent(factoryContext, route) is SendRoute.Amount -> getAmountComponent(factoryContext, route) - is SendRoute.Fee -> getFeeComponent() + is SendRoute.Fee -> getFeeComponent(factoryContext) SendRoute.Confirm -> getConfirmComponent() } @@ -99,7 +103,30 @@ internal class DefaultSendComponent @AssistedInject constructor( ), ) - private fun getFeeComponent() = getStubComponent() // todo + private fun getFeeComponent(factoryContext: AppComponentContext): ComposableContentComponent { + val state = model.uiState.value + val sendAmount = (state.amountUM as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.value + return if (sendAmount != null && destinationAddress != null) { + SendFeeComponent( + appComponentContext = factoryContext, + params = SendFeeComponentParams.FeeParams( + state = model.uiState.value.feeUM, + currentRoute = currentRoute.filterIsInstance(), + analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, + userWallet = model.userWallet, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus, + appCurrency = model.appCurrency, + sendAmount = sendAmount, + destinationAddress = destinationAddress, + callback = model, + ), + ) + } else { + getStubComponent() + } + } private fun getConfirmComponent() = getStubComponent() // todo diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 5c9840758f..b122c64efd 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -11,6 +11,8 @@ import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -25,13 +27,15 @@ internal class SendModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, ) : Model(), SendDestinationComponent.ModelCallback, - SendAmountComponent.ModelCallback { + SendAmountComponent.ModelCallback, + SendFeeComponent.ModelCallback { private val _uiState = MutableStateFlow(initialState()) val uiState = _uiState.asStateFlow() var userWallet: UserWallet by Delegates.notNull() var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() var appCurrency: AppCurrency = AppCurrency.Default var predefinedAmountValue: String? = null @@ -39,12 +43,17 @@ internal class SendModel @Inject constructor( _uiState.update { it.copy(destinationUM = destinationUM) } } - override fun onAmountResult(state: AmountState) { - _uiState.update { it.copy(amountUM = state) } + override fun onAmountResult(amountUM: AmountState) { + _uiState.update { it.copy(amountUM = amountUM) } + } + + override fun onFeeResult(feeUM: FeeUM) { + _uiState.update { it.copy(feeUM = feeUM) } } private fun initialState(): SendUM = SendUM( amountUM = AmountState.Empty(), destinationUM = DestinationUM.Empty(), + feeUM = FeeUM.Empty(), ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt index 41cfc2f614..49a4f918a7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt @@ -2,8 +2,10 @@ package com.tangem.features.send.v2.send.ui.state import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM internal data class SendUM( val amountUM: AmountState, val destinationUM: DestinationUM, + val feeUM: FeeUM, ) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/Notifications.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/Notifications.kt new file mode 100644 index 0000000000..4704183c22 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/Notifications.kt @@ -0,0 +1,48 @@ +package com.tangem.features.send.v2.subcomponents + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList + +internal fun LazyListScope.notifications( + notifications: ImmutableList, + modifier: Modifier = Modifier, + hasPaddingAbove: Boolean = false, + isClickDisabled: Boolean = false, +) { + itemsIndexed( + items = notifications, + key = { _, item -> item::class.java }, + contentType = { _, item -> item::class.java }, + itemContent = { i, item -> + val topPadding = if (i == 0 && hasPaddingAbove) 0.dp else 12.dp + Notification( + config = item.config, + modifier = modifier + .padding(top = topPadding) + .animateItem(), + containerColor = when (item) { + is NotificationUM.Error.TokenExceedsBalance, + is NotificationUM.Warning.NetworkFeeUnreachable, + is NotificationUM.Warning.HighFeeError, + -> TangemTheme.colors.background.action + else -> TangemTheme.colors.button.disabled + }, + iconTint = when (item) { + is NotificationUM.Error.TokenExceedsBalance, + is NotificationUM.Warning, + -> null + is NotificationUM.Error -> TangemTheme.colors.icon.warning + is NotificationUM.Info -> TangemTheme.colors.icon.accent + }, + isEnabled = !isClickDisabled, + ) + }, + ) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt index 3110982216..c67e2b6b74 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt @@ -21,6 +21,8 @@ internal class SendDestinationComponent( private val model: SendDestinationModel = getOrCreateModel(params = params, router = router) + fun updateState(state: DestinationUM) = model.updateState(state) + @Composable override fun Content(modifier: Modifier) { val state = model.uiState.collectAsStateWithLifecycle() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeBlockComponent.kt new file mode 100644 index 0000000000..4190c4a79d --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeBlockComponent.kt @@ -0,0 +1,42 @@ +package com.tangem.features.send.v2.subcomponents.fee + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.subcomponents.fee.ui.FeeBlock +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +internal class SendFeeBlockComponent( + appComponentContext: AppComponentContext, + private val params: SendFeeComponentParams.FeeBlockParams, + val onResult: (FeeUM) -> Unit, + val onClick: () -> Unit, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: SendFeeModel = getOrCreateModel(params = params) + + init { + model.uiState.onEach { + onResult(it) + }.launchIn(componentScope) + } + + fun updateState(state: FeeUM) = model.updateState(state) + + @Composable + override fun Content(modifier: Modifier) { + val state = model.uiState.collectAsStateWithLifecycle() + val isClickEnabled = params.blockClickEnableFlow.collectAsStateWithLifecycle() + FeeBlock( + feeUM = state.value, + isClickEnabled = isClickEnabled.value, + onClick = onClick, + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt new file mode 100644 index 0000000000..b5a67be3a4 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt @@ -0,0 +1,30 @@ +package com.tangem.features.send.v2.subcomponents.fee + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeModel +import com.tangem.features.send.v2.subcomponents.fee.ui.SendFeeContent + +internal class SendFeeComponent( + appComponentContext: AppComponentContext, + private val params: SendFeeComponentParams.FeeParams, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: SendFeeModel = getOrCreateModel(params = params) + + @Composable + override fun Content(modifier: Modifier) { + val state = model.uiState.collectAsStateWithLifecycle() + + SendFeeContent(state = state.value, clickIntents = model) + } + + interface ModelCallback { + fun onFeeResult(state: FeeUM) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt new file mode 100644 index 0000000000..82c9f88081 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt @@ -0,0 +1,47 @@ +package com.tangem.features.send.v2.subcomponents.fee + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import java.math.BigDecimal + +internal sealed class SendFeeComponentParams { + + abstract val state: FeeUM + abstract val analyticsCategoryName: String + abstract val userWallet: UserWallet + abstract val cryptoCurrencyStatus: CryptoCurrencyStatus + abstract val feeCryptoCurrencyStatus: CryptoCurrencyStatus + abstract val appCurrency: AppCurrency + abstract val sendAmount: BigDecimal + abstract val destinationAddress: String + + data class FeeParams( + override val state: FeeUM, + override val analyticsCategoryName: String, + override val userWallet: UserWallet, + override val cryptoCurrencyStatus: CryptoCurrencyStatus, + override val feeCryptoCurrencyStatus: CryptoCurrencyStatus, + override val appCurrency: AppCurrency, + override val sendAmount: BigDecimal, + override val destinationAddress: String, + val currentRoute: Flow, + val callback: SendFeeComponent.ModelCallback, + ) : SendFeeComponentParams() + + data class FeeBlockParams( + override val state: FeeUM, + override val analyticsCategoryName: String, + override val userWallet: UserWallet, + override val cryptoCurrencyStatus: CryptoCurrencyStatus, + override val feeCryptoCurrencyStatus: CryptoCurrencyStatus, + override val appCurrency: AppCurrency, + override val sendAmount: BigDecimal, + override val destinationAddress: String, + val blockClickEnableFlow: StateFlow, + ) : SendFeeComponentParams() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeReloadTrigger.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeReloadTrigger.kt new file mode 100644 index 0000000000..6502eb6fa2 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeReloadTrigger.kt @@ -0,0 +1,59 @@ +package com.tangem.features.send.v2.subcomponents.fee + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import javax.inject.Inject +import javax.inject.Singleton + +interface SendFeeReloadTrigger { + + /** Flow triggers fee reload */ + val reloadTriggerFlow: Flow + + /** Trigger fee update */ + suspend fun triggerUpdate() +} + +interface SendFeeCheckReloadTrigger { + + /** + * Flow triggers fee check reload before transaction. + * Usually after significant time after fee was updated last time + */ + val checkReloadTriggerFlow: Flow + + /** Flow return result of fee check update */ + val checkReloadResultFlow: Flow + + /** Trigger return callback with check result */ + suspend fun callbackCheckResult(isSuccess: Boolean) + + /** Trigger fee check reload */ + suspend fun triggerCheckUpdate() +} + +@Singleton +internal class DefaultSendFeeReloadTrigger @Inject constructor() : SendFeeReloadTrigger, SendFeeCheckReloadTrigger { + + private val _reloadTriggerFlow = MutableSharedFlow() + override val reloadTriggerFlow = _reloadTriggerFlow.asSharedFlow() + + private val _checkReloadTriggerFlow = MutableSharedFlow() + override val checkReloadTriggerFlow = _checkReloadTriggerFlow.asSharedFlow() + + private val _checkReloadResultFlow = MutableSharedFlow() + override val checkReloadResultFlow = _checkReloadResultFlow.asSharedFlow() + + override suspend fun triggerUpdate() { + _reloadTriggerFlow.emit(Unit) + } + + override suspend fun triggerCheckUpdate() { + _checkReloadTriggerFlow.emit(Unit) + } + + override suspend fun callbackCheckResult(isSuccess: Boolean) { + _checkReloadResultFlow.emit(isSuccess) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/analytics/SendFeeAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/analytics/SendFeeAnalyticEvents.kt new file mode 100644 index 0000000000..c65789aaa4 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/analytics/SendFeeAnalyticEvents.kt @@ -0,0 +1,39 @@ +package com.tangem.features.send.v2.subcomponents.fee.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam + +internal sealed class SendFeeAnalyticEvents( + category: String, + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = category, event = event, params = params) { + + abstract val categoryName: String + + /** Selected fee (send after next screen opened) */ + data class SelectedFee( + override val categoryName: String, + val feeType: AnalyticsParam.FeeType, + ) : SendFeeAnalyticEvents( + category = categoryName, + event = "Fee Selected", + params = mapOf("Fee Type" to feeType.value), + ) + + /** Custom fee selected */ + data class CustomFeeButtonClicked( + override val categoryName: String, + ) : SendFeeAnalyticEvents( + category = categoryName, + event = "Custom Fee Clicked", + ) + + /** Custom fee edited */ + data class GasPriceInserter( + override val categoryName: String, + ) : SendFeeAnalyticEvents( + category = categoryName, + event = "Gas Price Inserted", + ) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/di/SendFeeModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/di/SendFeeModule.kt new file mode 100644 index 0000000000..27cc5cacd4 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/di/SendFeeModule.kt @@ -0,0 +1,27 @@ +package com.tangem.features.send.v2.subcomponents.fee.di + +import com.tangem.features.send.v2.subcomponents.fee.DefaultSendFeeReloadTrigger +import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger +import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +internal object SendFeeModule { + + @Provides + @Singleton + fun provideSendFeeReloadTrigger(): SendFeeReloadTrigger { + return DefaultSendFeeReloadTrigger() + } + + @Provides + @Singleton + fun provideSendFeeCheckReloadTrigger(): SendFeeCheckReloadTrigger { + return DefaultSendFeeReloadTrigger() + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/FeeCalculation.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/FeeCalculation.kt new file mode 100644 index 0000000000..939bb73a64 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/FeeCalculation.kt @@ -0,0 +1,90 @@ +package com.tangem.features.send.v2.subcomponents.fee.model + +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.utils.extensions.isZero +import java.math.BigDecimal +import java.math.RoundingMode + +private val FEE_MAX_DIFF = BigDecimal("5") + +/** + * Check and calculates subtracted amount + */ +internal fun checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable: Boolean, + cryptoCurrencyStatus: CryptoCurrencyStatus, + amountValue: BigDecimal, + feeValue: BigDecimal, + reduceAmountBy: BigDecimal, +): BigDecimal { + val balance = cryptoCurrencyStatus.value.amount ?: return amountValue + val isFeeCoverage = checkFeeCoverage( + isSubtractAvailable = isAmountSubtractAvailable, + balance = balance, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + return if (isFeeCoverage) { + balance.minus(reduceAmountBy).minus(feeValue) + } else { + amountValue + } +} + +/** + * Check if custom fee is too high + */ +internal fun checkIfFeeTooHigh(feeSelectorState: FeeSelectorUM.Content): Pair { + val defaultResult = false to "" + val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return defaultResult + val highValue = multipleFees.priority.amount.value ?: return defaultResult + val customAmount = feeSelectorState.customValues.firstOrNull() ?: return defaultResult + val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) + val diff = if (highValue > BigDecimal.ZERO) { + customValue / highValue + } else { + BigDecimal.ZERO + } + val isFeeTooHigh = feeSelectorState.selectedType == FeeType.Custom && diff > FEE_MAX_DIFF + return isFeeTooHigh to diff.parseBigDecimal(0, RoundingMode.HALF_UP) +} + +/** + * Check if custom fee is too low + */ +internal fun checkIfFeeTooLow(feeSelectorUM: FeeSelectorUM.Content): Boolean { + val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return false + val minimumValue = multipleFees.minimum.amount.value ?: return false + val customAmount = feeSelectorUM.customValues.firstOrNull() ?: return false + val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) + + return feeSelectorUM.selectedType == FeeType.Custom && minimumValue > customValue +} + +/** + * Checks if sending amount with fee is greater than balance + */ +internal fun checkFeeCoverage( + isSubtractAvailable: Boolean, + balance: BigDecimal, + amountValue: BigDecimal, + feeValue: BigDecimal, + reduceAmountBy: BigDecimal?, +): Boolean { + if (!isSubtractAvailable) return false + val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO) + return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue +} + +/** + * Checks if fee exceeds fee paid currency balance + */ +fun checkExceedBalance(feeBalance: BigDecimal?, feeAmount: BigDecimal?): Boolean { + return feeAmount == null || feeBalance == null || feeAmount.isZero() || feeAmount > feeBalance +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeAlertFactory.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeAlertFactory.kt new file mode 100644 index 0000000000..6526312b27 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeAlertFactory.kt @@ -0,0 +1,127 @@ +package com.tangem.features.send.v2.subcomponents.fee.model + +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import java.math.BigDecimal +import javax.inject.Inject + +@ModelScoped +internal class SendFeeAlertFactory @Inject constructor( + private val messageSender: UiMessageSender, +) { + + fun checkAndShowAlerts(feeSelectorUM: FeeSelectorUM.Content, onConfirmClick: () -> Unit) { + val showFeeTooLow = checkAndShowFeeTooLow(feeSelectorUM, onConfirmClick) + val showFeeTooHigh = checkAndShowFeeTooHigh(feeSelectorUM, onConfirmClick) + + if (!showFeeTooLow && !showFeeTooHigh) { + onConfirmClick() + } + } + + /** + * Check if custom fee is too low + */ + private fun checkAndShowFeeTooLow(feeSelectorUM: FeeSelectorUM.Content, onConfirmClick: () -> Unit): Boolean { + val isFeeTooLow = checkIfFeeTooLow(feeSelectorUM) + if (isFeeTooLow) { + messageSender.send( + DialogMessage( + message = resourceReference(id = R.string.send_alert_fee_too_low_text), + dismissOnFirstAction = true, + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_continue), + onClick = onConfirmClick, + ) + }, + secondActionBuilder = { cancelAction() }, + ), + ) + } + return isFeeTooLow + } + + /** + * Check if custom fee is too high + */ + private fun checkAndShowFeeTooHigh(feeSelectorUM: FeeSelectorUM.Content, onConfirmClick: () -> Unit): Boolean { + val (isFeeTooHigh, diff) = checkIfFeeTooHigh(feeSelectorUM) + if (isFeeTooHigh) { + messageSender.send( + DialogMessage( + message = resourceReference( + id = R.string.send_alert_fee_too_high_text, + formatArgs = wrappedList(diff), + ), + dismissOnFirstAction = true, + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_continue), + onClick = onConfirmClick, + ) + }, + secondActionBuilder = { cancelAction() }, + ), + ) + } + return isFeeTooHigh + } + + fun getFeeUpdatedAlert(newFee: TransactionFee, feeUM: FeeUM, onFeeNotIncreased: () -> Unit) { + if (feeUM !is FeeUM.Content) return + val feeSelectorUM = feeUM.feeSelectorUM as? FeeSelectorUM.Content ?: return + val newFee = when (newFee) { + is TransactionFee.Single -> newFee.normal + is TransactionFee.Choosable -> { + when (feeSelectorUM.selectedType) { + FeeType.Slow -> newFee.minimum + FeeType.Market -> newFee.normal + FeeType.Fast -> newFee.priority + FeeType.Custom -> return + } + } + } + + val newFeeValue = newFee.amount.value ?: BigDecimal.ZERO + val oldFeeValue = feeSelectorUM.selectedFee?.amount?.value ?: BigDecimal.ZERO + + if (newFeeValue > oldFeeValue) { + messageSender.send( + DialogMessage( + message = resourceReference(id = R.string.send_notification_high_fee_title), + dismissOnFirstAction = true, + firstActionBuilder = { okAction() }, + secondActionBuilder = { cancelAction() }, + ), + ) + } else { + onFeeNotIncreased() + } + } + + fun getFeeUnreachableErrorState(onFeeReload: () -> Unit) { + messageSender.send( + DialogMessage( + title = resourceReference(R.string.send_fee_unreachable_error_title), + message = resourceReference(R.string.send_fee_unreachable_error_text), + dismissOnFirstAction = true, + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.warning_button_refresh), + onClick = onFeeReload, + ) + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeClickIntents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeClickIntents.kt new file mode 100644 index 0000000000..656699429a --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeClickIntents.kt @@ -0,0 +1,16 @@ +package com.tangem.features.send.v2.subcomponents.fee.model + +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType + +internal interface SendFeeClickIntents { + + fun feeReload() + + fun onFeeSelectorClick(feeType: FeeType) + + fun onCustomFeeValueChange(index: Int, value: String) + + fun onReadMoreClick() + + fun onNextClick() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt new file mode 100644 index 0000000000..ae53c69895 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt @@ -0,0 +1,290 @@ +package com.tangem.features.send.v2.subcomponents.fee.model + +import androidx.compose.runtime.Stable +import com.tangem.blockchain.common.AmountType +import com.tangem.core.analytics.api.AnalyticsEventHandler +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.navigation.Router +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase +import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger +import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams +import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger +import com.tangem.features.send.v2.subcomponents.fee.analytics.SendFeeAnalyticEvents +import com.tangem.features.send.v2.subcomponents.fee.analytics.SendFeeAnalyticEvents.GasPriceInserter +import com.tangem.features.send.v2.subcomponents.fee.model.transformers.* +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.transformer.update +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import java.util.Locale +import javax.inject.Inject + +@Suppress("LongParameterList") +@Stable +@ModelScoped +internal class SendFeeModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val isFeeApproximateUseCase: IsFeeApproximateUseCase, + private val getFeeUseCase: GetFeeUseCase, + private val urlOpener: UrlOpener, + private val analyticsEventHandler: AnalyticsEventHandler, + private val sendFeeAlertFactory: SendFeeAlertFactory, + private val feeReloadTrigger: SendFeeReloadTrigger, + private val feeCheckReloadTrigger: SendFeeCheckReloadTrigger, +) : Model(), SendFeeClickIntents { + + private val params: SendFeeComponentParams = paramsContainer.require() + + private val _uiState = MutableStateFlow(params.state) + val uiState = _uiState.asStateFlow() + + private val analyticsCategoryName = params.analyticsCategoryName + private val appCurrency = params.appCurrency + private val cryptoCurrencyStatus = params.cryptoCurrencyStatus + private val feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus + + private var feeJobHolder = JobHolder() + + init { + configFeeNavigation() + subscribeOnFeeReloadTriggerUpdates() + subscribeOnFeeCheckReloadTriggerUpdates() + initialState() + loadFee() + } + + fun updateState(state: FeeUM) { + _uiState.value = state + } + + override fun feeReload() = loadFee() + + override fun onFeeSelectorClick(feeType: FeeType) { + _uiState.update( + SendFeeSelectTransformer( + feeType = feeType, + clickIntents = this@SendFeeModel, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + ), + ) + updateFeeNotifications() + if (feeType == FeeType.Custom) { + analyticsEventHandler.send( + SendFeeAnalyticEvents.CustomFeeButtonClicked(categoryName = analyticsCategoryName), + ) + } + } + + override fun onCustomFeeValueChange(index: Int, value: String) { + _uiState.update( + SendFeeCustomValueChangeTransformer( + index = index, + value = value, + clickIntents = this@SendFeeModel, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + ), + ) + updateFeeNotifications() + } + + override fun onReadMoreClick() { + val locale = if (Locale.getDefault().language == RU_LOCALE) RU_LOCALE else EN_LOCALE + val url = buildString { + append(FEE_READ_MORE_URL_FIRST_PART) + append(locale) + append(FEE_READ_MORE_URL_SECOND_PART) + } + urlOpener.openUrl(url) + } + + override fun onNextClick() { + val feeUM = uiState.value as? FeeUM.Content + val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content + + if (feeSelectorUM != null) { + sendFeeAlertFactory.checkAndShowAlerts(feeSelectorUM) { + val isCustomFeeEdited = + feeSelectorUM.selectedFee?.amount?.value != feeSelectorUM.fees.normal.amount.value + if (feeSelectorUM.selectedType == FeeType.Custom && isCustomFeeEdited) { + analyticsEventHandler.send(GasPriceInserter(categoryName = analyticsCategoryName)) + } + analyticsEventHandler.send( + SendFeeAnalyticEvents.SelectedFee( + categoryName = analyticsCategoryName, + feeType = feeSelectorUM.selectedType.toAnalyticType(feeSelectorUM), + ), + ) + + navigate() + } + } else { + navigate() + } + } + + private fun initialState() { + if (uiState.value is FeeUM.Empty) { + _uiState.update( + SendFeeInitialStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + appCurrency = appCurrency, + ), + ) + } + } + + private fun subscribeOnFeeReloadTriggerUpdates() { + feeReloadTrigger.reloadTriggerFlow + .onEach { feeReload() } + .launchIn(modelScope) + } + + private fun subscribeOnFeeCheckReloadTriggerUpdates() { + feeCheckReloadTrigger.checkReloadTriggerFlow + .onEach { checkLoadFee() } + .launchIn(modelScope) + } + + private fun saveResult() { + _uiState.update( + SendFeeCustomAutoFixTransformer( + clickIntents = this@SendFeeModel, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + ), + ) + val params = params as? SendFeeComponentParams.FeeParams ?: return + params.callback.onFeeResult(uiState.value) + } + + private fun loadFee() { + modelScope.launch { + val isShowLoading = (uiState.value as? FeeUM.Content)?.feeSelectorUM !is FeeSelectorUM.Content + if (isShowLoading) { + _uiState.update(SendFeeLoadingTransformer) + } + callFeeUseCase().fold( + ifRight = { + _uiState.update( + SendFeeLoadedTransformer( + fees = it, + clickIntents = this@SendFeeModel, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + isFeeApproximate = isFeeApproximate(it.normal.amount.type), + ), + ) + updateFeeNotifications() + }, + ifLeft = { feeError -> + if (isShowLoading) { + _uiState.update(SendFeeFailedTransformer(feeError)) + } + updateFeeNotifications() + }, + ) + }.saveIn(feeJobHolder) + } + + private fun checkLoadFee() { + modelScope.launch { + callFeeUseCase().fold( + ifRight = { + feeCheckReloadTrigger.callbackCheckResult(true) + _uiState.update( + SendFeeLoadedTransformer( + fees = it, + clickIntents = this@SendFeeModel, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + isFeeApproximate = isFeeApproximate(it.normal.amount.type), + ), + ) + updateFeeNotifications() + }, + ifLeft = { feeError -> + feeCheckReloadTrigger.callbackCheckResult(false) + _uiState.update(SendFeeFailedTransformer(feeError)) + sendFeeAlertFactory.getFeeUnreachableErrorState(::checkLoadFee) + updateFeeNotifications() + }, + ) + }.saveIn(feeJobHolder) + } + + private suspend fun callFeeUseCase() = getFeeUseCase.invoke( + amount = params.sendAmount, + destination = params.destinationAddress, + userWallet = params.userWallet, + cryptoCurrency = cryptoCurrencyStatus.currency, + ) + + private fun isFeeApproximate(amountType: AmountType): Boolean { + val networkId = feeCryptoCurrencyStatus.currency.network.id + return isFeeApproximateUseCase( + networkId = networkId, + amountType = amountType, + ) + } + + private fun updateFeeNotifications() { + _uiState.update( + SendFeeNotificationsTransformer( + cryptoCurrencyName = cryptoCurrencyStatus.currency.name, + onFeeReload = ::feeReload, + ), + ) + } + + private fun navigate() { + saveResult() + router.pop() + } + + private fun configFeeNavigation() { + val params = params as? SendFeeComponentParams.FeeParams ?: return + combine( + flow = uiState, + flow2 = params.currentRoute, + transform = { state, route -> state to route }, + ).onEach { (_, _) -> + // todo + // params.callback.onNavigationResult( + // NavigationUM.Content( + // title = resourceReference(R.string.common_fee_selector_title), + // subtitle = null, + // backIconRes = R.drawable.ic_back_24, + // backIconClick = router::pop, + // primaryButton = ButtonsUM.PrimaryButtonUM( + // text = resourceReference(R.string.common_continue), + // isEnabled = state.isPrimaryButtonEnabled, + // onClick = ::onNextClick, + // ), + // prevButton = null, + // secondaryPairButtonsUM = null, + // ), + // ) + }.launchIn(modelScope) + } + + private companion object { + const val RU_LOCALE = "ru" + const val EN_LOCALE = "en" + const val FEE_READ_MORE_URL_FIRST_PART = "https://tangem.com/" + const val FEE_READ_MORE_URL_SECOND_PART = "/blog/post/what-is-a-transaction-fee-and-why-do-we-need-it/" + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt new file mode 100644 index 0000000000..2a5863ed1d --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt @@ -0,0 +1,50 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.converters + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList + +internal class FeeConverter( + clickIntents: SendFeeClickIntents, + appCurrency: AppCurrency, + feeCryptoCurrencyStatus: CryptoCurrencyStatus, + private val fees: TransactionFee, +) : Converter { + + private val customFeeConverter = SendFeeCustomFieldConverter( + clickIntents = clickIntents, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + normalFee = fees.normal, + ) + + override fun convert(value: Data): Fee { + return when (fees) { + is TransactionFee.Choosable -> { + when (value.feeType) { + FeeType.Slow -> fees.minimum + FeeType.Market -> fees.normal + FeeType.Fast -> fees.priority + FeeType.Custom -> customFeeConverter.convertBack(value.customValues) + } + } + is TransactionFee.Single -> + when (value.feeType) { + FeeType.Market -> fees.normal + FeeType.Custom -> customFeeConverter.convertBack(value.customValues) + else -> fees.normal + } + } + } + + data class Data( + val customValues: ImmutableList, + val feeType: FeeType, + ) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt new file mode 100644 index 0000000000..78304904a5 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt @@ -0,0 +1,112 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.converters + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.bitcoin.BitcoinCustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.kaspa.KaspaCustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.utils.converter.TwoWayConverter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +internal class SendFeeCustomFieldConverter( + private val clickIntents: SendFeeClickIntents, + private val appCurrency: AppCurrency, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus, + private val normalFee: Fee, +) : TwoWayConverter> { + + private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { + EthereumCustomFeeConverter( + clickIntents = clickIntents, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + ) + } + + private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { + BitcoinCustomFeeConverter( + clickIntents = clickIntents, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + ) + } + + private val kaspaCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { + KaspaCustomFeeConverter( + clickIntents = clickIntents, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + ) + } + + override fun convert(value: Fee): ImmutableList { + return when (value) { + is Fee.Ethereum -> ethereumCustomFeeConverter.convert(value) + is Fee.Bitcoin -> bitcoinCustomFeeConverter.convert(value) + is Fee.Kaspa -> kaspaCustomFeeConverter.convert(value) + else -> persistentListOf() + } + } + + override fun convertBack(value: ImmutableList): Fee { + return if (value.isEmpty()) { + normalFee + } else { + when (normalFee) { + is Fee.Ethereum -> ethereumCustomFeeConverter.convertBack(normalFee = normalFee, value = value) + is Fee.Bitcoin -> bitcoinCustomFeeConverter.convertBack(normalFee = normalFee, value = value) + is Fee.Kaspa -> kaspaCustomFeeConverter.convertBack(normalFee = normalFee, value = value) + else -> { + val customFee = value.firstOrNull() + Fee.Common( + normalFee.amount.copy( + value = customFee?.value?.parseToBigDecimal(customFee.decimals), + ), + ) + } + } + } + } + + fun onValueChange(feeSelectorState: FeeSelectorUM.Content, index: Int, value: String) = + when (val fee = feeSelectorState.fees.normal) { + is Fee.Ethereum -> ethereumCustomFeeConverter.onValueChange( + feeValue = fee, + customValues = feeSelectorState.customValues, + index = index, + value = value, + ) + is Fee.Bitcoin -> bitcoinCustomFeeConverter.onValueChange( + customValues = feeSelectorState.customValues, + index = index, + value = value, + txSize = fee.txSize, + ) + is Fee.Kaspa -> kaspaCustomFeeConverter.onValueChange( + customValues = feeSelectorState.customValues, + index = index, + value = value, + ) + else -> feeSelectorState.customValues + } + + fun tryAutoFixValue(feeSelectorState: FeeSelectorUM.Content) = when (feeSelectorState.fees) { + is TransactionFee.Choosable -> feeSelectorState.fees.minimum + is TransactionFee.Single -> feeSelectorState.fees.normal + }.let { + when (it) { + is Fee.Kaspa -> kaspaCustomFeeConverter.tryAutoFixValue( + minimumFee = it, + customValues = feeSelectorState.customValues, + ) + else -> feeSelectorState.customValues + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/CustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/CustomFeeConverter.kt new file mode 100644 index 0000000000..4250d48580 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/CustomFeeConverter.kt @@ -0,0 +1,14 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList + +internal interface CustomFeeConverter : Converter> { + fun convertBack(normalFee: T, value: ImmutableList): T +} + +internal fun MutableList.setEmpty(index: Int) { + set(index, this[index].copy(value = "")) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt new file mode 100644 index 0000000000..c2c024fab1 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt @@ -0,0 +1,143 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.bitcoin + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.utils.getFiatReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.lib.crypto.BlockchainUtils +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal +import java.math.RoundingMode + +internal class BitcoinCustomFeeConverter( + private val clickIntents: SendFeeClickIntents, + private val appCurrency: AppCurrency, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus, +) : CustomFeeConverter { + + private val currencyStatus = feeCryptoCurrencyStatus.value + private val network = feeCryptoCurrencyStatus.currency.network.id.value + + override fun convert(value: Fee.Bitcoin): ImmutableList { + val feeValue = value.amount.value + return if (BlockchainUtils.isUseBitcoinFeeConverter(network)) { + persistentListOf( + CustomFeeFieldUM( + value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(), + decimals = value.amount.decimals, + symbol = value.amount.currencySymbol, + onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Companion.Next, + keyboardType = KeyboardType.Companion.Number, + ), + title = resourceReference(R.string.send_max_fee), + footer = resourceReference(R.string.send_bitcoin_custom_fee_footer), + label = getFiatReference( + rate = currencyStatus.fiatRate, + value = feeValue, + appCurrency = appCurrency, + ), + keyboardActions = KeyboardActions(), + isReadonly = true, + ), + CustomFeeFieldUM( + value = toSatoshiPerByte( + amount = feeValue, + decimals = value.amount.decimals, + txSize = value.txSize, + ).toString(), + decimals = SATOSHI_DECIMALS, + symbol = "", + title = resourceReference(R.string.send_satoshi_per_byte_title), + footer = resourceReference(R.string.send_satoshi_per_byte_text), + onValueChange = { clickIntents.onCustomFeeValueChange(FEE_SATOSHI_INDEX, it) }, + keyboardOptions = KeyboardOptions( + imeAction = if (checkExceedBalance( + feeBalance = currencyStatus.amount, + feeAmount = feeValue, + ) + ) { + ImeAction.Companion.None + } else { + ImeAction.Companion.Done + }, + keyboardType = KeyboardType.Companion.Number, + ), + keyboardActions = KeyboardActions( + onDone = { clickIntents.onNextClick() }, + ), + ), + ) + } else { + persistentListOf() + } + } + + override fun convertBack(normalFee: Fee.Bitcoin, value: ImmutableList): Fee.Bitcoin { + val feeAmount = value[FEE_AMOUNT_INDEX].value.parseToBigDecimal(value[FEE_AMOUNT_INDEX].decimals) + val satoshiPerByte = value[FEE_SATOSHI_INDEX].value.parseToBigDecimal(value[FEE_SATOSHI_INDEX].decimals) + return normalFee.copy( + amount = normalFee.amount.copy(value = feeAmount), + satoshiPerByte = satoshiPerByte, + ) + } + + fun onValueChange( + customValues: ImmutableList, + index: Int, + value: String, + txSize: BigDecimal, + ): ImmutableList { + val mutableCustomValues = customValues.toMutableList() + return mutableCustomValues.apply { + if (index == FEE_SATOSHI_INDEX) { + val newSatoshiPerKb = value.parseToBigDecimal(this[FEE_SATOSHI_INDEX].decimals) + val newFeeAmount = newSatoshiPerKb.multiply(txSize) + .movePointLeft(this[FEE_AMOUNT_INDEX].decimals) + .setScale(this[FEE_AMOUNT_INDEX].decimals, RoundingMode.DOWN) + set( + FEE_AMOUNT_INDEX, + this[FEE_AMOUNT_INDEX].copy( + value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT_INDEX].decimals), + label = getFiatReference( + rate = feeCryptoCurrencyStatus.value.fiatRate, + value = newFeeAmount, + appCurrency = appCurrency, + ), + ), + ) + set(index, this[index].copy(value = value)) + } + }.toImmutableList() + } + + private fun toSatoshiPerByte(amount: BigDecimal?, decimals: Int, txSize: BigDecimal): BigDecimal? { + val newFeeAmount = amount?.movePointRight(decimals) + return newFeeAmount?.divide( + txSize, + SATOSHI_DECIMALS, + RoundingMode.HALF_UP, + )?.setScale(SATOSHI_DECIMALS, RoundingMode.HALF_UP) + } + + private companion object { + private const val FEE_AMOUNT_INDEX = 0 + private const val FEE_SATOSHI_INDEX = 1 + private const val SATOSHI_DECIMALS = 0 + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/BaseEthereumCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/BaseEthereumCustomFeeConverter.kt new file mode 100644 index 0000000000..c0828d3d60 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/BaseEthereumCustomFeeConverter.kt @@ -0,0 +1,25 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import kotlinx.collections.immutable.ImmutableList + +/** + * Base ethereum custom fee converter + * + * @param T subtype of [Fee.Ethereum] + * +[REDACTED_AUTHOR] + */ +internal interface BaseEthereumCustomFeeConverter : CustomFeeConverter { + + fun getGasLimitIndex(feeValue: T): Int + + fun onValueChange( + feeValue: T, + customValues: ImmutableList, + index: Int, + value: String, + ): ImmutableList +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt new file mode 100644 index 0000000000..ce2ba52261 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt @@ -0,0 +1,126 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.utils.getFiatReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal class EthereumCustomFeeConverter( + private val clickIntents: SendFeeClickIntents, + private val appCurrency: AppCurrency, + feeCryptoCurrencyStatus: CryptoCurrencyStatus, +) : BaseEthereumCustomFeeConverter { + + private val currencyStatus = feeCryptoCurrencyStatus.value + + private val legacyFeeConverter = EthereumLegacyCustomFeeConverter( + clickIntents = clickIntents, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + ) + + private val eipFeeConverter = EthereumEIPCustomFeeConverter( + clickIntents = clickIntents, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + ) + + override fun convert(value: Fee.Ethereum): ImmutableList { + return buildList { + convertFeeValue(value).let(::add) + + when (value) { + is Fee.Ethereum.EIP1559 -> eipFeeConverter.convert(value) + is Fee.Ethereum.Legacy -> legacyFeeConverter.convert(value) + }.let(::addAll) + + convertGasLimitValue(value).let(::add) + } + .toImmutableList() + } + + override fun convertBack(normalFee: Fee.Ethereum, value: ImmutableList): Fee.Ethereum { + return when (normalFee) { + is Fee.Ethereum.EIP1559 -> eipFeeConverter.convertBack(normalFee = normalFee, value = value) + is Fee.Ethereum.Legacy -> legacyFeeConverter.convertBack(normalFee = normalFee, value = value) + } + } + + override fun getGasLimitIndex(feeValue: Fee.Ethereum): Int { + return when (feeValue) { + is Fee.Ethereum.EIP1559 -> eipFeeConverter.getGasLimitIndex(feeValue) + is Fee.Ethereum.Legacy -> legacyFeeConverter.getGasLimitIndex(feeValue) + } + } + + override fun onValueChange( + feeValue: Fee.Ethereum, + customValues: ImmutableList, + index: Int, + value: String, + ): ImmutableList { + return when (feeValue) { + is Fee.Ethereum.EIP1559 -> eipFeeConverter.onValueChange(feeValue, customValues, index, value) + is Fee.Ethereum.Legacy -> legacyFeeConverter.onValueChange(feeValue, customValues, index, value) + } + } + + private fun convertFeeValue(value: Fee.Ethereum): CustomFeeFieldUM { + val feeValue = value.amount.value + + return CustomFeeFieldUM( + value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(), + decimals = value.amount.decimals, + symbol = value.amount.currencySymbol, + onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT, it) }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Number), + title = resourceReference(R.string.send_max_fee), + footer = resourceReference(R.string.send_custom_amount_fee_footer), + label = getFiatReference( + rate = currencyStatus.fiatRate, + value = feeValue, + appCurrency = appCurrency, + ), + keyboardActions = KeyboardActions(), + ) + } + + private fun convertGasLimitValue(value: Fee.Ethereum): CustomFeeFieldUM { + val isExceedBalance = checkExceedBalance(feeBalance = currencyStatus.amount, feeAmount = value.amount.value) + + return CustomFeeFieldUM( + value = value.gasLimit.toString(), + decimals = GAS_DECIMALS, + symbol = "", + title = resourceReference(R.string.send_gas_limit), + footer = resourceReference(R.string.send_gas_limit_footer), + onValueChange = { clickIntents.onCustomFeeValueChange(getGasLimitIndex(value), it) }, + keyboardOptions = KeyboardOptions( + imeAction = if (isExceedBalance) ImeAction.None else ImeAction.Done, + keyboardType = KeyboardType.Number, + ), + keyboardActions = KeyboardActions( + onDone = { clickIntents.onNextClick() }, + ), + ) + } + + companion object { + const val ETHEREUM_GAS_UNIT = "GWEI" + const val GIGA_DECIMALS = 9 + const val GAS_DECIMALS = 0 + const val FEE_AMOUNT = 0 + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt new file mode 100644 index 0000000000..2a7d389e13 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt @@ -0,0 +1,204 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.utils.getFiatReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.ETHEREUM_GAS_UNIT +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.FEE_AMOUNT +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GAS_DECIMALS +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.setEmpty +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import java.math.RoundingMode + +internal class EthereumEIPCustomFeeConverter( + private val clickIntents: SendFeeClickIntents, + private val appCurrency: AppCurrency, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus, +) : BaseEthereumCustomFeeConverter { + + private val currencyStatus = feeCryptoCurrencyStatus.value + + override fun convert(value: Fee.Ethereum.EIP1559): ImmutableList { + return persistentListOf( + CustomFeeFieldUM( + value = value.maxFeePerGas.toBigDecimal().movePointLeft(GIGA_DECIMALS).parseBigDecimal(GIGA_DECIMALS), + decimals = GIGA_DECIMALS, + symbol = ETHEREUM_GAS_UNIT, + title = resourceReference(R.string.send_custom_evm_max_fee), + footer = resourceReference(R.string.send_custom_evm_max_fee_footer), + onValueChange = { clickIntents.onCustomFeeValueChange(MAX_FEE, it) }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Number), + keyboardActions = KeyboardActions(), + ), + CustomFeeFieldUM( + value = value.priorityFee.toBigDecimal().movePointLeft(GIGA_DECIMALS).parseBigDecimal(GIGA_DECIMALS), + decimals = GIGA_DECIMALS, + symbol = ETHEREUM_GAS_UNIT, + title = resourceReference(R.string.send_custom_evm_priority_fee), + footer = resourceReference(R.string.send_custom_evm_priority_fee_footer), + onValueChange = { clickIntents.onCustomFeeValueChange(PRIORITY_FEE, it) }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Number), + keyboardActions = KeyboardActions(), + ), + ) + } + + override fun convertBack( + normalFee: Fee.Ethereum.EIP1559, + value: ImmutableList, + ): Fee.Ethereum.EIP1559 { + val feeAmount = value[FEE_AMOUNT].value.parseToBigDecimal(value[FEE_AMOUNT].decimals) + val maxFeeDecimals = value[MAX_FEE].decimals + val maxFee = value[MAX_FEE].value.parseToBigDecimal(maxFeeDecimals) + .movePointRight(maxFeeDecimals) + .toBigInteger() + val priorityFeeDecimals = value[PRIORITY_FEE].decimals + val priorityFee = value[PRIORITY_FEE].value.parseToBigDecimal(priorityFeeDecimals) + .movePointRight(priorityFeeDecimals) + .toBigInteger() + val gasLimit = value[GAS_LIMIT].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger() + + return normalFee.copy( + amount = normalFee.amount.copy(value = feeAmount), + maxFeePerGas = maxFee, + priorityFee = priorityFee, + gasLimit = gasLimit, + ) + } + + override fun getGasLimitIndex(feeValue: Fee.Ethereum.EIP1559): Int = GAS_LIMIT + + override fun onValueChange( + feeValue: Fee.Ethereum.EIP1559, + customValues: ImmutableList, + index: Int, + value: String, + ): ImmutableList { + val mutableCustomValues = customValues.toMutableList() + return mutableCustomValues.apply { + when (index) { + FEE_AMOUNT -> setOnAmountChange(value, index) + MAX_FEE -> setOnMaxFeeChange(value, index) + GAS_LIMIT -> setOnGasLimitChange(value, index) + else -> set(index, this[index].copy(value = value)) + } + }.toImmutableList() + } + + private fun MutableList.setOnAmountChange(value: String, index: Int) { + val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals) + if (value.isBlank()) { + setEmpty(FEE_AMOUNT) + setEmpty(MAX_FEE) + } else { + val newFeeAmountDecimal = value.parseToBigDecimal(this[FEE_AMOUNT].decimals) + val newFeeAmount = newFeeAmountDecimal.movePointRight(GIGA_DECIMALS) // from ETH to GWEI + + val newMaxFee = newFeeAmount.divide(gasLimit, this[MAX_FEE].decimals, RoundingMode.HALF_UP) + + set( + index = MAX_FEE, + element = this[MAX_FEE].copy(value = newMaxFee.parseBigDecimal(this[MAX_FEE].decimals)), + ) + + set( + index = index, + element = this[index].copy( + value = value, + label = getFiatReference( + rate = feeCryptoCurrencyStatus.value.fiatRate, + value = newFeeAmountDecimal, + appCurrency = appCurrency, + ), + ), + ) + } + } + + private fun MutableList.setOnMaxFeeChange(value: String, index: Int) { + val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals) + + if (value.isBlank()) { + setEmpty(FEE_AMOUNT) + setEmpty(MAX_FEE) + } else { + val newMaxFee = value.parseToBigDecimal(this[MAX_FEE].decimals).movePointLeft(this[MAX_FEE].decimals) + val newFeeAmount = gasLimit * newMaxFee + set( + FEE_AMOUNT, + this[FEE_AMOUNT].copy( + value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals), + label = getFiatReference( + rate = currencyStatus.fiatRate, + value = newFeeAmount, + appCurrency = appCurrency, + ), + ), + ) + set(index, this[index].copy(value = value)) + } + } + + private fun MutableList.setOnGasLimitChange(value: String, index: Int) { + if (value.isBlank()) { + setEmpty(FEE_AMOUNT) + setEmpty(GAS_LIMIT) + } else { + val newGasLimit = value.parseToBigDecimal(this[GAS_LIMIT].decimals) + + val maxFee = this[MAX_FEE].value.parseToBigDecimal(this[MAX_FEE].decimals) + .movePointLeft(this[MAX_FEE].decimals) // from GWEI to ETH + + val newFeeAmount = newGasLimit * maxFee + + set( + index = FEE_AMOUNT, + element = this[FEE_AMOUNT].copy( + value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals), + label = getFiatReference( + rate = currencyStatus.fiatRate, + value = newFeeAmount, + appCurrency = appCurrency, + ), + ), + ) + + val isNotExceedBalance = checkExceedBalance( + feeBalance = currencyStatus.amount, + feeAmount = newFeeAmount, + ) + + set( + index = index, + element = this[index].copy( + value = value, + keyboardOptions = KeyboardOptions( + imeAction = if (!isNotExceedBalance) ImeAction.None else ImeAction.Done, + keyboardType = KeyboardType.Number, + ), + ), + ) + } + } + + private companion object { + const val MAX_FEE = 1 + const val PRIORITY_FEE = 2 + const val GAS_LIMIT = 3 + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt new file mode 100644 index 0000000000..8b2f86beb7 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt @@ -0,0 +1,181 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.utils.getFiatReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.ETHEREUM_GAS_UNIT +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.FEE_AMOUNT +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GAS_DECIMALS +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.setEmpty +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import java.math.RoundingMode + +internal class EthereumLegacyCustomFeeConverter( + private val clickIntents: SendFeeClickIntents, + private val appCurrency: AppCurrency, + feeCryptoCurrencyStatus: CryptoCurrencyStatus, +) : BaseEthereumCustomFeeConverter { + + private val currencyStatus = feeCryptoCurrencyStatus.value + + override fun convert(value: Fee.Ethereum.Legacy): ImmutableList { + return persistentListOf( + CustomFeeFieldUM( + value = value.gasPrice.toBigDecimal().movePointLeft(GIGA_DECIMALS).parseBigDecimal(GIGA_DECIMALS), + decimals = GIGA_DECIMALS, + symbol = ETHEREUM_GAS_UNIT, + title = resourceReference(R.string.send_gas_price), + footer = resourceReference(R.string.send_gas_price_footer), + onValueChange = { clickIntents.onCustomFeeValueChange(GAS_PRICE, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + keyboardActions = KeyboardActions(), + ), + ) + } + + override fun convertBack( + normalFee: Fee.Ethereum.Legacy, + value: ImmutableList, + ): Fee.Ethereum.Legacy { + val feeAmount = value[FEE_AMOUNT].value.parseToBigDecimal(value[FEE_AMOUNT].decimals) + val gasPrice = value[GAS_PRICE].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger() + val gasLimit = value[GAS_LIMIT].value.parseToBigDecimal(GAS_DECIMALS).toBigInteger() + + return normalFee.copy( + amount = normalFee.amount.copy(value = feeAmount), + gasPrice = gasPrice, + gasLimit = gasLimit, + ) + } + + override fun getGasLimitIndex(feeValue: Fee.Ethereum.Legacy): Int = GAS_LIMIT + + override fun onValueChange( + feeValue: Fee.Ethereum.Legacy, + customValues: ImmutableList, + index: Int, + value: String, + ): ImmutableList { + val mutableCustomValues = customValues.toMutableList() + return mutableCustomValues.apply { + when (index) { + FEE_AMOUNT -> setOnAmountChange(value, index) + GAS_PRICE -> setOnGasPriceChange(value, index) + GAS_LIMIT -> setOnGasLimitChange(value, index) + else -> set(index, this[index].copy(value = value)) + } + }.toImmutableList() + } + + private fun MutableList.setOnAmountChange(value: String, index: Int) { + val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals) + if (value.isBlank()) { + setEmpty(FEE_AMOUNT) + setEmpty(GAS_PRICE) + } else { + val newFeeAmountDecimal = value.parseToBigDecimal(this[FEE_AMOUNT].decimals) + val newFeeAmount = newFeeAmountDecimal.movePointRight(this[GAS_PRICE].decimals) // from ETH to GWEI + val newGasPrice = newFeeAmount.divide(gasLimit, this[GAS_PRICE].decimals, RoundingMode.HALF_UP) + set(GAS_PRICE, this[GAS_PRICE].copy(value = newGasPrice.parseBigDecimal(this[GAS_PRICE].decimals))) + set( + index, + this[index].copy( + value = value, + label = getFiatReference( + rate = currencyStatus.fiatRate, + value = newFeeAmountDecimal, + appCurrency = appCurrency, + ), + ), + ) + } + } + + private fun MutableList.setOnGasPriceChange(value: String, index: Int) { + val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals) + if (value.isBlank()) { + setEmpty(FEE_AMOUNT) + setEmpty(GAS_PRICE) + } else { + val newGasPrice = value.parseToBigDecimal(this[GAS_PRICE].decimals) + .movePointLeft(this[GAS_PRICE].decimals) // from GWEI to ETH + val newFeeAmount = gasLimit * newGasPrice + set( + FEE_AMOUNT, + this[FEE_AMOUNT].copy( + value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals), + label = getFiatReference( + rate = currencyStatus.fiatRate, + value = newFeeAmount, + appCurrency = appCurrency, + ), + ), + ) + set(index, this[index].copy(value = value)) + } + } + + private fun MutableList.setOnGasLimitChange(value: String, index: Int) { + if (value.isBlank()) { + setEmpty(FEE_AMOUNT) + setEmpty(GAS_LIMIT) + } else { + val newGasLimit = value.parseToBigDecimal(this[GAS_LIMIT].decimals) + val gasPrice = this[GAS_PRICE].value.parseToBigDecimal(this[GAS_PRICE].decimals) + .movePointLeft(this[GAS_PRICE].decimals) // from GWEI to ETH + + val newFeeAmount = newGasLimit * gasPrice + + set( + index = FEE_AMOUNT, + element = this[FEE_AMOUNT].copy( + value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT].decimals), + label = getFiatReference( + rate = currencyStatus.fiatRate, + value = newFeeAmount, + appCurrency = appCurrency, + ), + ), + ) + + val isNotExceedBalance = checkExceedBalance( + feeBalance = currencyStatus.amount, + feeAmount = newFeeAmount, + ) + + set( + index = index, + element = this[index].copy( + value = value, + keyboardOptions = KeyboardOptions( + imeAction = if (!isNotExceedBalance) ImeAction.None else ImeAction.Done, + keyboardType = KeyboardType.Number, + ), + ), + ) + } + } + + private companion object { + const val GAS_PRICE = 1 + const val GAS_LIMIT = 2 + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt new file mode 100644 index 0000000000..2714e7d9f6 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt @@ -0,0 +1,131 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.kaspa + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.utils.getFiatReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import java.math.RoundingMode + +internal class KaspaCustomFeeConverter( + private val clickIntents: SendFeeClickIntents, + private val appCurrency: AppCurrency, + feeCryptoCurrencyStatus: CryptoCurrencyStatus, +) : CustomFeeConverter { + + private val currencyStatus = feeCryptoCurrencyStatus.value + + override fun convert(value: Fee.Kaspa): ImmutableList { + val feeValue = value.amount.value + return persistentListOf( + CustomFeeFieldUM( + value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(), + decimals = value.amount.decimals, + symbol = value.amount.currencySymbol, + onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Companion.Next, + keyboardType = KeyboardType.Companion.Number, + ), + title = resourceReference(R.string.send_max_fee), + footer = resourceReference(R.string.send_custom_amount_fee_footer), + label = getFiatReference( + rate = currencyStatus.fiatRate, + value = feeValue, + appCurrency = appCurrency, + ), + keyboardActions = KeyboardActions(), + ), + ) + } + + override fun convertBack(normalFee: Fee.Kaspa, value: ImmutableList): Fee.Kaspa { + val decimals = value[FEE_AMOUNT_INDEX].decimals + val feeAmount = value[FEE_AMOUNT_INDEX].value.parseToBigDecimal(decimals) + return normalFee.copy( + amount = normalFee.amount.copy(value = feeAmount), + mass = normalFee.mass, + feeRate = feeAmount + .divide(normalFee.mass.toBigDecimal(), decimals, RoundingMode.HALF_UP) + .movePointRight(decimals) + .toBigInteger(), + ) + } + + fun onValueChange( + customValues: ImmutableList, + index: Int, + value: String, + ): ImmutableList { + val mutableCustomValues = customValues.toMutableList() + return mutableCustomValues.apply { + when (index) { + FEE_AMOUNT_INDEX -> { + val valueDecimal = value.parseToBigDecimal(this[FEE_AMOUNT_INDEX].decimals) + set( + index, + this[index].copy( + value = value, + label = getFiatReference( + rate = currencyStatus.fiatRate, + value = valueDecimal, + appCurrency = appCurrency, + ), + ), + ) + } + } + }.toImmutableList() + } + + fun tryAutoFixValue( + minimumFee: Fee.Kaspa, + customValues: ImmutableList, + ): ImmutableList { + val mutableCustomValues = customValues.toMutableList() + val minimumFeeAmountValue = minimumFee.amount.value + + return mutableCustomValues.apply { + // check that there is reveal transaction info (= krc-20 token transfer) + // return without changes otherwise + if (minimumFee.revealTransactionFee != null && minimumFeeAmountValue != null) { + getOrNull(FEE_AMOUNT_INDEX)?.let { + val valueDecimal = it.value.parseToBigDecimal(it.decimals) + // krc-20 transaction will be failed if custom fee value is less than minimum, + // so we set value to minimum in this case + if (valueDecimal < minimumFee.amount.value) { + val fixedValue = minimumFeeAmountValue.parseBigDecimal(it.decimals) + set( + FEE_AMOUNT_INDEX, + it.copy( + value = fixedValue, + label = getFiatReference( + rate = currencyStatus.fiatRate, + value = valueDecimal, + appCurrency = appCurrency, + ), + ), + ) + } + } + } + }.toImmutableList() + } + + private companion object { + private const val FEE_AMOUNT_INDEX = 0 + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomAutoFixTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomAutoFixTransformer.kt new file mode 100644 index 0000000000..65d07205da --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomAutoFixTransformer.kt @@ -0,0 +1,45 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.transformers + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter +import com.tangem.utils.transformer.Transformer + +internal class SendFeeCustomAutoFixTransformer( + private val clickIntents: SendFeeClickIntents, + private val appCurrency: AppCurrency, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus, +) : Transformer { + + override fun transform(prevState: FeeUM): FeeUM { + val state = prevState as? FeeUM.Content ?: return prevState + val feeSelectorUM = state.feeSelectorUM as? FeeSelectorUM.Content ?: return state + + val customFeeConverter = SendFeeCustomFieldConverter( + clickIntents = clickIntents, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + normalFee = feeSelectorUM.fees.normal, + ) + + return when (feeSelectorUM.selectedType) { + FeeType.Slow, + FeeType.Market, + FeeType.Fast, + -> state + FeeType.Custom -> { + val updatedCustomValues = customFeeConverter.tryAutoFixValue(feeSelectorUM) + state.copy( + feeSelectorUM = feeSelectorUM.copy( + customValues = updatedCustomValues, + selectedFee = customFeeConverter.convertBack(updatedCustomValues), + ), + ) + } + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomValueChangeTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomValueChangeTransformer.kt new file mode 100644 index 0000000000..5d95767e59 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeCustomValueChangeTransformer.kt @@ -0,0 +1,39 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.transformers + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter +import com.tangem.utils.transformer.Transformer + +internal class SendFeeCustomValueChangeTransformer( + private val index: Int, + private val value: String, + private val clickIntents: SendFeeClickIntents, + private val appCurrency: AppCurrency, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus, +) : Transformer { + + override fun transform(prevState: FeeUM): FeeUM { + val state = prevState as? FeeUM.Content ?: return prevState + val feeSelectorUM = state.feeSelectorUM as? FeeSelectorUM.Content ?: return state + + val customFeeConverter = SendFeeCustomFieldConverter( + clickIntents = clickIntents, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + normalFee = feeSelectorUM.fees.normal, + ) + + val updatedCustomValues = customFeeConverter.onValueChange(feeSelectorUM, index, value) + + return state.copy( + feeSelectorUM = feeSelectorUM.copy( + customValues = updatedCustomValues, + selectedFee = customFeeConverter.convertBack(updatedCustomValues), + ), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeFailedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeFailedTransformer.kt new file mode 100644 index 0000000000..e3f728dd23 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeFailedTransformer.kt @@ -0,0 +1,20 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.transformers + +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.utils.transformer.Transformer + +internal class SendFeeFailedTransformer( + private val error: GetFeeError, +) : Transformer { + + override fun transform(prevState: FeeUM): FeeUM { + val state = prevState as? FeeUM.Content ?: return prevState + + return state.copy( + feeSelectorUM = FeeSelectorUM.Error(error), + isPrimaryButtonEnabled = false, + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeInitialStateTransformer.kt new file mode 100644 index 0000000000..723147ce78 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeInitialStateTransformer.kt @@ -0,0 +1,34 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.transformers + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.lib.crypto.BlockchainUtils.isTron +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf + +internal class SendFeeInitialStateTransformer( + cryptoCurrencyStatus: CryptoCurrencyStatus, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus, + private val appCurrency: AppCurrency, +) : Transformer { + + private val cryptoCurrency = cryptoCurrencyStatus.currency + + override fun transform(prevState: FeeUM): FeeUM { + return FeeUM.Content( + isPrimaryButtonEnabled = false, + feeSelectorUM = FeeSelectorUM.Loading, + notifications = persistentListOf(), + rate = feeCryptoCurrencyStatus.value.fiatRate, + appCurrency = appCurrency, + isFeeApproximate = false, + isCustomSelected = false, + isFeeConvertibleToFiat = feeCryptoCurrencyStatus.currency.network.hasFiatFeeRate, + isTronToken = cryptoCurrency is CryptoCurrency.Token && + isTron(cryptoCurrency.network.id.value), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadedTransformer.kt new file mode 100644 index 0000000000..b1a7fb7467 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadedTransformer.kt @@ -0,0 +1,64 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.transformers + +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.features.send.v2.subcomponents.fee.model.converters.FeeConverter +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter +import com.tangem.utils.transformer.Transformer + +internal class SendFeeLoadedTransformer( + clickIntents: SendFeeClickIntents, + appCurrency: AppCurrency, + feeCryptoCurrencyStatus: CryptoCurrencyStatus, + private val fees: TransactionFee, + private val isFeeApproximate: Boolean, +) : Transformer { + + private val customFeeFieldConverter = SendFeeCustomFieldConverter( + clickIntents = clickIntents, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + normalFee = fees.normal, + ) + + private val feeConverter = FeeConverter( + clickIntents = clickIntents, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + fees = fees, + ) + + override fun transform(prevState: FeeUM): FeeUM { + val state = prevState as? FeeUM.Content ?: return prevState + val feeSelectorUM = state.feeSelectorUM as? FeeSelectorUM.Content + + val updatedFeeSelector = if (feeSelectorUM == null) { + FeeSelectorUM.Content( + fees = fees, + customValues = customFeeFieldConverter.convert(fees.normal), + selectedFee = fees.normal, + ) + } else { + FeeSelectorUM.Content( + fees = fees, + customValues = feeSelectorUM.customValues, + selectedType = feeSelectorUM.selectedType, + selectedFee = feeConverter.convert( + FeeConverter.Data( + feeType = feeSelectorUM.selectedType, + customValues = feeSelectorUM.customValues, + ), + ), + ) + } + + return state.copy( + feeSelectorUM = updatedFeeSelector, + isFeeApproximate = isFeeApproximate, + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadingTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadingTransformer.kt new file mode 100644 index 0000000000..96c3e3d00a --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeLoadingTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.transformers + +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf + +internal object SendFeeLoadingTransformer : Transformer { + override fun transform(prevState: FeeUM): FeeUM { + val state = prevState as? FeeUM.Content ?: return prevState + return state.copy( + feeSelectorUM = state.feeSelectorUM as? FeeSelectorUM.Content ?: FeeSelectorUM.Loading, + notifications = persistentListOf(), + isPrimaryButtonEnabled = false, + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeNotificationsTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeNotificationsTransformer.kt new file mode 100644 index 0000000000..d8416e2718 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeNotificationsTransformer.kt @@ -0,0 +1,51 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.transformers + +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.utils.extensions.isZero +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal class SendFeeNotificationsTransformer( + private val cryptoCurrencyName: String, + private val onFeeReload: () -> Unit, +) : Transformer { + + override fun transform(prevState: FeeUM): FeeUM { + val state = prevState as? FeeUM.Content ?: return prevState + + val notifications = state.getNotifications() + return state.copy( + notifications = notifications, + isPrimaryButtonEnabled = state.isPrimaryButtonEnabled(notifications), + ) + } + + private fun FeeUM.Content.getNotifications() = buildList { + addFeeUnreachableNotification( + feeError = (feeSelectorUM as? FeeSelectorUM.Error)?.error, + tokenName = cryptoCurrencyName, + onReload = onFeeReload, + ) + }.toImmutableList() + + private fun FeeUM.Content.isPrimaryButtonEnabled(notifications: ImmutableList): Boolean { + val feeSelectorState = feeSelectorUM as? FeeSelectorUM.Content ?: return false + val customValue = feeSelectorState.customValues.firstOrNull() + + val isNotCustom = feeSelectorState.selectedType != FeeType.Custom + val isNotEmptyCustom = if (customValue != null) { + !customValue.value.parseToBigDecimal(customValue.decimals).isZero() && !isNotCustom + } else { + false + } + val noErrors = notifications.none { it is NotificationUM.Error } + + return noErrors && (isNotEmptyCustom || isNotCustom) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeSelectTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeSelectTransformer.kt new file mode 100644 index 0000000000..6ae4b1b0d3 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/transformers/SendFeeSelectTransformer.kt @@ -0,0 +1,46 @@ +package com.tangem.features.send.v2.subcomponents.fee.model.transformers + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.features.send.v2.subcomponents.fee.model.converters.FeeConverter +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.utils.transformer.Transformer + +internal class SendFeeSelectTransformer( + private val feeType: FeeType, + private val clickIntents: SendFeeClickIntents, + private val appCurrency: AppCurrency, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus, +) : Transformer { + + override fun transform(prevState: FeeUM): FeeUM { + val state = prevState as? FeeUM.Content ?: return prevState + val feeSelectorUM = state.feeSelectorUM as? FeeSelectorUM.Content ?: return state + + val feeConverter = FeeConverter( + clickIntents = clickIntents, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + fees = feeSelectorUM.fees, + ) + + val updatedFeeSelectorState = feeSelectorUM.copy( + selectedType = feeType, + selectedFee = feeConverter.convert( + FeeConverter.Data( + customValues = feeSelectorUM.customValues, + feeType = feeType, + ), + ), + ) + + val isCustomFeeWasSelected = state.isCustomSelected || updatedFeeSelectorState.selectedType == FeeType.Custom + return state.copy( + isCustomSelected = isCustomFeeWasSelected, + feeSelectorUM = updatedFeeSelectorState, + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt new file mode 100644 index 0000000000..b1c82b14af --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt @@ -0,0 +1,132 @@ +package com.tangem.features.send.v2.subcomponents.fee.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.amountScreen.utils.getFiatReference +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM + +@Composable +internal fun FeeBlock(feeUM: FeeUM, isClickEnabled: Boolean, onClick: () -> Unit) { + val feeUM = feeUM as? FeeUM.Content ?: return + val feeSelectorUM = feeUM.feeSelectorUM as? FeeSelectorUM.Content + val isEditingDisabled = feeSelectorUM?.fees is TransactionFee.Single + + val backgroundColor = if (isEditingDisabled) { + TangemTheme.colors.button.disabled + } else { + TangemTheme.colors.background.action + } + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(backgroundColor) + .clickable(enabled = isClickEnabled && !isEditingDisabled, onClick = onClick) + .padding(12.dp), + ) { + Text( + text = stringResourceSafe(R.string.common_network_fee_title), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.secondary, + ) + + Box( + modifier = Modifier.padding(top = 8.dp), + ) { + val feeSelectorUM = feeUM.feeSelectorUM + val feeAmount = (feeSelectorUM as? FeeSelectorUM.Content)?.selectedFee?.amount + val (title, icon) = if (feeSelectorUM is FeeSelectorUM.Content) { + when (feeSelectorUM.selectedType) { + FeeType.Slow -> R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24 + FeeType.Market -> R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 + FeeType.Fast -> R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24 + FeeType.Custom -> R.string.common_custom to R.drawable.ic_edit_24 + } + } else { + R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 + } + SelectorRowItem( + titleRes = title, + iconRes = icon, + preDot = stringReference( + feeAmount?.value.format { + crypto( + symbol = feeAmount?.currencySymbol.orEmpty(), + decimals = feeAmount?.decimals ?: 0, + ).fee(canBeLower = feeUM.isFeeApproximate) + }, + ), + postDot = if (feeUM.isFeeConvertibleToFiat) { + getFiatReference(feeAmount?.value, feeUM.rate, feeUM.appCurrency) + } else { + null + }, + ellipsizeOffset = feeAmount?.currencySymbol?.length, + isSelected = true, + showDivider = false, + showSelectedAppearance = false, + paddingValues = PaddingValues(), + ) + FeeLoading(feeSelectorUM) + FeeError(feeSelectorUM) + } + } +} + +@Composable +private fun BoxScope.FeeLoading(feeSelectorUM: FeeSelectorUM) { + AnimatedContent( + targetState = feeSelectorUM, + label = "Fee Loading State Change", + modifier = Modifier.align(Alignment.CenterEnd), + ) { + if (it == FeeSelectorUM.Loading) { + RectangleShimmer( + radius = 3.dp, + modifier = Modifier.size( + height = 12.dp, + width = 90.dp, + ), + ) + } + } +} + +@Composable +private fun BoxScope.FeeError(feeSelectorUM: FeeSelectorUM) { + AnimatedContent( + targetState = feeSelectorUM, + label = "Fee Error State Change", + modifier = Modifier.align(Alignment.CenterEnd), + ) { + if (it is FeeSelectorUM.Error) { + Text( + text = EMPTY_BALANCE_SIGN, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body2, + ) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt new file mode 100644 index 0000000000..cf31789446 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt @@ -0,0 +1,82 @@ +package com.tangem.features.send.v2.subcomponents.fee.ui + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.containers.FooterContainer +import com.tangem.core.ui.components.inputrow.InputRowEnterAmount +import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmount +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import kotlinx.collections.immutable.ImmutableList + +@Composable +internal fun SendCustomFee( + customValues: ImmutableList, + selectedFee: FeeType, + hasNotifications: Boolean, + onValueChange: (Int, String) -> Unit, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = selectedFee == FeeType.Custom && customValues.isNotEmpty(), + label = "Custom Fee Selected Animation", + enter = expandVertically().plus(fadeIn()), + exit = shrinkVertically().plus(fadeOut()), + ) { + val bottomPadding = if (hasNotifications) 12.dp else 0.dp + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = modifier.padding(bottom = bottomPadding), + ) { + repeat(customValues.size) { index -> + val value = customValues[index] + FooterContainer( + footer = value.footer, + ) { + if (value.label != null) { + InputRowEnterInfoAmount( + text = value.value, + decimals = value.decimals, + symbol = value.symbol, + title = value.title, + info = value.label, + keyboardOptions = value.keyboardOptions, + keyboardActions = value.keyboardActions, + onValueChange = { onValueChange(index, it) }, + showDivider = false, + isReadOnly = value.isReadonly, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } else { + InputRowEnterAmount( + text = value.value, + decimals = value.decimals, + title = value.title, + symbol = value.symbol, + onValueChange = { onValueChange(index, it) }, + keyboardOptions = value.keyboardOptions, + keyboardActions = value.keyboardActions, + showDivider = false, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendFeeContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendFeeContent.kt new file mode 100644 index 0000000000..728f537a37 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendFeeContent.kt @@ -0,0 +1,79 @@ +package com.tangem.features.send.v2.subcomponents.fee.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.notifications + +private const val FEE_SELECTOR_KEY = "FEE_SELECTOR_KEY" +private const val FEE_CUSTOM_KEY = "FEE_CUSTOM_KEY" + +@Composable +internal fun SendFeeContent(state: FeeUM, clickIntents: SendFeeClickIntents) { + if (state !is FeeUM.Content) return + + val notifications = state.notifications + val feeSelectorUM = state.feeSelectorUM as? FeeSelectorUM.Content + val isCustomSelected = feeSelectorUM?.selectedType == FeeType.Custom + val hasNotifications = notifications.isNotEmpty() + LazyColumn( + modifier = Modifier // Do not put fillMaxSize() in here + .background(TangemTheme.colors.background.tertiary) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) { + feeSelector(state, clickIntents) + if (feeSelectorUM != null) { + customFee( + feeSelectorUM = feeSelectorUM, + onValueChange = clickIntents::onCustomFeeValueChange, + hasNotifications = hasNotifications, + ) + } + notifications(notifications = notifications, hasPaddingAbove = isCustomSelected) + } +} + +private fun LazyListScope.feeSelector(state: FeeUM.Content, clickIntents: SendFeeClickIntents) { + item(key = FEE_SELECTOR_KEY) { + SendSpeedSelector( + state = state, + clickIntents = clickIntents, + modifier = Modifier.animateItem(), + ) + } +} + +internal fun LazyListScope.customFee( + feeSelectorUM: FeeSelectorUM.Content, + hasNotifications: Boolean, + onValueChange: (Int, String) -> Unit, + modifier: Modifier = Modifier, +) { + item(key = FEE_CUSTOM_KEY) { + SendCustomFee( + customValues = feeSelectorUM.customValues, + selectedFee = feeSelectorUM.selectedType, + hasNotifications = hasNotifications, + onValueChange = onValueChange, + modifier = modifier + .fillMaxWidth() + .animateItem() + .background(TangemTheme.colors.background.tertiary) + .padding(top = 12.dp), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelector.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelector.kt new file mode 100644 index 0000000000..0437760435 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelector.kt @@ -0,0 +1,102 @@ +package com.tangem.features.send.v2.subcomponents.fee.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.ClickableText +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents + +@Suppress("LongMethod") +@Composable +internal fun SendSpeedSelector( + state: FeeUM.Content, + clickIntents: SendFeeClickIntents, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) { + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_slow, + iconRes = R.drawable.ic_tortoise_24, + feeType = FeeType.Slow, + state = state, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.Slow) }, + ) + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_market, + iconRes = R.drawable.ic_bird_24, + feeType = FeeType.Market, + state = state, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.Market) }, + ) + SendSpeedSelectorItem( + titleRes = R.string.common_fee_selector_option_fast, + iconRes = R.drawable.ic_hare_24, + feeType = FeeType.Fast, + state = state, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.Fast) }, + ) + SendSpeedSelectorItem( + titleRes = R.string.common_custom, + iconRes = R.drawable.ic_edit_24, + feeType = FeeType.Custom, + state = state, + onSelect = { clickIntents.onFeeSelectorClick(FeeType.Custom) }, + ) + } + FooterText(clickIntents::onReadMoreClick) + } +} + +@Composable +private fun FooterText(onReadMoreClick: () -> Unit) { + val linkText = stringResourceSafe(R.string.common_read_more) + val fullString = stringResourceSafe(R.string.common_fee_selector_footer, linkText) + val linkTextPosition = fullString.length - linkText.length + val defaultStyle = TangemTheme.colors.text.tertiary + val linkStyle = TangemTheme.colors.text.accent + val annotatedString = remember(defaultStyle, linkStyle) { + buildAnnotatedString { + withStyle(SpanStyle(defaultStyle)) { + append(fullString.substring(0, linkTextPosition)) + } + withStyle(SpanStyle(linkStyle)) { + append(fullString.substring(linkTextPosition, fullString.length)) + } + } + } + + val click = { i: Int -> + val readMoreStyle = requireNotNull(annotatedString.spanStyles.getOrNull(1)) + if (i in readMoreStyle.start..readMoreStyle.end) { + onReadMoreClick() + } + } + + ClickableText( + text = annotatedString, + style = TangemTheme.typography.caption2.copy(textAlign = TextAlign.Start), + modifier = Modifier.padding(top = 8.dp), + onClick = click, + ) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt new file mode 100644 index 0000000000..3cc6d6042b --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt @@ -0,0 +1,152 @@ +package com.tangem.features.send.v2.subcomponents.fee.ui + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.animation.* +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.amountScreen.utils.getFiatReference +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM + +@Composable +internal fun SendSpeedSelectorItem( + @StringRes titleRes: Int, + @DrawableRes iconRes: Int, + feeType: FeeType, + state: FeeUM.Content, + onSelect: () -> Unit, + modifier: Modifier = Modifier, +) { + val feeSelectorState = state.feeSelectorUM + val content = feeSelectorState as? FeeSelectorUM.Content + val amount = content?.getAmount(feeType) + val (showDivider, isVisible) = content.getDividerAndVisibility(feeType) + AnimatedVisibility( + visible = isVisible, + label = "Fee Selector Visibility Animation", + enter = expandVertically().plus(fadeIn()), + exit = shrinkVertically().plus(fadeOut()), + ) { + Box( + modifier = modifier + .fillMaxWidth() + .clickable { onSelect() }, + ) { + SelectorRowItem( + titleRes = titleRes, + iconRes = iconRes, + onSelect = onSelect, + modifier = modifier, + preDot = stringReference( + amount?.value.format { + crypto( + symbol = amount?.currencySymbol.orEmpty(), + decimals = amount?.decimals ?: 0, + ).fee(canBeLower = state.isFeeApproximate) + }, + ), + postDot = if (state.isFeeConvertibleToFiat) { + getFiatReference(amount?.value, state.rate, state.appCurrency) + } else { + null + }, + ellipsizeOffset = amount?.currencySymbol?.length, + isSelected = content?.selectedType == feeType, + showDivider = showDivider, + ) + FeeLoading(feeSelectorState) + FeeError(feeSelectorState) + } + } +} + +@Composable +private fun FeeLoading(feeSelectorState: FeeSelectorUM) { + Row { + SpacerWMax() + AnimatedVisibility( + visible = feeSelectorState == FeeSelectorUM.Loading, + label = "Fee Loading State Change", + modifier = Modifier.align(Alignment.CenterVertically), + ) { + RectangleShimmer( + radius = 3.dp, + modifier = Modifier + .padding( + vertical = 18.dp, + horizontal = 12.dp, + ) + .size( + height = 12.dp, + width = 90.dp, + ), + ) + } + } +} + +@Composable +private fun FeeError(feeSelectorState: FeeSelectorUM) { + Row { + SpacerWMax() + AnimatedVisibility( + visible = feeSelectorState is FeeSelectorUM.Error, + label = "Fee Error State Change", + modifier = Modifier.align(Alignment.CenterVertically), + ) { + Text( + text = EMPTY_BALANCE_SIGN, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body2, + modifier = Modifier + .padding( + vertical = 14.dp, + horizontal = 12.dp, + ), + ) + } + } +} + +private fun FeeSelectorUM.Content.getAmount(feeType: FeeType): Amount? { + val choosableFees = fees as? TransactionFee.Choosable + val decimals = fees.normal.amount.decimals + val customValue = this.customValues.firstOrNull()?.value?.parseToBigDecimal(decimals) + val customAmount = fees.normal.amount.copy(value = customValue) + return when (feeType) { + FeeType.Slow -> choosableFees?.minimum?.amount + FeeType.Market -> fees.normal.amount + FeeType.Fast -> choosableFees?.priority?.amount + FeeType.Custom -> customAmount + } +} + +private fun FeeSelectorUM.Content?.getDividerAndVisibility(feeType: FeeType): Pair { + val hasCustomValues = !this?.customValues.isNullOrEmpty() + val isNotSingle = this?.fees !is TransactionFee.Single + return when (feeType) { + FeeType.Slow -> true to isNotSingle + FeeType.Market -> (isNotSingle || hasCustomValues) to true + FeeType.Fast -> hasCustomValues to isNotSingle + FeeType.Custom -> false to hasCustomValues + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/CustomFeeFieldUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/CustomFeeFieldUM.kt new file mode 100644 index 0000000000..5b52d2c6b8 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/CustomFeeFieldUM.kt @@ -0,0 +1,20 @@ +package com.tangem.features.send.v2.subcomponents.fee.ui.state + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal data class CustomFeeFieldUM( + val value: String, + val onValueChange: (String) -> Unit, + val keyboardOptions: KeyboardOptions, + val keyboardActions: KeyboardActions, + val symbol: String?, + val decimals: Int, + val title: TextReference, + val footer: TextReference, + val label: TextReference? = null, + val isReadonly: Boolean = false, +) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt new file mode 100644 index 0000000000..a9f5aae1b4 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt @@ -0,0 +1,44 @@ +package com.tangem.features.send.v2.subcomponents.fee.ui.state + +import androidx.compose.runtime.Stable +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.transaction.error.GetFeeError +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Stable +internal sealed class FeeSelectorUM { + + data class Content( + val fees: TransactionFee, + val selectedType: FeeType = FeeType.Market, + val selectedFee: Fee?, + val customValues: ImmutableList = persistentListOf(), + ) : FeeSelectorUM() + + data object Loading : FeeSelectorUM() + + data class Error( + val error: GetFeeError, + ) : FeeSelectorUM() +} + +internal enum class FeeType { + Slow, + Market, + Fast, + Custom, + ; + + fun toAnalyticType(feeSelectorUM: FeeSelectorUM.Content): AnalyticsParam.FeeType = when (feeSelectorUM.fees) { + is TransactionFee.Single -> AnalyticsParam.FeeType.Fixed + is TransactionFee.Choosable -> when (feeSelectorUM.selectedType) { + Slow -> AnalyticsParam.FeeType.Min + Market -> AnalyticsParam.FeeType.Normal + Fast -> AnalyticsParam.FeeType.Max + Custom -> AnalyticsParam.FeeType.Custom + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt new file mode 100644 index 0000000000..6bf499e1c4 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt @@ -0,0 +1,32 @@ +package com.tangem.features.send.v2.subcomponents.fee.ui.state + +import androidx.compose.runtime.Stable +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.domain.appcurrency.model.AppCurrency +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal + +@Stable +internal sealed class FeeUM { + + abstract val isPrimaryButtonEnabled: Boolean + + data class Content( + override val isPrimaryButtonEnabled: Boolean, + val feeSelectorUM: FeeSelectorUM, + val rate: BigDecimal?, + val appCurrency: AppCurrency, + val isFeeConvertibleToFiat: Boolean, + val isFeeApproximate: Boolean, + val isCustomSelected: Boolean, + val isTronToken: Boolean, + val customValues: ImmutableList = persistentListOf(), + val notifications: ImmutableList, + val isEditingDisabled: Boolean = false, + ) : FeeUM() + + data class Empty( + override val isPrimaryButtonEnabled: Boolean = false, + ) : FeeUM() +} \ No newline at end of file From 80d661ab55989f58ef99f73b9560d41f897f4af7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Mar 2025 12:28:17 +0300 Subject: [PATCH 049/138] Updated on 2026-08-14 --- .../domain/scanCard/LegacyScanProcessor.kt | 16 +- .../chains/CheckForOnboardingChain.kt | 20 +- .../features/onboarding/OnboardingHelper.kt | 2 +- .../tangem/tap/routing/utils/ChildFactory.kt | 18 +- .../com/tangem/common/routing/AppRoute.kt | 4 +- .../v2/entry/OnboardingEntryComponent.kt | 8 +- features/onboarding-v2/impl/build.gradle.kts | 1 + .../v2/common/analytics/OnboardingEvent.kt | 18 + .../onboarding/v2/common/ui/Dialogs.kt | 16 +- .../impl/DefaultOnboardingEntryComponent.kt | 1 + .../entry/impl/model/OnboardingEntryModel.kt | 117 +++-- .../impl/routing/OnboardingChildFactory.kt | 20 +- .../v2/entry/impl/routing/OnboardingRoute.kt | 7 + .../v2/multiwallet/impl/common/ui/Dialogs.kt | 18 +- .../impl/model/OnboardingMultiWalletModel.kt | 27 +- .../impl/ui/OnboardingMultiWallet.kt | 20 - .../impl/ui/state/OnboardingMultiWalletUM.kt | 3 - .../v2/twin/api/OnboardingTwinComponent.kt | 5 +- .../impl/DefaultOnboardingTwinComponent.kt | 11 +- .../onboarding/v2/twin/impl/model/Dialogs.kt | 34 ++ .../v2/twin/impl/model/OnboardingTwinModel.kt | 419 +++++++++++++++++- .../v2/twin/impl/ui/OnboardingTwin.kt | 12 +- .../v2/twin/impl/ui/TwinWalletArtwork.kt | 43 +- .../v2/twin/impl/ui/state/OnboardingTwinUM.kt | 65 +++ .../model/WalletSettingsModel.kt | 2 +- .../router/DefaultWalletRouter.kt | 2 +- 26 files changed, 771 insertions(+), 138 deletions(-) create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/Dialogs.kt create mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index 27fa8ead9e..6ea8247faf 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -235,13 +235,25 @@ internal class LegacyScanProcessor @Inject constructor( } else { Analytics.setContext(scanResponse) + val isTwinRefactoringEnabled = + store.inject(DaggerGraphState::onboardingV2FeatureToggles).isTwinRefactoringEnabled + val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync() if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) { onWalletNotCreated() - store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse))) - navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) } + if (isTwinRefactoringEnabled) { + navigateTo( + AppRoute.Onboarding( + scanResponse = scanResponse, + mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, + ), + ) { onProgressStateChange(it) } + } else { + store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse))) + navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) } + } } else { delay(DELAY_SDK_DIALOG_CLOSE) onSuccess(scanResponse) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt index 7bd5572200..b9d6aff5fd 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt @@ -59,15 +59,27 @@ class CheckForOnboardingChain( else -> { Analytics.setContext(previousChainResult) + val isTwinRefactoringEnabled = + store.inject(DaggerGraphState::onboardingV2FeatureToggles).isTwinRefactoringEnabled + val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase) .invokeSync() // If twins was twinned previously but twins welcome not shown if (previousChainResult.twinsIsTwinned() && !wasTwinsOnboardingShown) { - store.dispatchOnMain( - TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)), - ) - ScanChainException.OnboardingNeeded(AppRoute.OnboardingTwins).left() + if (isTwinRefactoringEnabled) { + ScanChainException.OnboardingNeeded( + AppRoute.Onboarding( + scanResponse = previousChainResult, + mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, + ), + ).left() + } else { + store.dispatchOnMain( + TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)), + ) + ScanChainException.OnboardingNeeded(AppRoute.OnboardingTwins).left() + } } else { delay(DELAY_SDK_DIALOG_CLOSE) previousChainResult.right() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 4d81a04308..7eda3eb6c5 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -85,7 +85,7 @@ object OnboardingHelper { scanResponse = scanResponse, mode = when (backupState.startedSource) { BackupStartedSource.Onboarding -> AppRoute.Onboarding.Mode.Onboarding - BackupStartedSource.CreateBackup -> AppRoute.Onboarding.Mode.AddBackup + BackupStartedSource.CreateBackup -> AppRoute.Onboarding.Mode.AddBackupWallet1 }, ) } diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index d4cd6dd17c..31a4ba6b7a 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -210,9 +210,12 @@ internal class ChildFactory @Inject constructor( contextProvider = contextProvider(route, contextFactory), params = OnboardingEntryComponent.Params( scanResponse = route.scanResponse, - multiWalletMode = when (route.mode) { - AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.MultiWalletMode.Onboarding - AppRoute.Onboarding.Mode.AddBackup -> OnboardingEntryComponent.MultiWalletMode.AddBackup + mode = when (route.mode) { + AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.Mode.Onboarding + AppRoute.Onboarding.Mode.AddBackupWallet1 -> OnboardingEntryComponent.Mode.AddBackupWallet1 + AppRoute.Onboarding.Mode.WelcomeOnlyTwin -> OnboardingEntryComponent.Mode.WelcomeOnlyTwin + AppRoute.Onboarding.Mode.RecreateWalletTwin -> + OnboardingEntryComponent.Mode.RecreateWalletTwin }, ), componentFactory = onboardingEntryComponentFactory, @@ -702,9 +705,12 @@ internal class ChildFactory @Inject constructor( contextProvider = contextProvider(route, contextFactory), params = OnboardingEntryComponent.Params( scanResponse = route.scanResponse, - multiWalletMode = when (route.mode) { - AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.MultiWalletMode.Onboarding - AppRoute.Onboarding.Mode.AddBackup -> OnboardingEntryComponent.MultiWalletMode.AddBackup + mode = when (route.mode) { + AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.Mode.Onboarding + AppRoute.Onboarding.Mode.AddBackupWallet1 -> OnboardingEntryComponent.Mode.AddBackupWallet1 + AppRoute.Onboarding.Mode.WelcomeOnlyTwin -> OnboardingEntryComponent.Mode.WelcomeOnlyTwin + AppRoute.Onboarding.Mode.RecreateWalletTwin -> + OnboardingEntryComponent.Mode.RecreateWalletTwin }, ), componentFactory = onboardingEntryComponentFactory, diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index f7e8d9ea7b..1238ad4305 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -260,7 +260,9 @@ sealed class AppRoute(val path: String) : Route { enum class Mode { Onboarding, // general Mode - AddBackup, // continue backup process for existing wallet 1 + AddBackupWallet1, // continue backup process for existing wallet 1 + WelcomeOnlyTwin, // show welcome screen and then navigate to wallet for twins + RecreateWalletTwin, // reset twins } } diff --git a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt index 9649b1e0c7..e591e8393f 100644 --- a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt +++ b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt @@ -8,12 +8,14 @@ interface OnboardingEntryComponent : ComposableContentComponent { data class Params( val scanResponse: ScanResponse, - val multiWalletMode: MultiWalletMode, + val mode: Mode, ) - enum class MultiWalletMode { + enum class Mode { Onboarding, - AddBackup, + AddBackupWallet1, + WelcomeOnlyTwin, + RecreateWalletTwin, } interface Factory : ComponentFactory diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index 3c11edf62d..f0b9fcacbb 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -77,6 +77,7 @@ dependencies { implementation(deps.kotlin.serialization) implementation(deps.timber) implementation(deps.firebase.crashlytics) + implementation(tangemDeps.card.core) /** DI */ implementation(deps.hilt.android) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt index 185b058558..053c5f9930 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt @@ -92,4 +92,22 @@ sealed class OnboardingEvent( params = mapOf("Action" to "Cancel"), ) } + + sealed class Twins( + event: String, + params: Map = mapOf(), + ) : OnboardingEvent("Onboarding / Twins", event, params) { + + data object ScreenOpened : Twins("Twinning Screen Opened") + data object SetupStarted : Twins("Twin Setup Started") + data object SetupFinished : Twins("Twin Setup Finished") + } + + data class OfflineAttestationFailed( + val source: AnalyticsParam.ScreensSources, + ) : OnboardingEvent( + category = "Error", + event = "Offline Attestation Failed", + params = mapOf(AnalyticsParam.SOURCE to source.value), + ) } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/Dialogs.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/Dialogs.kt index 2d26b8608b..7fb5ae3b9b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/Dialogs.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/Dialogs.kt @@ -22,4 +22,18 @@ internal fun exitOnboardingDialog(onConfirm: () -> Unit): DialogMessage { ) }, ) -} \ No newline at end of file +} + +internal fun interruptBackupDialog(onConfirm: () -> Unit) = DialogMessage( + title = resourceReference(R.string.onboarding_exit_alert_title), + message = resourceReference(R.string.onboarding_exit_alert_message), + firstAction = EventMessageAction( + title = resourceReference(R.string.common_ok), + warning = true, + onClick = onConfirm, + ), + secondAction = EventMessageAction( + title = resourceReference(R.string.common_cancel), + onClick = {}, + ), +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt index 8e6cf0d6c5..ae53ea07e1 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt @@ -71,6 +71,7 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor( route = configuration, childContext = childByContext(factoryContext), onManageTokensDone = model::onManageTokensDone, + model = model, ) }, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index f8baf38360..231ffff139 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -15,6 +15,8 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.biometry.BiometryFeatureToggles @@ -22,9 +24,11 @@ import com.tangem.features.onboarding.v2.TitleProvider import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent +import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent.Mode import com.tangem.features.onboarding.v2.entry.impl.analytics.OnboardingEntryEvent import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.NonCancellable @@ -44,6 +48,7 @@ internal class OnboardingEntryModel @Inject constructor( private val askBiometryFeatureToggles: BiometryFeatureToggles, private val analyticsEventHandler: AnalyticsEventHandler, private val uiMessageSender: UiMessageSender, + private val userWalletsListManager: UserWalletsListManager, ) : Model() { private val params = paramsContainer.require() @@ -57,6 +62,7 @@ internal class OnboardingEntryModel @Inject constructor( } val startRoute = routeByProductType(params.scanResponse) + val onboardingTwinModelCallbacks = OnboardingTwinModelCallbacks() fun onManageTokensDone() { navigateToFinalScreenFlow() @@ -67,28 +73,25 @@ internal class OnboardingEntryModel @Inject constructor( } private fun routeByProductType(scanResponse: ScanResponse): OnboardingRoute { - val multiWalletNavigationMode = when (params.multiWalletMode) { - OnboardingEntryComponent.MultiWalletMode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding - OnboardingEntryComponent.MultiWalletMode.AddBackup -> OnboardingMultiWalletComponent.Mode.AddBackup - } - return when (scanResponse.productType) { - ProductType.Wallet -> OnboardingRoute.MultiWallet( - scanResponse = scanResponse, - withSeedPhraseFlow = false, - titleProvider = titleProvider, - onDone = ::onMultiWalletOnboardingDone, - mode = multiWalletNavigationMode, - ) - ProductType.Ring, + ProductType.Wallet, ProductType.Wallet2, - -> OnboardingRoute.MultiWallet( - scanResponse = scanResponse, - withSeedPhraseFlow = true, - titleProvider = titleProvider, - onDone = ::onMultiWalletOnboardingDone, - mode = multiWalletNavigationMode, - ) + ProductType.Ring, + -> { + val multiWalletNavigationMode = when (params.mode) { + Mode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding + Mode.AddBackupWallet1 -> OnboardingMultiWalletComponent.Mode.AddBackup + else -> error("Incorrect onboarding type") + } + + OnboardingRoute.MultiWallet( + scanResponse = scanResponse, + withSeedPhraseFlow = scanResponse.productType != ProductType.Wallet, + titleProvider = titleProvider, + onDone = ::onMultiWalletOnboardingDone, + mode = multiWalletNavigationMode, + ) + } ProductType.Visa -> OnboardingRoute.Visa( scanResponse = scanResponse, titleProvider = titleProvider, @@ -99,17 +102,31 @@ internal class OnboardingEntryModel @Inject constructor( titleProvider = titleProvider, onDone = ::navigateToFinalScreenFlow, ) + ProductType.Twins -> { + val mode = when (params.mode) { + Mode.Onboarding -> OnboardingTwinComponent.Params.Mode.CreateWallet + Mode.WelcomeOnlyTwin -> OnboardingTwinComponent.Params.Mode.WelcomeOnly + Mode.RecreateWalletTwin -> OnboardingTwinComponent.Params.Mode.RecreateWallet + else -> error("Incorrect onboarding type") + } + + OnboardingRoute.Twins( + scanResponse = scanResponse, + titleProvider = titleProvider, + mode = mode, + ) + } else -> error("Unsupported") } } private fun onMultiWalletOnboardingDone(userWallet: UserWallet) { when { - params.multiWalletMode == OnboardingEntryComponent.MultiWalletMode.AddBackup -> { + params.mode == Mode.AddBackupWallet1 -> { stackNavigation.replaceAll( OnboardingRoute.Done( mode = OnboardingDoneComponent.Mode.WalletCreated, - onDone = ::navigateToWalletScreen, + onDone = ::exitComponentScreen, ), ) } @@ -139,7 +156,7 @@ internal class OnboardingEntryModel @Inject constructor( stackNavigation.replaceAll( OnboardingRoute.Done( mode = doneMode, - onDone = ::navigateToWalletScreen, + onDone = ::exitComponentScreen, ), ) } @@ -148,7 +165,7 @@ internal class OnboardingEntryModel @Inject constructor( stackNavigation.replaceAll( OnboardingRoute.Done( mode = doneMode, - onDone = ::navigateToWalletScreen, + onDone = ::exitComponentScreen, ), ) } @@ -162,7 +179,7 @@ internal class OnboardingEntryModel @Inject constructor( stackNavigation.replaceAll( OnboardingRoute.Done( mode = doneMode, - onDone = ::navigateToWalletScreen, + onDone = ::exitComponentScreen, ), ) } @@ -172,23 +189,55 @@ internal class OnboardingEntryModel @Inject constructor( stackNavigation.replaceAll( OnboardingRoute.Done( mode = doneMode, - onDone = ::navigateToWalletScreen, + onDone = ::exitComponentScreen, ), ) } } - private fun navigateToWalletScreen() { + private fun exitComponentScreen() { if (askBiometryFeatureToggles.isAskForBiometryEnabled) { - router.replaceAll(AppRoute.Wallet) - } else { - modelScope.launch(NonCancellable) { - router.replaceAll(AppRoute.Wallet) - if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) { - delay(timeMillis = 1_800) - router.push(AppRoute.SaveWallet) + if (userWalletsListManager.hasUserWallets) { + val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false } + + if (isLocked) { + router.replaceAll(AppRoute.Welcome()) + } else { + router.replaceAll(AppRoute.Wallet) } + } else { + router.replaceAll(AppRoute.Home) + } + } else { + if (userWalletsListManager.hasUserWallets) { + val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false } + + if (isLocked) { + router.replaceAll(AppRoute.Welcome()) + } else { + modelScope.launch(NonCancellable) { + router.replaceAll(AppRoute.Wallet) + if (tangemSdkManager.checkCanUseBiometry() && + settingsRepository.shouldShowSaveUserWalletScreen() + ) { + delay(timeMillis = 1_800) + router.push(AppRoute.SaveWallet) + } + } + } + } else { + router.replaceAll(AppRoute.Home) } } } + + inner class OnboardingTwinModelCallbacks : OnboardingTwinComponent.ModelCallbacks { + override fun onDone() { + navigateToFinalScreenFlow() + } + + override fun onBack() { + router.pop() + } + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt index edbc555782..d0e8a13f03 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt @@ -4,11 +4,14 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.managetokens.component.OnboardingManageTokensComponent import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent +import com.tangem.features.onboarding.v2.entry.impl.model.OnboardingEntryModel import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent import javax.inject.Inject +@Suppress("LongParameterList") internal class OnboardingChildFactory @Inject constructor( private val multiWalletComponentFactory: OnboardingMultiWalletComponent.Factory, private val onboardingManageTokensComponentFactory: OnboardingManageTokensComponent.Factory, @@ -16,9 +19,15 @@ internal class OnboardingChildFactory @Inject constructor( private val onBoardingVisaComponentFactory: OnboardingVisaComponent.Factory, private val askBiometryComponentFactory: AskBiometryComponent.Factory, private val onboardingNoteComponentFactory: OnboardingNoteComponent.Factory, + private val onboardingTwinComponentFactory: OnboardingTwinComponent.Factory, ) { - fun createChild(route: OnboardingRoute, childContext: AppComponentContext, onManageTokensDone: () -> Unit): Any { + fun createChild( + route: OnboardingRoute, + childContext: AppComponentContext, + model: OnboardingEntryModel, + onManageTokensDone: () -> Unit, + ): Any { return when (route) { is OnboardingRoute.MultiWallet -> multiWalletComponentFactory.create( context = childContext, @@ -60,6 +69,15 @@ internal class OnboardingChildFactory @Inject constructor( onDone = route.onDone, ), ) + is OnboardingRoute.Twins -> onboardingTwinComponentFactory.create( + context = childContext, + params = OnboardingTwinComponent.Params( + scanResponse = route.scanResponse, + modelCallbacks = model.onboardingTwinModelCallbacks, + titleProvider = route.titleProvider, + mode = route.mode, + ), + ) is OnboardingRoute.AskBiometry -> askBiometryComponentFactory.create( context = childContext, params = AskBiometryComponent.Params( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt index 1175f2948b..4d0b03129a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt @@ -7,6 +7,7 @@ import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.onboarding.v2.TitleProvider import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent sealed class OnboardingRoute : Route { @@ -32,6 +33,12 @@ sealed class OnboardingRoute : Route { val onDone: () -> Unit, ) : OnboardingRoute() + data class Twins( + val titleProvider: TitleProvider, + val scanResponse: ScanResponse, + val mode: OnboardingTwinComponent.Params.Mode, + ) : OnboardingRoute() + data class ManageTokens( val userWallet: UserWallet, ) : OnboardingRoute() diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/Dialogs.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/Dialogs.kt index 771c7b9085..9b1a7be5b1 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/Dialogs.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/Dialogs.kt @@ -20,20 +20,4 @@ internal fun resetCardDialog(onConfirm: () -> Unit, dismiss: () -> Unit, onDismi onDismissButtonClick() dismiss() }, - ) - -internal fun interruptBackupDialog(onConfirm: () -> Unit, dismiss: () -> Unit) = OnboardingDialogUM( - title = resourceReference(R.string.onboarding_exit_alert_title), - message = resourceReference(R.string.onboarding_exit_alert_message), - confirmButtonText = resourceReference(R.string.common_ok), - dismissButtonText = resourceReference(R.string.common_cancel), - dismissWarningColor = true, - onDismiss = dismiss, - onConfirmClick = { - onConfirm() - dismiss() - }, - onDismissButtonClick = { - dismiss() - }, -) \ No newline at end of file + ) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt index f868e3915a..c55cb1d1a9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt @@ -5,14 +5,15 @@ 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.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams -import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.interruptBackupDialog import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.FinalizeStage import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM import com.tangem.operations.backup.BackupService @@ -35,6 +36,7 @@ internal class OnboardingMultiWalletModel @Inject constructor( private val backupServiceHolder: BackupServiceHolder, private val onboardingRepository: OnboardingRepository, private val getCardImageUseCase: GetCardImageUseCase, + private val uiMessageSender: UiMessageSender, ) : Model() { private val params = paramsContainer.require() private val _uiState = MutableStateFlow(OnboardingMultiWalletUM()) @@ -60,19 +62,16 @@ internal class OnboardingMultiWalletModel @Inject constructor( } fun onBack() { - _uiState.update { st -> - st.copy( - dialog = interruptBackupDialog( - onConfirm = { - modelScope.launch { - onboardingRepository.clearUnfinishedFinalizeOnboarding() - router.pop() - } - }, - dismiss = { _uiState.update { it.copy(dialog = null) } }, - ), - ) - } + uiMessageSender.send( + interruptBackupDialog( + onConfirm = { + modelScope.launch { + onboardingRepository.clearUnfinishedFinalizeOnboarding() + router.pop() + } + }, + ), + ) } private fun subscribeToBackups() { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt index 62bc6aaf7a..4af97756ab 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt @@ -8,9 +8,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM @@ -23,23 +20,6 @@ internal fun OnboardingMultiWallet( modifier: Modifier = Modifier, childContent: @Composable (Modifier) -> Unit = {}, ) { - if (state.dialog != null) { - BasicDialog( - title = state.dialog.title.resolveReference(), - message = state.dialog.message.resolveReference(), - confirmButton = DialogButtonUM( - title = state.dialog.confirmButtonText.resolveReference(), - onClick = state.dialog.onConfirmClick, - ), - dismissButton = DialogButtonUM( - title = state.dialog.dismissButtonText.resolveReference(), - warning = state.dialog.dismissWarningColor, - onClick = state.dialog.onDismissButtonClick, - ), - onDismissDialog = state.dialog.onDismiss, - ) - } - Column( modifier = modifier.fillMaxSize(), ) { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt index 74f7cc7bec..829828761d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt @@ -1,10 +1,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.ui.state -import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM - internal data class OnboardingMultiWalletUM( val artwork1Url: String? = null, val artwork2Url: String? = null, val artwork3Url: String? = null, - val dialog: OnboardingDialogUM? = null, ) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt index 20893e09f8..2a28cf2e89 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt @@ -8,18 +8,19 @@ import com.tangem.features.onboarding.v2.TitleProvider interface OnboardingTwinComponent : ComposableContentComponent { data class Params( - val titleProvider: TitleProvider, val scanResponse: ScanResponse, val modelCallbacks: ModelCallbacks, + val titleProvider: TitleProvider, val mode: Mode, ) { enum class Mode { - CreateWallet, RecreateWallet + WelcomeOnly, CreateWallet, RecreateWallet } } interface ModelCallbacks { fun onDone() + fun onBack() } interface Factory : ComponentFactory diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt index 318143250b..f8130f2d71 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt @@ -1,9 +1,12 @@ package com.tangem.features.onboarding.v2.twin.impl +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent import com.tangem.features.onboarding.v2.twin.impl.model.OnboardingTwinModel import dagger.assisted.Assisted @@ -17,9 +20,15 @@ internal class DefaultOnboardingTwinComponent @AssistedInject constructor( private val model: OnboardingTwinModel = getOrCreateModel(params) + init { + params.titleProvider.changeTitle(resourceReference(R.string.twins_recreate_toolbar)) + } + @Composable override fun Content(modifier: Modifier) { - TODO("Not yet implemented") + BackHandler { model.onBack() } + + // TODO } @AssistedFactory diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/Dialogs.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/Dialogs.kt new file mode 100644 index 0000000000..a819a60957 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/Dialogs.kt @@ -0,0 +1,34 @@ +package com.tangem.features.onboarding.v2.twin.impl.model + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.features.onboarding.v2.impl.R + +internal val TwinProcessNotCompleted = DialogMessage( + message = resourceReference(R.string.onboarding_twin_exit_warning), + firstAction = EventMessageAction( + title = resourceReference(R.string.warning_button_ok), + onClick = {}, + ), +) + +internal fun cardVerificationFailedDialog( + errorDescription: TextReference, + onRequestSupport: () -> Unit, + onCancelClick: () -> Unit = {}, +): DialogMessage { + return DialogMessage( + message = errorDescription, + title = resourceReference(id = R.string.security_alert_title), + isDismissable = false, + firstActionBuilder = { + EventMessageAction( + title = resourceReference(id = R.string.alert_button_request_support), + onClick = onRequestSupport, + ) + }, + secondActionBuilder = { cancelAction(onClick = onCancelClick) }, + ) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index 047e897976..ae6846a3ec 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -1,19 +1,434 @@ package com.tangem.features.onboarding.v2.twin.impl.model +import com.tangem.Message +import com.tangem.common.CompletionResult +import com.tangem.common.KeyPair +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.hexToBytes +import com.tangem.common.extensions.toHexString +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam 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.navigation.url.UrlOpener +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.toWrappedList +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.datasource.local.config.issuers.IssuersConfigStorage +import com.tangem.domain.card.repository.CardRepository +import com.tangem.domain.common.TwinCardNumber +import com.tangem.domain.common.getTwinCardNumber +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase +import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase +import com.tangem.domain.tokens.FetchCurrencyStatusUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog +import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent.Params.Mode +import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM +import com.tangem.features.onboarding.v2.twin.impl.ui.state.OnboardingTwinUM +import com.tangem.sdk.api.TangemSdkManager +import com.tangem.sdk.extensions.localizedDescriptionRes import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList", "LargeClass") @ModelScoped internal class OnboardingTwinModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, + private val userWalletsListManager: UserWalletsListManager, + private val analyticsEventHandler: AnalyticsEventHandler, + private val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase, + private val tangemSdkManager: TangemSdkManager, + private val issuersConfigStorage: IssuersConfigStorage, + private val cardRepository: CardRepository, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, + private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase, + private val urlOpener: UrlOpener, + private val uiMessageSender: UiMessageSender, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, ) : Model() { - val params = paramsContainer.require() + private val params = paramsContainer.require() + private val firstCardTwinNumber = params.scanResponse.card.getTwinCardNumber() ?: error("Not twin") + private val cryptoCurrencyStatusJobHolder = JobHolder() - // TODO + private val _uiState = MutableStateFlow( + when (params.mode) { + Mode.WelcomeOnly -> { + OnboardingTwinUM.Welcome(onContinueClick = ::saveWalletAndDone) + } + Mode.RecreateWallet -> { + OnboardingTwinUM.ResetWarning( + onAcceptClick = { toggle -> + update { it.copy(acceptToggle = toggle) } + }, + onContinueClick = { + navigateToFirstScan() + }, + ) + } + Mode.CreateWallet -> { + OnboardingTwinUM.Welcome(onContinueClick = ::navigateToFirstScan) + } + }, + ) + + val uiState = _uiState.asStateFlow() + + init { + if (_uiState.value is OnboardingTwinUM.Welcome) { + analyticsEventHandler.send(OnboardingEvent.Twins.ScreenOpened) + modelScope.launch { + saveTwinsOnboardingShownUseCase() + } + } + } + + fun onBack() { + val state = _uiState.value + val twinningInProgress = state is OnboardingTwinUM.ScanCard && + (state.step == OnboardingTwinUM.ScanCard.Step.Second || state.step == OnboardingTwinUM.ScanCard.Step.Third) + + if (twinningInProgress) { + uiMessageSender.send(TwinProcessNotCompleted) + } else { + uiMessageSender.send( + interruptBackupDialog( + onConfirm = { params.modelCallbacks.onBack() }, + ), + ) + } + } + + private fun navigateToFirstScan() { + _uiState.value = OnboardingTwinUM.ScanCard( + artworkStep = when (firstCardTwinNumber) { + TwinCardNumber.First -> TwinWalletArtworkUM.Leapfrog.Step.FirstCard + TwinCardNumber.Second -> TwinWalletArtworkUM.Leapfrog.Step.SecondCard + }, + step = OnboardingTwinUM.ScanCard.Step.First, + onScanClick = { createFirstWallet() }, + ) + } + + private fun createFirstWallet() { + setLoading(true) + + modelScope.launch { + val result = tangemSdkManager.createFirstTwinWallet( + cardId = params.scanResponse.card.cardId, + initialMessage = Message( + tangemSdkManager.getString( + R.string.twins_recreate_title_format, + wrappedList(firstCardTwinNumber.number), + ), + ), + ) + + when (result) { + is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error) + is CompletionResult.Success -> { + if (params.mode == Mode.CreateWallet) { + cardRepository.startCardActivation(result.data.cardId) + } + + // remove wallet only after first step of retwin + if (params.mode == Mode.RecreateWallet) { + userWalletsListManager.delete( + listOfNotNull(UserWalletIdBuilder.scanResponse(params.scanResponse).build()), + ) + } + + analyticsEventHandler.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) + + update { + it.copy( + isLoading = false, + artworkStep = it.artworkStep.next(), + step = OnboardingTwinUM.ScanCard.Step.Second, + onScanClick = { + createSecondWallet(firstPublicKey = result.data.wallet.publicKey.toHexString()) + }, + ) + } + } + } + } + } + + private fun createSecondWallet(firstPublicKey: String) { + setLoading(true) + + modelScope.launch { + val secondCardNumber = firstCardTwinNumber.pairNumber().number + val result = tangemSdkManager.createSecondTwinWallet( + firstPublicKey = firstPublicKey, + firstCardId = params.scanResponse.card.cardId, + issuerKeys = getIssuerKeys(), + preparingMessage = Message( + tangemSdkManager.getString(R.string.twins_recreate_title_preparing), + ), + creatingWalletMessage = Message( + tangemSdkManager.getString(R.string.twins_recreate_title_creating_wallet), + ), + initialMessage = Message( + tangemSdkManager.getString( + R.string.twins_recreate_title_format, + wrappedList(secondCardNumber), + ), + ), + ) + + when (result) { + is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error) + is CompletionResult.Success -> { + if (params.mode == Mode.CreateWallet) { + cardRepository.startCardActivation(result.data.cardId) + } + + update { + it.copy( + isLoading = false, + artworkStep = it.artworkStep.next(), + step = OnboardingTwinUM.ScanCard.Step.Third, + onScanClick = { + createThirdWallet( + secondCardPublicKey = result.data.wallet.publicKey, + ) + }, + ) + } + } + } + } + } + + private fun createThirdWallet(secondCardPublicKey: ByteArray) { + setLoading(true) + + modelScope.launch { + val result = tangemSdkManager.finalizeTwin( + secondCardPublicKey = secondCardPublicKey, + issuerKeyPair = getIssuerKeys(), + cardId = params.scanResponse.card.cardId, + initialMessage = Message( + tangemSdkManager.getString( + R.string.twins_recreate_title_format, + wrappedList(firstCardTwinNumber.number), + ), + ), + ) + + when (result) { + is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error) + is CompletionResult.Success -> { + scanComplete(result.data) + } + } + } + } + + private fun scanComplete(scanResponse: ScanResponse) { + analyticsEventHandler.send(OnboardingEvent.Twins.SetupFinished) + + when (params.mode) { + Mode.WelcomeOnly -> saveWalletAndDone() + Mode.RecreateWallet -> params.modelCallbacks.onDone() + Mode.CreateWallet -> { + modelScope.launch { + setLoading(true) + + val userWallet = userWalletBuilderFactory.create(scanResponse).build() ?: run { + Timber.e("User wallet not created") + setLoading(false) + return@launch + } + + fetchCurrencyStatusUseCase( + userWalletId = userWallet.walletId, + refresh = true, + ) + + val cryptoCurrencyStatus = + getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId).firstOrNull()?.getOrNull() + ?: run { + setLoading(false) + return@launch + } + + launch { + getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId) + .collect { + it.onRight { status -> + applyCryptoCurrencyStatusToState(status) + } + } + } + + _uiState.value = OnboardingTwinUM.TopUp( + onBuyCryptoClick = { onBuyCryptoClick(cryptoCurrencyStatus) }, + onRefreshClick = { onRefreshBalanceClick(userWallet) }, + onShowAddressClick = { onShowAddressClick(cryptoCurrencyStatus) }, + isLoading = true, + ) + }.saveIn(cryptoCurrencyStatusJobHolder) + } + } + } + + private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { + update { + it.copy( + balance = (status.value as? CryptoCurrencyStatus.Loaded)?.amount + ?.format { crypto(status.currency) } ?: "", + onBuyCryptoClick = { onBuyCryptoClick(status) }, + onShowAddressClick = { onShowAddressClick(status) }, + isLoading = false, + ) + } + } + + private fun onBuyCryptoClick(status: CryptoCurrencyStatus) { + modelScope.launch { + getLegacyTopUpUrlUseCase(status).onRight { + urlOpener.openUrl(it) + } + } + } + + private fun onShowAddressClick(status: CryptoCurrencyStatus) { + val currency = status.currency + val networkAddress = status.value.networkAddress ?: return + + update { + it.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = { + update { + it.copy(bottomSheetConfig = TangemBottomSheetConfig.Empty) + } + }, + content = TokenReceiveBottomSheetConfig( + name = currency.name, + symbol = currency.symbol, + network = currency.network.name, + addresses = networkAddress.availableAddresses + .mapToAddressModels(currency) + .toImmutableList(), + showMemoDisclaimer = + currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, + onCopyClick = {}, + onShareClick = {}, + ), + ), + ) + } + } + + private fun onRefreshBalanceClick(userWallet: UserWallet) { + update { + it.copy(isLoading = true) + } + modelScope.launch { + fetchCurrencyStatusUseCase( + userWalletId = userWallet.walletId, + refresh = true, + ) + } + } + + private fun saveWalletAndDone() { + setLoading(true) + + modelScope.launch { + val userWallet = userWalletBuilderFactory.create(params.scanResponse).build() ?: run { + Timber.e("User wallet not created") + setLoading(false) + return@launch + } + + userWalletsListManager.save(userWallet, canOverride = true) + params.modelCallbacks.onDone() + } + } + + private fun showCardVerificationFailedDialog(error: TangemError) { + setLoading(false) + if (error !is TangemSdkError.CardVerificationFailed) return + + analyticsEventHandler.send(OnboardingEvent.OfflineAttestationFailed(AnalyticsParam.ScreensSources.Onboarding)) + + val resource = error.localizedDescriptionRes() + val resId = resource.resId ?: R.string.common_unknown_error + val resArgs = resource.args.map { it.value } + + uiMessageSender.send( + cardVerificationFailedDialog( + errorDescription = resourceReference(id = resId, resArgs.toWrappedList()), + onRequestSupport = { + modelScope.launch { + sendFeedbackEmailUseCase(type = FeedbackEmailType.CardAttestationFailed) + } + }, + ), + ) + } + + private suspend fun getIssuerKeys(): KeyPair { + val issuer = issuersConfigStorage.getConfig() + .first { it.publicKey == params.scanResponse.card.issuer.publicKey.toHexString() } + + return KeyPair( + publicKey = issuer.publicKey.hexToBytes(), + privateKey = issuer.privateKey.hexToBytes(), + ) + } + + private inline fun update(updateBlock: (T) -> OnboardingTwinUM) { + _uiState.update { st -> + (st as? T)?.let { updateBlock(it) } ?: st + } + } + + private fun setLoading(flag: Boolean) { + _uiState.update { it.copySealed(isLoading = flag) } + } + + private fun TwinWalletArtworkUM.Leapfrog.Step.next(): TwinWalletArtworkUM.Leapfrog.Step = when (this) { + TwinWalletArtworkUM.Leapfrog.Step.FirstCard -> TwinWalletArtworkUM.Leapfrog.Step.SecondCard + TwinWalletArtworkUM.Leapfrog.Step.SecondCard -> TwinWalletArtworkUM.Leapfrog.Step.FirstCard + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt index 2a8405c685..5ea7351de4 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt @@ -5,22 +5,28 @@ import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.twin.impl.ui.state.OnboardingTwinUM @Suppress("UnusedPrivateMember") @Composable -fun OnboardingTwin(modifier: Modifier = Modifier) { - Column { +internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifier) { + Column(modifier = modifier) { Box { // TODO } } + + if (state is OnboardingTwinUM.TopUp) { + TokenReceiveBottomSheet(config = state.bottomSheetConfig) + } } @Preview @Composable private fun Preview() { TangemThemePreview { - OnboardingTwin() + OnboardingTwin(OnboardingTwinUM.Welcome()) } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt index c2a89c2b69..2326dd841b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt @@ -33,18 +33,19 @@ import com.tangem.features.onboarding.v2.impl.R import kotlinx.coroutines.delay import java.util.concurrent.TimeUnit -internal sealed class TwinWalletArtworkState { - data object Spread : TwinWalletArtworkState() +@Immutable +internal sealed class TwinWalletArtworkUM { + data object Spread : TwinWalletArtworkUM() data class Leapfrog( val step: Step, - ) : TwinWalletArtworkState() { + ) : TwinWalletArtworkUM() { enum class Step { FirstCard, SecondCard } } - data object TopUp : TwinWalletArtworkState() + data object TopUp : TwinWalletArtworkUM() } private data class CardsTransitionState( @@ -65,7 +66,7 @@ private data class WalletCardTransitionState( @Suppress("LongMethod") @Composable internal fun TwinWalletArtworks( - state: TwinWalletArtworkState, + state: TwinWalletArtworkUM, balance: String, isRefreshing: Boolean, onRefreshBalanceClick: () -> Unit, @@ -109,7 +110,7 @@ internal fun TwinWalletArtworks( } AnimatedVisibility( - visible = state == TwinWalletArtworkState.TopUp, + visible = state == TwinWalletArtworkUM.TopUp, enter = fadeIn(), exit = fadeOut(), ) { @@ -134,7 +135,7 @@ internal fun TwinWalletArtworks( AnimatedVisibility( modifier = Modifier.align(Alignment.Center), - visible = state == TwinWalletArtworkState.TopUp, + visible = state == TwinWalletArtworkUM.TopUp, enter = fadeIn(), exit = fadeOut(), ) { @@ -151,7 +152,7 @@ internal fun TwinWalletArtworks( ) SpacerH8() Text( - text = balance.orEmpty(), + text = balance, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -162,7 +163,7 @@ internal fun TwinWalletArtworks( AnimatedVisibility( modifier = Modifier.align(Alignment.BottomCenter), - visible = state == TwinWalletArtworkState.TopUp, + visible = state == TwinWalletArtworkUM.TopUp, enter = fadeIn(), exit = fadeOut(), ) { @@ -234,12 +235,12 @@ private fun AnimatedTwinCards( } @Suppress("MagicNumber", "LongMethod", "UnnecessaryParentheses") -private fun TwinWalletArtworkState.toTransitionSetState( +private fun TwinWalletArtworkUM.toTransitionSetState( maxWidthDp: Float, maxHeightDp: Float, density: Float, ): List = when (this) { - TwinWalletArtworkState.Spread -> { + TwinWalletArtworkUM.Spread -> { val scale = 0.8f listOf( CardsTransitionState( @@ -262,7 +263,7 @@ private fun TwinWalletArtworkState.toTransitionSetState( ), ) } - is TwinWalletArtworkState.Leapfrog -> { + is TwinWalletArtworkUM.Leapfrog -> { val translationY = -(maxHeightDp / 2) * density val secondCardPlace = WalletCardTransitionState().copy( xTranslation = 0f, @@ -272,7 +273,7 @@ private fun TwinWalletArtworkState.toTransitionSetState( yScale = 0.83f, ) - if (step == TwinWalletArtworkState.Leapfrog.Step.FirstCard) { + if (step == TwinWalletArtworkUM.Leapfrog.Step.FirstCard) { listOf( CardsTransitionState( walletCard1 = WalletCardTransitionState( @@ -306,7 +307,7 @@ private fun TwinWalletArtworkState.toTransitionSetState( ) } } - TwinWalletArtworkState.TopUp -> { + TwinWalletArtworkUM.TopUp -> { val scale = 0.4f val yTranslation = -maxHeightDp * density - 24 * density listOf( @@ -339,7 +340,7 @@ private fun Preview() { .fillMaxSize(), contentAlignment = Alignment.Center, ) { - var state: TwinWalletArtworkState by remember { mutableStateOf(TwinWalletArtworkState.TopUp) } + var state: TwinWalletArtworkUM by remember { mutableStateOf(TwinWalletArtworkUM.TopUp) } TwinWalletArtworks( state = state, @@ -359,12 +360,12 @@ private fun Preview() { .align(Alignment.TopStart), onClick = { val list = listOf( - TwinWalletArtworkState.Spread, - TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.FirstCard), - TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.SecondCard), - TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.FirstCard), - TwinWalletArtworkState.Leapfrog(step = TwinWalletArtworkState.Leapfrog.Step.SecondCard), - TwinWalletArtworkState.TopUp, + TwinWalletArtworkUM.Spread, + TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.FirstCard), + TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard), + TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.FirstCard), + TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard), + TwinWalletArtworkUM.TopUp, ) state = list[index % list.size] diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt new file mode 100644 index 0000000000..88bd97bcbc --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt @@ -0,0 +1,65 @@ +package com.tangem.features.onboarding.v2.twin.impl.ui.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM + +@Immutable +internal sealed class OnboardingTwinUM { + + abstract val stepIndex: Int + abstract val isLoading: Boolean + abstract val artwork: TwinWalletArtworkUM + + data class Welcome( + override val isLoading: Boolean = false, + val onContinueClick: () -> Unit = {}, + ) : OnboardingTwinUM() { + override val stepIndex = 0 + override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Spread + } + + data class ResetWarning( + val acceptToggle: Boolean = false, + val onAcceptClick: (Boolean) -> Unit = {}, + val onContinueClick: () -> Unit = {}, + ) : OnboardingTwinUM() { + override val stepIndex: Int = 0 + override val isLoading: Boolean = false + override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Spread + } + + data class ScanCard( + override val isLoading: Boolean = false, + val artworkStep: TwinWalletArtworkUM.Leapfrog.Step, + val step: Step = Step.First, + val onScanClick: () -> Unit = {}, + ) : OnboardingTwinUM() { + + enum class Step { + First, Second, Third + } + + override val stepIndex: Int = 1 + override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Leapfrog(artworkStep) + } + + data class TopUp( + override val isLoading: Boolean = false, + val balance: String = "", + val bottomSheetConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, + val onBuyCryptoClick: () -> Unit = {}, + val onShowAddressClick: () -> Unit = {}, + val onRefreshClick: () -> Unit = {}, + ) : OnboardingTwinUM() { + override val stepIndex: Int = 2 + override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.TopUp + } + + fun copySealed(isLoading: Boolean = this.isLoading): OnboardingTwinUM = when (this) { + is Welcome -> copy(isLoading = isLoading) + is ResetWarning -> copy() + is ScanCard -> copy(isLoading = isLoading) + is TopUp -> copy(isLoading = isLoading) + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 52f23e9529..cc48296697 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -164,7 +164,7 @@ internal class WalletSettingsModel @Inject constructor( router.push( AppRoute.Onboarding( scanResponse = scanResponse, - mode = AppRoute.Onboarding.Mode.AddBackup, + mode = AppRoute.Onboarding.Mode.AddBackupWallet1, ), ) } else { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 967c19ebed..05a688f302 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -54,7 +54,7 @@ internal class DefaultWalletRouter @Inject constructor( AppRoute.Onboarding( scanResponse = scanResponse, mode = if (continueBackup) { - AppRoute.Onboarding.Mode.AddBackup + AppRoute.Onboarding.Mode.AddBackupWallet1 } else { AppRoute.Onboarding.Mode.Onboarding }, From 918ab4613f6f9e25ba14f8a1629ee601adc6cca0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 12 Mar 2025 20:44:27 +0400 Subject: [PATCH 050/138] Updated on 2026-08-14 --- domain/core/build.gradle.kts | 5 + .../domain/core/flow/FlowCachingSupplier.kt | 59 ++++++++ .../tangem/domain/core/flow/FlowFetcher.kt | 15 ++ .../tangem/domain/core/flow/FlowProducer.kt | 39 +++++ .../tangem/domain/core/flow/FlowSupplier.kt | 17 +++ .../core/flow/FlowCachingSupplierTest.kt | 139 ++++++++++++++++++ 6 files changed, 274 insertions(+) create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowCachingSupplier.kt create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowFetcher.kt create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowProducer.kt create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowSupplier.kt create mode 100644 domain/core/src/test/kotlin/com/tangem/domain/core/flow/FlowCachingSupplierTest.kt diff --git a/domain/core/build.gradle.kts b/domain/core/build.gradle.kts index 46d2670d68..9910d828a3 100644 --- a/domain/core/build.gradle.kts +++ b/domain/core/build.gradle.kts @@ -10,4 +10,9 @@ dependencies { api(deps.arrow.fx) implementation(deps.kotlin.serialization) + + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowCachingSupplier.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowCachingSupplier.kt new file mode 100644 index 0000000000..2a86b81acf --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowCachingSupplier.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.core.flow + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.update + +private typealias FlowsStore = MutableStateFlow>> + +/** + * [Flow] supplier with caching mechanism + * + * @param Producer producer of flow [Flow] + * @param Params type of data that required to get [Flow] + * @param Data data type of [Flow] + * @property flowsStore store of flows + */ +abstract class FlowCachingSupplier, Params : Any, Data : Any>( + private val flowsStore: FlowsStore = MutableStateFlow(value = emptyMap()), +) : FlowSupplier { + + /** Factory of [FlowProducer] */ + abstract val factory: FlowProducer.Factory + + /** Key creator */ + abstract val keyCreator: (Params) -> String + + /** + * Supply [Flow] by [params]. + + */ + override operator fun invoke(params: Params): Flow { + val key = keyCreator(params) + val saved = flowsStore.value[key] + + if (saved != null) return saved + + val flowProducer = factory.create(params = params) + + return runCatching(flowProducer::produceWithFallback) + .onSuccess { flow -> + flowsStore.update { + it.toMutableMap().apply { + put(key = key, value = flow) + } + } + } + .getOrThrow() + .catch { cause -> + flowsStore.update { + it.toMutableMap().apply { + remove(key) + } + } + + throw cause + } + } +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowFetcher.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowFetcher.kt new file mode 100644 index 0000000000..3fcba3d9d4 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowFetcher.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.core.flow + +import arrow.core.Either + +/** + * Flow fetcher + * + * @param Params data that required to fetch flow + * +[REDACTED_AUTHOR] + */ +interface FlowFetcher { + + suspend operator fun invoke(params: Params): Either +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowProducer.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowProducer.kt new file mode 100644 index 0000000000..92e95f552d --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowProducer.kt @@ -0,0 +1,39 @@ +package com.tangem.domain.core.flow + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.retryWhen + +/** + * [Flow] producer + * + * @param Data data type of [Flow] + * +[REDACTED_AUTHOR] + */ +interface FlowProducer { + + /** Fallback value if [Flow] throws exception */ + val fallback: Data + + /** Produce [Flow] */ + fun produce(): Flow + + /** Produce [Flow] with retry mechanism */ + fun produceWithFallback(): Flow { + return produce().retryWhen { _, _ -> + emit(value = fallback) + + delay(timeMillis = 2000) + + true + } + } + + /** Factory for creating [Producer]. It helps to provide [Params] by constructor */ + interface Factory> { + + /** Create [Producer] using [params] */ + fun create(params: Params): Producer + } +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowSupplier.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowSupplier.kt new file mode 100644 index 0000000000..8de04d12bd --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowSupplier.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.core.flow + +import kotlinx.coroutines.flow.Flow + +/** + * [Flow] supplier + * + * @param Params type of data that required to get [Flow] + * @param Data data type of [Flow] + * +[REDACTED_AUTHOR] + */ +interface FlowSupplier { + + /** Supply [Flow] by [params] */ + operator fun invoke(params: Params): Flow +} \ No newline at end of file diff --git a/domain/core/src/test/kotlin/com/tangem/domain/core/flow/FlowCachingSupplierTest.kt b/domain/core/src/test/kotlin/com/tangem/domain/core/flow/FlowCachingSupplierTest.kt new file mode 100644 index 0000000000..42822e7796 --- /dev/null +++ b/domain/core/src/test/kotlin/com/tangem/domain/core/flow/FlowCachingSupplierTest.kt @@ -0,0 +1,139 @@ +package com.tangem.domain.core.flow + +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class FlowCachingSupplierTest { + + private val factory = mockk() + private val errorFactory = mockk() + + @Test + fun `flow was created before`() = runTest { + val flowsStore = MutableStateFlow>>(value = emptyMap()) + val supplier = MockFlowCachingSupplier(factory = factory, flowsStore = flowsStore) + + val params = 1 + + every { factory.create(params = params) } returns MockFlowProducer(params = params) + + val actual = supplier(params = params) + + verify { factory.create(params = params) } + + Truth.assertThat(actual.first()).isEqualTo("test_1") + + Truth.assertThat(flowsStore.value.size).isEqualTo(1) + Truth.assertThat(flowsStore.value.containsKey("mock_1")).isTrue() + Truth.assertThat(flowsStore.value["mock_1"]!!.first()).isEqualTo("test_1") + } + + @Test + fun `flow wasn't created before`() = runTest { + val storedFlow = flowOf("test_1") + val storedMap = mapOf("mock_1" to storedFlow) + val flowsStore = MutableStateFlow(value = storedMap) + + val supplier = MockFlowCachingSupplier(factory = factory, flowsStore = flowsStore) + + val params = 1 + + val actual = supplier(params = params) + + verify(inverse = true) { factory.create(params = params) } + + Truth.assertThat(actual).isEqualTo(storedFlow) + Truth.assertThat(flowsStore.value).isEqualTo(storedMap) + } + + @Test + fun `flow wasn't created before and store isn't empty`() = runTest { + val flowsStore = MutableStateFlow( + value = mapOf("mock_1" to flowOf("test_1")), + ) + + val supplier = MockFlowCachingSupplier(factory = factory, flowsStore = flowsStore) + + val params = 2 + + every { factory.create(params = params) } returns MockFlowProducer(params = params) + + val actual = supplier(params = params) + + verify { factory.create(params = params) } + + Truth.assertThat(actual.first()).isEqualTo("test_2") + + Truth.assertThat(flowsStore.value.size).isEqualTo(2) + Truth.assertThat(flowsStore.value.keys).isEqualTo(setOf("mock_1", "mock_2")) + Truth.assertThat(flowsStore.value["mock_1"]!!.first()).isEqualTo("test_1") + Truth.assertThat(flowsStore.value["mock_2"]!!.first()).isEqualTo("test_2") + } + + @Test + fun `flow throws exception`() = runTest { + val flowsStore = MutableStateFlow>>(value = emptyMap()) + val supplier = MockErrorFlowCachingSupplier(factory = errorFactory, flowsStore = flowsStore) + + val params = 1 + + every { errorFactory.create(params = params) } returns MockErrorFlowProducer() + + val actual = supplier(params = params) + + verify { errorFactory.create(params = params) } + + Truth.assertThat(actual.first()).isEqualTo("fallback") + Truth.assertThat(flowsStore.value.size).isEqualTo(1) + } + + private class MockFlowCachingSupplier( + override val factory: FlowProducer.Factory, + flowsStore: MutableStateFlow>>, + ) : FlowCachingSupplier(flowsStore = flowsStore) { + + override val keyCreator: (Int) -> String = { "mock_$it" } + } + + private class MockFlowProducer(private val params: Int) : FlowProducer { + + override val fallback: String + get() = "fallback" + + override fun produce(): Flow = flowOf("test_$params") + + class Factory : FlowProducer.Factory { + override fun create(params: Int): MockFlowProducer = MockFlowProducer(params = params) + } + } + + private class MockErrorFlowCachingSupplier( + override val factory: FlowProducer.Factory, + flowsStore: MutableStateFlow>>, + ) : FlowCachingSupplier(flowsStore = flowsStore) { + + override val keyCreator: (Int) -> String = { "mock_$it" } + } + + private class MockErrorFlowProducer : FlowProducer { + + override val fallback: String + get() = "fallback" + + override fun produce(): Flow = flow { + throw IllegalStateException() + } + + class Factory : FlowProducer.Factory { + override fun create(params: Int): MockErrorFlowProducer = MockErrorFlowProducer() + } + } +} \ No newline at end of file From 819a80a4446cbc077a7f0f3ac6ab3b1372d84de5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Mar 2025 12:51:16 +0300 Subject: [PATCH 051/138] Updated on 2026-08-14 --- .../cardsettings/model/CardSettingsModel.kt | 23 +- .../details/ui/resetcard/ResetCardScreen.kt | 4 +- .../layout/layout_backup_access_code_info.xml | 2 +- .../tangem/core/ui/res/TangemAnimations.kt | 4 +- .../src/main/res/drawable/ic_accepted_20.xml | 0 ...access_code.xml => ic_access_code_119.xml} | 0 .../src/main/res/drawable/ic_unticked_20.xml | 0 .../accesscode/ui/MultiWalletAccessCodeBS.kt | 2 +- .../ui/MultiWalletAccessCodeIntro.kt | 2 +- .../seedphrase/ui/MultiWalletSeedPhrase.kt | 2 +- .../impl/DefaultOnboardingTwinComponent.kt | 30 +- .../v2/twin/impl/model/OnboardingTwinModel.kt | 69 ++-- .../v2/twin/impl/ui/OnboardingTwin.kt | 312 +++++++++++++++++- .../v2/twin/impl/ui/TwinWalletArtwork.kt | 2 +- .../v2/twin/impl/ui/state/OnboardingTwinUM.kt | 8 +- .../accesscode/ui/OnboardingVisaAccessCode.kt | 2 +- 16 files changed, 414 insertions(+), 48 deletions(-) rename app/src/main/res/drawable/ic_accepted.xml => core/ui/src/main/res/drawable/ic_accepted_20.xml (100%) rename core/ui/src/main/res/drawable/{ic_access_code.xml => ic_access_code_119.xml} (100%) rename app/src/main/res/drawable/ic_unticked.xml => core/ui/src/main/res/drawable/ic_unticked_20.xml (100%) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt index 773aa32d80..894ab1be67 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt @@ -20,6 +20,7 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings @@ -55,6 +56,7 @@ internal class CardSettingsModel @Inject constructor( private val getUserWalletUseCase: GetUserWalletUseCase, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsRepository: SettingsRepository, + private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles, ) : Model() { private val params = paramsContainer.require() @@ -198,13 +200,24 @@ internal class CardSettingsModel @Inject constructor( } if (scanResponse.cardTypesResolver.isTangemTwins()) { - // needs to prepare twin state if it's twin cards (depends on onboarding refactoring) - store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) - store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse))) + if (onboardingV2FeatureToggles.isTwinRefactoringEnabled) { + store.dispatchNavigationAction { + push( + AppRoute.Onboarding( + scanResponse = scanResponse, + mode = AppRoute.Onboarding.Mode.RecreateWalletTwin, + ), + ) + } + } else { + // needs to prepare twin state if it's twin cards (depends on onboarding refactoring) + store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) + store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse))) - cardSettingsInteractor.clear() + cardSettingsInteractor.clear() - store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) } + store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) } + } } else { val card = scanResponse.card diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index f68ae0c367..dd3093deb9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -140,9 +140,9 @@ private fun ConditionCheckBox(checkedState: Boolean, onCheckedChange: (Boolean) Icon( painter = painterResource( if (checked) { - R.drawable.ic_accepted + R.drawable.ic_accepted_20 } else { - R.drawable.ic_unticked + R.drawable.ic_unticked_20 }, ), contentDescription = null, diff --git a/app/src/main/res/layout/layout_backup_access_code_info.xml b/app/src/main/res/layout/layout_backup_access_code_info.xml index c53bb8fc3d..5095c1eca0 100644 --- a/app/src/main/res/layout/layout_backup_access_code_info.xml +++ b/app/src/main/res/layout/layout_backup_access_code_info.xml @@ -9,7 +9,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/access_code_info_protect_margin_top" - android:src="@drawable/ic_access_code" + android:src="@drawable/ic_access_code_119" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt index 28d531895d..b1ca7da16a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt @@ -22,9 +22,9 @@ object TangemAnimations { forwardWhen: (initial: S, target: S) -> Boolean, ): AnimatedContentTransitionScope.() -> ContentTransform = { val direction = if (forwardWhen(initialState, targetState)) { - AnimatedContentTransitionScope.SlideDirection.End - } else { AnimatedContentTransitionScope.SlideDirection.Start + } else { + AnimatedContentTransitionScope.SlideDirection.End } slideIntoContainer(towards = direction, animationSpec = tween()) diff --git a/app/src/main/res/drawable/ic_accepted.xml b/core/ui/src/main/res/drawable/ic_accepted_20.xml similarity index 100% rename from app/src/main/res/drawable/ic_accepted.xml rename to core/ui/src/main/res/drawable/ic_accepted_20.xml diff --git a/core/ui/src/main/res/drawable/ic_access_code.xml b/core/ui/src/main/res/drawable/ic_access_code_119.xml similarity index 100% rename from core/ui/src/main/res/drawable/ic_access_code.xml rename to core/ui/src/main/res/drawable/ic_access_code_119.xml diff --git a/app/src/main/res/drawable/ic_unticked.xml b/core/ui/src/main/res/drawable/ic_unticked_20.xml similarity index 100% rename from app/src/main/res/drawable/ic_unticked.xml rename to core/ui/src/main/res/drawable/ic_unticked_20.xml diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeBS.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeBS.kt index 8ac18706c6..2e7c8e89c6 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeBS.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeBS.kt @@ -36,7 +36,7 @@ private fun Content(state: MultiWalletAccessCodeUM, modifier: Modifier = Modifie modifier = Modifier.weight(1f), targetState = state.step, transitionSpec = TangemAnimations.AnimatedContent - .slide { initial, target -> initial.ordinal > target.ordinal }, + .slide { initial, target -> target.ordinal > initial.ordinal }, label = "AnimatedContent", ) { step -> when (step) { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeIntro.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeIntro.kt index 3f0fe0f290..2e6166a337 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeIntro.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeIntro.kt @@ -31,7 +31,7 @@ internal fun MultiWalletAccessCodeIntro(modifier: Modifier = Modifier) { color = TangemTheme.colors.text.primary1, ) Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_access_code), + imageVector = ImageVector.vectorResource(R.drawable.ic_access_code_119), tint = TangemTheme.colors.icon.informative, contentDescription = null, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhrase.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhrase.kt index 3ac5e6a920..985a7b68e4 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhrase.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhrase.kt @@ -12,7 +12,7 @@ internal fun MultiWalletSeedPhrase(state: MultiWalletSeedPhraseUM, modifier: Mod AnimatedContent( modifier = modifier.navigationBarsPadding(), targetState = state, - transitionSpec = TangemAnimations.AnimatedContent.slide { from, to -> to.order < from.order }, + transitionSpec = TangemAnimations.AnimatedContent.slide { from, to -> to.order > from.order }, contentKey = { st -> st::class.java }, label = "animatedContent", ) { st -> diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt index f8130f2d71..ded3238c83 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt @@ -3,20 +3,27 @@ package com.tangem.features.onboarding.v2.twin.impl import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.decompose.navigation.inner.InnerNavigation +import com.tangem.core.decompose.navigation.inner.InnerNavigationHolder +import com.tangem.core.decompose.navigation.inner.InnerNavigationState import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent import com.tangem.features.onboarding.v2.twin.impl.model.OnboardingTwinModel +import com.tangem.features.onboarding.v2.twin.impl.ui.OnboardingTwin import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.StateFlow internal class DefaultOnboardingTwinComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted params: OnboardingTwinComponent.Params, -) : OnboardingTwinComponent, AppComponentContext by appComponentContext { +) : OnboardingTwinComponent, AppComponentContext by appComponentContext, InnerNavigationHolder { private val model: OnboardingTwinModel = getOrCreateModel(params) @@ -24,11 +31,30 @@ internal class DefaultOnboardingTwinComponent @AssistedInject constructor( params.titleProvider.changeTitle(resourceReference(R.string.twins_recreate_toolbar)) } + override val innerNavigation: InnerNavigation = object : InnerNavigation { + override val state: StateFlow = model.innerNavigationState + + override fun pop(onComplete: (Boolean) -> Unit) { + model.onBack() + } + } + @Composable override fun Content(modifier: Modifier) { BackHandler { model.onBack() } - // TODO + val state by model.uiState.collectAsStateWithLifecycle() + + OnboardingTwin( + state = state, + modifier = modifier, + ) + } + + data class TwinInnerNavigationState( + override val stackSize: Int, + ) : InnerNavigationState { + override val stackMaxSize: Int? = 5 } @AssistedFactory diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index ae6846a3ec..1fb3811bcf 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -19,7 +19,6 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBotto import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList -import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.datasource.local.config.issuers.IssuersConfigStorage @@ -44,6 +43,7 @@ import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent.Params.Mode +import com.tangem.features.onboarding.v2.twin.impl.DefaultOnboardingTwinComponent import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM import com.tangem.features.onboarding.v2.twin.impl.ui.state.OnboardingTwinUM import com.tangem.sdk.api.TangemSdkManager @@ -58,6 +58,7 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber +import java.math.BigDecimal import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -87,7 +88,10 @@ internal class OnboardingTwinModel @Inject constructor( private val _uiState = MutableStateFlow( when (params.mode) { Mode.WelcomeOnly -> { - OnboardingTwinUM.Welcome(onContinueClick = ::saveWalletAndDone) + OnboardingTwinUM.Welcome( + pairCardNumber = firstCardTwinNumber.pairNumber().number, + onContinueClick = ::saveWalletAndDone, + ) } Mode.RecreateWallet -> { OnboardingTwinUM.ResetWarning( @@ -100,12 +104,16 @@ internal class OnboardingTwinModel @Inject constructor( ) } Mode.CreateWallet -> { - OnboardingTwinUM.Welcome(onContinueClick = ::navigateToFirstScan) + OnboardingTwinUM.Welcome( + pairCardNumber = firstCardTwinNumber.pairNumber().number, + onContinueClick = ::navigateToFirstScan, + ) } }, ) val uiState = _uiState.asStateFlow() + val innerNavigationState = MutableStateFlow(DefaultOnboardingTwinComponent.TwinInnerNavigationState(1)) init { if (_uiState.value is OnboardingTwinUM.Welcome) { @@ -144,15 +152,15 @@ internal class OnboardingTwinModel @Inject constructor( } private fun createFirstWallet() { - setLoading(true) - modelScope.launch { + setLoading(false) + val result = tangemSdkManager.createFirstTwinWallet( cardId = params.scanResponse.card.cardId, initialMessage = Message( tangemSdkManager.getString( R.string.twins_recreate_title_format, - wrappedList(firstCardTwinNumber.number), + firstCardTwinNumber.number, ), ), ) @@ -183,6 +191,10 @@ internal class OnboardingTwinModel @Inject constructor( }, ) } + + innerNavigationState.update { + it.copy(stackSize = 2) + } } } } @@ -206,7 +218,7 @@ internal class OnboardingTwinModel @Inject constructor( initialMessage = Message( tangemSdkManager.getString( R.string.twins_recreate_title_format, - wrappedList(secondCardNumber), + secondCardNumber, ), ), ) @@ -230,14 +242,16 @@ internal class OnboardingTwinModel @Inject constructor( }, ) } + + innerNavigationState.update { + it.copy(stackSize = 3) + } } } } } private fun createThirdWallet(secondCardPublicKey: ByteArray) { - setLoading(true) - modelScope.launch { val result = tangemSdkManager.finalizeTwin( secondCardPublicKey = secondCardPublicKey, @@ -246,7 +260,7 @@ internal class OnboardingTwinModel @Inject constructor( initialMessage = Message( tangemSdkManager.getString( R.string.twins_recreate_title_format, - wrappedList(firstCardTwinNumber.number), + firstCardTwinNumber.number, ), ), ) @@ -276,15 +290,21 @@ internal class OnboardingTwinModel @Inject constructor( return@launch } - fetchCurrencyStatusUseCase( + userWalletsListManager.save(userWallet, canOverride = true) + + fetchCurrencyStatusUseCase.invoke( userWalletId = userWallet.walletId, refresh = true, - ) + ).onLeft { + Timber.e("Unable to fetch currency status: $it") + setLoading(false) + } val cryptoCurrencyStatus = getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId).firstOrNull()?.getOrNull() ?: run { setLoading(false) + Timber.e("Unable to get currency status") return@launch } @@ -303,20 +323,28 @@ internal class OnboardingTwinModel @Inject constructor( onShowAddressClick = { onShowAddressClick(cryptoCurrencyStatus) }, isLoading = true, ) + + innerNavigationState.update { + it.copy(stackSize = 4) + } }.saveIn(cryptoCurrencyStatusJobHolder) } } } private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { - update { - it.copy( - balance = (status.value as? CryptoCurrencyStatus.Loaded)?.amount - ?.format { crypto(status.currency) } ?: "", - onBuyCryptoClick = { onBuyCryptoClick(status) }, - onShowAddressClick = { onShowAddressClick(status) }, - isLoading = false, - ) + val amount = (status.value as? CryptoCurrencyStatus.Loaded)?.amount ?: return + if (amount > BigDecimal.ZERO) { + params.modelCallbacks.onDone() + } else { + update { + it.copy( + balance = BigDecimal.ZERO.format { crypto(status.currency) }, + onBuyCryptoClick = { onBuyCryptoClick(status) }, + onShowAddressClick = { onShowAddressClick(status) }, + isLoading = false, + ) + } } } @@ -386,7 +414,6 @@ internal class OnboardingTwinModel @Inject constructor( } private fun showCardVerificationFailedDialog(error: TangemError) { - setLoading(false) if (error !is TangemSdkError.CardVerificationFailed) return analyticsEventHandler.send(OnboardingEvent.OfflineAttestationFailed(AnalyticsParam.ScreensSources.Onboarding)) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt index 5ea7351de4..c4535a2f95 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt @@ -1,20 +1,68 @@ package com.tangem.features.onboarding.v2.twin.impl.ui -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.IconToggleButton +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemAnimations +import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.twin.impl.ui.state.OnboardingTwinUM -@Suppress("UnusedPrivateMember") +@Suppress("UnusedPrivateMember", "MagicNumber") @Composable internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifier) { - Column(modifier = modifier) { - Box { - // TODO + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + ) { + TwinWalletArtworks( + modifier = Modifier + .padding(top = 64.dp) + .padding(horizontal = 24.dp) + .weight(.48f) + .fillMaxWidth(), + state = state.artwork, + balance = (state as? OnboardingTwinUM.TopUp)?.balance ?: "", + isRefreshing = state.isLoading, + onRefreshBalanceClick = { + (state as? OnboardingTwinUM.TopUp)?.onRefreshClick() + }, + ) + + AnimatedContent( + modifier = Modifier.weight(.52f), + targetState = state, + transitionSpec = TangemAnimations.AnimatedContent + .slide { initial, target -> target.stepIndex > initial.stepIndex }, + contentKey = { it::class }, + ) { st -> + when (st) { + is OnboardingTwinUM.ResetWarning -> ResetWarning(st) + is OnboardingTwinUM.ScanCard -> ScanCard(st) + is OnboardingTwinUM.TopUp -> TopUp(st) + is OnboardingTwinUM.Welcome -> Welcome(st) + } } } @@ -23,10 +71,256 @@ internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifi } } -@Preview +@Suppress("LongMethod") @Composable -private fun Preview() { +private fun ResetWarning(state: OnboardingTwinUM.ResetWarning, modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column( + modifier = Modifier + .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) + .weight(1f) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResourceSafe(R.string.common_warning), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h2, + ) + + SpacerH16() + + Text( + text = stringResourceSafe(R.string.twins_recreate_warning), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.body1, + ) + + SpacerH16() + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { state.onAcceptClick(!state.acceptToggle) }, + ), + ) { + IconToggleButton(checked = state.acceptToggle, onCheckedChange = state.onAcceptClick) { + AnimatedContent(targetState = state.acceptToggle, label = "Update checked state") { checked -> + Icon( + painter = painterResource( + if (checked) { + R.drawable.ic_accepted_20 + } else { + R.drawable.ic_unticked_20 + }, + ), + contentDescription = null, + tint = if (checked) { + TangemTheme.colors.control.checked + } else { + TangemTheme.colors.icon.secondary + }, + ) + } + } + + Text( + text = stringResourceSafe(R.string.common_understand), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.body1, + ) + } + } + + PrimaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.common_continue), + enabled = state.acceptToggle, + onClick = state.onContinueClick, + ) + } +} + +@Composable +private fun TopUp(state: OnboardingTwinUM.TopUp, modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column( + modifier = Modifier + .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) + .weight(1f) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResourceSafe(R.string.onboarding_topup_title), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h2, + ) + + SpacerH16() + + Text( + text = stringResourceSafe(R.string.onboarding_top_up_body), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.body1, + ) + } + + PrimaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto), + onClick = state.onBuyCryptoClick, + ) + + SecondaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address), + onClick = state.onShowAddressClick, + ) + } +} + +@Composable +private fun ScanCard(state: OnboardingTwinUM.ScanCard, modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column( + modifier = Modifier + .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) + .weight(1f) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResourceSafe(R.string.twins_recreate_title_format, state.cardNumber), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h2, + ) + + SpacerH16() + + Text( + text = stringResourceSafe(R.string.onboarding_twins_interrupt_warning), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.body1, + ) + } + + PrimaryButtonIconEnd( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.onboarding_button_backup_card_format, state.cardNumber), + showProgress = state.isLoading, + iconResId = R.drawable.ic_tangem_24, + onClick = state.onScanClick, + ) + } +} + +@Composable +private fun Welcome(state: OnboardingTwinUM.Welcome, modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column( + modifier = Modifier + .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) + .weight(1f) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResourceSafe(R.string.twins_onboarding_subtitle), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h2, + ) + + SpacerH16() + + Text( + text = stringResourceSafe(R.string.twins_onboarding_description_format, state.pairCardNumber), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.body1, + ) + } + + PrimaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.common_continue), + onClick = state.onContinueClick, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewTopUp() { + TangemThemePreview { + OnboardingTwin(OnboardingTwinUM.TopUp()) + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewWelcome() { TangemThemePreview { OnboardingTwin(OnboardingTwinUM.Welcome()) } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewScanCard() { + TangemThemePreview { + OnboardingTwin(OnboardingTwinUM.ScanCard()) + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewResetWarning() { + TangemThemePreview { + var toggle by remember { mutableStateOf(false) } + + OnboardingTwin( + OnboardingTwinUM.ResetWarning( + acceptToggle = toggle, + onAcceptClick = { toggle = it }, + ), + ) + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt index 2326dd841b..5fba3214f8 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt @@ -116,7 +116,7 @@ internal fun TwinWalletArtworks( ) { Box( modifier = Modifier - .padding(vertical = 24.dp, horizontal = 32.dp) + .padding(vertical = 24.dp, horizontal = 16.dp) .fillMaxSize() .background( TangemTheme.colors.button.secondary, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt index 88bd97bcbc..89ffc6efc6 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt @@ -13,6 +13,7 @@ internal sealed class OnboardingTwinUM { data class Welcome( override val isLoading: Boolean = false, + val pairCardNumber: Int = 2, val onContinueClick: () -> Unit = {}, ) : OnboardingTwinUM() { override val stepIndex = 0 @@ -31,7 +32,7 @@ internal sealed class OnboardingTwinUM { data class ScanCard( override val isLoading: Boolean = false, - val artworkStep: TwinWalletArtworkUM.Leapfrog.Step, + val artworkStep: TwinWalletArtworkUM.Leapfrog.Step = TwinWalletArtworkUM.Leapfrog.Step.FirstCard, val step: Step = Step.First, val onScanClick: () -> Unit = {}, ) : OnboardingTwinUM() { @@ -40,6 +41,11 @@ internal sealed class OnboardingTwinUM { First, Second, Third } + val cardNumber: Int = when (artworkStep) { + TwinWalletArtworkUM.Leapfrog.Step.FirstCard -> 1 + TwinWalletArtworkUM.Leapfrog.Step.SecondCard -> 2 + } + override val stepIndex: Int = 1 override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Leapfrog(artworkStep) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt index d80ef6209b..090ce03d84 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt @@ -44,7 +44,7 @@ internal fun OnboardingVisaAccessCode(state: OnboardingVisaAccessCodeUM, modifie .weight(1f), targetState = state.step, transitionSpec = TangemAnimations.AnimatedContent - .slide { initial, target -> initial.ordinal > target.ordinal }, + .slide { initial, target -> target.ordinal > initial.ordinal }, ) { step -> Content( state = state, From d40822e9942825f10b8dcf1ad8d97bea24595d28 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Mar 2025 07:24:44 -0700 Subject: [PATCH 052/138] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 3 -- .../app/TangemWcBlockchainHelper.kt | 11 +----- .../DefaultLegacyWalletConnectRepository.kt | 4 +- .../di/WalletConnectInteractorModule.kt | 5 +-- .../toggles/WalletConnectFeatureToggles.kt | 3 -- .../tangem/tap/features/main/MainViewModel.kt | 38 ++++++++----------- .../configs/feature_toggles_config.json | 20 ---------- .../tokens/GetCryptoCurrencyActionsUseCase.kt | 5 +-- .../features/onramp/OnrampFeatureToggles.kt | 2 - .../onramp/DefaultOnrampFeatureToggles.kt | 3 -- .../DefaultOnrampOperationComponent.kt | 4 +- .../features/swap/SwapFeatureToggles.kt | 4 +- .../feature/swap/DefaultSwapFeatureToggles.kt | 8 +--- .../feature/swap/di/SwapFeatureModule.kt | 5 +-- .../tangem/feature/swap/model/SwapModel.kt | 8 +--- .../featuretoggles/WalletFeatureToggles.kt | 5 +-- .../wallet/child/wallet/model/WalletModel.kt | 8 +--- .../model/intents/WalletClickIntents.kt | 4 +- .../WalletCurrencyActionsClickIntents.kt | 5 +-- .../DefaultWalletFeatureToggles.kt | 9 +---- .../implementors/MultiWalletContentLoader.kt | 14 +++---- .../MultiWalletContentLoaderFactory.kt | 3 -- .../SingleWalletWithTokenContentLoader.kt | 14 +++---- ...ngleWalletWithTokenContentLoaderFactory.kt | 3 -- .../transformers/AddWalletTransformer.kt | 3 -- .../InitializeWalletsTransformer.kt | 5 +-- .../ReinitializeWalletTransformer.kt | 3 -- .../transformers/UnlockWalletTransformer.kt | 3 -- .../state/utils/WalletLoadingStateFactory.kt | 4 +- .../WalletManagerFactoryCreator.kt | 4 +- .../di/BlockchainSDKFactoryModule.kt | 4 -- .../BlockchainSDKFeatureToggles.kt | 5 +-- .../DefaultBlockchainSDKFeatureToggles.kt | 10 +---- 33 files changed, 47 insertions(+), 182 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 6f4c7f7bcc..248192b95d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -10,7 +10,6 @@ import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.features.swap.SwapFeatureToggles import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -211,7 +210,6 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, stakingRepository: StakingRepository, promoRepository: PromoRepository, - swapFeatureToggles: SwapFeatureToggles, dispatchers: CoroutineDispatcherProvider, currencyStatusOperations: BaseCurrencyStatusOperations, ): GetCryptoCurrencyActionsUseCase { @@ -221,7 +219,6 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, stakingRepository = stakingRepository, promoRepository = promoRepository, - swapFeatureToggles = swapFeatureToggles, dispatchers = dispatchers, currencyStatusOperations = currencyStatusOperations, ) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt index 07700916ac..95046dfdb8 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt @@ -5,17 +5,10 @@ import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper import com.tangem.tap.domain.walletconnect2.domain.models.Account -import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles -internal class TangemWcBlockchainHelper( - featureToggles: WalletConnectFeatureToggles, -) : WcBlockchainHelper { +internal class TangemWcBlockchainHelper : WcBlockchainHelper { - private val supportedNonEvmBlockchains = if (featureToggles.isSolanaTxSignEnabled) { - setOf(Blockchain.Solana, Blockchain.SolanaTestnet) - } else { - emptySet() - } + private val supportedNonEvmBlockchains = setOf(Blockchain.Solana, Blockchain.SolanaTestnet) override fun chainIdToNetworkIdOrNull(chainId: String): String? { val parsedId = chainId.parseId() ?: return null diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 982bcd4438..4ebc531bed 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -16,7 +16,6 @@ import com.tangem.tap.domain.walletconnect2.domain.WcJrpcMethods import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer import com.tangem.tap.domain.walletconnect2.domain.WcRequest import com.tangem.tap.domain.walletconnect2.domain.models.* -import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow @@ -28,7 +27,6 @@ internal class DefaultLegacyWalletConnectRepository( private val application: Application, private val wcRequestDeserializer: WcJrpcRequestsDeserializer, private val analyticsHandler: AnalyticsEventHandler, - private val walletConnectFeatureToggles: WalletConnectFeatureToggles, ) : LegacyWalletConnectRepository { private var sessionProposal: Wallet.Model.SessionProposal? = null @@ -40,7 +38,7 @@ internal class DefaultLegacyWalletConnectRepository( private val _activeSessions: MutableSharedFlow> = MutableSharedFlow() override val activeSessions: Flow> = _activeSessions - private val blockchainHelper by lazy { TangemWcBlockchainHelper(walletConnectFeatureToggles) } + private val blockchainHelper by lazy { TangemWcBlockchainHelper() } override var currentSessions: List = emptyList() private set diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt index 80319c8ce3..3df4ed99be 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt @@ -33,7 +33,6 @@ internal object WalletConnectInteractorModule { fun provideWalletConnectInteractor( wcRepository: LegacyWalletConnectRepository, wcSessionsRepository: WalletConnectSessionsRepository, - walletConnectFeatureToggles: WalletConnectFeatureToggles, currenciesRepository: CurrenciesRepository, walletManagersFacade: WalletManagersFacade, userWalletsListManager: UserWalletsListManager, @@ -44,7 +43,7 @@ internal object WalletConnectInteractorModule { walletConnectRepository = wcRepository, sessionsRepository = wcSessionsRepository, sdkHelper = WalletConnectSdkHelper(), - blockchainHelper = TangemWcBlockchainHelper(walletConnectFeatureToggles), + blockchainHelper = TangemWcBlockchainHelper(), currenciesRepository = currenciesRepository, walletManagersFacade = walletManagersFacade, userWalletsListManager = userWalletsListManager, @@ -69,13 +68,11 @@ internal object WalletConnectModule { application: Application, wcRequestDeserializer: WcJrpcRequestsDeserializer, analyticsHandler: AnalyticsEventHandler, - walletConnectFeatureToggles: WalletConnectFeatureToggles, ): LegacyWalletConnectRepository { return DefaultLegacyWalletConnectRepository( application = application, wcRequestDeserializer = wcRequestDeserializer, analyticsHandler = analyticsHandler, - walletConnectFeatureToggles = walletConnectFeatureToggles, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt index e0f2e4a722..48b84481db 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt @@ -6,9 +6,6 @@ internal class WalletConnectFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) { - val isSolanaTxSignEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "WC_SOLANA_TX_SIGN_ENABLED") - val isRedesignedWalletConnectEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled("WALLET_CONNECT_REDESIGN_ENABLED") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 143a8d92a4..859d82a082 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -30,8 +30,6 @@ import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.swap.analytics.StoriesEvents -import com.tangem.features.onramp.OnrampFeatureToggles -import com.tangem.features.swap.SwapFeatureToggles import com.tangem.tap.common.extensions.setContext import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel @@ -60,8 +58,6 @@ internal class MainViewModel @Inject constructor( private val getStoryContentUseCase: GetStoryContentUseCase, private val imagePreloader: ImagePreloader, private val fetchHotCryptoUseCase: FetchHotCryptoUseCase, - private val swapFeatureToggles: SwapFeatureToggles, - onrampFeatureToggles: OnrampFeatureToggles, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel() { @@ -82,9 +78,7 @@ internal class MainViewModel @Inject constructor( } } - if (onrampFeatureToggles.isHotTokensEnabled) { - viewModelScope.launch { fetchHotCryptoUseCase() } - } + viewModelScope.launch { fetchHotCryptoUseCase() } updateAppCurrencies() observeFlips() @@ -301,23 +295,21 @@ internal class MainViewModel @Inject constructor( private fun preloadImages() { try { viewModelScope.launch { - if (swapFeatureToggles.isPromoStoriesEnabled) { - val swapStories = getStoryContentUseCase.invokeSync( - id = StoryContentIds.STORY_FIRST_TIME_SWAP.id, - refresh = true, - ).getOrNull() + val swapStories = getStoryContentUseCase.invokeSync( + id = StoryContentIds.STORY_FIRST_TIME_SWAP.id, + refresh = true, + ).getOrNull() - if (swapStories == null) { - analyticsEventHandler.send( - StoriesEvents.Error( - type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType, - ), - ) - } else { - val storiesImages = swapStories.getImageUrls() - // try to preload images for stories - storiesImages.forEach(imagePreloader::preload) - } + if (swapStories == null) { + analyticsEventHandler.send( + StoriesEvents.Error( + type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType, + ), + ) + } else { + val storiesImages = swapStories.getImageUrls() + // try to preload images for stories + storiesImages.forEach(imagePreloader::preload) } } } catch (ex: Exception) { 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 e051641afc..046b05e60e 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 @@ -3,22 +3,10 @@ "name": "NEW_CARD_SCANNING_ENABLED", "version": "undefined" }, - { - "name": "WC_SOLANA_TX_SIGN_ENABLED", - "version": "5.18.0" - }, - { - "name": "IS_ETHEREUM_EIP_1559_ENABLED", - "version": "5.17.0" - }, { "name": "ONRAMP_ENABLED", "version": "undefined" }, - { - "name": "MAIN_ACTION_BUTTONS_ENABLED", - "version": "5.19.0" - }, { "name": "ONBOARDING_CODE_REFACTORING_ENABLED", "version": "5.22.0" @@ -27,18 +15,10 @@ "name": "NAVIGATION_REFACTORING", "version": "undefined" }, - { - "name": "SWAP_STORIES_ENABLED", - "version": "5.21.0" - }, { "name": "VISA_ONBOARDING_ENABLED", "version": "undefined" }, - { - "name": "ONRAMP_HOT_TOKENS_ENABLED", - "version": "5.21.0" - }, { "name": "STAKING_TON_ENABLED", "version": "undefined" diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 981a56de13..7e2b95240b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -14,7 +14,6 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.swap.SwapFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.isNullOrZero import kotlinx.coroutines.flow.* @@ -33,7 +32,6 @@ class GetCryptoCurrencyActionsUseCase( private val stakingRepository: StakingRepository, private val promoRepository: PromoRepository, private val dispatchers: CoroutineDispatcherProvider, - private val swapFeatureToggles: SwapFeatureToggles, private val currencyStatusOperations: BaseCurrencyStatusOperations, ) { @@ -420,7 +418,6 @@ class GetCryptoCurrencyActionsUseCase( val exchangeableState = withTimeoutOrNull(REQUEST_EXCHANGE_DATA_TIMEOUT) { rampManager.availableForSwap(userWallet.walletId, cryptoCurrency) } ?: ExchangeableState.Loading - val swapStoriesEnabled = swapFeatureToggles.isPromoStoriesEnabled val currencyName = cryptoCurrency.name val reason = when (exchangeableState) { @@ -433,7 +430,7 @@ class GetCryptoCurrencyActionsUseCase( ExchangeableState.NotExchangeable -> ScenarioUnavailabilityReason.NotExchangeable(currencyName) } - val isShowBadge = reason == ScenarioUnavailabilityReason.None && shouldShowSwapStories && swapStoriesEnabled + val isShowBadge = reason == ScenarioUnavailabilityReason.None && shouldShowSwapStories TokenActionsState.ActionState.Swap( unavailabilityReason = reason, showBadge = isShowBadge, diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt index 9202fd9fc8..7d46cb6ad1 100644 --- a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt @@ -3,6 +3,4 @@ package com.tangem.features.onramp interface OnrampFeatureToggles { val isFeatureEnabled: Boolean - - val isHotTokensEnabled: Boolean } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt index 5a0c6f5a15..db7f335efe 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt @@ -8,7 +8,4 @@ internal class DefaultOnrampFeatureToggles( override val isFeatureEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "ONRAMP_ENABLED") - - override val isHotTokensEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "ONRAMP_HOT_TOKENS_ENABLED") } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt index 9888b7dddb..f4fe8672a8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.features.onramp.hottokens.HotCryptoComponent import com.tangem.features.onramp.selecttoken.model.OnrampOperationModel import com.tangem.features.onramp.selecttoken.ui.OnrampSelectToken @@ -20,7 +19,6 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, onrampTokenListComponentFactory: OnrampTokenListComponent.Factory, @Assisted private val params: OnrampOperationComponent.Params, - private val onrampFeatureToggles: OnrampFeatureToggles, private val hotCryptoComponentFactory: HotCryptoComponent.Factory, ) : AppComponentContext by appComponentContext, OnrampOperationComponent { @@ -54,7 +52,7 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor( } private fun createHotCryptoComponent(): HotCryptoComponent? { - return if (onrampFeatureToggles.isHotTokensEnabled && params is OnrampOperationComponent.Params.Buy) { + return if (params is OnrampOperationComponent.Params.Buy) { hotCryptoComponentFactory.create( context = child(key = "hot_crypto"), params = HotCryptoComponent.Params( diff --git a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt index f43c34f8ac..e0fe076fab 100644 --- a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt +++ b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt @@ -1,5 +1,3 @@ package com.tangem.features.swap -interface SwapFeatureToggles { - val isPromoStoriesEnabled: Boolean -} \ No newline at end of file +interface SwapFeatureToggles \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt index cd341751d8..c202111fb6 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt @@ -1,11 +1,5 @@ package com.tangem.feature.swap -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.features.swap.SwapFeatureToggles -internal class DefaultSwapFeatureToggles( - private val featureToggles: FeatureTogglesManager, -) : SwapFeatureToggles { - override val isPromoStoriesEnabled: Boolean - get() = featureToggles.isFeatureEnabled("SWAP_STORIES_ENABLED") -} \ No newline at end of file +internal class DefaultSwapFeatureToggles : SwapFeatureToggles \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/di/SwapFeatureModule.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/di/SwapFeatureModule.kt index 5b732e9051..990f8c8ec9 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/di/SwapFeatureModule.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/di/SwapFeatureModule.kt @@ -1,6 +1,5 @@ package com.tangem.feature.swap.di -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.feature.swap.DefaultSwapComponent import com.tangem.feature.swap.DefaultSwapFeatureToggles import com.tangem.features.swap.SwapComponent @@ -18,8 +17,8 @@ internal object SwapFeatureModule { @Provides @Singleton - fun provideSwapFeatureToggles(featureToggles: FeatureTogglesManager): SwapFeatureToggles { - return DefaultSwapFeatureToggles(featureToggles) + fun provideSwapFeatureToggles(): SwapFeatureToggles { + return DefaultSwapFeatureToggles() } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index c2eeff8a86..ea78698ede 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -56,7 +56,6 @@ import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder import com.tangem.feature.swap.utils.formatToUIRepresentation import com.tangem.features.swap.SwapComponent -import com.tangem.features.swap.SwapFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero @@ -96,7 +95,6 @@ internal class SwapModel @Inject constructor( private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, private val shouldShowStoriesUseCase: ShouldShowStoriesUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, - private val featureToggles: SwapFeatureToggles, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, swapInteractorFactory: SwapInteractor.Factory, private val urlOpener: UrlOpener, @@ -166,10 +164,8 @@ internal class SwapModel @Inject constructor( init { modelScope.launch { - if (featureToggles.isPromoStoriesEnabled) { - initStories() - swapRouter.openScreen(SwapNavScreen.PromoStories) - } + initStories() + swapRouter.openScreen(SwapNavScreen.PromoStories) } modelScope.launch(dispatchers.io) { val fromStatus = getCryptoCurrencyStatusUseCase(userWalletId, initialCurrencyFrom.id).getOrNull() diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt index 97d2d5800f..8621c8c0d1 100644 --- a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt @@ -5,7 +5,4 @@ package com.tangem.features.wallet.featuretoggles * [REDACTED_AUTHOR] */ -interface WalletFeatureToggles { - - val isMainActionButtonsEnabled: Boolean -} \ No newline at end of file +interface WalletFeatureToggles \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 538b05c99a..80175b7ddf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -40,7 +40,6 @@ import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.biometry.BiometryFeatureToggles import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -76,7 +75,6 @@ internal class WalletModel @Inject constructor( private val walletImageResolver: WalletImageResolver, private val tokenListStore: MultiWalletTokenListStore, private val onrampStatusFactory: OnrampStatusFactory, - private val walletFeatureToggles: WalletFeatureToggles, private val biometryFeatureToggles: BiometryFeatureToggles, private val analyticsEventsHandler: AnalyticsEventHandler, private val fetchNFTCollectionsUseCase: FetchNFTCollectionsUseCase, @@ -346,7 +344,6 @@ internal class WalletModel @Inject constructor( wallets = action.wallets, clickIntents = clickIntents, walletImageResolver = walletImageResolver, - walletFeatureToggles = walletFeatureToggles, ), ) @@ -387,7 +384,6 @@ internal class WalletModel @Inject constructor( newUserWallet = action.selectedWallet, clickIntents = clickIntents, walletImageResolver = walletImageResolver, - walletFeatureToggles = walletFeatureToggles, ), ) } @@ -404,7 +400,6 @@ internal class WalletModel @Inject constructor( userWallet = action.selectedWallet, clickIntents = clickIntents, walletImageResolver = walletImageResolver, - walletFeatureToggles = walletFeatureToggles, ), ) @@ -415,7 +410,7 @@ internal class WalletModel @Inject constructor( } } - private suspend fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) { + private fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) { walletScreenContentLoader.cancel(action.deletedWalletId) tokenListStore.remove(action.deletedWalletId) @@ -460,7 +455,6 @@ internal class WalletModel @Inject constructor( unlockedWallets = action.unlockedWallets, clickIntents = clickIntents, walletImageResolver = walletImageResolver, - walletFeatureToggles = walletFeatureToggles, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index a78fac8554..ee324b252e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -141,9 +141,7 @@ internal class WalletClickIntents @Inject constructor( async { rampStateManager.fetchSellServiceData() }.let(::add) - if (onrampFeatureToggles.isHotTokensEnabled) { - async { fetchHotCryptoUseCase() }.let(::add) - } + async { fetchHotCryptoUseCase() }.let(::add) if (isWalletNFTEnabledSyncUseCase.invoke(userWallet.walletId)) { async { fetchNFTCollectionsUseCase(userWalletId = userWallet.walletId) }.let(::add) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index d96b0968ad..6e1ec6a43d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -56,7 +56,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensLis import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.features.onramp.OnrampFeatureToggles -import com.tangem.features.swap.SwapFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -125,7 +124,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val appRouter: AppRouter, private val rampStateManager: RampStateManager, private val onrampFeatureToggles: OnrampFeatureToggles, - private val swapFeatureToggles: SwapFeatureToggles, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick( @@ -632,9 +630,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } private suspend fun getSwapRoute(targetRoute: AppRoute): AppRoute { - val isSwapStoriesEnabled = swapFeatureToggles.isPromoStoriesEnabled val maybeSwapStories = getStoryContentUseCase.invokeSync(StoryContentIds.STORY_FIRST_TIME_SWAP.id) - val showSwapStories = maybeSwapStories.getOrNull() != null && isSwapStoriesEnabled + val showSwapStories = maybeSwapStories.getOrNull() != null return if (showSwapStories) { AppRoute.Stories( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt index c3aba027cc..f283fadd39 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt @@ -1,13 +1,6 @@ package com.tangem.feature.wallet.featuretoggles -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import javax.inject.Inject -internal class DefaultWalletFeatureToggles @Inject constructor( - private val featureTogglesManager: FeatureTogglesManager, -) : WalletFeatureToggles { - - override val isMainActionButtonsEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "MAIN_ACTION_BUTTONS_ENABLED") -} \ No newline at end of file +internal class DefaultWalletFeatureToggles @Inject constructor() : WalletFeatureToggles \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index c69025ad0a..238eecd063 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -24,7 +24,6 @@ import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletToke import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber import com.tangem.features.nft.NFTFeatureToggles -import com.tangem.features.swap.SwapFeatureToggles @Suppress("LongParameterList") @ModelScoped @@ -44,7 +43,6 @@ internal class MultiWalletContentLoader( private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, - private val swapFeatureToggles: SwapFeatureToggles, private val deepLinksRegistry: DeepLinksRegistry, private val nftFeatureToggles: NFTFeatureToggles, private val walletsRepository: WalletsRepository, @@ -81,13 +79,11 @@ internal class MultiWalletContentLoader( walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, walletWarningsSingleEventSender = walletWarningsSingleEventSender, ).let(::add) - if (swapFeatureToggles.isPromoStoriesEnabled) { - MultiWalletActionButtonsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - getStoryContentUseCase = getStoryContentUseCase, - ).let(::add) - } + MultiWalletActionButtonsSubscriber( + userWallet = userWallet, + stateHolder = stateHolder, + getStoryContentUseCase = getStoryContentUseCase, + ).let(::add) WalletDropDownItemsSubscriber( stateHolder = stateHolder, shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index f5cb299df3..0f4e9d37b4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -19,7 +19,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.features.nft.NFTFeatureToggles -import com.tangem.features.swap.SwapFeatureToggles import javax.inject.Inject @Suppress("LongParameterList") @@ -37,7 +36,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, - private val swapFeatureToggles: SwapFeatureToggles, private val deepLinksRegistry: DeepLinksRegistry, private val nftFeatureToggles: NFTFeatureToggles, private val walletsRepository: WalletsRepository, @@ -60,7 +58,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, getStoryContentUseCase = getStoryContentUseCase, shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, - swapFeatureToggles = swapFeatureToggles, deepLinksRegistry = deepLinksRegistry, nftFeatureToggles = nftFeatureToggles, walletsRepository = walletsRepository, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index fd9277c21a..109e4deec2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -15,7 +15,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import com.tangem.features.swap.SwapFeatureToggles @Suppress("LongParameterList") internal class SingleWalletWithTokenContentLoader( @@ -32,7 +31,6 @@ internal class SingleWalletWithTokenContentLoader( private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, - private val swapFeatureToggles: SwapFeatureToggles, private val deepLinksRegistry: DeepLinksRegistry, ) : WalletContentLoader(id = userWallet.walletId) { @@ -57,13 +55,11 @@ internal class SingleWalletWithTokenContentLoader( walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, walletWarningsSingleEventSender = walletWarningsSingleEventSender, ).let(::add) - if (swapFeatureToggles.isPromoStoriesEnabled) { - MultiWalletActionButtonsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - getStoryContentUseCase = getStoryContentUseCase, - ).let(::add) - } + MultiWalletActionButtonsSubscriber( + userWallet = userWallet, + stateHolder = stateHolder, + getStoryContentUseCase = getStoryContentUseCase, + ).let(::add) WalletDropDownItemsSubscriber( stateHolder = stateHolder, shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt index 1a7e15d40d..829bb4b3d6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -15,7 +15,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.features.swap.SwapFeatureToggles import javax.inject.Inject // TODO: Refactor @@ -33,7 +32,6 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, - private val swapFeatureToggles: SwapFeatureToggles, private val deepLinksRegistry: DeepLinksRegistry, ) { @@ -52,7 +50,6 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, getStoryContentUseCase = getStoryContentUseCase, shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, - swapFeatureToggles = swapFeatureToggles, deepLinksRegistry = deepLinksRegistry, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt index 72d2e0f783..8a258581d3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt @@ -5,21 +5,18 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.toImmutableList internal class AddWalletTransformer( private val userWallet: UserWallet, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, - private val walletFeatureToggles: WalletFeatureToggles, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, - walletFeatureToggles = walletFeatureToggles, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index b22b7ea772..ee9f47ff70 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt @@ -8,7 +8,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import com.tangem.feature.wallet.presentation.wallet.state.utils.createStateByWalletType import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -18,14 +17,12 @@ internal class InitializeWalletsTransformer( private val wallets: List, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, - private val walletFeatureToggles: WalletFeatureToggles, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, - walletFeatureToggles = walletFeatureToggles, ) } @@ -98,7 +95,7 @@ internal class InitializeWalletsTransformer( private fun createMultiWalletEnabledButtons(userWallet: UserWallet): PersistentList { val isSingleWalletWithToken = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() - if (!walletFeatureToggles.isMainActionButtonsEnabled || isSingleWalletWithToken) return persistentListOf() + if (isSingleWalletWithToken) return persistentListOf() return persistentListOf( WalletManageButton.Buy(enabled = false, dimContent = false, onClick = {}), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt index 02d33a5f4c..b1b567d556 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt @@ -6,7 +6,6 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.toImmutableList /** @@ -23,14 +22,12 @@ internal class ReinitializeWalletTransformer( private val newUserWallet: UserWallet, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, - private val walletFeatureToggles: WalletFeatureToggles, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, - walletFeatureToggles = walletFeatureToggles, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt index 847bf806a1..d741af8e9a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt @@ -7,7 +7,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenSta import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.toImmutableList import timber.log.Timber @@ -15,14 +14,12 @@ internal class UnlockWalletTransformer( private val unlockedWallets: List, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, - private val walletFeatureToggles: WalletFeatureToggles, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, - walletFeatureToggles = walletFeatureToggles, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 562cbd95c1..439993d834 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -9,7 +9,6 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.* -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow @@ -22,7 +21,6 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class WalletLoadingStateFactory( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, - private val walletFeatureToggles: WalletFeatureToggles, ) { fun create(userWallet: UserWallet): WalletState { @@ -99,7 +97,7 @@ internal class WalletLoadingStateFactory( private fun createMultiWalletActions(userWallet: UserWallet): PersistentList { val isSingleWalletWithToken = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() - if (!walletFeatureToggles.isMainActionButtonsEnabled || isSingleWalletWithToken) return persistentListOf() + if (isSingleWalletWithToken) return persistentListOf() return persistentListOf( WalletManageButton.Buy( diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt index 1c9cc512e4..d6e075bb8a 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -6,7 +6,6 @@ import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.common.datastorage.BlockchainDataStorage import com.tangem.blockchain.common.logging.BlockchainSDKLogger -import com.tangem.blockchainsdk.featuretoggles.BlockchainSDKFeatureToggles import com.tangem.blockchainsdk.providers.BlockchainProviderTypes import timber.log.Timber import javax.inject.Inject @@ -24,7 +23,6 @@ internal class WalletManagerFactoryCreator @Inject constructor( private val accountCreator: AccountCreator, private val blockchainDataStorage: BlockchainDataStorage, private val blockchainSDKLogger: BlockchainSDKLogger, - private val blockchainSDKFeatureToggles: BlockchainSDKFeatureToggles, ) { fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory { @@ -35,7 +33,7 @@ internal class WalletManagerFactoryCreator @Inject constructor( blockchainProviderTypes = blockchainProviderTypes, accountCreator = accountCreator, featureToggles = BlockchainFeatureToggles( - isEthereumEIP1559Enabled = blockchainSDKFeatureToggles.isEthereumEIP1559Enabled, + isEthereumEIP1559Enabled = true, ), blockchainDataStorage = blockchainDataStorage, loggers = listOf(blockchainSDKLogger), diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt index 0e46908b4f..cce9a8c942 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt @@ -12,13 +12,11 @@ import com.tangem.blockchainsdk.DefaultBlockchainSDKFactory import com.tangem.blockchainsdk.WalletManagerFactoryCreator import com.tangem.blockchainsdk.accountcreator.DefaultAccountCreator import com.tangem.blockchainsdk.datastorage.DefaultBlockchainDataStorage -import com.tangem.blockchainsdk.featuretoggles.DefaultBlockchainSDKFeatureToggles import com.tangem.blockchainsdk.providers.BlockchainProviderTypesStore import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.DevBlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.ProdBlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.dev.BlockchainProvidersResponseSerializer -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage @@ -92,13 +90,11 @@ internal object BlockchainSDKFactoryModule { tangemTechApi: TangemTechApi, appPreferencesStore: AppPreferencesStore, blockchainSDKLogger: BlockchainSDKLogger, - featureTogglesManager: FeatureTogglesManager, ): WalletManagerFactoryCreator { return WalletManagerFactoryCreator( accountCreator = DefaultAccountCreator(tangemTechApi), blockchainDataStorage = DefaultBlockchainDataStorage(appPreferencesStore), blockchainSDKLogger = blockchainSDKLogger, - blockchainSDKFeatureToggles = DefaultBlockchainSDKFeatureToggles(featureTogglesManager), ) } } \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt index 482df0cd5a..de92518ae5 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt @@ -1,6 +1,3 @@ package com.tangem.blockchainsdk.featuretoggles -internal interface BlockchainSDKFeatureToggles { - - val isEthereumEIP1559Enabled: Boolean -} \ No newline at end of file +internal interface BlockchainSDKFeatureToggles \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt index 2242f2675f..5be43a55e2 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt @@ -1,11 +1,3 @@ package com.tangem.blockchainsdk.featuretoggles -import com.tangem.core.configtoggle.feature.FeatureTogglesManager - -internal class DefaultBlockchainSDKFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : BlockchainSDKFeatureToggles { - - override val isEthereumEIP1559Enabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "IS_ETHEREUM_EIP_1559_ENABLED") -} \ No newline at end of file +internal class DefaultBlockchainSDKFeatureToggles : BlockchainSDKFeatureToggles \ No newline at end of file From 9aeb134afe3ac24e1913590ae3cc85e6ecdb22c4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Mar 2025 06:34:53 +0000 Subject: [PATCH 053/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 58cb7cc642..475ddc9d26 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "release-app_5.22-987" +tangemBlockchainSdk = "develop-989" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.22-440" +tangemCardSdk = "develop-445" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 659de4ee7f2e5060c5cc63eb36a2dec0799f915b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Mar 2025 11:39:35 +0500 Subject: [PATCH 054/138] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 1 + .../nft/receive/entity/NFTNetworkUM.kt | 10 ++ .../receive/entity/NFTReceiveNetworksUM.kt | 9 ++ .../features/nft/receive/ui/NFTNetwork.kt | 73 ++++++++++ .../nft/receive/ui/NFTReceiveNetworks.kt | 132 ++++++++++++++++++ 5 files changed, 225 insertions(+) create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTNetworkUM.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveNetworksUM.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTNetwork.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworks.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index d9ab14d913..97946b962c 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -514,6 +514,7 @@ Tap here to receive first NFT NFT collections Unable to load the data + Choose network Set up a single access code to protect all your devices. Protect Set an individual access code for each card or ring later. diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTNetworkUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTNetworkUM.kt new file mode 100644 index 0000000000..0ef73aa689 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTNetworkUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.nft.receive.entity + +import androidx.annotation.DrawableRes + +internal data class NFTNetworkUM( + val id: String, + @DrawableRes val iconRes: Int, + val name: String, + val onItemClick: () -> Unit, +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveNetworksUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveNetworksUM.kt new file mode 100644 index 0000000000..91abfb75ef --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveNetworksUM.kt @@ -0,0 +1,9 @@ +package com.tangem.features.nft.receive.entity + +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import kotlinx.collections.immutable.ImmutableList + +internal data class NFTReceiveNetworksUM( + val searchBar: SearchBarUM, + val networks: ImmutableList, +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTNetwork.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTNetwork.kt new file mode 100644 index 0000000000..9b60320b49 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTNetwork.kt @@ -0,0 +1,73 @@ +package com.tangem.features.nft.receive.ui + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.impl.R +import com.tangem.features.nft.receive.entity.NFTNetworkUM + +@Composable +internal fun NFTNetwork(state: NFTNetworkUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable { state.onItemClick() } + .padding(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + modifier = Modifier + .size(TangemTheme.dimens.size24) + .clip(CircleShape), + painter = painterResource(state.iconRes), + contentDescription = null, + ) + Text( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), + text = state.name, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTNetwork(@PreviewParameter(NFTNetworkProvider::class) state: NFTNetworkUM) { + TangemThemePreview { + NFTNetwork( + state = state, + ) + } +} + +private class NFTNetworkProvider : CollectionPreviewParameterProvider( + collection = listOf( + NFTNetworkUM( + id = "item1", + name = "Nethers #0853", + iconRes = R.drawable.img_eth_22, + onItemClick = { }, + ), + ), +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworks.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworks.kt new file mode 100644 index 0000000000..e51fb169bb --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworks.kt @@ -0,0 +1,132 @@ +package com.tangem.features.nft.receive.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.impl.R +import com.tangem.features.nft.receive.entity.NFTNetworkUM +import com.tangem.features.nft.receive.entity.NFTReceiveNetworksUM +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun NFTReceiveNetworks(state: NFTReceiveNetworksUM, modifier: Modifier = Modifier) { + val listState = rememberLazyListState() + + Column( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.secondary) + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + SearchBar( + state = state.searchBar, + // TODO fix colors after merging [REDACTED_TASK_KEY] + ) + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .padding( + vertical = TangemTheme.dimens.spacing16, + ) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.primary), + state = listState, + ) { + item( + key = "title", + ) { + Title() + } + items( + items = state.networks, + key = NFTNetworkUM::id, + ) { network -> + NFTNetwork(state = network) + } + } + } +} + +@Composable +private fun Title(modifier: Modifier = Modifier) { + Text( + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing4, + ), + text = stringResourceSafe(R.string.nft_receive_choose_network), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) +} + +@Suppress("LongMethod") +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTReceiveNetworksUM() { + TangemThemePreview { + NFTReceiveNetworks( + state = NFTReceiveNetworksUM( + searchBar = SearchBarUM( + placeholderText = resourceReference(R.string.common_search), + query = "", + onQueryChange = { }, + isActive = false, + onActiveChange = { }, + ), + networks = persistentListOf( + NFTNetworkUM( + id = "item1", + name = "Ethereum", + iconRes = R.drawable.img_eth_22, + onItemClick = { }, + ), + NFTNetworkUM( + id = "item2", + name = "Polygon", + iconRes = R.drawable.img_polygon_22, + onItemClick = { }, + ), + NFTNetworkUM( + id = "item3", + name = "BSC", + iconRes = R.drawable.img_bsc_22, + onItemClick = { }, + ), + NFTNetworkUM( + id = "item4", + name = "Avalanche", + iconRes = R.drawable.img_avalanche_22, + onItemClick = { }, + ), + ), + ), + ) + } +} \ No newline at end of file From 375afeea12898996d462719aeb4fc1a7cae2ed64 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Mar 2025 12:03:09 +0300 Subject: [PATCH 055/138] Updated on 2026-08-14 --- core/error/.gitignore | 1 + core/error/build.gradle.kts | 14 +++++++++++ .../com/tangem/core/error/UniversalError.kt | 23 +++++++++++++++++++ .../kotlin/com/tangem/core/error/Utils.kt | 14 +++++++++++ core/ui/build.gradle.kts | 1 + .../tangem/core/ui/message/dialog/Dialogs.kt | 15 ++++++++++++ .../core/ui/utils/UiMessageSenderUtils.kt | 9 ++++++++ settings.gradle.kts | 1 + 8 files changed, 78 insertions(+) create mode 100644 core/error/.gitignore create mode 100644 core/error/build.gradle.kts create mode 100644 core/error/src/main/kotlin/com/tangem/core/error/UniversalError.kt create mode 100644 core/error/src/main/kotlin/com/tangem/core/error/Utils.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/UiMessageSenderUtils.kt diff --git a/core/error/.gitignore b/core/error/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/core/error/.gitignore @@ -0,0 +1 @@ +/build diff --git a/core/error/build.gradle.kts b/core/error/build.gradle.kts new file mode 100644 index 0000000000..59a90763a4 --- /dev/null +++ b/core/error/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.core.error" +} + +dependencies { + implementation(tangemDeps.card.core) + implementation(tangemDeps.blockchain) +} \ No newline at end of file diff --git a/core/error/src/main/kotlin/com/tangem/core/error/UniversalError.kt b/core/error/src/main/kotlin/com/tangem/core/error/UniversalError.kt new file mode 100644 index 0000000000..dd7a15e9c2 --- /dev/null +++ b/core/error/src/main/kotlin/com/tangem/core/error/UniversalError.kt @@ -0,0 +1,23 @@ +package com.tangem.core.error + +/** + * Universal error interface for all errors in the app. + * Each error code must follow this format: xxxyyyzzz where + * xxx - Feature code + * yyy - Subsystem code + * zzz - Specific error code + * If you need to add new feature add it to list below incrementing last code. + * + * Features: + * `100` - App error + * `101` - TangemSdkError + * `102` - BlockchainSdkError + * `103` - Express + * `104` - Visa + * `105` - Staking + * `106` - NFT + * `107` - WalletConnect + */ +interface UniversalError { + val errorCode: Int +} \ No newline at end of file diff --git a/core/error/src/main/kotlin/com/tangem/core/error/Utils.kt b/core/error/src/main/kotlin/com/tangem/core/error/Utils.kt new file mode 100644 index 0000000000..daf75b0558 --- /dev/null +++ b/core/error/src/main/kotlin/com/tangem/core/error/Utils.kt @@ -0,0 +1,14 @@ +package com.tangem.core.error + +import com.tangem.common.core.TangemSdkError +import com.tangem.blockchain.common.BlockchainSdkError + +val TangemSdkError.universalError: UniversalError + get() = object : UniversalError { + override val errorCode: Int = 101000000 + code + } + +val BlockchainSdkError.universalError: UniversalError + get() = object : UniversalError { + override val errorCode: Int = 102000000 + code + } \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 0e9ea7edd7..d6b5e0c90f 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(projects.core.res) implementation(projects.core.utils) implementation(projects.core.decompose) + implementation(projects.core.error) /** AndroidX libraries */ implementation(deps.androidx.fragment.ktx) diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt index 86aa88795f..b8cac08019 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt @@ -1,8 +1,10 @@ package com.tangem.core.ui.message.dialog +import com.tangem.core.error.UniversalError import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction @@ -36,4 +38,17 @@ object Dialogs { secondActionBuilder = { cancelAction(onClick = onCancelClick) }, ) } + + /** + * Universal error dialog + */ + fun universalErrorDialog(universalError: UniversalError, onOkClick: () -> Unit): DialogMessage { + return DialogMessage( + message = resourceReference(R.string.universal_error, wrappedList(universalError.errorCode)), + firstAction = EventMessageAction( + title = resourceReference(R.string.common_ok), + onClick = onOkClick, + ), + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/UiMessageSenderUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/UiMessageSenderUtils.kt new file mode 100644 index 0000000000..d237f52daa --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/UiMessageSenderUtils.kt @@ -0,0 +1,9 @@ +package com.tangem.core.ui.utils + +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.error.UniversalError +import com.tangem.core.ui.message.dialog.Dialogs + +fun UiMessageSender.showErrorDialog(universalError: UniversalError) { + send(Dialogs.universalErrorDialog(universalError) {}) +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 0cf1c30675..f45ec8e8bc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -148,6 +148,7 @@ include(":core:deep-links") include(":core:deep-links:global") include(":core:decompose") include(":core:pagination") +include(":core:error") // endregion Core modules // region Common modules From 74e6745ce4032e95501cb9f82150f3b307a64001 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Mar 2025 09:35:27 +0000 Subject: [PATCH 056/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 58cb7cc642..475ddc9d26 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "release-app_5.22-987" +tangemBlockchainSdk = "develop-989" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.22-440" +tangemCardSdk = "develop-445" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From e2bdc12ed8a33cf87f66bbe5d83523767414e6b5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Mar 2025 00:23:50 +0700 Subject: [PATCH 057/138] Updated on 2026-08-14 --- data/wallet-connect/build.gradle.kts | 10 ++ .../di/WalletConnectDataModule.kt | 142 +++++++++++++++ .../initialize/DefaultWcInitializeUseCase.kt | 2 +- .../tangem/data/walletconnect/model/CAIP10.kt | 40 +++++ .../tangem/data/walletconnect/model/CAIP2.kt | 36 ++++ .../data/walletconnect/model/NamespaceKey.kt | 4 + .../network/ethereum/WcEthNetwork.kt | 78 ++++++++ .../network/solana/WcSolanaNetwork.kt | 37 ++++ .../pair/AssociateNetworksDelegate.kt | 103 +++++++++++ .../pair/CaipNamespaceDelegate.kt | 62 +++++++ .../pair/DefaultWcPairUseCase.kt | 169 +++++++++--------- .../request/DefaultWcRequestService.kt | 5 +- .../walletconnect/request/WcMethodHandler.kt | 6 +- .../DefaultWcSessionsManager.kt | 2 +- .../utils/WcNamespaceConverter.kt | 12 ++ .../domain/walletconnect/model/WcNetwork.kt | 14 ++ .../domain/walletconnect/model/WcPairError.kt | 15 ++ .../walletconnect/model/WcSessionApprove.kt | 8 + .../walletconnect/model/WcSessionProposal.kt | 24 +-- .../model/sdkcopy/WcAppMetaData.kt | 15 ++ .../usecase/WcUseCasesFlowProvider.kt | 2 +- .../usecase/ethereum/WcEthChain.kt | 53 ------ .../walletconnect/usecase/pair/WcPairState.kt | 17 +- .../usecase/pair/WcPairUseCase.kt | 9 +- 24 files changed, 696 insertions(+), 169 deletions(-) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/model/CAIP10.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/model/CAIP2.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/model/NamespaceKey.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt rename domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestHandler.kt => data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/WcMethodHandler.kt (78%) rename data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/{ => sessions}/DefaultWcSessionsManager.kt (96%) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNamespaceConverter.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcNetwork.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcAppMetaData.kt delete mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthChain.kt diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts index fab7b8d0e6..159348516a 100644 --- a/data/wallet-connect/build.gradle.kts +++ b/data/wallet-connect/build.gradle.kts @@ -14,7 +14,14 @@ dependencies { /* Project - Domain */ implementation(projects.domain.walletConnect) implementation(projects.domain.walletConnect.models) + implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.models) + implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) + implementation(projects.data.common) /* Project - Data */ implementation(projects.core.datasource) @@ -26,6 +33,9 @@ dependencies { implementation(deps.hilt.core) kapt(deps.hilt.kapt) + /* Tangem libraries */ + implementation(tangemDeps.blockchain) + /* Reown - WalletConnect */ implementation(deps.reownCore) { exclude(group = "app.cash.sqldelight", module = "android-driver") diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index 02c6f391db..57dd79d196 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -1,13 +1,40 @@ package com.tangem.data.walletconnect.di +import android.app.Application +import com.squareup.moshi.Moshi +import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.walletconnect.DefaultWalletConnectRepository +import com.tangem.data.walletconnect.initialize.DefaultWcInitializeUseCase +import com.tangem.data.walletconnect.model.NamespaceKey +import com.tangem.data.walletconnect.network.ethereum.WcEthNetwork +import com.tangem.data.walletconnect.network.solana.WcSolanaNetwork +import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate +import com.tangem.data.walletconnect.pair.CaipNamespaceDelegate +import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase +import com.tangem.data.walletconnect.request.DefaultWcRequestService +import com.tangem.data.walletconnect.request.WcMethodHandler +import com.tangem.data.walletconnect.respond.DefaultWcRespondService +import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager +import com.tangem.data.walletconnect.utils.WcNamespaceConverter +import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletconnect.model.WcMethod import com.tangem.domain.walletconnect.repository.WalletConnectRepository +import com.tangem.domain.walletconnect.repository.WcSessionsManager +import com.tangem.domain.walletconnect.request.WcRequestService +import com.tangem.domain.walletconnect.respond.WcRespondService +import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase +import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Module @@ -22,4 +49,119 @@ internal object WalletConnectDataModule { ): WalletConnectRepository { return DefaultWalletConnectRepository(userWalletsStore, dispatchers) } + + @Provides + @Singleton + fun wcInitializeUseCase( + application: Application, + sessionsManager: DefaultWcSessionsManager, + networkService: DefaultWcRequestService, + wcPairFlow: DefaultWcPairUseCase, + ): WcInitializeUseCase = DefaultWcInitializeUseCase( + application = application, + sessionsManager = sessionsManager, + networkService = networkService, + wcPairFlow = wcPairFlow, + ) + + @Provides + @Singleton + fun defaultWcPairUseCase( + sessionsManager: WcSessionsManager, + associateNetworksDelegate: AssociateNetworksDelegate, + caipNamespaceDelegate: CaipNamespaceDelegate, + ): DefaultWcPairUseCase = DefaultWcPairUseCase( + sessionsManager = sessionsManager, + associateNetworksDelegate = associateNetworksDelegate, + caipNamespaceDelegate = caipNamespaceDelegate, + ) + + @Provides + @Singleton + fun wcPairUseCase(default: DefaultWcPairUseCase): WcPairUseCase = default + + @Provides + @Singleton + fun defaultWcSessionsManager(): DefaultWcSessionsManager = DefaultWcSessionsManager() + + @Provides + @Singleton + fun wcSessionsManager(default: DefaultWcSessionsManager): WcSessionsManager = default + + @Provides + @Singleton + fun defaultWcRequestService( + sessionsManager: WcSessionsManager, + respondService: WcRespondService, + dispatchers: CoroutineDispatcherProvider, + diHelperBox: DiHelperBox, + ): DefaultWcRequestService { + val scope = CoroutineScope(SupervisorJob() + dispatchers.io) + return DefaultWcRequestService( + sessionsManager = sessionsManager, + respondService = respondService, + requestAdapters = diHelperBox.handlers, + scope = scope, + ) + } + + @Provides + @Singleton + fun wcRequestService(default: DefaultWcRequestService): WcRequestService = default + + @Provides + @Singleton + fun wcRespondService(): WcRespondService = DefaultWcRespondService() + + @Provides + @Singleton + fun wcEthNetwork(@SdkMoshi moshi: Moshi, respondService: WcRespondService): WcEthNetwork = WcEthNetwork( + moshi = moshi, + respondService = respondService, + ) + + @Provides + @Singleton + fun wcSolanaNetwork(): WcSolanaNetwork = WcSolanaNetwork() + + @Provides + @Singleton + fun caipNamespaceDelegate( + diHelperBox: DiHelperBox, + walletManagersFacade: WalletManagersFacade, + ): CaipNamespaceDelegate = CaipNamespaceDelegate( + namespaceConverters = diHelperBox.converters, + walletManagersFacade = walletManagersFacade, + ) + + @Provides + @Singleton + fun associateNetworksDelegate( + diHelperBox: DiHelperBox, + getWallets: GetWalletsUseCase, + currenciesRepository: CurrenciesRepository, + excludedBlockchains: ExcludedBlockchains, + ): AssociateNetworksDelegate = AssociateNetworksDelegate( + namespaceConverters = diHelperBox.converters, + getWallets = getWallets, + currenciesRepository = currenciesRepository, + excludedBlockchains = excludedBlockchains, + ) + + @Provides + @Singleton + fun diHelperBox(ethNetwork: WcEthNetwork, solanaNetwork: WcSolanaNetwork) = DiHelperBox( + handlers = setOf( + ethNetwork, + ), + converters = buildMap { + ethNetwork.namespaceKey to ethNetwork + solanaNetwork.namespaceKey to solanaNetwork + }, + ) + + internal class DiHelperBox( + val converters: Map, + val handlers: Set>, + ) } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt index c7e39cd519..5889dbf628 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt @@ -6,9 +6,9 @@ import com.reown.android.CoreClient import com.reown.android.relay.ConnectionType import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit -import com.tangem.data.walletconnect.DefaultWcSessionsManager import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase import com.tangem.data.walletconnect.request.DefaultWcRequestService +import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase import timber.log.Timber diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/model/CAIP10.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/model/CAIP10.kt new file mode 100644 index 0000000000..c338a4d343 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/model/CAIP10.kt @@ -0,0 +1,40 @@ +package com.tangem.data.walletconnect.model + +import com.tangem.data.walletconnect.model.CAIP2.Companion.CAIP_SEPARATOR + +/** + * CAIP-10 defines a way to identify an account in any blockchain specified by [CAIP2] blockchain id. + * + * Syntax + * account_id: chain_id + ":" + account_address + * chain_id: [-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32} @see[CAIP2] + * account_address: [-.%a-zA-Z0-9]{1,128} + * + * Examples + * # Ethereum mainnet + * eip155:1:0xab16a96D359eC26a11e2C2b3d8f8B8942d5Bfcdb + * + * # Bitcoin mainnet + * bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6 + * + * For more information https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md + */ +internal data class CAIP10 constructor( + val chainId: CAIP2, + val accountAddress: String, +) { + val raw get() = "${chainId.raw}$CAIP_SEPARATOR$accountAddress" + + companion object { + private const val CAIP10_SUBSTRINGS_SIZE = 3 + fun fromRaw(s: String): CAIP10? { + val parsed = s.split(CAIP_SEPARATOR) + if (parsed.size != CAIP10_SUBSTRINGS_SIZE) return null + val namespace = parsed[0] + val reference = parsed[1] + val accountAddress = parsed[2] + val chainId = CAIP2.fromRaw(namespace + CAIP_SEPARATOR + reference) ?: return null + return CAIP10(chainId, accountAddress) + } + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/model/CAIP2.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/model/CAIP2.kt new file mode 100644 index 0000000000..fdd24f1ddd --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/model/CAIP2.kt @@ -0,0 +1,36 @@ +package com.tangem.data.walletconnect.model + +/** + * CAIP-2 defines a way to identify a blockchain (e.g. Ethereum Mainnet, Görli, Bitcoin, Cosmos Hub) + * in a human-readable, developer-friendly and transaction-friendly way. + * + * Syntax + * chain_id: namespace + ":" + reference + * namespace: [-a-z0-9]{3,8} + * reference: [-_a-zA-Z0-9]{1,32} + * + * Examples + * # Ethereum mainnet + * eip155:1 + * + * # Bitcoin mainnet + * bip122:000000000019d6689c085ae165831e93 + * + * For more information https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md + */ +internal data class CAIP2 constructor( + val namespace: String, + val reference: String, +) { + val raw get() = "$namespace$CAIP_SEPARATOR$reference" + + companion object { + const val CAIP_SEPARATOR = ":" + + fun fromRaw(s: String): CAIP2? { + val parsed = s.split(CAIP_SEPARATOR) + if (parsed.size != 2) return null + return CAIP2(parsed[0], parsed[1]) + } + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/model/NamespaceKey.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/model/NamespaceKey.kt new file mode 100644 index 0000000000..d212088f0c --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/model/NamespaceKey.kt @@ -0,0 +1,4 @@ +package com.tangem.data.walletconnect.model + +@JvmInline +internal value class NamespaceKey(val key: String) \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt new file mode 100644 index 0000000000..0ce30801eb --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -0,0 +1,78 @@ +package com.tangem.data.walletconnect.network.ethereum + +import com.squareup.moshi.Moshi +import com.tangem.blockchain.common.Blockchain +import com.tangem.data.walletconnect.model.CAIP2 +import com.tangem.data.walletconnect.model.NamespaceKey +import com.tangem.data.walletconnect.request.WcMethodHandler +import com.tangem.data.walletconnect.utils.WcNamespaceConverter +import com.tangem.domain.walletconnect.model.WcMethod +import com.tangem.domain.walletconnect.model.WcRequest +import com.tangem.domain.walletconnect.respond.WcRespondService +import com.tangem.domain.walletconnect.usecase.WcUseCase +import com.tangem.domain.walletconnect.usecase.WcUseCasesFlowProvider +import com.tangem.domain.walletconnect.usecase.ethereum.EthPersonalSignUseCase +import com.tangem.domain.walletconnect.usecase.ethereum.WcEthMethod +import com.tangem.domain.walletconnect.usecase.ethereum.WcEthMethod.SignMessage +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow + +@Suppress("UnusedPrivateClass") // todo(wc) remove later +internal class WcEthNetwork( + private val moshi: Moshi, + private val respondService: WcRespondService, +) : WcMethodHandler, WcUseCasesFlowProvider, WcNamespaceConverter { + + private val _useCases: Channel = Channel(Channel.BUFFERED) + override val useCases = _useCases.receiveAsFlow() + + override val namespaceKey: NamespaceKey = NamespaceKey("eip155") + + override fun canHandle(methodName: String): Boolean { + return Name.entries.find { it.raw == methodName } != null + } + + override fun deserialize(methodName: String, params: String): WcEthMethod? { + val name = Name.entries.find { it.raw == methodName } ?: return null + return when (name) { + Name.Sign -> TODO() + Name.PersonalSign -> WcMethodHandler.fromJson(params, moshi) + Name.SignTypeData -> TODO() + Name.SignTypeDataV4 -> TODO() + Name.SignTransaction -> TODO() + Name.SendTransaction -> TODO() + } + } + + override fun handle(wcRequest: WcRequest) { + wcRequest as WcRequest + val useCase = when (wcRequest.method) { + is SignMessage -> EthPersonalSignUseCase(wcRequest as WcRequest, respondService) + } + _useCases.trySend(useCase) + } + + enum class Name(val raw: String) { + Sign("eth_sign"), + PersonalSign("personal_sign"), + SignTypeData("eth_signTypedData"), + SignTypeDataV4("eth_signTypedData_v4"), + SignTransaction("eth_signTransaction"), + SendTransaction("eth_sendTransaction"), + } + + override fun toBlockchain(chainId: CAIP2): Blockchain? { + if (chainId.namespace != namespaceKey.key) return null + val ethChainId = chainId.reference.toIntOrNull() ?: return null + return Blockchain.fromChainId(ethChainId) + } + + override fun toCAIP2(blockchain: Blockchain): CAIP2? { + if (!blockchain.isEvm()) return null + val chainId = blockchain.getChainId() ?: return null + return CAIP2( + namespace = namespaceKey.key, + reference = chainId.toString(), + ) + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt new file mode 100644 index 0000000000..bda63dde43 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt @@ -0,0 +1,37 @@ +package com.tangem.data.walletconnect.network.solana + +import com.tangem.blockchain.common.Blockchain +import com.tangem.data.walletconnect.model.CAIP2 +import com.tangem.data.walletconnect.model.NamespaceKey +import com.tangem.data.walletconnect.utils.WcNamespaceConverter + +internal class WcSolanaNetwork : WcNamespaceConverter { + override val namespaceKey: NamespaceKey = NamespaceKey("solana") + + override fun toBlockchain(chainId: CAIP2): Blockchain? { + if (chainId.namespace != namespaceKey.key) return null + return when (chainId.reference) { + MAINNET_CHAIN_ID -> Blockchain.Solana + TESTNET_CHAIN_ID -> Blockchain.SolanaTestnet + else -> null + } + } + + override fun toCAIP2(blockchain: Blockchain): CAIP2? { + val chainId = when (blockchain) { + Blockchain.Solana -> MAINNET_CHAIN_ID + Blockchain.SolanaTestnet -> TESTNET_CHAIN_ID + else -> null + } + chainId ?: return null + return CAIP2( + namespace = namespaceKey.key, + reference = chainId, + ) + } + + companion object { + private const val MAINNET_CHAIN_ID = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + private const val TESTNET_CHAIN_ID = "4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z" + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt new file mode 100644 index 0000000000..e24b2641ac --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt @@ -0,0 +1,103 @@ +package com.tangem.data.walletconnect.pair + +import com.reown.walletkit.client.Wallet +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.getNetwork +import com.tangem.data.walletconnect.model.CAIP2 +import com.tangem.data.walletconnect.model.NamespaceKey +import com.tangem.data.walletconnect.utils.WcNamespaceConverter +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletconnect.model.WcNetwork +import com.tangem.domain.walletconnect.model.WcPairError +import com.tangem.domain.walletconnect.model.WcSessionProposal.ProposalNetwork +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetWalletsUseCase + +internal class AssociateNetworksDelegate constructor( + private val namespaceConverters: Map, + private val getWallets: GetWalletsUseCase, + private val currenciesRepository: CurrenciesRepository, + private val excludedBlockchains: ExcludedBlockchains, +) { + + @Throws(WcPairError.UnsupportedNetworks::class) + suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map { + val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency } + val requiredNamespaces: Set = sessionProposal.requiredNamespaces.setOfChainId() + val optionalNamespaces: Set = sessionProposal.optionalNamespaces.setOfChainId() + + return userWallets.associate { wallet -> + wallet.walletId to mapNetworksForWallet(wallet, requiredNamespaces, optionalNamespaces) + } + } + + private suspend fun mapNetworksForWallet( + wallet: UserWallet, + requiredNamespaces: Set, + optionalNamespaces: Set, + ): ProposalNetwork { + val walletNetworks = currenciesRepository.getMultiCurrencyWalletCurrenciesSync(wallet.walletId) + .filterIsInstance() + .map { it.network } + + val unknownRequired = mutableSetOf() + val missingRequired = mutableSetOf() + val required = mutableSetOf() + val available = mutableSetOf() + val notAdded = mutableSetOf() + + fun CAIP2.toBlockchain() = namespaceConverters[NamespaceKey(this.namespace)]?.toBlockchain(this) + fun Blockchain.toNetwork() = getNetwork( + blockchain = this, + extraDerivationPath = null, + scanResponse = wallet.scanResponse, + excludedBlockchains = excludedBlockchains, + ) + + requiredNamespaces.forEach { chainId -> + val blockchain = chainId.toBlockchain() + if (blockchain == null) { + unknownRequired.add(WcNetwork.Unknown(missingNetworkName(chainId))) + return@forEach + } + val wcNetwork = blockchain.toNetwork() + if (wcNetwork == null) { + unknownRequired.add(WcNetwork.Unknown(missingNetworkName(blockchain))) + return@forEach + } + val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id } + if (walletNetwork == null) { + missingRequired.add(WcNetwork.Supported(wcNetwork)) + } else { + required.add(WcNetwork.Supported(walletNetwork)) + } + } + optionalNamespaces.forEach { chainId -> + val wcNetwork = chainId.toBlockchain()?.toNetwork() ?: return@forEach + val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id } + if (walletNetwork == null) { + available.add(WcNetwork.Supported(wcNetwork)) + } else { + notAdded.add(WcNetwork.Supported(walletNetwork)) + } + } + if (unknownRequired.isNotEmpty()) throw WcPairError.UnsupportedNetworks(unknownRequired) + return ProposalNetwork( + walletId = wallet.walletId, + missingRequired = missingRequired, + required = required, + available = available, + notAdded = notAdded, + ) + } + + private fun Map.setOfChainId(): Set = + this.values.flatMap { proposal -> proposal.chains ?: listOf() } + .mapNotNull { rawChainId -> CAIP2.fromRaw(rawChainId) }.toSet() + + private fun missingNetworkName(chainId: CAIP2): String = chainId.namespace.replaceFirstChar(Char::titlecase) + private fun missingNetworkName(blockchain: Blockchain): String = blockchain.getCoinName() +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt new file mode 100644 index 0000000000..98e5e6be32 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt @@ -0,0 +1,62 @@ +package com.tangem.data.walletconnect.pair + +import com.reown.walletkit.client.Wallet +import com.tangem.blockchain.common.Blockchain +import com.tangem.data.walletconnect.model.CAIP10 +import com.tangem.data.walletconnect.model.NamespaceKey +import com.tangem.data.walletconnect.utils.WcNamespaceConverter +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId + +internal class CaipNamespaceDelegate constructor( + private val namespaceConverters: Map, + private val walletManagersFacade: WalletManagersFacade, +) { + + suspend fun associate( + sessionProposal: Wallet.Model.SessionProposal, + userWalletId: UserWalletId, + networks: List, + ): Map { + val converters = namespaceConverters.values + + val result = mutableMapOf() + + networks.map { network -> + val blockchain = Blockchain.fromId(network.id.value) + val address = walletManagersFacade.getDefaultAddress(userWalletId, network) + val chainId = converters.firstOrNull { it.toCAIP2(blockchain) != null }?.toCAIP2(blockchain) + requireNotNull(chainId) + requireNotNull(address) + CAIP10(chainId = chainId, accountAddress = address) + }.forEach { account -> + val namespaceKey = account.chainId.namespace + val session = result.getOrPut(namespaceKey) { Session() } + val requiredNamespaces = sessionProposal.requiredNamespaces + val optionalNamespaces = sessionProposal.optionalNamespaces + val methods = buildSet { + requiredNamespaces[namespaceKey]?.methods?.let { addAll(it) } + optionalNamespaces[namespaceKey]?.methods?.let { addAll(it) } + } + session.chains.add(account.chainId.raw) + session.accounts.add(account.raw) + session.methods.addAll(methods) + } + return result.mapValues { (namespaceKey, session) -> + Wallet.Model.Namespace.Session( + chains = session.chains.toList(), + accounts = session.accounts.toList(), + methods = session.methods.toList(), + events = session.events.toList(), + ) + } + } + + private data class Session( + val chains: MutableSet = mutableSetOf(), + val accounts: MutableSet = mutableSetOf(), + val methods: MutableSet = mutableSetOf(), + val events: MutableSet = mutableSetOf(), + ) +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index 89099dc766..f574bcd29e 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -7,15 +7,21 @@ import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.data.walletconnect.utils.toOurModel +import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.model.WcSessionProposal -import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionProposal +import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.usecase.pair.WcPairState import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.suspendCancellableCoroutine import timber.log.Timber import kotlin.coroutines.resume @@ -25,10 +31,10 @@ val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox") @Suppress("UnusedPrivateMember") // todo(wc) remove after add mapping internal class DefaultWcPairUseCase( private val sessionsManager: WcSessionsManager, + private val associateNetworksDelegate: AssociateNetworksDelegate, + private val caipNamespaceDelegate: CaipNamespaceDelegate, ) : WcPairUseCase, WcSdkObserver { - private val onWalletSelect = Channel() - private val onAccountSelect = Channel() private val onCallTerminalAction = Channel() private val onSessionProposal = Channel>(Channel.BUFFERED) @@ -40,91 +46,66 @@ internal class DefaultWcPairUseCase( // call sdk.pair and wait result, finish flow on error walletKitPair(uri).onLeft { throwable -> - emit(WcPairState.Error(throwable)) + emit(WcPairState.Error(WcPairError.Unknown(throwable.localizedMessage.orEmpty()))) return@flow } // wait for sdk onSessionProposal callback - val (sdkSessionProposal, verifyContext) = onSessionProposal.receiveAsFlow().map { (fist, second) -> - val ourCopy = WcSdkSessionProposal( - name = fist.name, - description = fist.description, - url = fist.url, - proposerPublicKey = fist.proposerPublicKey, - ) - ourCopy to second - }.first { (sessionProposal, verifyContext) -> - true // todo(wc) check verifyContext? compare uri? - } + val (sdkSessionProposal, verifyContext) = onSessionProposal.receiveAsFlow() + .first { (sessionProposal, verifyContext) -> + true // todo(wc) check verifyContext? compare uri? + } // check unsupported dApps, just local constant for now, finish if unsupported if (sdkSessionProposal.name in unsupportedDApps) { Timber.i("Unsupported DApp") - val error = WcPairState - .Error(RuntimeException("todo(wc) use domain exception")) // todo(wc) WalletConnectError.UnsupportedDApp + val error = WcPairState.Error(WcPairError.UnsupportedDApp) emit(error) return@flow } - // flow that collect terminal and all middle actions - val actionsFlow = channelFlow { - val userWallet = onWalletSelect.receiveAsFlow() - .stateIn(scope = this, started = SharingStarted.Lazily, initialValue = selectedWallet) - val accountSelect = onAccountSelect.receiveAsFlow() - .stateIn(scope = this, started = SharingStarted.Lazily, initialValue = Any()) - // combine all middle variables and emit new ProposalState to main FlowCollector - combine(accountSelect, userWallet) { account, selectedWallet -> - buildProposalState(sdkSessionProposal, verifyContext, account, selectedWallet) - }.onEach { newProposalState -> this@flow.emit(newProposalState) } - .launchIn(this) - - // collect terminal action and emit to this inner channelFlow, unlike combine above - onCallTerminalAction.receiveAsFlow() - .onEach { this.channel.send(it) } - .launchIn(this) - } + val proposalState = buildProposalState(sdkSessionProposal, verifyContext) + .fold(ifLeft = { WcPairState.Error(it) }, ifRight = { it }) + emit(proposalState) // wait first terminal action and continue WC pair flow - val sessionForApprove: WcSessionProposal = when (val terminalAction = actionsFlow.first()) { + val terminalAction = onCallTerminalAction.receiveAsFlow().first() + val sessionForApprove: WcSessionApprove? = when (terminalAction) { is TerminalAction.Approve -> terminalAction.sessionForApprove TerminalAction.Reject -> { // non suspending WalletKit.rejectSession call - rejectSession(sdkSessionProposal) - return@flow + rejectSession(sdkSessionProposal.proposerPublicKey) + null } } + // finish flow if rejected above + sessionForApprove ?: return@flow // start flow of approving in wc sdk emit(WcPairState.Approving.Loading(sessionForApprove)) - fun mapResult(either: Either) = - WcPairState.Approving.Result(sessionForApprove, either) // call sdk approve and wait result - walletKitApproveSession(sessionForApprove).onLeft { emit(mapResult(it.left())) }.onRight { - // wait for sdk callback - val result = when (val settledSession = onSessionSettleResponse.receiveAsFlow().first()) { - is Wallet.Model.SettledSessionResponse.Error -> mapResult( - // WalletConnectError.ExternalApprovalError(settledSession.errorMessage) todo(wc) - RuntimeException("todo(wc) use domain exception").left(), - ) + val either = walletKitApproveSession( + sessionForApprove = sessionForApprove, + sdkSessionProposal = sdkSessionProposal, + ).fold( + ifLeft = { WcPairError.ExternalApprovalError(it.localizedMessage.orEmpty()).left() }, + ifRight = { + when (val settledSession = onSessionSettleResponse.receiveAsFlow().first()) { + is Wallet.Model.SettledSessionResponse.Error -> WcPairError.ExternalApprovalError( + settledSession.errorMessage, + ).left() - is Wallet.Model.SettledSessionResponse.Result -> { - val newSession = settledSession.session.toDomain(sessionForApprove.wallet) - sessionsManager.saveSessions(sessionForApprove.wallet.walletId, newSession) - mapResult(newSession.right()) + is Wallet.Model.SettledSessionResponse.Result -> { + val newSession = settledSession.session.toDomain(sessionForApprove.walletId) + sessionsManager.saveSessions(sessionForApprove.walletId, newSession) + newSession.right() + } } - } - emit(result) - } + }, + ) + emit(WcPairState.Approving.Result(sessionForApprove, either)) } - override fun onWalletSelect(selectedWallet: UserWallet) { - onWalletSelect.trySend(selectedWallet) - } - - override fun onAccountSelect(account: Any) { - onAccountSelect.trySend(account) - } - - override fun approve(sessionForApprove: WcSessionProposal) { + override fun approve(sessionForApprove: WcSessionApprove) { onCallTerminalAction.trySend(TerminalAction.Approve(sessionForApprove)) } @@ -162,10 +143,22 @@ internal class DefaultWcPairUseCase( ) } - private suspend fun walletKitApproveSession(sessionForApprove: WcSessionProposal): Either = - suspendCancellableCoroutine { continuation -> + private suspend fun walletKitApproveSession( + sessionForApprove: WcSessionApprove, + sdkSessionProposal: Wallet.Model.SessionProposal, + ): Either { + val namespaces = caipNamespaceDelegate.associate( + sdkSessionProposal, + sessionForApprove.walletId, + sessionForApprove.network.map { it.network }, + ) + val sessionApprove = Wallet.Params.SessionApprove( + proposerPublicKey = sdkSessionProposal.proposerPublicKey, + namespaces = namespaces, + ) + return suspendCancellableCoroutine { continuation -> WalletKit.approveSession( - params = TODO("wc sdk model"), + params = sessionApprove, onSuccess = { Timber.i("Approved successfully: $it") continuation.resume(Unit.right()) @@ -176,11 +169,12 @@ internal class DefaultWcPairUseCase( }, ) } + } - private fun rejectSession(sessionProposal: WcSdkSessionProposal) { + private fun rejectSession(proposerPublicKey: String) { WalletKit.rejectSession( params = Wallet.Params.SessionReject( - proposerPublicKey = sessionProposal.proposerPublicKey, + proposerPublicKey = proposerPublicKey, reason = "", ), onSuccess = { @@ -192,25 +186,38 @@ internal class DefaultWcPairUseCase( ) } - private fun buildProposalState( - sessionProposal: WcSdkSessionProposal, + private suspend fun buildProposalState( + sessionProposal: Wallet.Model.SessionProposal, verifyContext: Wallet.Model.VerifyContext, - selectedAccount: Any, - userWallet: UserWallet, - ): WcPairState.Proposal { - // todo(wc) a lot of mapping from Wallet.Model.SessionProposal to our Blockchain, Network, ect - // todo(wc) check security status by Wallet.Model.VerifyContext or Blockaid, don't know for now - val mock: WcSessionProposal = TODO() - return WcPairState.Proposal(mock, Any()) - } + ): Either = runCatching { + val proposalNetwork = associateNetworksDelegate.associate(sessionProposal) + val appMetaData = WcAppMetaData( + name = sessionProposal.name, + description = sessionProposal.description, + url = sessionProposal.url, + icons = sessionProposal.icons.map { it.toString() }, + redirect = sessionProposal.redirect, + ) + val dAppSession = WcSessionProposal( + dAppMetaData = appMetaData, + proposalNetwork = proposalNetwork, + securityStatus = Any(), + ) + WcPairState.Proposal(dAppSession) + }.fold(onSuccess = { it.right() }, onFailure = { + when (it) { + is WcPairError -> it.left() + else -> WcPairError.Unknown(it.localizedMessage.orEmpty()).left() + } + },) - private fun Wallet.Model.Session.toDomain(userWallet: UserWallet): WcSession = WcSession( - userWalletId = userWallet.walletId, + private fun Wallet.Model.Session.toDomain(walletId: UserWalletId): WcSession = WcSession( + userWalletId = walletId, sdkModel = this.toOurModel(), ) private sealed interface TerminalAction { - data class Approve(val sessionForApprove: WcSessionProposal) : TerminalAction + data class Approve(val sessionForApprove: WcSessionApprove) : TerminalAction data object Reject : TerminalAction } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt index c36cfbdb75..f69f2f4563 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt @@ -6,7 +6,6 @@ import com.tangem.data.walletconnect.utils.toOurModel import com.tangem.domain.walletconnect.model.WcMethod import com.tangem.domain.walletconnect.model.WcRequest import com.tangem.domain.walletconnect.repository.WcSessionsManager -import com.tangem.domain.walletconnect.request.WcRequestHandler import com.tangem.domain.walletconnect.request.WcRequestService import com.tangem.domain.walletconnect.respond.WcRespondService import kotlinx.coroutines.CoroutineScope @@ -16,7 +15,7 @@ import kotlinx.coroutines.launch internal class DefaultWcRequestService( private val sessionsManager: WcSessionsManager, private val respondService: WcRespondService, - private val requestAdapters: List>, + private val requestAdapters: Set>, private val scope: CoroutineScope, // todo(wc) is ok inject scope? featureScope like Decompose Model scope ) : WcRequestService, WcSdkObserver { @@ -32,7 +31,7 @@ internal class DefaultWcRequestService( val params = sr.request.params scope.launch { val session = sessionsManager.findSessionByTopic(sr.topic) - val handler: WcRequestHandler? = requestAdapters.firstOrNull { it.canHandle(method) } + val handler: WcMethodHandler? = requestAdapters.firstOrNull { it.canHandle(method) } val deserialized: WcMethod? = handler?.deserialize(method, params) if (handler == null || deserialized == null || session == null) { diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestHandler.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/WcMethodHandler.kt similarity index 78% rename from domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestHandler.kt rename to data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/WcMethodHandler.kt index 410815ae03..b31b707d58 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestHandler.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/WcMethodHandler.kt @@ -1,14 +1,14 @@ -package com.tangem.domain.walletconnect.request +package com.tangem.data.walletconnect.request import com.squareup.moshi.Moshi import com.squareup.moshi.adapter import com.tangem.domain.walletconnect.model.WcMethod import com.tangem.domain.walletconnect.model.WcRequest -interface WcRequestHandler { +interface WcMethodHandler { fun canHandle(methodName: String): Boolean fun deserialize(methodName: String, params: String): M? - fun handle(wcRequest: WcRequest) + fun handle(wcRequest: WcRequest) companion object { @OptIn(ExperimentalStdlibApi::class) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt similarity index 96% rename from data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWcSessionsManager.kt rename to data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index 388c5c30a1..cfd0a023d0 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -1,4 +1,4 @@ -package com.tangem.data.walletconnect +package com.tangem.data.walletconnect.sessions import com.reown.walletkit.client.Wallet import com.tangem.data.walletconnect.utils.WcSdkObserver diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNamespaceConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNamespaceConverter.kt new file mode 100644 index 0000000000..bb1e86298b --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNamespaceConverter.kt @@ -0,0 +1,12 @@ +package com.tangem.data.walletconnect.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.data.walletconnect.model.CAIP2 +import com.tangem.data.walletconnect.model.NamespaceKey + +internal interface WcNamespaceConverter { + + val namespaceKey: NamespaceKey + fun toBlockchain(chainId: CAIP2): Blockchain? + fun toCAIP2(blockchain: Blockchain): CAIP2? +} \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcNetwork.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcNetwork.kt new file mode 100644 index 0000000000..1ef5352c31 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcNetwork.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.walletconnect.model + +import com.tangem.domain.tokens.model.Network + +sealed interface WcNetwork { + + val name: String + + data class Supported(val network: Network) : WcNetwork { + override val name: String get() = network.name + } + + data class Unknown(override val name: String) : WcNetwork +} \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt new file mode 100644 index 0000000000..8f90d64512 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.walletconnect.model + +sealed class WcPairError(override val message: String) : Exception(message) { + + data object UnsupportedDApp : WcPairError("UnsupportedDApp") + data class UnsupportedNetworks( + val chains: Set, + ) : WcPairError("ApprovalErrorMissingNetworks") + + data class ExternalApprovalError( + override val message: String, + ) : WcPairError("ExternalApprovalError") + + data class Unknown(override val message: String) : WcPairError(message) +} \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt new file mode 100644 index 0000000000..06f4291649 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.walletconnect.model + +import com.tangem.domain.wallets.models.UserWalletId + +data class WcSessionApprove( + val walletId: UserWalletId, + val network: List, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt index 45c189cf20..648163817b 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt @@ -1,15 +1,19 @@ package com.tangem.domain.walletconnect.model -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionProposal -import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData +import com.tangem.domain.wallets.models.UserWalletId data class WcSessionProposal( - val dAppInfo: WcSdkSessionProposal, + val dAppMetaData: WcAppMetaData, + val proposalNetwork: Map, + val securityStatus: Any, +) { - // todo(wc) should map for all user wallets? - val wallet: UserWallet, - val missingChains: List, - val availableChains: List, - val notAddedChains: List, -) \ No newline at end of file + data class ProposalNetwork( + val walletId: UserWalletId, + val missingRequired: Set, + val required: Set, + val available: Set, + val notAdded: Set, + ) +} \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcAppMetaData.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcAppMetaData.kt new file mode 100644 index 0000000000..f8de996d34 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcAppMetaData.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.walletconnect.model.sdkcopy + +/** + * copy of [com.reown.android.Core.Model.AppMetaData] + */ +data class WcAppMetaData( + val name: String, + val description: String, + val url: String, + val icons: List, + val redirect: String?, + val appLink: String? = null, + val linkMode: Boolean = false, + val verifyUrl: String? = null, +) \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCasesFlowProvider.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCasesFlowProvider.kt index 700cf1865c..d82ecfdb41 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCasesFlowProvider.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCasesFlowProvider.kt @@ -3,5 +3,5 @@ package com.tangem.domain.walletconnect.usecase import kotlinx.coroutines.flow.Flow interface WcUseCasesFlowProvider { - val flow: Flow + val useCases: Flow } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthChain.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthChain.kt deleted file mode 100644 index ab18902b6e..0000000000 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthChain.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.tangem.domain.walletconnect.usecase.ethereum - -import com.squareup.moshi.Moshi -import com.tangem.domain.walletconnect.model.WcRequest -import com.tangem.domain.walletconnect.request.WcRequestHandler -import com.tangem.domain.walletconnect.respond.WcRespondService -import com.tangem.domain.walletconnect.usecase.WcUseCase -import com.tangem.domain.walletconnect.usecase.WcUseCasesFlowProvider -import com.tangem.domain.walletconnect.usecase.ethereum.WcEthMethod.SignMessage -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.receiveAsFlow - -@Suppress("UnusedPrivateClass") // todo(wc) remove later -private class WcEthChain( - val moshi: Moshi, - private val respondService: WcRespondService, -) : WcRequestHandler, WcUseCasesFlowProvider { - - private val _flow: Channel = Channel(Channel.BUFFERED) - override val flow = _flow.receiveAsFlow() - - override fun canHandle(methodName: String): Boolean { - return Name.entries.find { it.raw == methodName } != null - } - - override fun deserialize(methodName: String, params: String): WcEthMethod? { - val name = Name.entries.find { it.raw == methodName } ?: return null - return when (name) { - Name.Sign -> TODO() - Name.PersonalSign -> WcRequestHandler.fromJson(params, moshi) - Name.SignTypeData -> TODO() - Name.SignTypeDataV4 -> TODO() - Name.SignTransaction -> TODO() - Name.SendTransaction -> TODO() - } - } - - override fun handle(wcRequest: WcRequest) { - val useCase = when (wcRequest.method) { - is SignMessage -> EthPersonalSignUseCase(wcRequest as WcRequest, respondService) - } - _flow.trySend(useCase) - } - - enum class Name(val raw: String) { - Sign("eth_sign"), - PersonalSign("personal_sign"), - SignTypeData("eth_signTypedData"), - SignTypeDataV4("eth_signTypedData_v4"), - SignTransaction("eth_signTransaction"), - SendTransaction("eth_sendTransaction"), - } -} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt index 1472db789e..07edd2b5f8 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt @@ -1,24 +1,23 @@ package com.tangem.domain.walletconnect.usecase.pair import arrow.core.Either +import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.model.WcSessionProposal sealed interface WcPairState { data object Loading : WcPairState - data class Error(val throwable: Throwable) : WcPairState - data class Proposal( - val dAppSession: WcSessionProposal, - val securityStatus: Any, - ) : WcPairState + data class Error(val error: WcPairError) : WcPairState + data class Proposal(val dAppSession: WcSessionProposal) : WcPairState sealed interface Approving : WcPairState { - val session: WcSessionProposal + val session: WcSessionApprove - data class Loading(override val session: WcSessionProposal) : Approving + data class Loading(override val session: WcSessionApprove) : Approving data class Result( - override val session: WcSessionProposal, - val result: Either, + override val session: WcSessionApprove, + val result: Either, ) : Approving } } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt index 4bf3e9b047..2600c0d889 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt @@ -1,6 +1,6 @@ package com.tangem.domain.walletconnect.usecase.pair -import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.Flow @@ -8,12 +8,7 @@ interface WcPairUseCase { fun pairFlow(uri: String, source: Source, selectedWallet: UserWallet): Flow - // todo(wc) should map for all user wallets without this action in domain layer? - fun onWalletSelect(selectedWallet: UserWallet) - // todo(wc) accounts in future? - fun onAccountSelect(account: Any) - - fun approve(sessionForApprove: WcSessionProposal) + fun approve(sessionForApprove: WcSessionApprove) fun reject() enum class Source { QR, DEEPLINK, ETC } From e9fac0e50eb8816cc24e00dffa94d7baf2ca18c8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Mar 2025 21:21:17 +0400 Subject: [PATCH 058/138] Updated on 2026-08-14 --- app/build.gradle.kts | 3 +- .../card/DefaultDerivationsRepositoryTest.kt | 18 ++++++----- .../card/MissedDerivationsFinderTest.kt | 28 +++++++++-------- .../BiometricUserWalletsListManagerTest.kt | 4 +-- common/test/.gitignore | 1 + common/test/build.gradle.kts | 25 ++++++++++++++++ .../domain/card/MockScanResponseFactory.kt | 4 +-- .../domain/token/MockCryptoCurrencyFactory.kt | 30 ++++++++++++------- .../tangem/common/test/utils/CoroutineExt.kt | 20 +++++++++++++ settings.gradle.kts | 7 +++-- 10 files changed, 101 insertions(+), 39 deletions(-) create mode 100644 common/test/.gitignore create mode 100644 common/test/build.gradle.kts rename app/src/test/kotlin/com/tangem/tap/domain/card/ScanResponseMockFactory.kt => common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt (98%) rename app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt => common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt (78%) create mode 100644 common/test/src/main/java/com/tangem/common/test/utils/CoroutineExt.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3312184dff..76ad6c75ba 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,5 @@ -import java.util.Properties import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants +import java.util.Properties plugins { alias(deps.plugins.android.application) @@ -291,6 +291,7 @@ dependencies { kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) /** Testing libraries */ + testImplementation(projects.common.test) testImplementation(deps.test.coroutine) testImplementation(deps.test.junit) testImplementation(deps.test.mockk) diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt index 095156e1c2..c66581f06d 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt @@ -4,6 +4,8 @@ import android.annotation.SuppressLint import com.google.common.truth.Truth import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.common.CompletionResult +import com.tangem.common.test.domain.card.MockScanResponseFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.ScanCardException import com.tangem.domain.common.configs.GenericCardConfig @@ -40,7 +42,7 @@ internal class DefaultDerivationsRepositoryTest { artworkUrl = "", cardsInWallet = setOf(), isMultiCurrency = false, - scanResponse = ScanResponseMockFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()), + scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()), hasBackupError = false, ) @@ -77,7 +79,7 @@ internal class DefaultDerivationsRepositoryTest { @Test fun `success if currencies is empty`() = runTest { val userWallet = defaultUserWallet.copy( - scanResponse = ScanResponseMockFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), + scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), ) coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns userWallet @@ -94,7 +96,7 @@ internal class DefaultDerivationsRepositoryTest { @Test fun `success if card already has derivations`() = runTest { val userWallet = defaultUserWallet.copy( - scanResponse = ScanResponseMockFactory.create( + scanResponse = MockScanResponseFactory.create( cardConfig = MultiWalletCardConfig, derivedKeys = DerivedKeysMocks.ethereumDerivedKeys, ), @@ -105,7 +107,7 @@ internal class DefaultDerivationsRepositoryTest { runCatching { repository.derivePublicKeys( userWalletId = defaultUserWalletId, - currencies = CryptoCurrenciesMocks(userWallet.scanResponse).ethereum, + currencies = MockCryptoCurrencyFactory(userWallet.scanResponse).ethereum.let(::listOf), ) } .onSuccess { Truth.assertThat(it) } @@ -119,7 +121,7 @@ internal class DefaultDerivationsRepositoryTest { @Test fun `error if tangemSdkManager throws exception`() = runTest { val userWallet = defaultUserWallet.copy( - scanResponse = ScanResponseMockFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), + scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), ) coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns userWallet coEvery { tangemSdkManager.derivePublicKeys(null, any(), any()) } throws ScanCardException.UserCancelled @@ -127,7 +129,7 @@ internal class DefaultDerivationsRepositoryTest { runCatching { repository.derivePublicKeys( userWalletId = defaultUserWalletId, - currencies = CryptoCurrenciesMocks(userWallet.scanResponse).ethereum, + currencies = MockCryptoCurrencyFactory(userWallet.scanResponse).ethereum.let(::listOf), ) } .onSuccess { error("Should throws exception") } @@ -142,7 +144,7 @@ internal class DefaultDerivationsRepositoryTest { @Test fun `success case`() = runTest { val userWallet = defaultUserWallet.copy( - scanResponse = ScanResponseMockFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), + scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), ) coEvery { userWalletsStore.getSyncOrNull(defaultUserWalletId) } returns userWallet coEvery { tangemSdkManager.derivePublicKeys(null, any(), any()) } returns CompletionResult.Success( @@ -153,7 +155,7 @@ internal class DefaultDerivationsRepositoryTest { runCatching { repository.derivePublicKeys( userWalletId = defaultUserWalletId, - currencies = CryptoCurrenciesMocks(userWallet.scanResponse).ethereum, + currencies = MockCryptoCurrencyFactory(userWallet.scanResponse).ethereum.let(::listOf), ) } .onSuccess { Truth.assertThat(it) } diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/MissedDerivationsFinderTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/MissedDerivationsFinderTest.kt index 7091e26f4f..063a5bc85b 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/MissedDerivationsFinderTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/card/MissedDerivationsFinderTest.kt @@ -6,6 +6,8 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationConfigV2 import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.test.domain.card.MockScanResponseFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.common.configs.GenericCardConfig import com.tangem.domain.common.configs.MultiWalletCardConfig @@ -20,7 +22,7 @@ internal class MissedDerivationsFinderTest { @Test fun `empty derivations for empty currencies`() { - val scanResponse = ScanResponseMockFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()) + val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()) val finder = MissedDerivationsFinder(scanResponse) val actual = finder.find(emptyList()) @@ -31,10 +33,10 @@ internal class MissedDerivationsFinderTest { @Test fun `empty derivations for non supported blockchains`() { // Bls is not supported - val scanResponse = ScanResponseMockFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()) + val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()) val finder = MissedDerivationsFinder(scanResponse) - val currencies = CryptoCurrenciesMocks(scanResponse).chia + val currencies = MockCryptoCurrencyFactory(scanResponse).chia.let(::listOf) val actual = finder.find(currencies) Truth.assertThat(actual).isEmpty() @@ -43,7 +45,7 @@ internal class MissedDerivationsFinderTest { @Test fun `derivations ONLY for supported blockchains`() { // Bls is not supported - val scanResponse = ScanResponseMockFactory.create( + val scanResponse = MockScanResponseFactory.create( cardConfig = GenericCardConfig(2), derivedKeys = emptyMap(), ).let { @@ -55,7 +57,7 @@ internal class MissedDerivationsFinderTest { } val finder = MissedDerivationsFinder(scanResponse) - val currencies = CryptoCurrenciesMocks(scanResponse).chiaAndEthereum + val currencies = MockCryptoCurrencyFactory(scanResponse).chiaAndEthereum val actual = finder.find(currencies) Truth.assertThat(actual).containsExactly( @@ -66,10 +68,10 @@ internal class MissedDerivationsFinderTest { @Test fun `derivations for custom token`() { - val scanResponse = ScanResponseMockFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()) + val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()) val finder = MissedDerivationsFinder(scanResponse) - val currencies = CryptoCurrenciesMocks(scanResponse).ethereumTokenWithBinanceDerivation + val currencies = MockCryptoCurrencyFactory(scanResponse).ethereumTokenWithBinanceDerivation val actual = finder.find(currencies) Truth.assertThat(actual).containsExactly( @@ -83,10 +85,10 @@ internal class MissedDerivationsFinderTest { @Test fun `derivations for cardano`() { - val scanResponse = ScanResponseMockFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()) + val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()) val finder = MissedDerivationsFinder(scanResponse) - val currencies = CryptoCurrenciesMocks(scanResponse).cardano + val currencies = MockCryptoCurrencyFactory(scanResponse).cardano.let(::listOf) val actual = finder.find(currencies) Truth.assertThat(actual).containsExactly( @@ -105,13 +107,13 @@ internal class MissedDerivationsFinderTest { @Test fun `empty derivations for already derived currencies`() { - val scanResponse = ScanResponseMockFactory.create( + val scanResponse = MockScanResponseFactory.create( cardConfig = Wallet2CardConfig, derivedKeys = DerivedKeysMocks.ethereumDerivedKeys, ) val finder = MissedDerivationsFinder(scanResponse) - val currencies = CryptoCurrenciesMocks(scanResponse).ethereum + val currencies = MockCryptoCurrencyFactory(scanResponse).ethereum.let(::listOf) val actual = finder.find(currencies) Truth.assertThat(actual).isEmpty() @@ -119,13 +121,13 @@ internal class MissedDerivationsFinderTest { @Test fun `derivations ONLY for never derived currencies`() { - val scanResponse = ScanResponseMockFactory.create( + val scanResponse = MockScanResponseFactory.create( cardConfig = MultiWalletCardConfig, derivedKeys = DerivedKeysMocks.ethereumDerivedKeys, ) val finder = MissedDerivationsFinder(scanResponse) - val currencies = CryptoCurrenciesMocks(scanResponse).ethereumAndStellar + val currencies = MockCryptoCurrencyFactory(scanResponse).ethereumAndStellar val actual = finder.find(currencies) Truth.assertThat(actual).containsExactly( diff --git a/app/src/test/kotlin/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManagerTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManagerTest.kt index 6d22031f4c..8180ca85ab 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManagerTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManagerTest.kt @@ -1,10 +1,10 @@ package com.tangem.tap.domain.userWalletList.implementation import com.google.common.truth.Truth +import com.tangem.common.test.domain.card.MockScanResponseFactory import com.tangem.domain.common.configs.GenericCardConfig import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.tap.domain.card.ScanResponseMockFactory import io.mockk.mockk import org.junit.Test import org.junit.runner.RunWith @@ -198,7 +198,7 @@ internal class BiometricUserWalletsListManagerTest(private val model: Model) { cardsInWallet = emptySet(), isMultiCurrency = true, hasBackupError = false, - scanResponse = ScanResponseMockFactory.create( + scanResponse = MockScanResponseFactory.create( cardConfig = GenericCardConfig(maxWalletCount = 1), derivedKeys = emptyMap(), ).let { diff --git a/common/test/.gitignore b/common/test/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/common/test/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/common/test/build.gradle.kts b/common/test/build.gradle.kts new file mode 100644 index 0000000000..4fd8c4f361 --- /dev/null +++ b/common/test/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.common.test" +} + +dependencies { + implementation(projects.data.common) + + implementation(projects.domain.legacy) + implementation(projects.domain.models) + implementation(projects.domain.tokens.models) + + implementation(projects.libs.blockchainSdk) + + implementation(deps.androidx.datastore) + implementation(deps.test.coroutine) + + implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) +} \ No newline at end of file diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/ScanResponseMockFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt similarity index 98% rename from app/src/test/kotlin/com/tangem/tap/domain/card/ScanResponseMockFactory.kt rename to common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt index da79e800f0..54108f07a9 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/ScanResponseMockFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.card +package com.tangem.common.test.domain.card import com.tangem.common.card.CardWallet import com.tangem.common.card.FirmwareVersion @@ -14,7 +14,7 @@ import java.util.Date /** [REDACTED_AUTHOR] */ -object ScanResponseMockFactory { +object MockScanResponseFactory { private val genericFirmwareVersion = CardDTO.FirmwareVersion( major = 3, diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt similarity index 78% rename from app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt rename to common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt index dceee7e356..9af7183d2a 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/CryptoCurrenciesMocks.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt @@ -1,13 +1,15 @@ -package com.tangem.tap.domain.card +package com.tangem.common.test.domain.token import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.FeePaidCurrency import com.tangem.blockchain.common.Token import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.common.test.domain.card.MockScanResponseFactory import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.currency.getNetworkDerivationPath import com.tangem.data.common.currency.getNetworkStandardType +import com.tangem.domain.common.configs.GenericCardConfig import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency @@ -16,13 +18,13 @@ import com.tangem.domain.tokens.model.Network /** [REDACTED_AUTHOR] */ -internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) { +class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = defaultScanResponse) { private val factory = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()) - val cardano by lazy { listOf(createCoin(blockchain = Blockchain.Cardano)) } - val chia by lazy { listOf(element = createCoin(Blockchain.Chia)) } - val ethereum by lazy { listOf(element = createCoin(Blockchain.Ethereum)) } + val cardano by lazy { createCoin(blockchain = Blockchain.Cardano) } + val chia by lazy { createCoin(Blockchain.Chia) } + val ethereum by lazy { createCoin(Blockchain.Ethereum) } val chiaAndEthereum by lazy { listOf( @@ -44,16 +46,16 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) { ) } - private fun createCoin(blockchain: Blockchain): CryptoCurrency { + fun createCoin(blockchain: Blockchain): CryptoCurrency { val network = Network( id = Network.ID(blockchain.id), backendId = blockchain.toNetworkId(), name = blockchain.fullName, isTestnet = blockchain.isTestnet(), derivationPath = getNetworkDerivationPath( - blockchain, + blockchain = blockchain, extraDerivationPath = null, - scanResponse.derivationStyleProvider, + cardDerivationStyleProvider = scanResponse.derivationStyleProvider, ), currencySymbol = blockchain.currency, standardType = getNetworkStandardType(blockchain), @@ -66,7 +68,7 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) { } // Impossible to create custom token by CryptoCurrencyFactory because it works with URI under the hood - private fun createCustomToken(blockchain: Blockchain, derivationBlockchain: Blockchain): CryptoCurrency { + fun createCustomToken(blockchain: Blockchain, derivationBlockchain: Blockchain): CryptoCurrency { return CryptoCurrency.Token( id = CryptoCurrency.ID( prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, @@ -98,7 +100,7 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) { ) } - private fun createToken(blockchain: Blockchain): CryptoCurrency { + fun createToken(blockchain: Blockchain): CryptoCurrency { return factory.createToken( sdkToken = Token(symbol = "NEVER-MIND", contractAddress = "NEVER-MIND", decimals = 8), blockchain = blockchain, @@ -106,4 +108,12 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) { scanResponse = scanResponse, )!! } + + private companion object { + + val defaultScanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(2), + derivedKeys = emptyMap(), + ) + } } \ No newline at end of file diff --git a/common/test/src/main/java/com/tangem/common/test/utils/CoroutineExt.kt b/common/test/src/main/java/com/tangem/common/test/utils/CoroutineExt.kt new file mode 100644 index 0000000000..537e01b8f3 --- /dev/null +++ b/common/test/src/main/java/com/tangem/common/test/utils/CoroutineExt.kt @@ -0,0 +1,20 @@ +package com.tangem.common.test.utils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.UnconfinedTestDispatcher + +@OptIn(ExperimentalCoroutinesApi::class) +fun CoroutineScope.getEmittedValues(testScheduler: TestCoroutineScheduler, actual: Flow): List { + val values = mutableListOf() + + launch(UnconfinedTestDispatcher(testScheduler)) { + actual.toList(values) + } + + return values +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index f45ec8e8bc..b85da198aa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -153,10 +153,11 @@ include(":core:error") // region Common modules include(":common") -include(":common:ui-charts") -include(":common:routing") -include(":common:ui") include(":common:google") +include(":common:routing") +include(":common:test") +include(":common:ui") +include(":common:ui-charts") // endregion // region Libs modules From 5ecc4c67756108fac150e1fa90af6a1504360780 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Mar 2025 16:59:53 +0200 Subject: [PATCH 059/138] Updated on 2026-08-14 --- app/build.gradle.kts | 2 -- build.gradle.kts | 36 ++----------------- core/datasource/build.gradle.kts | 2 -- .../datasource/api/common/config/ApiConfig.kt | 2 -- .../datasource/api/common/config/Express.kt | 2 -- .../managers/ProdApiConfigsManagerTest.kt | 6 ---- .../extension/AppExtensionConfigurations.kt | 16 +++------ .../plugin/configuration/model/BuildType.kt | 34 ------------------ 8 files changed, 6 insertions(+), 94 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 76ad6c75ba..f95653d523 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -314,9 +314,7 @@ dependencies { /** Chucker */ debugImplementation(deps.chucker) - debugPGImplementation(deps.chucker) mockedImplementation(deps.chuckerStub) - externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub) diff --git a/build.gradle.kts b/build.gradle.kts index 4cbeef6686..1b240b2cfd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -59,41 +59,9 @@ val assembleInternalQA by tasks.registering { } } -val assembleExternalQA by tasks.registering { - group = "build" - description = "Builds external APK to 'build/outputs' directory" - - val appOutputApkDir = "$projectDir/app/build/outputs/apk/external" - val rootOutputApkDir = "$buildDir/outputs" - val injected = objects.newInstance() - - dependsOn(":app:assembleExternal") - - doFirst { - injected.fs.delete { - delete(appOutputApkDir) - delete("$rootOutputApkDir/app-external.apk") - } - } - doLast { - injected.fs.copy { - from("$appOutputApkDir/app-external.apk") - into(rootOutputApkDir) - } - } -} - -val assembleQA by tasks.registering { - group = "build" - description = "Builds internal and external APKs to 'build/outputs' directory" - - dependsOn(assembleInternalQA) - dependsOn(assembleExternalQA) -} - val generateComposeMetrics by tasks.registering { group = "other" - description = "Build external APK and generates compose metrics to 'build/compose-metrics' directory" + description = "Build internal APK and generates compose metrics to 'build/compose-metrics' directory" subprojects { tasks.withType { @@ -118,5 +86,5 @@ val generateComposeMetrics by tasks.registering { } } - finalizedBy(assembleExternalQA) + finalizedBy(assembleInternalQA) } \ No newline at end of file diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 380fdce973..3e2fdba3e3 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -70,9 +70,7 @@ dependencies { /** Chucker */ debugImplementation(deps.chucker) - debugPGImplementation(deps.chucker) mockedImplementation(deps.chuckerStub) - externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt index 28d4220428..952c80a5dc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt @@ -40,10 +40,8 @@ sealed class ApiConfig { companion object { internal const val DEBUG_BUILD_TYPE = "debug" - internal const val DEBUG_PG_BUILD_TYPE = "debugPG" internal const val INTERNAL_BUILD_TYPE = "internal" internal const val MOCKED_BUILD_TYPE = "mocked" - internal const val EXTERNAL_BUILD_TYPE = "external" internal const val RELEASE_BUILD_TYPE = "release" } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt index 8a850c20ea..3ad70b9000 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt @@ -69,12 +69,10 @@ internal class Express( fun getInitialEnvironment(): ApiEnvironment { return when (BuildConfig.BUILD_TYPE) { DEBUG_BUILD_TYPE, - DEBUG_PG_BUILD_TYPE, -> ApiEnvironment.DEV INTERNAL_BUILD_TYPE, MOCKED_BUILD_TYPE, -> ApiEnvironment.STAGE - EXTERNAL_BUILD_TYPE, RELEASE_BUILD_TYPE, -> ApiEnvironment.PROD else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index 9097d47c30..005c4be7fc 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -6,8 +6,6 @@ import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.config.* import com.tangem.datasource.api.common.config.ApiConfig.Companion.DEBUG_BUILD_TYPE -import com.tangem.datasource.api.common.config.ApiConfig.Companion.DEBUG_PG_BUILD_TYPE -import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE @@ -97,12 +95,10 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { private fun createExpressModel(): Model { val environment = when (BuildConfig.BUILD_TYPE) { DEBUG_BUILD_TYPE, - DEBUG_PG_BUILD_TYPE, -> ApiEnvironment.DEV INTERNAL_BUILD_TYPE, MOCKED_BUILD_TYPE, -> ApiEnvironment.STAGE - EXTERNAL_BUILD_TYPE, RELEASE_BUILD_TYPE, -> ApiEnvironment.PROD else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") @@ -114,12 +110,10 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { environment = environment, baseUrl = when (BuildConfig.BUILD_TYPE) { DEBUG_BUILD_TYPE, - DEBUG_PG_BUILD_TYPE, -> "[REDACTED_ENV_URL]" INTERNAL_BUILD_TYPE, MOCKED_BUILD_TYPE, -> "[REDACTED_ENV_URL]" - EXTERNAL_BUILD_TYPE, RELEASE_BUILD_TYPE, -> "https://express.tangem.com/v1/" else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index a4bc580673..89c5d2f72d 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -69,25 +69,17 @@ private fun AppExtension.configureBuildTypes() { private fun AndroidBuildType.configureBuildVariant(appExtension: AppExtension, buildType: BuildType) { when (buildType) { - BuildType.Release -> { - isDebuggable = false - } BuildType.Debug -> { isDebuggable = true } - BuildType.DebugPG -> { - // build is not debuggable to let R8 make optimizations - // otherwise, only shrinking will be applied - matchingFallbacks.add(BuildType.Debug.id) - signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) - } - BuildType.Internal, - BuildType.External, - -> { + BuildType.Internal -> { initWith(appExtension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) } + BuildType.Release -> { + isDebuggable = false + } BuildType.Mocked -> { initWith(appExtension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt index 1ca95f8b20..485c960dcb 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt @@ -32,19 +32,6 @@ internal enum class BuildType( ), ), - DebugPG( - id = "debugPG", - appIdSuffix = "debug", - obfuscating = true, - configFields = listOf( - BuildConfigField.Environment(value = "dev"), - BuildConfigField.TestActionEnabled(isEnabled = true), - BuildConfigField.LogEnabled(isEnabled = true), - BuildConfigField.TesterMenuAvailability(isEnabled = true), - BuildConfigField.MockDataSource(isEnabled = false), - ), - ), - /** * Build type for QA and business * @@ -91,27 +78,6 @@ internal enum class BuildType( ), ), - /** - * Build type for QA and business - * - * Features: - * - Env: prod - * - Signing config: debug - * - Proguard - * */ - External( - id = "external", - appIdSuffix = "external", - versionSuffix = "external", - configFields = listOf( - BuildConfigField.Environment(value = "prod"), - BuildConfigField.TestActionEnabled(isEnabled = false), - BuildConfigField.LogEnabled(isEnabled = false), - BuildConfigField.TesterMenuAvailability(isEnabled = false), - BuildConfigField.MockDataSource(isEnabled = false), - ), - ), - /** * Production ready build type for clients * From c38c185a2d09679006e82e311de3b0555ef30101 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Mar 2025 22:06:02 +0400 Subject: [PATCH 060/138] Updated on 2026-08-14 --- .../test/datastore/MockStateDataStore.kt | 16 ++ .../network/MockNetworkStatusFactory.kt | 58 +++++ .../converter/NetworkAddressConverter.kt | 2 +- .../converter/NetworkAmountsConverter.kt | 2 +- .../NetworkDerivationPathConverter.kt | 2 +- .../NetworkStatusDataModelConverter.kt | 2 +- .../local/network/entity/NetworkStatusDM.kt | 2 +- data/networks/.gitignore | 1 + data/networks/build.gradle.kts | 43 ++++ .../SimpleNetworkStatusConverter.kt | 50 ++++ .../networks/models/SimpleNetworkStatus.kt | 27 +++ .../store/DefaultNetworksStatusesStoreV2.kt | 133 +++++++++++ .../networks/store/NetworksStatusesStoreV2.kt | 23 ++ .../tangem/data/networks/NetworkStatusExt.kt | 17 ++ .../NetworkStatusesStoreUpdateMethodsTest.kt | 214 ++++++++++++++++++ .../NetworksStatusesStoreGetMethodTest.kt | 83 +++++++ ...NetworksStatusesStoreInitializationTest.kt | 83 +++++++ .../store/StoreActualNetworkStatusTest.kt | 104 +++++++++ settings.gradle.kts | 1 + 19 files changed, 858 insertions(+), 5 deletions(-) create mode 100644 common/test/src/main/java/com/tangem/common/test/datastore/MockStateDataStore.kt create mode 100644 common/test/src/main/java/com/tangem/common/test/domain/network/MockNetworkStatusFactory.kt create mode 100644 data/networks/.gitignore create mode 100644 data/networks/build.gradle.kts create mode 100644 data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt create mode 100644 data/networks/src/main/java/com/tangem/data/networks/models/SimpleNetworkStatus.kt create mode 100644 data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt create mode 100644 data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/NetworkStatusExt.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreGetMethodTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreInitializationTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/StoreActualNetworkStatusTest.kt diff --git a/common/test/src/main/java/com/tangem/common/test/datastore/MockStateDataStore.kt b/common/test/src/main/java/com/tangem/common/test/datastore/MockStateDataStore.kt new file mode 100644 index 0000000000..2fc75fe7f1 --- /dev/null +++ b/common/test/src/main/java/com/tangem/common/test/datastore/MockStateDataStore.kt @@ -0,0 +1,16 @@ +package com.tangem.common.test.datastore + +import androidx.datastore.core.DataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.updateAndGet + +class MockStateDataStore(default: T) : DataStore { + + private val _data = MutableStateFlow(value = default) + override val data: Flow = _data + + override suspend fun updateData(transform: suspend (t: T) -> T): T { + return _data.updateAndGet { transform(it) } + } +} \ No newline at end of file diff --git a/common/test/src/main/java/com/tangem/common/test/domain/network/MockNetworkStatusFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/network/MockNetworkStatusFactory.kt new file mode 100644 index 0000000000..34153765ce --- /dev/null +++ b/common/test/src/main/java/com/tangem/common/test/domain/network/MockNetworkStatusFactory.kt @@ -0,0 +1,58 @@ +package com.tangem.common.test.domain.network + +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.domain.tokens.model.NetworkStatus +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +object MockNetworkStatusFactory { + + private val defaultNetwork = MockCryptoCurrencyFactory().ethereum.network + + fun createVerified(network: Network = defaultNetwork): NetworkStatus { + return NetworkStatus( + network = network, + value = NetworkStatus.Verified( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "0x123", + type = NetworkAddress.Address.Type.Primary, + ), + ), + amounts = mapOf(), + pendingTransactions = mapOf(), + source = StatusSource.ACTUAL, + ), + ) + } + + fun createNoAccount(network: Network = defaultNetwork): NetworkStatus { + return NetworkStatus( + network = network, + value = NetworkStatus.NoAccount( + address = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + value = "0x123", + type = NetworkAddress.Address.Type.Primary, + ), + ), + amountToCreateAccount = BigDecimal.ONE, + errorMessage = "", + source = StatusSource.ACTUAL, + ), + ) + } + + fun createMissedDerivation(network: Network = defaultNetwork): NetworkStatus { + return NetworkStatus(network = network, value = NetworkStatus.MissedDerivation) + } + + fun createUnreachable(network: Network = defaultNetwork): NetworkStatus { + return NetworkStatus(network = network, value = NetworkStatus.Unreachable(address = null)) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAddressConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAddressConverter.kt index e0c0192c84..5d3ba03610 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAddressConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAddressConverter.kt @@ -10,7 +10,7 @@ import timber.log.Timber * [REDACTED_AUTHOR] */ -internal class NetworkAddressConverter( +class NetworkAddressConverter( private val selectedAddress: String, ) : TwoWayConverter, NetworkAddress> { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAmountsConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAmountsConverter.kt index cea7a8a4b7..e3bac6232a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAmountsConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAmountsConverter.kt @@ -14,7 +14,7 @@ private typealias AmountsDomainModel = Map { +object NetworkAmountsConverter : TwoWayConverter { override fun convert(value: AmountsDataModel): AmountsDomainModel { return value diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkDerivationPathConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkDerivationPathConverter.kt index 92925a866f..1cffbc73f9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkDerivationPathConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkDerivationPathConverter.kt @@ -10,7 +10,7 @@ import com.tangem.utils.converter.TwoWayConverter * [REDACTED_AUTHOR] */ -internal object NetworkDerivationPathConverter : +object NetworkDerivationPathConverter : TwoWayConverter { override fun convert(value: NetworkStatusDM.DerivationPath): Network.DerivationPath { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt index 34dc695aba..55ab2be060 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt @@ -9,7 +9,7 @@ import com.tangem.utils.converter.Converter * [REDACTED_AUTHOR] */ -internal object NetworkStatusDataModelConverter : Converter { +object NetworkStatusDataModelConverter : Converter { override fun convert(value: NetworkStatus): NetworkStatusDM? { return when (val status = value.value) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt index 0ada82055f..40f26e9e08 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt @@ -6,7 +6,7 @@ import com.tangem.domain.tokens.model.Network import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel import java.math.BigDecimal -internal sealed interface NetworkStatusDM { +sealed interface NetworkStatusDM { val networkId: Network.ID val derivationPath: DerivationPath diff --git a/data/networks/.gitignore b/data/networks/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/networks/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/networks/build.gradle.kts b/data/networks/build.gradle.kts new file mode 100644 index 0000000000..f74ca832ca --- /dev/null +++ b/data/networks/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.networks" +} + +dependencies { + implementation(projects.libs.blockchainSdk) + + implementation(projects.core.datasource) + implementation(projects.core.utils) + + implementation(projects.data.common) + implementation(projects.data.tokens) + + implementation(projects.domain.core) + implementation(projects.domain.demo) + implementation(projects.domain.legacy) + implementation(projects.domain.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) + + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + implementation(deps.androidx.datastore) + implementation(deps.moshi) + implementation(deps.timber) + + implementation(tangemDeps.blockchain) + + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) + testImplementation(tangemDeps.card.core) + testImplementation(projects.common.test) +} \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt new file mode 100644 index 0000000000..5bfe00747d --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt @@ -0,0 +1,50 @@ +package com.tangem.data.networks.converters + +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.datasource.local.network.converter.NetworkAddressConverter +import com.tangem.datasource.local.network.converter.NetworkAmountsConverter +import com.tangem.datasource.local.network.converter.NetworkDerivationPathConverter +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.utils.converter.Converter + +/** + * Converter from [NetworkStatusDM] to [SimpleNetworkStatus] + * +[REDACTED_AUTHOR] + */ +internal object SimpleNetworkStatusConverter : Converter { + + override fun convert(value: NetworkStatusDM): SimpleNetworkStatus { + val address = NetworkAddressConverter(selectedAddress = value.selectedAddress) + .convert(value = value.availableAddresses) + + val status = when (value) { + is NetworkStatusDM.Verified -> { + NetworkStatus.Verified( + address = address, + amounts = NetworkAmountsConverter.convert(value = value.amounts), + pendingTransactions = mapOf(), + source = StatusSource.CACHE, + ) + } + is NetworkStatusDM.NoAccount -> { + NetworkStatus.NoAccount( + address = address, + amountToCreateAccount = value.amountToCreateAccount, + errorMessage = value.errorMessage, + source = StatusSource.CACHE, + ) + } + } + + return SimpleNetworkStatus( + id = SimpleNetworkStatus.Id( + networkId = value.networkId, + derivationPath = NetworkDerivationPathConverter.convert(value = value.derivationPath), + ), + value = status, + ) + } +} \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/models/SimpleNetworkStatus.kt b/data/networks/src/main/java/com/tangem/data/networks/models/SimpleNetworkStatus.kt new file mode 100644 index 0000000000..6ee9a975b9 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/models/SimpleNetworkStatus.kt @@ -0,0 +1,27 @@ +package com.tangem.data.networks.models + +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus + +/** + * Simplified model of network status. Like [NetworkStatus] but without metadata. + * + * @property id identifier + * @property value value + */ +internal data class SimpleNetworkStatus(val id: Id, val value: NetworkStatus.Value) { + + /** + * Identifier of [SimpleNetworkStatus] + * + * @property networkId network id + * @property derivationPath derivation path + */ + data class Id(val networkId: Network.ID, val derivationPath: Network.DerivationPath) { + + constructor(network: Network) : this( + networkId = network.id, + derivationPath = network.derivationPath, + ) + } +} \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt new file mode 100644 index 0000000000..776b446e54 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt @@ -0,0 +1,133 @@ +package com.tangem.data.networks.store + +import androidx.datastore.core.DataStore +import com.tangem.data.networks.converters.SimpleNetworkStatusConverter +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.network.converter.NetworkStatusDataModelConverter +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch + +internal typealias WalletIdWithSimpleStatus = Map> +internal typealias WalletIdWithStatusDM = Map> + +/** + * Default implementation of [NetworksStatusesStoreV2] + * + * @property runtimeStore runtime store + * @property persistenceDataStore persistence store + * @param dispatchers dispatchers + */ +internal class DefaultNetworksStatusesStoreV2( + private val runtimeStore: RuntimeSharedStore, + private val persistenceDataStore: DataStore, + dispatchers: CoroutineDispatcherProvider, +) : NetworksStatusesStoreV2 { + + private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io) + + init { + scope.launch { + val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch + + runtimeStore.store( + value = cachedStatuses.mapValues { (_, statuses) -> + SimpleNetworkStatusConverter.convertSet(statuses) + }, + ) + } + } + + override fun get(userWalletId: UserWalletId): Flow> { + return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] } + } + + override suspend fun refresh(userWalletId: UserWalletId, network: Network) { + updateStatusSourceInRuntime(userWalletId = userWalletId, network = network, source = StatusSource.CACHE) + } + + override suspend fun storeActual(userWalletId: UserWalletId, value: NetworkStatus) { + if (value.value.source != StatusSource.ACTUAL) { + error("Method storeActual can be called only with StatusSource.ACTUAL") + } + + coroutineScope { + launch { storeInRuntime(userWalletId = userWalletId, status = value) } + launch { storeInPersistence(userWalletId = userWalletId, status = value) } + } + } + + override suspend fun storeError(userWalletId: UserWalletId, network: Network) { + updateStatusSourceInRuntime(userWalletId = userWalletId, network = network, source = StatusSource.ONLY_CACHE) + } + + private suspend fun updateStatusSourceInRuntime( + userWalletId: UserWalletId, + network: Network, + source: StatusSource, + ) { + val simpleStatusId = SimpleNetworkStatus.Id(network) + + runtimeStore.update(default = emptyMap()) { stored -> + stored.toMutableMap().apply { + val status = this[userWalletId.stringValue].orEmpty() + .firstOrNull { it.id == simpleStatusId } + ?: SimpleNetworkStatus( + id = simpleStatusId, + value = NetworkStatus.Unreachable(address = null), + ) + + val updatedStatus = status.copy( + value = status.value.copySealed(source = source), + ) + + val updatedStatuses = this[userWalletId.stringValue].orEmpty() + .addOrReplace(updatedStatus) { it.id == simpleStatusId } + + put(key = userWalletId.stringValue, value = updatedStatuses) + } + } + } + + private suspend fun storeInRuntime(userWalletId: UserWalletId, status: NetworkStatus) { + val simpleStatusId = SimpleNetworkStatus.Id(network = status.network) + + runtimeStore.update(default = emptyMap()) { stored -> + stored.toMutableMap().apply { + val simpleStatus = SimpleNetworkStatus(id = simpleStatusId, value = status.value) + + val updatedStatuses = this[userWalletId.stringValue].orEmpty() + .addOrReplace(simpleStatus) { it.id == simpleStatusId } + + put(key = userWalletId.stringValue, value = updatedStatuses) + } + } + } + + private suspend fun storeInPersistence(userWalletId: UserWalletId, status: NetworkStatus) { + // Converter will return null if the network status is not supported + val statusDM = NetworkStatusDataModelConverter.convert(value = status) ?: return + + persistenceDataStore.updateData { storedStatuses -> + storedStatuses.toMutableMap().apply { + val updatedValues = this[userWalletId.stringValue].orEmpty().addOrReplace(statusDM) { + it.networkId == statusDM.networkId && it.derivationPath == statusDM.derivationPath + } + + this[userWalletId.stringValue] = updatedValues + } + } + } +} \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt b/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt new file mode 100644 index 0000000000..356df1dbf1 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt @@ -0,0 +1,23 @@ +package com.tangem.data.networks.store + +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +/** Store of [NetworkStatus]'es set */ +internal interface NetworksStatusesStoreV2 { + + /** Get statuses by [userWalletId] */ + fun get(userWalletId: UserWalletId): Flow> + + /** Refresh status of [network] by [userWalletId] */ + suspend fun refresh(userWalletId: UserWalletId, network: Network) + + /** Store actual [NetworkStatus] by [userWalletId] */ + suspend fun storeActual(userWalletId: UserWalletId, value: NetworkStatus) + + /** Store error by [userWalletId] and [network] */ + suspend fun storeError(userWalletId: UserWalletId, network: Network) +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/NetworkStatusExt.kt b/data/networks/src/test/java/com/tangem/data/networks/NetworkStatusExt.kt new file mode 100644 index 0000000000..1b19c1bc0f --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/NetworkStatusExt.kt @@ -0,0 +1,17 @@ +package com.tangem.data.networks + +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.datasource.local.network.converter.NetworkStatusDataModelConverter +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.tokens.model.NetworkStatus + +internal fun NetworkStatus.toSimple(): SimpleNetworkStatus { + return SimpleNetworkStatus( + id = SimpleNetworkStatus.Id(network = network), + value = value, + ) +} + +internal fun NetworkStatus.toDataModel(): NetworkStatusDM? { + return NetworkStatusDataModelConverter.convert(value = this) +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt new file mode 100644 index 0000000000..d2e5f3c533 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt @@ -0,0 +1,214 @@ +package com.tangem.data.networks.store + +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.data.networks.toDataModel +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class NetworkStatusesStoreUpdateMethodsTest { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `refresh if runtime store is empty`() = runTest { + store.refresh(userWalletId = userWalletId, network = network) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + SimpleNetworkStatus( + id = SimpleNetworkStatus.Id(network), + value = NetworkStatus.Unreachable(address = null), + ), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `refresh if runtime store contains status with this network`() = runTest { + val status = MockNetworkStatusFactory.createVerified().toSimple() + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(status)), + ) + + store.refresh(userWalletId = userWalletId, network = network) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + status.copy(value = status.value.copySealed(source = StatusSource.CACHE)), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `store actual with any status sources`() = runTest { + val expectedErrorMessage = "Method storeActual can be called only with StatusSource.ACTUAL" + + // #1: StatusSource.ACTUAL + val status = MockNetworkStatusFactory.createVerified().copy( + value = MockNetworkStatusFactory.createVerified().value.copySealed(source = StatusSource.ACTUAL), + ) + + val actual = runCatching { store.storeActual(userWalletId, status) } + + Truth.assertThat(actual.isSuccess).isTrue() + + // #2: StatusSource.CACHE + val cacheStatus = MockNetworkStatusFactory.createVerified().copy( + value = MockNetworkStatusFactory.createVerified().value.copySealed(source = StatusSource.CACHE), + ) + + val cacheActual = runCatching { store.storeActual(userWalletId, cacheStatus) } + + Truth.assertThat(cacheActual.isFailure).isTrue() + Truth.assertThat(cacheActual.exceptionOrNull()).isInstanceOf(IllegalStateException::class.java) + Truth.assertThat(cacheActual.exceptionOrNull()).hasMessageThat().isEqualTo(expectedErrorMessage) + + // #3: StatusSource.ONLY_CACHE + val onlyCacheStatus = MockNetworkStatusFactory.createVerified().copy( + value = MockNetworkStatusFactory.createVerified().value.copySealed(source = StatusSource.ONLY_CACHE), + ) + + val onlyCacheActual = runCatching { store.storeActual(userWalletId, onlyCacheStatus) } + + Truth.assertThat(onlyCacheActual.isFailure).isTrue() + Truth.assertThat(onlyCacheActual.exceptionOrNull()).isInstanceOf(IllegalStateException::class.java) + Truth.assertThat(onlyCacheActual.exceptionOrNull()).hasMessageThat().isEqualTo(expectedErrorMessage) + } + + @Test + fun `store actual if runtime and cache stores contain status with this network`() = runTest { + val prevStatus = MockNetworkStatusFactory.createVerified().copy( + value = MockNetworkStatusFactory.createVerified().value.copySealed(source = StatusSource.ONLY_CACHE), + ) + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(prevStatus.toSimple())), + ) + + persistenceStore.updateData { + it.toMutableMap().apply { + put(userWalletId.stringValue, setOf(prevStatus.toDataModel()!!)) + } + } + + val newStatus = MockNetworkStatusFactory.createVerified().copy( + value = MockNetworkStatusFactory.createVerified().value.copySealed(source = StatusSource.ACTUAL), + ) + + store.storeActual(userWalletId, newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `store actual unreachable status if cache store contains verified status`() = runTest { + val prevStatus = MockNetworkStatusFactory.createVerified() + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(prevStatus.toSimple())), + ) + + persistenceStore.updateData { + it.toMutableMap().apply { + put(userWalletId.stringValue, setOf(prevStatus.toDataModel()!!)) + } + } + + val newStatus = MockNetworkStatusFactory.createUnreachable() + + store.storeActual(userWalletId, newStatus) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf(newStatus.toSimple()), + ) + + val persistenceExpected = mapOf( + userWalletId.stringValue to setOf(prevStatus.toDataModel()!!), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) + } + + @Test + fun `store error if runtime store is empty`() = runTest { + store.storeError(userWalletId = userWalletId, network = network) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + SimpleNetworkStatus( + id = SimpleNetworkStatus.Id(network), + value = NetworkStatus.Unreachable(address = null), + ), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `store error if runtime store contains status with this network`() = runTest { + val status = MockNetworkStatusFactory.createVerified().toSimple() + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(status)), + ) + + store.storeError(userWalletId = userWalletId, network = network) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + status.copy(value = status.value.copySealed(source = StatusSource.ONLY_CACHE)), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + + val network = MockCryptoCurrencyFactory().ethereum.network + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreGetMethodTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreGetMethodTest.kt new file mode 100644 index 0000000000..df73b079bb --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreGetMethodTest.kt @@ -0,0 +1,83 @@ +package com.tangem.data.networks.store + +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class NetworksStatusesStoreGetMethodTest { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `test get if runtime store is empty`() = runTest { + val actual = store.get(userWalletId = userWalletId) + + val values = backgroundScope.getEmittedValues(testScheduler, actual) + + Truth.assertThat(values).isEqualTo(emptyList>()) + } + + @Test + fun `test get if runtime store contains empty map`() = runTest { + runtimeStore.store(value = emptyMap()) + + val actual = store.get(userWalletId = userWalletId) + + val values = backgroundScope.getEmittedValues(testScheduler, actual) + + Truth.assertThat(values).isEqualTo(emptyList>()) + } + + @Test + fun `test get if runtime store contains portfolio with empty statuses`() = runTest { + runtimeStore.store( + value = mapOf(userWalletId.stringValue to emptySet()), + ) + + val actual = store.get(userWalletId = userWalletId) + + val values = backgroundScope.getEmittedValues(testScheduler, actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(emptySet())) + } + + @Test + fun `test get if runtime store is not empty`() = runTest { + val status = MockNetworkStatusFactory.createVerified() + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(status.toSimple())), + ) + + val actual = store.get(userWalletId = userWalletId) + + val values = backgroundScope.getEmittedValues(testScheduler, actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(setOf(status.toSimple()))) + } + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreInitializationTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreInitializationTest.kt new file mode 100644 index 0000000000..08314ee68e --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/NetworksStatusesStoreInitializationTest.kt @@ -0,0 +1,83 @@ +package com.tangem.data.networks.store + +import androidx.datastore.core.DataStore +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.data.networks.toDataModel +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class NetworksStatusesStoreInitializationTest { + + @Test + fun `test initialization if cache store is empty`() = runTest { + val runtimeStore = RuntimeSharedStore() + val persistenceStore: DataStore = mockk() + + every { persistenceStore.data } returns emptyFlow() + + DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, // local mock + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(null) + } + + @Test + fun `test initialization if cache store contains empty map`() = runTest { + val runtimeStore = RuntimeSharedStore() + val persistenceStore = MockStateDataStore(default = emptyMap()) + + DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `test initialization if cache store is not empty`() = runTest { + val runtimeStore = RuntimeSharedStore() + val persistenceStore = MockStateDataStore(default = emptyMap()) + + val status = MockNetworkStatusFactory.createVerified() + + persistenceStore.updateData { + it.toMutableMap().apply { + put(userWalletId.stringValue, setOf(status.toDataModel()!!)) + } + } + + DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + val expectedStatus = status.toSimple().copy(value = status.value.copySealed(source = StatusSource.CACHE)) + val expected = mapOf(userWalletId.stringValue to setOf(expectedStatus)) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(expected) + } + + private companion object { + val userWalletId = UserWalletId(stringValue = "011") + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/StoreActualNetworkStatusTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/StoreActualNetworkStatusTest.kt new file mode 100644 index 0000000000..0824241177 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/StoreActualNetworkStatusTest.kt @@ -0,0 +1,104 @@ +package com.tangem.data.networks.store + +import com.google.common.truth.Truth +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.data.networks.toDataModel +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** +[REDACTED_AUTHOR] + */ +@RunWith(Parameterized::class) +internal class StoreActualNetworkStatusTest(private val model: Model) { + + private val runtimeStore = RuntimeSharedStore() + private val persistenceStore = MockStateDataStore(default = emptyMap()) + + private val store = DefaultNetworksStatusesStoreV2( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `store actual`() = runTest { + store.storeActual(userWalletId = userWalletId, value = model.status) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(model.runtimeExpected) + + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(model.persistenceExpected) + } + + data class Model( + val status: NetworkStatus, + val runtimeExpected: Map>, + val persistenceExpected: WalletIdWithStatusDM, + ) + + private companion object { + + val userWalletId = UserWalletId(stringValue = "011") + + @JvmStatic + @Parameterized.Parameters + fun data(): Collection { + return listOf( + Model( + status = MockNetworkStatusFactory.createVerified(), + runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + MockNetworkStatusFactory.createVerified().toSimple(), + ), + ), + persistenceExpected = mapOf( + userWalletId.stringValue to setOf( + MockNetworkStatusFactory.createVerified().toDataModel()!!, + ), + ), + ), + Model( + status = MockNetworkStatusFactory.createNoAccount(), + runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + MockNetworkStatusFactory.createNoAccount().toSimple(), + ), + ), + persistenceExpected = mapOf( + userWalletId.stringValue to setOf( + MockNetworkStatusFactory.createNoAccount().toDataModel()!!, + ), + ), + ), + Model( + status = MockNetworkStatusFactory.createMissedDerivation(), + runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + MockNetworkStatusFactory.createMissedDerivation().toSimple(), + ), + ), + persistenceExpected = mapOf(), + ), + Model( + status = MockNetworkStatusFactory.createUnreachable(), + runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + MockNetworkStatusFactory.createUnreachable().toSimple(), + ), + ), + persistenceExpected = mapOf(), + ), + ) + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index b85da198aa..a63804ffe8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -309,6 +309,7 @@ include(":data:staking") include(":data:wallet-connect") include(":data:markets") include(":data:manage-tokens") +include(":data:networks") include(":data:nft") include(":data:onramp") // endregion Data modules \ No newline at end of file From 039fbb5868a9f0c34a418f06ce44f7b4ff66e402 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Mar 2025 18:17:56 +0500 Subject: [PATCH 061/138] Updated on 2026-08-14 --- .../ui/notifications/NotificationsFactory.kt | 62 +++ .../notifications/NotificationsComponent.kt | 55 +++ .../NotificationsUpdateTrigger.kt | 40 ++ .../analytics/NotificationsAnalyticEvents.kt | 32 ++ .../notifications/di/NotificationsModule.kt | 20 + .../notifications/model/NotificationData.kt | 14 + .../notifications/model/NotificationsModel.kt | 366 ++++++++++++++++++ .../notifications/ui/NotificationsContent.kt | 48 +++ .../state/confirm/SendNotificationFactory.kt | 7 +- .../tangem/blockchainsdk/utils/Blockchain.kt | 4 - .../com/tangem/lib/crypto/BlockchainUtils.kt | 3 +- 11 files changed, 641 insertions(+), 10 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsUpdateTrigger.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/analytics/NotificationsAnalyticEvents.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/di/NotificationsModule.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationData.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/ui/NotificationsContent.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt index b5e98fa800..55ee3a6906 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -16,6 +16,8 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.lib.crypto.BlockchainUtils.getTezosThreshold +import com.tangem.lib.crypto.BlockchainUtils.isTezos import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.orZero import java.math.BigDecimal @@ -240,6 +242,31 @@ object NotificationsFactory { } } + fun MutableList.addFeeCoverageNotification( + isFeeCoverage: Boolean, + enteredAmountValue: BigDecimal, + sendingValue: BigDecimal, + appCurrency: AppCurrency, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ) { + val cryptoCurrency = cryptoCurrencyStatus.currency + val fiatRate = cryptoCurrencyStatus.value.fiatRate + + val cryptoDiff = enteredAmountValue.minus(sendingValue) + if (isFeeCoverage) { + add( + NotificationUM.Warning.FeeCoverageNotification( + cryptoAmount = cryptoDiff.format { crypto(cryptoCurrency).uncapped() }, + fiatAmount = getFiatString( + value = cryptoDiff, + rate = fiatRate, + appCurrency = appCurrency, + ), + ), + ) + } + } + fun MutableList.addDustWarningNotification( dustValue: BigDecimal?, feeValue: BigDecimal, @@ -425,6 +452,41 @@ object NotificationsFactory { add(NotificationUM.Solana.RentInfo(rentWarning)) } + fun MutableList.addHighFeeWarningNotification( + enteredAmountValue: BigDecimal, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ignoreAmountReduce: Boolean, + onReduceClick: ( + reduceAmountBy: BigDecimal, + reduceAmountByDiff: BigDecimal, + notification: Class, + ) -> Unit, + onCloseClick: (Class) -> Unit, + ) { + val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + val isTezos = isTezos(cryptoCurrencyStatus.currency.network.id.value) + val threshold = getTezosThreshold() + val isTotalBalance = enteredAmountValue >= balance && balance > threshold + if (!ignoreAmountReduce && isTotalBalance && isTezos) { + add( + NotificationUM.Warning.HighFeeError( + currencyName = cryptoCurrencyStatus.currency.name, + amount = threshold.toPlainString(), + onConfirmClick = { + onReduceClick( + threshold, + threshold, + NotificationUM.Warning.HighFeeError::class.java, + ) + }, + onCloseClick = { + onCloseClick(NotificationUM.Warning.HighFeeError::class.java) + }, + ), + ) + } + } + private fun checkDustLimits( feeAmount: BigDecimal, sendingAmount: BigDecimal, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsComponent.kt new file mode 100644 index 0000000000..a7a941c816 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsComponent.kt @@ -0,0 +1,55 @@ +package com.tangem.features.send.v2.subcomponents.notifications + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.send.v2.subcomponents.notifications +import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationsModel +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.StateFlow +import java.math.BigDecimal + +internal class NotificationsComponent( + appComponentContext: AppComponentContext, + params: Params, +) : AppComponentContext by appComponentContext { + + private val model: NotificationsModel = getOrCreateModel(params) + + val state: StateFlow> = model.uiState + + fun LazyListScope.content( + state: ImmutableList, + modifier: Modifier = Modifier, + hasPaddingAbove: Boolean = false, + isClickDisabled: Boolean = false, + ) { + notifications( + notifications = state, + modifier = modifier, + hasPaddingAbove = hasPaddingAbove, + isClickDisabled = isClickDisabled, + ) + } + + data class Params( + val analyticsCategoryName: String, + val userWalletId: UserWalletId, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val feeCryptoCurrencyStatus: CryptoCurrencyStatus, + val appCurrency: AppCurrency, + val destinationAddress: String, + val amountValue: BigDecimal, + val reduceAmountBy: BigDecimal, + val isIgnoreReduce: Boolean, + val fee: Fee?, + val feeError: GetFeeError?, + ) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsUpdateTrigger.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsUpdateTrigger.kt new file mode 100644 index 0000000000..bdcf4b4076 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsUpdateTrigger.kt @@ -0,0 +1,40 @@ +package com.tangem.features.send.v2.subcomponents.notifications + +import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import javax.inject.Inject +import javax.inject.Singleton + +interface NotificationsUpdateTrigger { + /** Flow triggers notifications update */ + val updateTriggerFlow: Flow + + /** Flow returns whether there is error notifications */ + val hasErrorFlow: Flow + + /** Trigger return callback with check result */ + suspend fun callbackHasError(hasError: Boolean) + + /** Trigger fee check reload */ + suspend fun triggerUpdate(data: NotificationData) +} + +@Singleton +internal class DefaultNotificationsUpdateTrigger @Inject constructor() : NotificationsUpdateTrigger { + + private val _updateTriggerFlow = MutableSharedFlow() + override val updateTriggerFlow = _updateTriggerFlow.asSharedFlow() + + private val _hasErrorFlow = MutableSharedFlow() + override val hasErrorFlow = _hasErrorFlow.asSharedFlow() + + override suspend fun callbackHasError(hasError: Boolean) { + _hasErrorFlow.emit(hasError) + } + + override suspend fun triggerUpdate(data: NotificationData) { + _updateTriggerFlow.emit(data) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/analytics/NotificationsAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/analytics/NotificationsAnalyticEvents.kt new file mode 100644 index 0000000000..eeaeb2e51d --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/analytics/NotificationsAnalyticEvents.kt @@ -0,0 +1,32 @@ +package com.tangem.features.send.v2.subcomponents.notifications.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM + +internal sealed class NotificationsAnalyticEvents( + category: String, + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = category, event = event, params = params) { + + abstract val categoryName: String + + /** If not enough fee notification is present */ + data class NoticeNotEnoughFee( + override val categoryName: String, + val token: String, + val blockchain: String, + ) : NotificationsAnalyticEvents( + category = categoryName, + event = "Notice - Not Enough Fee", + params = mapOf(TOKEN_PARAM to token, BLOCKCHAIN to blockchain), + ) + + data class NoticeFeeCoverage( + override val categoryName: String, + ) : NotificationsAnalyticEvents( + category = categoryName, + event = "Notice - Network Fee Coverage", + ) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/di/NotificationsModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/di/NotificationsModule.kt new file mode 100644 index 0000000000..2c52cdeab5 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/di/NotificationsModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.send.v2.subcomponents.notifications.di + +import com.tangem.features.send.v2.subcomponents.notifications.DefaultNotificationsUpdateTrigger +import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +internal object NotificationsModule { + + @Provides + @Singleton + fun providesNotificationsUpdateTrigger(): NotificationsUpdateTrigger { + return DefaultNotificationsUpdateTrigger() + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationData.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationData.kt new file mode 100644 index 0000000000..adf6dc6e5f --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationData.kt @@ -0,0 +1,14 @@ +package com.tangem.features.send.v2.subcomponents.notifications.model + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.transaction.error.GetFeeError +import java.math.BigDecimal + +data class NotificationData( + val destinationAddress: String, + val amountValue: BigDecimal, + val reduceAmountBy: BigDecimal, + val isIgnoreReduce: Boolean, + val fee: Fee?, + val feeError: GetFeeError?, +) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt new file mode 100644 index 0000000000..84e193f53d --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt @@ -0,0 +1,366 @@ +package com.tangem.features.send.v2.subcomponents.notifications.model + +import androidx.compose.runtime.Stable +import arrow.core.getOrElse +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addExceedBalanceNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addFeeCoverageNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addHighFeeWarningNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addMinimumAmountErrorNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addRentExemptionNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.tokens.GetBalanceNotEnoughForFeeWarningUseCase +import com.tangem.domain.tokens.GetCurrencyCheckUseCase +import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck +import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase +import com.tangem.domain.utils.convertToSdkAmount +import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceTrigger +import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger +import com.tangem.features.send.v2.subcomponents.fee.model.checkAndCalculateSubtractedAmount +import com.tangem.features.send.v2.subcomponents.fee.model.checkFeeCoverage +import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent +import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger +import com.tangem.features.send.v2.subcomponents.notifications.analytics.NotificationsAnalyticEvents +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.orZero +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import java.math.BigDecimal +import javax.inject.Inject + +@Suppress("LongParameterList") +@Stable +@ModelScoped +class NotificationsModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val appRouter: AppRouter, + private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, + private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, + private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, + private val validateTransactionUseCase: ValidateTransactionUseCase, + private val sendFeeReloadTrigger: SendFeeReloadTrigger, + private val sendAmountReduceTrigger: SendAmountReduceTrigger, + private val notificationsUpdateTrigger: NotificationsUpdateTrigger, + private val analyticsEventHandler: AnalyticsEventHandler, +) : Model() { + + private val params: NotificationsComponent.Params = paramsContainer.require() + + private val analyticsCategoryName = params.analyticsCategoryName + private val userWalletId = params.userWalletId + private val cryptoCurrencyStatus = params.cryptoCurrencyStatus + private val feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus + private val currency = cryptoCurrencyStatus.currency + private val appCurrency = params.appCurrency + + private var destinationAddress = params.destinationAddress + private var amountValue = params.amountValue + private var reduceAmountBy = params.reduceAmountBy + private var isIgnoreReduce = params.isIgnoreReduce + private var fee = params.fee + private var feeError = params.feeError + + private val _uiState = MutableStateFlow>(persistentListOf()) + val uiState = _uiState.asStateFlow() + + private var isAmountSubtractAvailable = false + + init { + subscribeToNotificationUpdateTrigger() + checkIfSubtractAvailable() + } + + private fun subscribeToNotificationUpdateTrigger() { + notificationsUpdateTrigger.updateTriggerFlow + .onEach { updateState(it) } + .launchIn(modelScope) + } + + private fun checkIfSubtractAvailable() { + modelScope.launch { + isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(userWalletId, currency).getOrElse { false } + buildNotifications() + } + } + + private suspend fun updateState(data: NotificationData) { + destinationAddress = data.destinationAddress + amountValue = data.amountValue + reduceAmountBy = data.reduceAmountBy + isIgnoreReduce = data.isIgnoreReduce + fee = data.fee + feeError = data.feeError + + buildNotifications() + } + + private suspend fun buildNotifications() { + val notifications = buildList { + addFeeUnreachableNotification( + tokenStatus = cryptoCurrencyStatus, + coinStatus = feeCryptoCurrencyStatus, + feeError = feeError, + onReload = { + modelScope.launch { + sendFeeReloadTrigger.triggerUpdate() + } + }, + onClick = ::showTokenDetails, + ) + addDomainNotifications( + destinationAddress = destinationAddress, + amountValue = amountValue, + reduceAmountBy = reduceAmountBy, + fee = fee, + ) + } + + notificationsUpdateTrigger.callbackHasError(notifications.any { it is NotificationUM.Error }) + + _uiState.value = notifications.toImmutableList() + } + + private fun showTokenDetails(currency: CryptoCurrency) { + appRouter.pop { isSuccess -> + if (isSuccess) { + appRouter.push( + AppRoute.CurrencyDetails( + userWalletId = userWalletId, + currency = currency, + ), + ) + } + } + } + + private suspend fun MutableList.addDomainNotifications( + destinationAddress: String, + amountValue: BigDecimal, + reduceAmountBy: BigDecimal, + fee: Fee?, + ) { + val balance = cryptoCurrencyStatus.value.amount ?: return + val feeValue = fee?.amount?.value ?: return + val isFeeCoverage = checkFeeCoverage( + isSubtractAvailable = isAmountSubtractAvailable, + balance = balance, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + val sendingAmount = checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isAmountSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatus, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + val feeCurrencyBalanceAfterTransaction = getFeeCurrencyBalanceAfterTx( + sendingAmount = sendingAmount, + feeValue = feeValue, + ) + val currencyCheck = getCurrencyCheckUseCase( + userWalletId = userWalletId, + currencyStatus = cryptoCurrencyStatus, + amount = sendingAmount, + fee = feeValue, + recipientAddress = destinationAddress, + feeCurrencyBalanceAfterTransaction = feeCurrencyBalanceAfterTransaction, + ) + + addErrorNotifications( + sendingAmount = sendingAmount, + feeValue = feeValue, + currencyCheck = currencyCheck, + ) + addWarningNotifications( + enteredAmount = amountValue, + fee = fee, + feeValue = feeValue, + sendingAmount = sendingAmount, + isFeeCoverage = isFeeCoverage, + currencyCheck = currencyCheck, + ) + } + + private fun getFeeCurrencyBalanceAfterTx(sendingAmount: BigDecimal, feeValue: BigDecimal): BigDecimal? { + val sendingCurrencyBalance = cryptoCurrencyStatus.value as? CryptoCurrencyStatus.Loaded + val feeCurrencyBalance = feeCryptoCurrencyStatus.value as? CryptoCurrencyStatus.Loaded + if (feeCryptoCurrencyStatus.value !is CryptoCurrencyStatus.Loaded) return null + return when { + feeCryptoCurrencyStatus == cryptoCurrencyStatus -> sendingCurrencyBalance?.let { + it.amount - sendingAmount - feeValue + } + else -> feeCurrencyBalance?.let { it.amount - feeValue } + } + } + + private suspend fun MutableList.addErrorNotifications( + sendingAmount: BigDecimal, + feeValue: BigDecimal, + currencyCheck: CryptoCurrencyCheck, + ) { + val currencyWarning = getBalanceNotEnoughForFeeWarningUseCase( + fee = feeValue, + userWalletId = userWalletId, + tokenStatus = cryptoCurrencyStatus, + coinStatus = feeCryptoCurrencyStatus, + ).getOrNull() + + addExceedBalanceNotification( + feeAmount = feeValue, + sendingAmount = sendingAmount, + isSubtractionAvailable = isAmountSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + addExceedsBalanceNotification( + cryptoCurrencyWarning = currencyWarning, + cryptoCurrencyStatus = cryptoCurrencyStatus, + shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(currency.network.backendId), + onClick = ::showTokenDetails, + onAnalyticsEvent = { + analyticsEventHandler.send( + NotificationsAnalyticEvents.NoticeNotEnoughFee( + categoryName = analyticsCategoryName, + token = cryptoCurrencyStatus.currency.symbol, + blockchain = cryptoCurrencyStatus.currency.network.name, + ), + ) + }, + ) + if (!BlockchainUtils.isCardano(currency.network.id.value)) { + addDustWarningNotification( + dustValue = currencyCheck.dustValue, + feeValue = feeValue, + sendingAmount = sendingAmount, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = feeCryptoCurrencyStatus, + ) + } + addTransactionLimitErrorNotification( + currencyCheck = currencyCheck, + sendingAmount = sendingAmount, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = feeCryptoCurrencyStatus, + feeValue = feeValue, + onReduceClick = { reduceTo, _ -> + modelScope.launch { + sendAmountReduceTrigger.triggerReduceTo(reduceTo) + } + }, + ) + addReserveAmountErrorNotification( + reserveAmount = currencyCheck.reserveAmount, + sendingAmount = sendingAmount, + cryptoCurrency = currency, + isAccountFunded = currencyCheck.isAccountFunded, + ) + addMinimumAmountErrorNotification( + minimumSendAmount = currencyCheck.minimumSendAmount, + sendingAmount = sendingAmount, + cryptoCurrency = currency, + ) + } + + private suspend fun MutableList.addWarningNotifications( + enteredAmount: BigDecimal, + sendingAmount: BigDecimal, + fee: Fee?, + feeValue: BigDecimal, + isFeeCoverage: Boolean, + currencyCheck: CryptoCurrencyCheck, + ) { + val validationError = validateTransactionUseCase( + userWalletId = userWalletId, + amount = enteredAmount.convertToSdkAmount(currency), + fee = fee, + memo = null, + destination = "", + network = currency.network, + ).leftOrNull() + + addRentExemptionNotification( + rentWarning = currencyCheck.rentWarning, + ) + + addExistentialWarningNotification( + existentialDeposit = currencyCheck.existentialDeposit, + feeAmount = feeValue, + sendingAmount = sendingAmount, + cryptoCurrencyStatus = cryptoCurrencyStatus, + onReduceClick = { reduceBy, reduceByDiff, _ -> + modelScope.launch { + sendAmountReduceTrigger.triggerReduceBy( + ReduceByData( + reduceAmountBy = reduceBy, + reduceAmountByDiff = reduceByDiff, + ), + ) + } + }, + ) + addFeeCoverageNotification( + isFeeCoverage = isFeeCoverage, + enteredAmountValue = enteredAmount, + sendingValue = sendingAmount, + appCurrency = appCurrency, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + addValidateTransactionNotifications( + dustValue = currencyCheck.dustValue.orZero(), + minAdaValue = (fee as? Fee.CardanoToken)?.minAdaValue, + validationError = validationError, + cryptoCurrency = currency, + onReduceClick = { reduceTo, _ -> + modelScope.launch { + sendAmountReduceTrigger.triggerReduceTo(reduceTo) + } + }, + ) + addHighFeeWarningNotification( + enteredAmountValue = enteredAmount, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ignoreAmountReduce = isIgnoreReduce, + onReduceClick = { reduceBy, reduceByDiff, _ -> + modelScope.launch { + sendAmountReduceTrigger.triggerReduceBy( + ReduceByData( + reduceAmountBy = reduceBy, + reduceAmountByDiff = reduceByDiff, + ), + ) + } + }, + onCloseClick = { + modelScope.launch { + buildNotifications() + } + }, + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/ui/NotificationsContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/ui/NotificationsContent.kt new file mode 100644 index 0000000000..aa9dd0d609 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/ui/NotificationsContent.kt @@ -0,0 +1,48 @@ +package com.tangem.features.send.v2.subcomponents.notifications.ui + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList + +internal fun LazyListScope.notifications( + notifications: ImmutableList, + modifier: Modifier = Modifier, + hasPaddingAbove: Boolean = false, + isClickDisabled: Boolean = false, +) { + itemsIndexed( + items = notifications, + key = { _, item -> item::class.java }, + contentType = { _, item -> item::class.java }, + itemContent = { i, item -> + val topPadding = if (i == 0 && hasPaddingAbove) 0.dp else 12.dp + Notification( + config = item.config, + modifier = modifier + .padding(top = topPadding) + .animateItem(), + containerColor = when (item) { + is NotificationUM.Error.TokenExceedsBalance, + is NotificationUM.Warning.NetworkFeeUnreachable, + is NotificationUM.Warning.HighFeeError, + -> TangemTheme.colors.background.action + else -> TangemTheme.colors.button.disabled + }, + iconTint = when (item) { + is NotificationUM.Error.TokenExceedsBalance, + is NotificationUM.Warning, + -> null + is NotificationUM.Error -> TangemTheme.colors.icon.warning + is NotificationUM.Info -> TangemTheme.colors.icon.accent + }, + isEnabled = !isClickDisabled, + ) + }, + ) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 6254300fa0..6b36be9240 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -1,9 +1,7 @@ package com.tangem.features.send.impl.presentation.state.confirm -import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.blockchainsdk.utils.minimalAmount import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification @@ -29,13 +27,14 @@ import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents +import com.tangem.features.send.impl.presentation.model.SendClickIntents import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.state.fee.* -import com.tangem.features.send.impl.presentation.model.SendClickIntents import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.lib.crypto.BlockchainUtils.getTezosThreshold import com.tangem.lib.crypto.BlockchainUtils.isTezos import com.tangem.utils.Provider import com.tangem.utils.extensions.orZero @@ -297,7 +296,7 @@ internal class SendNotificationFactory( val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO val isTezos = isTezos(cryptoCurrencyStatus.currency.network.id.value) - val threshold = Blockchain.Tezos.minimalAmount() + val threshold = getTezosThreshold() val isTotalBalance = sendAmount >= balance && balance > threshold if (!ignoreAmountReduce && isTotalBalance && isTezos) { add( diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index dc2f9b8a81..094ed851ec 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -463,10 +463,6 @@ fun Blockchain.amountToCreateAccount(walletManager: WalletManager, token: Token? } } -fun Blockchain.minimalAmount(): BigDecimal { - return BigDecimal.ONE.movePointLeft(decimals()) -} - const val OLD_POLYGON_NAME = "matic-network" const val NEW_POLYGON_NAME = "polygon-ecosystem-token" diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 3b8db3bccf..3decd72093 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -5,7 +5,6 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.compatibility.l2BlockchainsList import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.blockchainsdk.utils.minimalAmount import com.tangem.lib.crypto.converter.XrpTaggedAddressConverter import com.tangem.lib.crypto.models.XrpTaggedAddress import java.math.BigDecimal @@ -125,7 +124,7 @@ object BlockchainUtils { return l2BlockchainsList.contains(blockchain) } - fun getTezosThreshold(): BigDecimal = Blockchain.Tezos.minimalAmount() + fun getTezosThreshold(): BigDecimal = BigDecimal.ONE.movePointLeft(Blockchain.Tezos.decimals()) /** * Blockchains not affecting total balance counting on errors From 732df0ec4f799ec724f43a68f157ac84ba859938 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Mar 2025 17:00:02 +0400 Subject: [PATCH 062/138] Updated on 2026-08-14 --- .../tap/data/RuntimeUserWalletsStore.kt | 11 +- common/test/build.gradle.kts | 2 + .../domain/wallet/MockUserWalletFactory.kt | 33 +++ .../local/userwallet/UserWalletsStore.kt | 4 +- data/networks/build.gradle.kts | 2 + .../DefaultMultiNetworkStatusProducer.kt | 66 +++++ .../DefaultMultiNetworkStatusProducerTest.kt | 273 ++++++++++++++++++ domain/networks/build.gradle.kts | 10 + .../multi/MultiNetworkStatusProducer.kt | 17 ++ .../multi/MultiNetworkStatusSupplier.kt | 17 ++ settings.gradle.kts | 1 + 11 files changed, 430 insertions(+), 6 deletions(-) create mode 100644 common/test/src/main/java/com/tangem/common/test/domain/wallet/MockUserWalletFactory.kt create mode 100644 data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt create mode 100644 domain/networks/build.gradle.kts create mode 100644 domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusProducer.kt create mode 100644 domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusSupplier.kt diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt index 1661cedb1d..9870501655 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -20,11 +20,12 @@ internal class RuntimeUserWalletsStore( override val userWallets: Flow> get() = userWalletsListManager.userWallets - override suspend fun getSyncOrNull(key: UserWalletId): UserWallet? { - return userWalletsListManager - .userWallets - .firstOrNull() - ?.singleOrNull { it.walletId == key } + override fun getSyncOrNull(key: UserWalletId): UserWallet? { + return userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == key } + } + + override suspend fun getSyncStrict(key: UserWalletId): UserWallet { + return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" } } override suspend fun getAllSyncOrNull(): List? { diff --git a/common/test/build.gradle.kts b/common/test/build.gradle.kts index 4fd8c4f361..00c1510373 100644 --- a/common/test/build.gradle.kts +++ b/common/test/build.gradle.kts @@ -14,6 +14,8 @@ dependencies { implementation(projects.domain.legacy) implementation(projects.domain.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) implementation(projects.libs.blockchainSdk) diff --git a/common/test/src/main/java/com/tangem/common/test/domain/wallet/MockUserWalletFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/wallet/MockUserWalletFactory.kt new file mode 100644 index 0000000000..365267e1ba --- /dev/null +++ b/common/test/src/main/java/com/tangem/common/test/domain/wallet/MockUserWalletFactory.kt @@ -0,0 +1,33 @@ +package com.tangem.common.test.domain.wallet + +import com.tangem.common.test.domain.card.MockScanResponseFactory +import com.tangem.domain.common.configs.GenericCardConfig +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.models.UserWallet + +/** +[REDACTED_AUTHOR] + */ +object MockUserWalletFactory { + + private val defaultScanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(2), + derivedKeys = emptyMap(), + ) + + fun create(scanResponse: ScanResponse = defaultScanResponse): UserWallet { + val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()!! + + return UserWallet( + walletId = userWalletId, + name = "Wallet 1", + artworkUrl = "", + cardsInWallet = emptySet(), + scanResponse = scanResponse, + isMultiCurrency = scanResponse.cardTypesResolver.isMultiwalletAllowed(), + hasBackupError = false, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt index 54fe4ff322..56acd23a53 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt @@ -11,7 +11,9 @@ interface UserWalletsStore { val userWallets: Flow> - suspend fun getSyncOrNull(key: UserWalletId): UserWallet? + fun getSyncOrNull(key: UserWalletId): UserWallet? + + suspend fun getSyncStrict(key: UserWalletId): UserWallet suspend fun getAllSyncOrNull(): List? diff --git a/data/networks/build.gradle.kts b/data/networks/build.gradle.kts index f74ca832ca..079242811e 100644 --- a/data/networks/build.gradle.kts +++ b/data/networks/build.gradle.kts @@ -22,7 +22,9 @@ dependencies { implementation(projects.domain.demo) implementation(projects.domain.legacy) implementation(projects.domain.models) + implementation(projects.domain.networks) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(deps.hilt.android) diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt new file mode 100644 index 0000000000..2e060a8d75 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt @@ -0,0 +1,66 @@ +package com.tangem.data.networks.multi + +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.getNetwork +import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.networks.multi.MultiNetworkStatusProducer +import com.tangem.domain.tokens.model.NetworkStatus +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEmpty +import timber.log.Timber + +/** + * Default implementation of [MultiNetworkStatusProducer] + * + * @property params params + * @property networksStatusesStore networks statuses store + * @property userWalletsStore user wallets store + * @property excludedBlockchains excluded blockchains + */ +internal class DefaultMultiNetworkStatusProducer @AssistedInject constructor( + @Assisted val params: MultiNetworkStatusProducer.Params, + private val networksStatusesStore: NetworksStatusesStoreV2, + private val userWalletsStore: UserWalletsStore, + private val excludedBlockchains: ExcludedBlockchains, +) : MultiNetworkStatusProducer { + + override val fallback: Set + get() = setOf() + + override fun produce(): Flow> { + return networksStatusesStore + .get(userWalletId = params.userWalletId) + .mapNotNull { statuses -> + val userWallet = userWalletsStore.getSyncOrNull(params.userWalletId) + + if (userWallet == null) { + Timber.e("Unable to get UserWallet with provided ID: ${params.userWalletId}") + return@mapNotNull null + } + + statuses.mapNotNullTo(hashSetOf()) { status -> + val network = getNetwork( + networkId = status.id.networkId, + derivationPath = status.id.derivationPath, + scanResponse = userWallet.scanResponse, + excludedBlockchains = excludedBlockchains, + ) ?: return@mapNotNullTo null + + NetworkStatus(network = network, value = status.value) + } + } + .distinctUntilChanged() + .onEmpty { emit(value = hashSetOf()) } + } + + @AssistedFactory + interface Factory : MultiNetworkStatusProducer.Factory { + override fun create(params: MultiNetworkStatusProducer.Params): DefaultMultiNetworkStatusProducer + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt new file mode 100644 index 0000000000..ebed6000a6 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt @@ -0,0 +1,273 @@ +package com.tangem.data.networks.multi + +import com.google.common.truth.Truth +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.common.test.domain.card.MockScanResponseFactory +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.domain.wallet.MockUserWalletFactory +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.data.networks.models.SimpleNetworkStatus +import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.data.networks.toSimple +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.configs.GenericCardConfig +import com.tangem.domain.networks.multi.MultiNetworkStatusProducer +import com.tangem.domain.tokens.model.NetworkStatus +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultMultiNetworkStatusProducerTest { + + private val params = MultiNetworkStatusProducer.Params(userWalletId = userWallet.walletId) + + private val networksStatusesStore = mockk() + private val userWalletsStore = mockk() + private val excludedBlockchains = mockk() + + private val producer = DefaultMultiNetworkStatusProducer( + params = params, + networksStatusesStore = networksStatusesStore, + userWalletsStore = userWalletsStore, + excludedBlockchains = excludedBlockchains, + ) + + @Before + fun setup() { + every { excludedBlockchains.contains(any()) } returns false + } + + @Test + fun `test that flow is mapped for user wallet id from params`() = runTest { + val statuses = setOf( + MockNetworkStatusFactory.createVerified(ethNetwork), + MockNetworkStatusFactory.createVerified(cardanoNetwork), + ) + + val networksStatusesFlow = flowOf(statuses.map(NetworkStatus::toSimple).toSet()) + + every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow + every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + + val actual = producer.produce() + + // check after producer.produce() + verify { networksStatusesStore.get(params.userWalletId) } + + val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + // Check after flow was observed by subscriber (getEmittedValues). + // Otherwise, userWalletsStore.getSyncOrNull is not called. + verify { userWalletsStore.getSyncOrNull(params.userWalletId) } + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values.first()).isEqualTo(statuses) + } + + @Test + fun `test that flow is updated if statuses are updated`() = runTest { + val networksStatusesFlow = MutableSharedFlow>(replay = 2) + + every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow + every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + + val actual = producer.produce() + + // check after producer.produce() + verify { networksStatusesStore.get(params.userWalletId) } + + // first emit + val statuses = setOf( + MockNetworkStatusFactory.createUnreachable(ethNetwork), + MockNetworkStatusFactory.createUnreachable(cardanoNetwork), + ) + + networksStatusesFlow.emit(statuses.map(NetworkStatus::toSimple).toSet()) + + val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + // Check after flow was observed by subscriber (getEmittedValues). + // Otherwise, userWalletsStore.getSyncOrNull is not called. + verify { userWalletsStore.getSyncOrNull(params.userWalletId) } + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1.first()).isEqualTo(statuses) + + // second emit + val updatedStatuses = setOf( + MockNetworkStatusFactory.createVerified(ethNetwork), + MockNetworkStatusFactory.createVerified(cardanoNetwork), + ) + + networksStatusesFlow.emit(updatedStatuses.map(NetworkStatus::toSimple).toSet()) + + val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + // Check after flow was observed by subscriber (getEmittedValues). + // Otherwise, userWalletsStore.getSyncOrNull is not called. + verify { userWalletsStore.getSyncOrNull(params.userWalletId) } + + val expected = listOf(statuses, updatedStatuses) + Truth.assertThat(values2.size).isEqualTo(2) + Truth.assertThat(values2).isEqualTo(expected) + } + + @Test + fun `test that flow is filtered the same status`() = runTest { + val networksStatusesFlow = MutableSharedFlow>(replay = 2) + + every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow + every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + + val actual = producer.produce() + + // check after producer.produce() + verify { networksStatusesStore.get(params.userWalletId) } + + // first emit + val statuses = setOf( + MockNetworkStatusFactory.createUnreachable(ethNetwork), + MockNetworkStatusFactory.createUnreachable(cardanoNetwork), + ) + + networksStatusesFlow.emit(statuses.map(NetworkStatus::toSimple).toSet()) + + val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + // Check after flow was observed by subscriber (getEmittedValues). + // Otherwise, userWalletsStore.getSyncOrNull is not called. + verify { userWalletsStore.getSyncOrNull(params.userWalletId) } + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1.first()).isEqualTo(statuses) + + // second emit + networksStatusesFlow.emit(statuses.map(NetworkStatus::toSimple).toSet()) + + val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + // Check after flow was observed by subscriber (getEmittedValues). + // Otherwise, userWalletsStore.getSyncOrNull is not called. + verify { userWalletsStore.getSyncOrNull(params.userWalletId) } + + Truth.assertThat(values2.size).isEqualTo(1) + Truth.assertThat(values2.first()).isEqualTo(statuses) + } + + @Test + fun `test if flow throws exception`() = runTest { + val exception = IllegalStateException() + val statuses = setOf( + MockNetworkStatusFactory.createUnreachable(ethNetwork), + MockNetworkStatusFactory.createUnreachable(cardanoNetwork), + ) + + val innerFlow = MutableStateFlow(value = false) + val networksStatusesFlow = flow { + if (innerFlow.value) { + emit(statuses.map(NetworkStatus::toSimple).toSet()) + } else { + throw exception + } + } + .buffer(capacity = 5) + + every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow + every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + + val actual = producer.produceWithFallback() + + // check after producer.produce() + verify { networksStatusesStore.get(params.userWalletId) } + + val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + // Check after flow was observed by subscriber (getEmittedValues). + // Otherwise, userWalletsStore.getSyncOrNull is not called. + verify(inverse = true) { userWalletsStore.getSyncOrNull(params.userWalletId) } + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1).isEqualTo(listOf(emptySet())) + + innerFlow.emit(value = true) + + val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + // Check after flow was observed by subscriber (getEmittedValues). + // Otherwise, userWalletsStore.getSyncOrNull is not called. + verify { userWalletsStore.getSyncOrNull(params.userWalletId) } + + Truth.assertThat(values2.size).isEqualTo(1) + Truth.assertThat(values2).isEqualTo(listOf(statuses)) + } + + @Test + fun `test that flow is empty`() = runTest { + every { networksStatusesStore.get(params.userWalletId) } returns emptyFlow() + every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + + val actual = producer.produce() + + // check after producer.produce() + verify { networksStatusesStore.get(params.userWalletId) } + + val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + verify(inverse = true) { userWalletsStore.getSyncOrNull(params.userWalletId) } + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(emptySet())) + } + + @Test + fun `test if network is null`() = runTest { + val statuses = setOf( + MockNetworkStatusFactory.createVerified(ethNetwork), + MockNetworkStatusFactory.createVerified(cardanoNetwork), + ) + + val networksStatusesFlow = flowOf(statuses.map(NetworkStatus::toSimple).toSet()) + + every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow + every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + every { excludedBlockchains.contains(any()) } returns true // cause network is null + + val actual = producer.produce() + + // check after producer.produce() + verify { networksStatusesStore.get(params.userWalletId) } + + val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + // Check after flow was observed by subscriber (getEmittedValues). + // Otherwise, userWalletsStore.getSyncOrNull is not called. + verify { userWalletsStore.getSyncOrNull(params.userWalletId) } + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(emptySet())) + } + + private companion object { + + val scanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(2), + derivedKeys = emptyMap(), + ) + + val userWallet = MockUserWalletFactory.create(scanResponse = scanResponse) + + val ethNetwork = MockCryptoCurrencyFactory(scanResponse = scanResponse).ethereum.network.copy( + canHandleTokens = true, + ) + + val cardanoNetwork = MockCryptoCurrencyFactory(scanResponse = scanResponse).cardano.network + } +} \ No newline at end of file diff --git a/domain/networks/build.gradle.kts b/domain/networks/build.gradle.kts new file mode 100644 index 0000000000..8207889da2 --- /dev/null +++ b/domain/networks/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + api(projects.domain.core) + api(projects.domain.tokens.models) + api(projects.domain.wallets.models) +} \ No newline at end of file diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusProducer.kt b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusProducer.kt new file mode 100644 index 0000000000..eb47772afc --- /dev/null +++ b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusProducer.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.networks.multi + +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Producer of all networks statuses for wallet with [UserWalletId] + * +[REDACTED_AUTHOR] + */ +interface MultiNetworkStatusProducer : FlowProducer> { + + data class Params(val userWalletId: UserWalletId) + + interface Factory : FlowProducer.Factory +} \ No newline at end of file diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusSupplier.kt b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusSupplier.kt new file mode 100644 index 0000000000..7ca732a0d2 --- /dev/null +++ b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusSupplier.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.networks.multi + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.tokens.model.NetworkStatus + +/** + * Supplier of all networks statuses for selected wallet [MultiNetworkStatusProducer.Params] + * + * @property factory factory for creating [MultiNetworkStatusProducer] + * @property keyCreator key creator + * +[REDACTED_AUTHOR] + */ +abstract class MultiNetworkStatusSupplier( + override val factory: MultiNetworkStatusProducer.Factory, + override val keyCreator: (MultiNetworkStatusProducer.Params) -> String, +) : FlowCachingSupplier>() \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index a63804ffe8..b4efe097b4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -286,6 +286,7 @@ include(":domain:promo") include(":domain:promo:models") include(":domain:nft") include(":domain:nft:models") +include(":domain:networks") // endregion Domain modules // region Data modules From 1ad2c36a792dc77f2f250dc928db75408e99b45b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 31 Mar 2025 11:38:20 +0300 Subject: [PATCH 063/138] Updated on 2026-08-14 --- .../java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 1 + gradle/tangem_dependencies.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 717a096159..3079435400 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1909,6 +1909,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( is Fee.Tron, is Fee.VeChain, is Fee.Alephium, + is Fee.Hedera, -> null } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 475ddc9d26..21d426e758 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,5 +1,5 @@ [versions] -tangemBlockchainSdk = "develop-989" +tangemBlockchainSdk = "develop-990" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-445" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 502b5c53616456560fa741ed50feff0c36a24aaa Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Mar 2025 19:09:50 +0400 Subject: [PATCH 064/138] Updated on 2026-08-14 --- .../DefaultSingleNetworkStatusProducer.kt | 44 +++++ .../DefaultSingleNetworkStatusProducerTest.kt | 178 ++++++++++++++++++ .../single/SingleNetworkStatusProducer.kt | 18 ++ .../single/SingleNetworkStatusSupplier.kt | 17 ++ 4 files changed, 257 insertions(+) create mode 100644 data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducer.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducerTest.kt create mode 100644 domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusProducer.kt create mode 100644 domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusSupplier.kt diff --git a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducer.kt b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducer.kt new file mode 100644 index 0000000000..a91cf178d7 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducer.kt @@ -0,0 +1,44 @@ +package com.tangem.data.networks.single + +import com.tangem.domain.networks.multi.MultiNetworkStatusProducer +import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier +import com.tangem.domain.networks.single.SingleNetworkStatusProducer +import com.tangem.domain.tokens.model.NetworkStatus +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapNotNull + +/** + * Default implementation of [SingleNetworkStatusProducer] + * + * @property params params + * @property multiNetworkStatusSupplier multi network status supplier + * +[REDACTED_AUTHOR] + */ +internal class DefaultSingleNetworkStatusProducer @AssistedInject constructor( + @Assisted val params: SingleNetworkStatusProducer.Params, + private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, +) : SingleNetworkStatusProducer { + + override val fallback: NetworkStatus + get() = NetworkStatus(network = params.network, value = NetworkStatus.Unreachable(address = null)) + + override fun produce(): Flow { + return multiNetworkStatusSupplier( + params = MultiNetworkStatusProducer.Params(params.userWalletId), + ) + .mapNotNull { statuses -> + statuses.firstOrNull { it.network == params.network } + } + .distinctUntilChanged() + } + + @AssistedFactory + interface Factory : SingleNetworkStatusProducer.Factory { + override fun create(params: SingleNetworkStatusProducer.Params): DefaultSingleNetworkStatusProducer + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducerTest.kt b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducerTest.kt new file mode 100644 index 0000000000..220e8b819f --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducerTest.kt @@ -0,0 +1,178 @@ +package com.tangem.data.networks.single + +import com.google.common.truth.Truth +import com.tangem.common.test.domain.network.MockNetworkStatusFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.domain.networks.multi.MultiNetworkStatusProducer +import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier +import com.tangem.domain.networks.single.SingleNetworkStatusProducer +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultSingleNetworkStatusProducerTest { + + private val params = SingleNetworkStatusProducer.Params( + userWalletId = UserWalletId(stringValue = "011"), + network = ethereum, + ) + + private val multiNetworkStatusSupplier = mockk() + + private val producer = DefaultSingleNetworkStatusProducer( + params = params, + multiNetworkStatusSupplier = multiNetworkStatusSupplier, + ) + + @Test + fun `test that flow is mapped for network from params`() = runTest { + val status = MockNetworkStatusFactory.createMissedDerivation(params.network) + val expected = flowOf( + setOf( + status, + MockNetworkStatusFactory.createMissedDerivation(cardano), + ), + ) + + val multiParams = MultiNetworkStatusProducer.Params(userWalletId = params.userWalletId) + every { multiNetworkStatusSupplier(multiParams) } returns expected + + val actual = producer.produce() + + verify { multiNetworkStatusSupplier(multiParams) } + + val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(status)) + } + + @Test + fun `test that flow is updated if network status is updated`() = runTest { + val expected = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + + val multiParams = MultiNetworkStatusProducer.Params(userWalletId = params.userWalletId) + every { multiNetworkStatusSupplier(multiParams) } returns expected + + val actual = producer.produceWithFallback() + + verify { multiNetworkStatusSupplier(multiParams) } + + // first emit + val status = MockNetworkStatusFactory.createMissedDerivation(params.network) + expected.emit(value = setOf(status)) + + val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1).isEqualTo(listOf(status)) + + // second emit + val updatedStatus = status.copy(value = NetworkStatus.Unreachable(null)) + expected.emit(value = setOf(updatedStatus)) + + val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values2.size).isEqualTo(2) + Truth.assertThat(values2).isEqualTo(listOf(status, updatedStatus)) + } + + @Test + fun `test that flow is filtered the same status`() = runTest { + val expected = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + + val multiParams = MultiNetworkStatusProducer.Params(userWalletId = params.userWalletId) + every { multiNetworkStatusSupplier(multiParams) } returns expected + + val actual = producer.produceWithFallback() + + verify { multiNetworkStatusSupplier(multiParams) } + + // first emit + val status = MockNetworkStatusFactory.createMissedDerivation(params.network) + expected.emit(value = setOf(status)) + + val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1).isEqualTo(listOf(status)) + + // second emit + expected.emit(value = setOf(status)) + + val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values2.size).isEqualTo(1) + Truth.assertThat(values2).isEqualTo(listOf(status)) + } + + @Test + fun `test if flow throws exception`() = runTest { + val exception = IllegalStateException() + val status = MockNetworkStatusFactory.createMissedDerivation(params.network) + + val innerFlow = MutableStateFlow(value = false) + val expected = flow { + if (innerFlow.value) { + emit(setOf(status)) + } else { + throw exception + } + } + .buffer(capacity = 5) + + val multiParams = MultiNetworkStatusProducer.Params(userWalletId = params.userWalletId) + every { multiNetworkStatusSupplier(multiParams) } returns expected + + val actual = producer.produceWithFallback() + + verify { multiNetworkStatusSupplier(multiParams) } + + val values1 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values1.size).isEqualTo(1) + val fallbackStatus = MockNetworkStatusFactory.createUnreachable(params.network) + Truth.assertThat(values1).isEqualTo(listOf(fallbackStatus)) + + innerFlow.emit(value = true) + + val values2 = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + Truth.assertThat(values2.size).isEqualTo(1) + Truth.assertThat(values2).isEqualTo(listOf(status)) + } + + @Test + fun `test if flow doesn't contain network from params`() = runTest { + val expected = flowOf( + setOf(MockNetworkStatusFactory.createMissedDerivation(cardano)), + ) + + val multiParams = MultiNetworkStatusProducer.Params(userWalletId = params.userWalletId) + every { multiNetworkStatusSupplier(multiParams) } returns expected + + val actual = producer.produce() + + verify { multiNetworkStatusSupplier(multiParams) } + + val values = backgroundScope.getEmittedValues(testScheduler = testScheduler, actual = actual) + + Truth.assertThat(values.size).isEqualTo(0) + } + + private companion object { + + val mocks = MockCryptoCurrencyFactory() + + val ethereum = mocks.ethereum.network + val cardano = mocks.cardano.network + } +} \ No newline at end of file diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusProducer.kt b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusProducer.kt new file mode 100644 index 0000000000..78262a1049 --- /dev/null +++ b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusProducer.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.networks.single + +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Producer of network status [Network] for wallet with [UserWalletId] + * +[REDACTED_AUTHOR] + */ +interface SingleNetworkStatusProducer : FlowProducer { + + data class Params(val userWalletId: UserWalletId, val network: Network) + + interface Factory : FlowProducer.Factory +} \ No newline at end of file diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusSupplier.kt b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusSupplier.kt new file mode 100644 index 0000000000..12afefd145 --- /dev/null +++ b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusSupplier.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.networks.single + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.tokens.model.NetworkStatus + +/** + * Supplier of network status for selected wallet [SingleNetworkStatusProducer.Params] + * + * @property factory factory for creating [SingleNetworkStatusProducer] + * @property keyCreator key creator + * +[REDACTED_AUTHOR] + */ +abstract class SingleNetworkStatusSupplier( + override val factory: SingleNetworkStatusProducer.Factory, + override val keyCreator: (SingleNetworkStatusProducer.Params) -> String, +) : FlowCachingSupplier() \ No newline at end of file From ded9de148c026e68ab8411a4c6e02dbd21e25712 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 31 Mar 2025 16:35:03 +0500 Subject: [PATCH 065/138] Updated on 2026-08-14 --- .../com/tangem/tap/common/DialogManager.kt | 2 - .../domain/WalletConnectInteractor.kt | 5 - .../walletconnect/WalletConnectAction.kt | 5 +- .../walletconnect/WalletConnectMiddleware.kt | 24 +-- .../redux/walletconnect/WalletConnectState.kt | 1 - .../ui/walletconnect/WalletConnectModel.kt | 6 +- .../dialogs/ClipboardOrScanQrDialog.kt | 32 ---- .../tangem/tap/routing/utils/ChildFactory.kt | 12 +- .../com/tangem/common/routing/AppRoute.kt | 19 ++- .../qrscanning/model/QrScanningModel.kt | 11 +- .../presentation/QrScanningContent.kt | 145 +++++++++++------- .../presentation/QrScanningState.kt | 11 +- .../InitializeQrScanningStateTransformer.kt | 24 ++- .../destination/model/SendDestinationModel.kt | 5 +- .../send/impl/navigation/DefaultSendRouter.kt | 5 +- features/walletconnect/impl/build.gradle.kts | 6 + .../connections/model/WcConnectionsModel.kt | 26 +++- 17 files changed, 183 insertions(+), 156 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index c69530d3be..533cdac6a9 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -91,8 +91,6 @@ class DialogManager : StoreSubscriber { context = context, ) } - is WalletConnectDialog.ClipboardOrScanQr -> - ClipboardOrScanQrDialog.create(state.dialog.clipboardUri, context) is WalletConnectDialog.RequestTransaction -> TransactionDialog.create(state.dialog.data, context) is WalletConnectDialog.PersonalSign -> PersonalSignDialog.create(state.dialog.data, context) is WalletConnectDialog.BnbTransactionDialog -> diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index 2ba6a15344..3d11b576d0 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -368,10 +368,6 @@ class WalletConnectInteractor( } } - fun isWalletConnectUri(uri: String): Boolean { - return uri.lowercase().startsWith(WC_SCHEME) - } - /** * Handles Wallet Connect deep links. * If wallet connect is able to handle the deeplink, session is started with deeplink. @@ -436,7 +432,6 @@ class WalletConnectInteractor( } private companion object { - const val WC_SCHEME = "wc" const val WC_TOPIC_QUERY_NAME = "sessionTopic" const val WC_PARAM_REGEX = "([a-zA-Z\\d-]+)=([a-zA-Z\\d]+)" } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt index 3383203b41..89437fadf4 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt @@ -9,9 +9,7 @@ import org.rekotlin.Action sealed class WalletConnectAction : Action { data class HandleDeepLink(val wcUri: String?) : WalletConnectAction() - data class StartWalletConnect( - val copiedUri: String?, - ) : WalletConnectAction() + data object StartWalletConnect : WalletConnectAction() data class OpenSession( val wcUri: String, @@ -24,7 +22,6 @@ sealed class WalletConnectAction : Action { data class RejectRequest(val topic: String, val id: Long) : WalletConnectAction() - data class ShowClipboardOrScanQrDialog(val wcUri: String) : WalletConnectAction() //region WalletConnect 2.0 data class ApproveProposal(val proposal: WalletConnectEvents.SessionProposal) : WalletConnectAction() data object RejectProposal : WalletConnectAction() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index 5239a2cbd9..6bbd548de4 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -56,26 +56,10 @@ class WalletConnectMiddleware { walletConnectInteractor.disconnectSession(action.topic) } is WalletConnectAction.StartWalletConnect -> { - val uri = action.copiedUri - if (uri != null && isWalletConnectUri(uri)) { - store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri)) - } else { - store.dispatchNavigationAction { - push( - AppRoute.QrScanning(AppRoute.QrScanning.Source.WALLET_CONNECT), - ) - } + store.dispatchNavigationAction { + push(AppRoute.QrScanning(AppRoute.QrScanning.Source.WalletConnect)) } } - is WalletConnectAction.ShowClipboardOrScanQrDialog -> { - store.dispatchOnMain( - GlobalAction.ShowDialog( - WalletConnectDialog.ClipboardOrScanQr( - action.wcUri, - ), - ), - ) - } is WalletConnectAction.OpenSession -> { val index = action.wcUri.indexOf("@") when (action.wcUri[index + 1]) { @@ -200,8 +184,4 @@ class WalletConnectMiddleware { return walletManagerFacade.getStoredWalletManagers(userWallet.walletId) } - - private fun isWalletConnectUri(uri: String): Boolean { - return walletConnectInteractor.isWalletConnectUri(uri) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index 9eb02ddd35..ac28ca0b95 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -83,7 +83,6 @@ data class WalletForSession( sealed class WalletConnectDialog : StateDialog { data object UnsupportedWcVersion : WalletConnectDialog() - data class ClipboardOrScanQr(val clipboardUri: String) : WalletConnectDialog() data object UnsupportedCard : WalletConnectDialog() data class UnsupportedNetwork(val networks: List? = null) : WalletConnectDialog() data object UnsupportedDapp : WalletConnectDialog() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt index b940ddcc29..21deae59a7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt @@ -4,7 +4,6 @@ import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model -import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction @@ -23,7 +22,6 @@ import javax.inject.Inject internal class WalletConnectModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val listenToQrScanningUseCase: ListenToQrScanningUseCase, - private val clipboardManager: ClipboardManager, ) : Model() { init { @@ -43,9 +41,7 @@ internal class WalletConnectModel @Inject constructor( isLoading = state.loading, onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, sessions) }, onAddSession = { - store.dispatch( - WalletConnectAction.StartWalletConnect(copiedUri = clipboardManager.getText()), - ) + store.dispatch(WalletConnectAction.StartWalletConnect) }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt deleted file mode 100644 index 3a1c1dd7ea..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.tap.features.details.ui.walletconnect.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.common.routing.AppRoute -import com.tangem.domain.qrscanning.models.SourceType -import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.store -import com.tangem.wallet.R - -object ClipboardOrScanQrDialog { - fun create(wcUri: String, context: Context): AlertDialog { - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(context.getString(R.string.common_select_action)) - setMessage(context.getText(R.string.wallet_connect_clipboard_alert)) - setPositiveButton(context.getText(R.string.wallet_connect_paste_from_clipboard)) { _, _ -> - store.dispatch(WalletConnectAction.OpenSession(wcUri, WalletConnectAction.OpenSession.SourceType.ETC)) - } - setNegativeButton(context.getText(R.string.wallet_connect_scan_new_code)) { _, _ -> - store.dispatchNavigationAction { - push(AppRoute.QrScanning(source = AppRoute.QrScanning.Source.WALLET_CONNECT)) - } - } - setOnDismissListener { - store.dispatch(GlobalAction.HideDialog) - } - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 56711d9800..732fe9d4d0 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -318,14 +318,14 @@ internal class ChildFactory @Inject constructor( } is AppRoute.QrScanning -> { val source = when (route.source) { - AppRoute.QrScanning.Source.SEND -> SourceType.SEND - AppRoute.QrScanning.Source.WALLET_CONNECT -> SourceType.WALLET_CONNECT + is AppRoute.QrScanning.Source.Send -> SourceType.SEND + is AppRoute.QrScanning.Source.WalletConnect -> SourceType.WALLET_CONNECT } createComponentChild( contextProvider = contextProvider(route, contextFactory), params = QrScanningComponent.Params( source = source, - networkName = route.networkName, + networkName = (route.source as? AppRoute.QrScanning.Source.Send)?.networkName, ), componentFactory = qrScanningComponentFactory, ) @@ -536,14 +536,14 @@ internal class ChildFactory @Inject constructor( } is AppRoute.QrScanning -> { val source = when (route.source) { - AppRoute.QrScanning.Source.SEND -> SourceType.SEND - AppRoute.QrScanning.Source.WALLET_CONNECT -> SourceType.WALLET_CONNECT + is AppRoute.QrScanning.Source.Send -> SourceType.SEND + is AppRoute.QrScanning.Source.WalletConnect -> SourceType.WALLET_CONNECT } route.asComponentChild( contextProvider = contextProvider(route, contextFactory), params = QrScanningComponent.Params( source = source, - networkName = route.networkName, + networkName = (route.source as? AppRoute.QrScanning.Source.Send)?.networkName, ), componentFactory = qrScanningComponentFactory, ) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 1238ad4305..6619d29662 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -145,14 +145,19 @@ sealed class AppRoute(val path: String) : Route { data object WalletConnectSessions : AppRoute(path = "/wallet_connect_sessions") @Serializable - data class QrScanning( - val source: Source, - val networkName: String? = null, - ) : AppRoute(path = "/$source/qr_scanning${if (networkName != null) "/$networkName" else ""}") { + data class QrScanning(val source: Source) : AppRoute(path = "/$source/qr_scanning${source.path}") { - enum class Source { - WALLET_CONNECT, - SEND, + @Serializable + sealed class Source { + val path: String + get() = when (this) { + is Send -> "/$networkName" + WalletConnect -> "" + } + + data class Send(val networkName: String) : Source() + + data object WalletConnect : Source() } } diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningModel.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningModel.kt index b120a8bd59..505a42e7ec 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningModel.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningModel.kt @@ -8,6 +8,7 @@ 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.navigation.settings.SettingsManager +import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase import com.tangem.feature.qrscanning.QrScanningComponent @@ -34,6 +35,7 @@ internal class QrScanningModel @Inject constructor( private val emitQrScannedEventUseCase: EmitQrScannedEventUseCase, private val settingsManager: SettingsManager, private val appRouter: AppRouter, + private val clipboardManager: ClipboardManager, ) : Model(), QrScanningClickIntents { private val params = paramsContainer.require() @@ -49,7 +51,14 @@ internal class QrScanningModel @Inject constructor( // samsung for some reason disables reader mode, and then it works unstable // to prevent this disable ir manually before scan QR cardSdkProvider.sdk.forceDisableReaderMode() - stateHolder.update(InitializeQrScanningStateTransformer(this, params.source, params.networkName)) + stateHolder.update( + InitializeQrScanningStateTransformer( + clickIntents = this, + clipboardManager = clipboardManager, + source = params.source, + network = params.networkName, + ), + ) } fun onCameraDeniedState() { diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt index 019b7b6134..da9c43577f 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt @@ -6,9 +6,7 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -22,11 +20,16 @@ import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.IntOffset import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.TopAppBarButton import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.qrscanning.impl.R @@ -57,6 +60,7 @@ internal fun QrScanningContent( ) Overlay( message = uiState.message, + pasteAction = uiState.pasteAction, ) TangemTopAppBar( @@ -97,69 +101,100 @@ internal fun QrScanningContent( } } +@Suppress("LongMethod") @Composable -private fun Overlay(message: TextReference?, overlayColor: Color = TangemColorPalette.Black.copy(alpha = 0.7f)) { +private fun Overlay( + message: TextReference?, + pasteAction: PasteAction, + overlayColor: Color = TangemColorPalette.Black.copy(alpha = 0.7f), +) { val whiteColor = TangemTheme.colors.text.constantWhite val borderWidth = with(LocalDensity.current) { TangemTheme.dimens.spacing4.toPx() } val borderLength = with(LocalDensity.current) { TangemTheme.dimens.size20.toPx() } val textMeasurer = rememberTextMeasurer() - val textPadding = with(LocalDensity.current) { TangemTheme.dimens.spacing24.toPx() } - val style = TangemTheme.typography.caption2.copy(textAlign = TextAlign.Center) + val textPadding = with(LocalDensity.current) { TangemTheme.dimens.spacing48.toPx() } + val style = TangemTheme.typography.body1.copy(textAlign = TextAlign.Center) val text = message?.resolveReference() - Canvas(modifier = Modifier.fillMaxSize()) { - val squareSize = size.minDimension * QR_CODE_SIZE - val verticalOffset = (size.height - squareSize) / 2f - val horizontalOffset = (size.width - squareSize) / 2f - drawRect( - color = overlayColor, - topLeft = Offset.Zero, - size = Size(size.width, verticalOffset), - ) - drawRect( - color = overlayColor, - topLeft = Offset(0f, verticalOffset), - size = Size(horizontalOffset, squareSize), - ) - drawRect( - color = overlayColor, - topLeft = Offset(horizontalOffset + squareSize, verticalOffset), - size = Size(horizontalOffset, squareSize), - ) - drawRect( - color = overlayColor, - topLeft = Offset(0f, verticalOffset + squareSize), - size = Size(size.width, verticalOffset), - ) - drawRect( - color = whiteColor, - topLeft = Offset(horizontalOffset, verticalOffset), - size = Size(squareSize, squareSize), - style = Stroke( - width = borderWidth, - join = StrokeJoin.Round, - pathEffect = PathEffect.dashPathEffect( - intervals = floatArrayOf(2 * borderLength, squareSize - 2 * borderLength), - phase = borderLength, + BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + val minDimension = minOf(this.constraints.minWidth, this.constraints.minHeight) + val squareSize = minDimension * QR_CODE_SIZE + val squareSizeInDp = with(LocalDensity.current) { squareSize.toDp() } + val verticalOffset = (constraints.minHeight - squareSize) / 2f + val horizontalOffset = (constraints.minWidth - squareSize) / 2f + + Canvas(modifier = Modifier.fillMaxSize()) { + drawRect( + color = overlayColor, + topLeft = Offset.Zero, + size = Size(size.width, verticalOffset), + ) + drawRect( + color = overlayColor, + topLeft = Offset(0f, verticalOffset), + size = Size(horizontalOffset, squareSize), + ) + drawRect( + color = overlayColor, + topLeft = Offset(horizontalOffset + squareSize, verticalOffset), + size = Size(horizontalOffset, squareSize), + ) + drawRect( + color = overlayColor, + topLeft = Offset(0f, verticalOffset + squareSize), + size = Size(size.width, verticalOffset), + ) + drawRect( + color = whiteColor, + topLeft = Offset(horizontalOffset, verticalOffset), + size = Size(squareSize, squareSize), + style = Stroke( + width = borderWidth, + join = StrokeJoin.Round, + pathEffect = PathEffect.dashPathEffect( + intervals = floatArrayOf(2 * borderLength, squareSize - 2 * borderLength), + phase = borderLength, + ), ), - ), - ) - text?.let { - val textLayoutResult = text.let { - textMeasurer.measure( - text = it, - style = style, - constraints = Constraints.fixedWidth(squareSize.roundToInt()), + ) + text?.let { + val textLayoutResult = text.let { + textMeasurer.measure( + text = it, + style = style, + constraints = Constraints.fixedWidth(squareSize.roundToInt()), + ) + } + drawText( + textLayoutResult = textLayoutResult, + color = whiteColor, + topLeft = Offset( + x = horizontalOffset, + y = verticalOffset - textPadding - textLayoutResult.size.height, + ), ) } - drawText( - textLayoutResult = textLayoutResult, - color = whiteColor, - topLeft = Offset( - x = horizontalOffset, - y = verticalOffset + squareSize + textPadding, - ), + } + if (pasteAction is PasteAction.Perform) { + TangemButton( + modifier = Modifier + .offset( + offset = { + IntOffset( + x = horizontalOffset.roundToInt(), + y = (verticalOffset + squareSize + textPadding).roundToInt(), + ) + }, + ) + .width(squareSizeInDp), + text = stringResourceSafe(id = R.string.wallet_connect_paste_from_clipboard), + colors = TangemButtonsDefaults.defaultTextButtonColors.copy(contentColor = whiteColor), + onClick = pasteAction.action, + textStyle = TangemTheme.typography.subtitle1, + icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_copy_24), + showProgress = false, + enabled = true, ) } } diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningState.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningState.kt index e9ca30f4e1..b18cf507af 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningState.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningState.kt @@ -5,10 +5,17 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference @Immutable -data class QrScanningState( +internal data class QrScanningState( val message: TextReference?, val onQrScanned: (String) -> Unit, val onBackClick: () -> Unit, val onGalleryClick: () -> Unit, + val pasteAction: PasteAction = PasteAction.None, val bottomSheetConfig: TangemBottomSheetConfig? = null, -) \ No newline at end of file +) + +@Immutable +internal sealed interface PasteAction { + data object None : PasteAction + class Perform(val action: () -> Unit) : PasteAction +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt index 7c6d08b333..f4124119a7 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt @@ -1,14 +1,20 @@ package com.tangem.feature.qrscanning.presentation.transformers +import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.qrscanning.models.SourceType import com.tangem.feature.qrscanning.impl.R -import com.tangem.feature.qrscanning.presentation.QrScanningState import com.tangem.feature.qrscanning.model.QrScanningClickIntents +import com.tangem.feature.qrscanning.presentation.PasteAction +import com.tangem.feature.qrscanning.presentation.QrScanningState + +private const val WC_SCHEME = "wc" internal class InitializeQrScanningStateTransformer( private val clickIntents: QrScanningClickIntents, + private val clipboardManager: ClipboardManager, private val source: SourceType, private val network: String?, ) : QrScanningTransformer { @@ -16,7 +22,7 @@ internal class InitializeQrScanningStateTransformer( override fun transform(prevState: QrScanningState): QrScanningState { val message = when (source) { SourceType.SEND -> network?.let { resourceReference(R.string.send_qrcode_scan_info, wrappedList(it)) } - else -> null + SourceType.WALLET_CONNECT -> stringReference("Open Web3 app and chose WalletConnect option") } return QrScanningState( @@ -24,6 +30,20 @@ internal class InitializeQrScanningStateTransformer( onBackClick = clickIntents::onBackClick, onQrScanned = clickIntents::onQrScanned, onGalleryClick = clickIntents::onGalleryClicked, + pasteAction = constructPasteAction(), ) } + + private fun constructPasteAction(): PasteAction { + val uri = clipboardManager.getText() + return if (source == SourceType.WALLET_CONNECT && uri != null && isWalletConnectUri(uri)) { + PasteAction.Perform { clickIntents.onQrScanned(uri) } + } else { + PasteAction.None + } + } + + private fun isWalletConnectUri(uri: String): Boolean { + return uri.lowercase().startsWith(WC_SCHEME) + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 51c0bc7f63..4deb3cfe46 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -117,10 +117,7 @@ internal class SendDestinationModel @Inject constructor( override fun onQrCodeScanClick() { analyticsEventHandler.send(SendDestinationAnalyticEvents.QrCodeButtonClicked(analyticsCategoryName)) router.push( - AppRoute.QrScanning( - source = AppRoute.QrScanning.Source.SEND, - networkName = cryptoCurrency.network.name, - ), + AppRoute.QrScanning(source = AppRoute.QrScanning.Source.Send(cryptoCurrency.network.name)), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt index 0a1397271b..0eaedec9ff 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt @@ -33,10 +33,7 @@ internal class DefaultSendRouter @Inject constructor( override fun openQrCodeScanner(network: String) { router.push( - AppRoute.QrScanning( - source = AppRoute.QrScanning.Source.SEND, - networkName = network, - ), + AppRoute.QrScanning(source = AppRoute.QrScanning.Source.Send(network)), ) } } \ No newline at end of file diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index f3822c54db..0fb71c954e 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -18,11 +18,16 @@ dependencies { implementation(projects.core.configToggles) implementation(projects.core.decompose) implementation(projects.core.ui) + implementation(projects.common.routing) /** Domain models */ + implementation(projects.domain.qrScanning.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + /** Domain */ + implementation(projects.domain.qrScanning) + /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) @@ -39,6 +44,7 @@ dependencies { implementation(deps.compose.ui.tooling) /** Other */ + implementation(deps.arrow.core) implementation(deps.decompose.ext.compose) implementation(deps.kotlin.immutable.collections) implementation(deps.timber) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt index 0d6c65c6d2..ada41bf7d8 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -1,5 +1,7 @@ package com.tangem.features.walletconnect.connections.model +import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router @@ -7,19 +9,21 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.features.walletconnect.connections.entity.WcConnectionsState import com.tangem.features.walletconnect.connections.ui.preview.WcConnectionsPreviewData import com.tangem.features.walletconnect.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.* +import timber.log.Timber import javax.inject.Inject @ModelScoped internal class WcConnectionsModel @Inject constructor( private val router: Router, private val uiMessageSender: UiMessageSender, + private val listenToQrScanningUseCase: ListenToQrScanningUseCase, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { @@ -31,8 +35,22 @@ internal class WcConnectionsModel @Inject constructor( ), ).asStateFlow() + init { + listenQrUpdates() + } + + private fun listenQrUpdates() { + listenToQrScanningUseCase(SourceType.WALLET_CONNECT) + .getOrElse { emptyFlow() } + .onEach { + // TODO: [REDACTED_JIRA] + Timber.d(it) + } + .launchIn(modelScope) + } + private fun openQrScanScreen() { - // TODO("[REDACTED_JIRA]") + router.push(AppRoute.QrScanning(source = AppRoute.QrScanning.Source.WalletConnect)) } private fun disconnectAll() { From 2267d9ca94010c09bb33d71b36152581edba361d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Mar 2025 18:40:31 +0500 Subject: [PATCH 066/138] Updated on 2026-08-14 --- .../ui/amountScreen/models/AmountState.kt | 2 + features/send-v2/impl/build.gradle.kts | 1 + .../features/send/v2/di/SendModelModule.kt | 6 + .../v2/send/analytics/SendAnalyticEvents.kt | 48 ++ .../v2/send/analytics/SendAnalyticHelper.kt | 53 ++ .../v2/send/confirm/SendConfirmComponent.kt | 160 +++++ .../confirm/model/SendConfirmAlertFactory.kt | 63 ++ .../confirm/model/SendConfirmClickIntents.kt | 18 + .../v2/send/confirm/model/SendConfirmModel.kt | 554 ++++++++++++++++++ .../SendConfirmInitialStateTransformer.kt | 137 +++++ .../SendConfirmSendingStateTransformer.kt | 19 + .../SendConfirmSentStateTransformer.kt | 20 + ...endConfirmationNotificationsTransformer.kt | 55 ++ .../v2/send/confirm/ui/SendConfirmContent.kt | 194 ++++++ .../v2/send/confirm/ui/state/ConfirmUM.kt | 31 + .../features/send/v2/send/model/SendModel.kt | 10 +- .../features/send/v2/send/ui/state/SendUM.kt | 2 + .../v2/subcomponents/fee/SendFeeComponent.kt | 2 +- .../notifications/model/NotificationsModel.kt | 2 +- .../res/drawable/ic_send_hint_shape_12.xml | 9 + 20 files changed, 1383 insertions(+), 3 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmAlertFactory.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmClickIntents.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmSendingStateTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmSentStateTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/state/ConfirmUM.kt create mode 100644 features/send-v2/impl/src/main/res/drawable/ic_send_hint_shape_12.xml diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt index 3833714e62..9f714127da 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt @@ -24,6 +24,7 @@ sealed class AmountState { * @param appCurrencyCode app currency code * @param isEditingDisabled indicated whether amount is editable * @param reduceAmountBy reduces amount to be sent by specified value + * @param isIgnoreReduce ignores reduce amount value */ data class Data( override val isPrimaryButtonEnabled: Boolean, @@ -37,6 +38,7 @@ sealed class AmountState { val appCurrencyCode: String, val isEditingDisabled: Boolean = false, val reduceAmountBy: BigDecimal = BigDecimal.ZERO, + val isIgnoreReduce: Boolean = false, ) : AmountState() data class Empty( diff --git a/features/send-v2/impl/build.gradle.kts b/features/send-v2/impl/build.gradle.kts index 245cf3a903..eff04c059b 100644 --- a/features/send-v2/impl/build.gradle.kts +++ b/features/send-v2/impl/build.gradle.kts @@ -59,6 +59,7 @@ dependencies { implementation(deps.compose.material3) implementation(deps.decompose.ext.compose) implementation(deps.androidx.activity.compose) + implementation(deps.androidx.paging.runtime) /** Other dependencies */ implementation(deps.kotlin.immutable.collections) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt index 60d45d9aa5..8bdd0be817 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt @@ -2,6 +2,7 @@ package com.tangem.features.send.v2.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model +import com.tangem.features.send.v2.send.confirm.model.SendConfirmModel import com.tangem.features.send.v2.send.model.SendModel import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel @@ -35,4 +36,9 @@ internal interface SendModelModule { @IntoMap @ClassKey(SendFeeModel::class) fun provideSendFeeModel(model: SendFeeModel): Model + + @Binds + @IntoMap + @ClassKey(SendConfirmModel::class) + fun provideSendConfirmModel(model: SendConfirmModel): Model } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt index 07d650a412..aca97719f1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt @@ -1,6 +1,9 @@ package com.tangem.features.send.v2.send.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE +import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM /** @@ -23,6 +26,18 @@ internal sealed class SendAnalyticEvents( /** Confirmation screen opened */ data object ConfirmationScreenOpened : SendAnalyticEvents(event = "Confirm Screen Opened") + /** Transaction send screen opened */ + data class TransactionScreenOpened( + val token: String, + val feeType: AnalyticsParam.FeeType, + ) : SendAnalyticEvents( + event = "Transaction Sent Screen Opened", + params = mapOf( + TOKEN_PARAM to token, + FEE_TYPE to feeType.value, + ), + ) + /** If transaction delays notification is present */ data class NoticeTransactionDelays( val token: String, @@ -37,7 +52,40 @@ internal sealed class SendAnalyticEvents( params = mapOf(TOKEN_PARAM to token), ) + /** Close button clicked */ + data class CloseButtonClicked( + val source: SendScreenSource, + val isFromSummary: Boolean, + val isValid: Boolean, + ) : SendAnalyticEvents( + event = "Button - Close", + params = mapOf( + SOURCE to source.name, + "FromSummary" to if (isFromSummary) "Yes" else "No", + "isValid" to if (isValid) "Yes" else "No", + ), + ) + + /** Share button clicked */ + data object ShareButtonClicked : SendAnalyticEvents(event = "Button - Share") + + /** Expore button clicked */ + data object ExploreButtonClicked : SendAnalyticEvents(event = "Button - Explore") + + /** Screen reopened from confirmation screen */ + data class ScreenReopened(val source: SendScreenSource) : SendAnalyticEvents( + event = "Screen Reopened", + params = mapOf(SOURCE to source.name), + ) + companion object { const val SEND_CATEGORY = "Token / Send" } + + internal enum class SendScreenSource { + Address, + Amount, + Fee, + Confirm, + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt new file mode 100644 index 0000000000..73f04ff63b --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt @@ -0,0 +1,53 @@ +package com.tangem.features.send.v2.send.analytics + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.features.send.v2.send.ui.state.SendUM +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import javax.inject.Inject + +@ModelScoped +internal class SendAnalyticHelper @Inject constructor( + private val analyticsEventHandler: AnalyticsEventHandler, +) { + + fun sendSuccessAnalytics(cryptoCurrency: CryptoCurrency, sendUM: SendUM) { + val destinationUM = sendUM.destinationUM as? DestinationUM.Content + val feeUM = sendUM.feeUM as? FeeUM.Content + val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content ?: return + val feeType = feeSelectorUM.selectedType.toAnalyticType(feeSelectorUM) + analyticsEventHandler.send( + SendAnalyticEvents.TransactionScreenOpened( + token = cryptoCurrency.symbol, + feeType = feeType, + ), + ) + analyticsEventHandler.send( + Basic.TransactionSent( + sentFrom = AnalyticsParam.TxSentFrom.Send( + blockchain = cryptoCurrency.network.name, + token = cryptoCurrency.symbol, + feeType = feeType, + ), + memoType = getSendTransactionMemoType(destinationUM?.memoTextField), + ), + ) + } + + private fun getSendTransactionMemoType( + recipientMemo: DestinationTextFieldUM.RecipientMemo?, + ): Basic.TransactionSent.MemoType { + val memo = recipientMemo?.value + return when { + memo?.isBlank() == true -> Basic.TransactionSent.MemoType.Empty + memo?.isNotBlank() == true -> Basic.TransactionSent.MemoType.Full + else -> Basic.TransactionSent.MemoType.Null + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt new file mode 100644 index 0000000000..52c4fd8737 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt @@ -0,0 +1,160 @@ +package com.tangem.features.send.v2.send.confirm + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +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.decompose.ComposableContentComponent +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.send.confirm.model.SendConfirmModel +import com.tangem.features.send.v2.send.confirm.ui.SendConfirmContent +import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM +import com.tangem.features.send.v2.send.ui.state.SendUM +import com.tangem.features.send.v2.subcomponents.amount.SendAmountBlockComponent +import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams +import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent +import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams +import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent +import com.tangem.utils.extensions.orZero +import kotlinx.coroutines.flow.* + +internal class SendConfirmComponent( + appComponentContext: AppComponentContext, + params: Params, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: SendConfirmModel = getOrCreateModel(params = params) + + private val blockClickEnableFlow = MutableStateFlow(false) + + // private val destinationBlockComponent = + // SendDestinationBlockComponent( + // appComponentContext = child("sendConfirmDestinationBlock"), + // params = SendDestinationComponentParams.DestinationBlockParams( + // state = model.uiState.value.destinationUM, + // analyticsCategoryName = params.analyticsCategoryName, + // userWallet = params.userWallet, + // cryptoCurrency = params.cryptoCurrencyStatus.currency, + // blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), + // isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content, + // predefinedAddressValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.address, + // predefinedMemoValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.tag, + // ), + // onResult = model::onDestinationResult, + // onClick = model::showEditDestination, + // ) + + private val amountBlockComponent = SendAmountBlockComponent( + appComponentContext = child("sendConfirmAmountBlock"), + params = SendAmountComponentParams.AmountBlockParams( + state = model.uiState.value.amountUM, + analyticsCategoryName = params.analyticsCategoryName, + userWallet = params.userWallet, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + appCurrency = params.appCurrency, + blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), + blockEditDisabledFlow = TODO(), + // isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content, + // predefinedAmountValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.amount, + ), + onResult = model::onAmountResult, + onClick = model::showEditAmount, + ) + + private val feeBlockComponent = SendFeeBlockComponent( + appComponentContext = child("sendConfirmFeeBlock"), + params = SendFeeComponentParams.FeeBlockParams( + state = model.uiState.value.feeUM, + analyticsCategoryName = params.analyticsCategoryName, + userWallet = params.userWallet, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + appCurrency = params.appCurrency, + sendAmount = model.enteredAmount.orZero(), + destinationAddress = model.enteredDestination.orEmpty(), + blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), + ), + onResult = model::onFeeResult, + onClick = model::showEditFee, + ) + + private val notificationsComponent = NotificationsComponent( + appComponentContext = child("sendConfirmNotifications"), + params = NotificationsComponent.Params( + analyticsCategoryName = params.analyticsCategoryName, + userWalletId = params.userWallet.walletId, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + appCurrency = params.appCurrency, + destinationAddress = model.enteredDestination.orEmpty(), + amountValue = model.enteredAmount.orZero(), + reduceAmountBy = model.reduceAmountBy.orZero(), + isIgnoreReduce = model.isIgnoreReduce, + fee = model.fee, + feeError = model.feeError, + ), + ) + + init { + model.uiState.onEach { state -> + val confirmUM = state.confirmUM as? ConfirmUM.Content + blockClickEnableFlow.value = confirmUM?.isSending == false + }.launchIn(componentScope) + } + + fun updateState(state: SendUM) { + // todo + // destinationBlockComponent.updateState(state.destinationUM) + amountBlockComponent.updateState(state.amountUM) + feeBlockComponent.updateState(state.feeUM) + model.updateState(state) + } + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + val notificationState by notificationsComponent.state.collectAsStateWithLifecycle() + + SendConfirmContent( + sendUM = state, + // destinationBlockComponent = destinationBlockComponent, + amountBlockComponent = amountBlockComponent, + feeBlockComponent = feeBlockComponent, + notificationsComponent = notificationsComponent, + notificationsUM = notificationState, + ) + } + + data class Params( + val state: SendUM, + val analyticsCategoryName: String, + val userWallet: UserWallet, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val feeCryptoCurrencyStatus: CryptoCurrencyStatus, + val appCurrency: AppCurrency, + val callback: ModelCallback, + val currentRoute: Flow, + val isBalanceHidingFlow: StateFlow, + val predefinedValues: PredefinedValues, + ) { + sealed class PredefinedValues { + data object Empty : PredefinedValues() + data class Content( + val transactionId: String, + val amount: String, + val address: String, + val tag: String?, + ) : PredefinedValues() + } + } + + interface ModelCallback { + fun onResult(sendUM: SendUM) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmAlertFactory.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmAlertFactory.kt new file mode 100644 index 0000000000..7eb09a280d --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmAlertFactory.kt @@ -0,0 +1,63 @@ +package com.tangem.features.send.v2.send.confirm.model + +import com.tangem.common.ui.alerts.TransactionErrorAlertConverter +import com.tangem.common.ui.alerts.models.AlertDemoModeUM +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.features.send.v2.impl.R +import javax.inject.Inject + +@ModelScoped +internal class SendConfirmAlertFactory @Inject constructor( + private val messageSender: UiMessageSender, +) { + + fun getGenericErrorState(onFailedTxEmailClick: () -> Unit) { + messageSender.send( + DialogMessage( + title = resourceReference(id = R.string.send_alert_transaction_failed_title), + message = resourceReference(id = R.string.common_unknown_error), + firstAction = EventMessageAction( + title = resourceReference(R.string.common_support), + onClick = onFailedTxEmailClick, + ), + ), + ) + } + + fun getSendTransactionErrorState( + error: SendTransactionError?, + popBack: () -> Unit, + onFailedTxEmailClick: (String) -> Unit, + ) { + val transactionErrorAlertConverter = TransactionErrorAlertConverter( + popBackStack = popBack, + onFailedTxEmailClick = onFailedTxEmailClick, + ) + + val errorAlert = error?.let { transactionErrorAlertConverter.convert(error) } ?: return + val onConfirmClick = errorAlert.onConfirmClick ?: return + + messageSender.send( + DialogMessage( + title = errorAlert.title, + message = errorAlert.message, + firstActionBuilder = { + EventMessageAction( + title = errorAlert.confirmButtonText, + onClick = onConfirmClick, + ) + }, + secondActionBuilder = if (errorAlert !is AlertDemoModeUM) { + { cancelAction() } + } else { + null + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmClickIntents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmClickIntents.kt new file mode 100644 index 0000000000..929fae6819 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmClickIntents.kt @@ -0,0 +1,18 @@ +package com.tangem.features.send.v2.send.confirm.model + +internal interface SendConfirmClickIntents { + + fun showEditDestination() + + fun showEditAmount() + + fun showEditFee() + + fun onSendClick() + + fun onExploreClick() + + fun onShareClick() + + fun onFailedTxEmailClick(errorMessage: String) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt new file mode 100644 index 0000000000..8dd8330471 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -0,0 +1,554 @@ +package com.tangem.features.send.v2.send.confirm.model + +import android.os.SystemClock +import androidx.compose.runtime.Stable +import arrow.core.getOrElse +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.routing.AppRouter +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.analytics.api.AnalyticsEventHandler +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.navigation.Router +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.SaveBlockchainErrorUseCase +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.BlockchainErrorInfo +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase +import com.tangem.domain.settings.NeverShowTapHelpUseCase +import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase +import com.tangem.domain.tokens.FetchPendingTransactionsUseCase +import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase +import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.usecase.CreateTransactionUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.utils.convertToSdkAmount +import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents +import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource +import com.tangem.features.send.v2.send.analytics.SendAnalyticHelper +import com.tangem.features.send.v2.send.confirm.SendConfirmComponent +import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmInitialStateTransformer +import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSendingStateTransformer +import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSentStateTransformer +import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmationNotificationsTransformer +import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM +import com.tangem.features.send.v2.send.ui.state.SendUM +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger +import com.tangem.features.send.v2.subcomponents.fee.model.checkAndCalculateSubtractedAmount +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger +import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData +import com.tangem.features.txhistory.TxHistoryFeatureToggles +import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.DelayedWork +import com.tangem.utils.extensions.orZero +import com.tangem.utils.extensions.stripZeroPlainString +import com.tangem.utils.transformer.update +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import timber.log.Timber +import java.math.BigDecimal +import javax.inject.Inject + +@Suppress("LongParameterList", "LargeClass") +@Stable +@ModelScoped +internal class SendConfirmModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, + private val appRouter: AppRouter, + private val router: Router, + private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase, + private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase, + private val createTransactionUseCase: CreateTransactionUseCase, + private val sendTransactionUseCase: SendTransactionUseCase, + private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, + private val getCardInfoUseCase: GetCardInfoUseCase, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, + private val fetchPendingTransactionsUseCase: FetchPendingTransactionsUseCase, + private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, + private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, + private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger, + private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter, + private val notificationsUpdateTrigger: NotificationsUpdateTrigger, + private val alertFactory: SendConfirmAlertFactory, + private val sendAnalyticHelper: SendAnalyticHelper, + private val txHistoryFeatureToggles: TxHistoryFeatureToggles, + @DelayedWork private val coroutineScope: CoroutineScope, + private val urlOpener: UrlOpener, + private val shareManager: ShareManager, +) : Model(), SendConfirmClickIntents { + + private val params: SendConfirmComponent.Params = paramsContainer.require() + + private val userWallet = params.userWallet + private val appCurrency = params.appCurrency + private val cryptoCurrencyStatus = params.cryptoCurrencyStatus + private val cryptoCurrency = cryptoCurrencyStatus.currency + + private val _uiState = MutableStateFlow(params.state) + val uiState = _uiState.asStateFlow() + + private val amountState + get() = uiState.value.amountUM as? AmountState.Data + private val destinationUM + get() = uiState.value.destinationUM as? DestinationUM.Content + private val feeUM + get() = uiState.value.feeUM as? FeeUM.Content + private val feeSelectorUM + get() = feeUM?.feeSelectorUM as? FeeSelectorUM.Content + + val enteredAmount: BigDecimal? + get() = amountState?.amountTextField?.cryptoAmount?.value + val reduceAmountBy: BigDecimal + get() = amountState?.reduceAmountBy.orZero() + val isIgnoreReduce: Boolean + get() = amountState?.isIgnoreReduce == true + val enteredDestination: String? + get() = destinationUM?.addressTextField?.value + val fee: Fee? + get() = feeSelectorUM?.selectedFee + val feeError: GetFeeError? + get() = (feeUM?.feeSelectorUM as? FeeSelectorUM.Error)?.error + + private var sendIdleTimer: Long = 0L + private var isAmountSubtractAvailable = false + + init { + modelScope.launch { + isAmountSubtractAvailable = + isAmountSubtractAvailableUseCase(userWallet.walletId, cryptoCurrency).getOrElse { false } + } + configConfirmNavigation() + subscribeOnNotificationsUpdateTrigger() + subscribeOnCheckFeeResultUpdates() + initialState() + } + + fun updateState(state: SendUM) { + _uiState.value = state + updateConfirmNotifications() + } + + fun onFeeResult(feeUM: FeeUM) { + sendIdleTimer = SystemClock.elapsedRealtime() + _uiState.update { it.copy(feeUM = feeUM) } + updateConfirmNotifications() + } + + fun onAmountResult(amountUM: AmountState) { + _uiState.update { it.copy(amountUM = amountUM) } + updateConfirmNotifications() + } + + fun onDestinationResult(destinationUM: DestinationUM) { + _uiState.update { it.copy(destinationUM = destinationUM) } + updateConfirmNotifications() + } + + override fun showEditDestination() { + modelScope.launch { + neverShowTapHelpUseCase() + _uiState.update { + val confirmUM = it.confirmUM as? ConfirmUM.Content + it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM) + } + analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address)) + router.push(SendRoute.Destination(isEditMode = true)) + } + } + + override fun showEditAmount() { + modelScope.launch { + neverShowTapHelpUseCase() + _uiState.update { + val confirmUM = it.confirmUM as? ConfirmUM.Content + it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM) + } + analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount)) + router.push(SendRoute.Amount(isEditMode = true)) + } + } + + override fun showEditFee() { + modelScope.launch { + neverShowTapHelpUseCase() + _uiState.update { + val confirmUM = it.confirmUM as? ConfirmUM.Content + it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM) + } + analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee)) + router.push(SendRoute.Fee()) + } + } + + override fun onSendClick() { + _uiState.update(SendConfirmSendingStateTransformer(isSending = true)) + if (SystemClock.elapsedRealtime() - sendIdleTimer < CHECK_FEE_UPDATE_DELAY) { + verifyAndSendTransaction() + } else { + modelScope.launch { + sendFeeCheckReloadTrigger.triggerCheckUpdate() + } + } + } + + override fun onExploreClick() { + val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return + analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked) + urlOpener.openUrl(confirmUM.txUrl) + } + + override fun onShareClick() { + val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return + analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked) + shareManager.shareText(confirmUM.txUrl) + } + + override fun onFailedTxEmailClick(errorMessage: String) { + val amountValue = amountState?.amountTextField?.cryptoAmount?.value + val feeValue = fee?.amount?.value + + val receivingAmount = if (amountValue != null && feeValue != null) { + checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isAmountSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatus, + amountValue = enteredAmount.orZero(), + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + } else { + null + } + + val amount = receivingAmount?.convertToSdkAmount(cryptoCurrency) + + saveBlockchainErrorUseCase( + error = BlockchainErrorInfo( + errorMessage = errorMessage, + blockchainId = cryptoCurrency.network.id.value, + derivationPath = cryptoCurrency.network.derivationPath.value, + destinationAddress = enteredDestination.orEmpty(), + tokenSymbol = if (amount?.type is AmountType.Token) { + amount.currencySymbol + } else { + "" + }, + amount = amount?.value?.stripZeroPlainString() ?: "unknown", + fee = feeValue?.convertToSdkAmount(cryptoCurrency) + ?.value?.stripZeroPlainString() ?: "unknown", + ), + ) + + val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrNull() ?: return + + modelScope.launch { + sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) + } + } + + private fun initialState() { + val confirmUM = uiState.value.confirmUM + val amountUM = uiState.value.amountUM + val feeUM = uiState.value.feeUM + + modelScope.launch { + val isShowTapHelp = isSendTapHelpEnabledUseCase().getOrElse { false } + if (confirmUM is ConfirmUM.Empty || feeUM is FeeUM.Empty) { + _uiState.update { + it.copy( + confirmUM = SendConfirmInitialStateTransformer( + appCurrency = appCurrency, + feeUM = feeUM, + amountUM = amountUM, + isShowTapHelp = isShowTapHelp, + isSubtracted = false, + ).transform(uiState.value.confirmUM), + ) + } + updateConfirmNotifications() + } + } + } + + private fun subscribeOnNotificationsUpdateTrigger() { + notificationsUpdateTrigger.hasErrorFlow + .onEach { hasError -> + _uiState.update { + val feeUM = it.feeUM as? FeeUM.Content + val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content + it.copy( + confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( + isPrimaryButtonEnabled = !hasError && feeSelectorUM != null, + ) ?: it.confirmUM, + ) + } + } + .launchIn(modelScope) + } + + private fun verifyAndSendTransaction() { + val amountValue = amountState?.amountTextField?.cryptoAmount?.value ?: return + val destination = destinationUM?.addressTextField?.value ?: return + val memo = destinationUM?.memoTextField?.value + val fee = feeSelectorUM?.selectedFee + val feeValue = fee?.amount?.value ?: return + + val receivingAmount = checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isAmountSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatus, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy.orZero(), + ) + + modelScope.launch { + createTransactionUseCase( + amount = receivingAmount.convertToSdkAmount(cryptoCurrency), + fee = fee, + memo = memo, + destination = destination, + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ).fold( + ifLeft = { error -> + Timber.e(error) + _uiState.update(SendConfirmSendingStateTransformer(isSending = false)) + alertFactory.getGenericErrorState { + onFailedTxEmailClick(error.localizedMessage.orEmpty()) + } + }, + ifRight = { txData -> + sendTransaction(txData) + }, + ) + } + } + + private suspend fun sendTransaction(txData: TransactionData.Uncompiled) { + val result = sendTransactionUseCase( + txData = txData, + userWallet = userWallet, + network = cryptoCurrency.network, + ) + + _uiState.update(SendConfirmSendingStateTransformer(isSending = false)) + + result.fold( + ifLeft = { error -> + alertFactory.getSendTransactionErrorState( + error = error, + popBack = appRouter::pop, + onFailedTxEmailClick = ::onFailedTxEmailClick, + ) + analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol)) + }, + ifRight = { + updateTransactionStatus(txData) + addTokenToWalletIfNeeded() + scheduleUpdates() + sendAnalyticHelper.sendSuccessAnalytics(cryptoCurrency, uiState.value) + }, + ) + } + + private fun addTokenToWalletIfNeeded() { + if (cryptoCurrency !is CryptoCurrency.Token) return + val wallets = destinationUM?.wallets ?: return + + val receivingUserWallet = wallets + .firstOrNull { it.address == enteredDestination } + ?: return + + val userWalletId = receivingUserWallet.userWalletId ?: return + val network = receivingUserWallet.network ?: return + + modelScope.launch { + addCryptoCurrenciesUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + network = network, + ) + } + } + + private suspend fun updateTransactionStatus(txData: TransactionData.Uncompiled) { + val txUrl = getExplorerTransactionUrlUseCase( + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ).getOrElse { "" } + _uiState.update(SendConfirmSentStateTransformer(txData, txUrl)) + } + + private fun scheduleUpdates() { + coroutineScope.launch { + listOf( + // we should update network to find pending tx after 1 sec + async { + fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrency.network)) + }, + // we should update tx history and network for new balance + async { + updateTxHistory() + }, + async { + updateDelayedCurrencyStatusUseCase( + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + delayMillis = BALANCE_UPDATE_DELAY, + refresh = true, + ) + }, + ).awaitAll() + } + } + + private suspend fun updateTxHistory() { + delay(BALANCE_UPDATE_DELAY) + val txHistoryItemsCountEither = getTxHistoryItemsCountUseCase( + userWalletId = userWallet.walletId, + currency = cryptoCurrency, + ) + + txHistoryItemsCountEither.onRight { + if (txHistoryFeatureToggles.isFeatureEnabled) { + txHistoryContentUpdateEmitter.triggerUpdate() + } else { + getTxHistoryItemsUseCase( + userWalletId = userWallet.walletId, + currency = cryptoCurrency, + refresh = true, + ) + } + } + } + + private fun subscribeOnCheckFeeResultUpdates() { + sendFeeCheckReloadTrigger.checkReloadResultFlow.onEach { isFeeResultSuccess -> + if (isFeeResultSuccess) { + sendIdleTimer = SystemClock.elapsedRealtime() + _uiState.update(SendConfirmSendingStateTransformer(isSending = true)) + verifyAndSendTransaction() + } else { + _uiState.update(SendConfirmSendingStateTransformer(isSending = false)) + } + }.launchIn(modelScope) + } + + private fun updateConfirmNotifications() { + modelScope.launch { + notificationsUpdateTrigger.triggerUpdate( + data = NotificationData( + destinationAddress = enteredDestination.orEmpty(), + amountValue = enteredAmount.orZero(), + reduceAmountBy = reduceAmountBy.orZero(), + isIgnoreReduce = isIgnoreReduce, + fee = fee, + feeError = feeError, + ), + ) + } + _uiState.update { + it.copy( + confirmUM = SendConfirmationNotificationsTransformer( + feeUM = uiState.value.feeUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrencyStatus.currency, + ).transform(uiState.value.confirmUM), + ) + } + } + + fun configConfirmNavigation() { + combine( + flow = uiState, + flow2 = params.currentRoute, + transform = { state, route -> state to route }, + ).onEach { (state, _) -> + // todo + // val amountUM = state.amountUM as? AmountState.Data + // val confirmUM = state.confirmUM + // params.callback.onResult( + // state.copy( + // navigationUM = NavigationUM.Content( + // title = resourceReference( + // id = R.string.send_summary_title, + // formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name), + // ), + // subtitle = amountUM?.title, + // backIconRes = R.drawable.ic_close_24, + // backIconClick = { + // analyticsEventHandler.send( + // SendAnalyticEvents.CloseButtonClicked( + // source = SendScreenSource.Confirm, + // isFromSummary = true, + // isValid = confirmUM.isPrimaryButtonEnabled, + // ), + // ) + // appRouter.pop() + // }, + // primaryButton = ButtonsUM.PrimaryButtonUM( + // text = when (confirmUM) { + // is ConfirmUM.Success -> resourceReference(R.string.common_close) + // is ConfirmUM.Content -> if (confirmUM.isSending) { + // resourceReference(R.string.send_sending) + // } else { + // resourceReference(R.string.common_send) + // } + // else -> resourceReference(R.string.common_send) + // }, + // iconResId = R.drawable.ic_tangem_24.takeIf { confirmUM is ConfirmUM.Content }, + // isEnabled = confirmUM.isPrimaryButtonEnabled, + // isHapticClick = confirmUM is ConfirmUM.Content && !confirmUM.isSending, + // onClick = { + // when (confirmUM) { + // is ConfirmUM.Success -> appRouter.pop() + // is ConfirmUM.Content -> if (confirmUM.isSending) { + // return@PrimaryButtonUM + // } else { + // onSendClick() + // } + // else -> return@PrimaryButtonUM + // } + // }, + // ), + // prevButton = null, + // secondaryPairButtonsUM = ButtonsUM.SecondaryPairButtonsUM( + // leftText = resourceReference(R.string.common_explore), + // leftIconResId = R.drawable.ic_web_24, + // onLeftClick = ::onExploreClick, + // rightText = resourceReference(R.string.common_share), + // rightIconResId = R.drawable.ic_share_24, + // onRightClick = ::onShareClick, + // ).takeIf { confirmUM is ConfirmUM.Success }, + // ), + // ), + // ) + }.launchIn(modelScope) + } + + private companion object { + const val CHECK_FEE_UPDATE_DELAY = 10_000L + const val BALANCE_UPDATE_DELAY = 11_000L + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt new file mode 100644 index 0000000000..a3da5886b9 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt @@ -0,0 +1,137 @@ +package com.tangem.features.send.v2.send.confirm.model.transformers + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf + +internal class SendConfirmInitialStateTransformer( + private val appCurrency: AppCurrency, + private val feeUM: FeeUM, + private val amountUM: AmountState, + private val isShowTapHelp: Boolean, + private val isSubtracted: Boolean, +) : Transformer { + override fun transform(prevState: ConfirmUM): ConfirmUM { + return ConfirmUM.Content( + isSending = false, + showTapHelp = isShowTapHelp, + sendingFooter = getSendingFooterText(), + notifications = persistentListOf(), + ) + } + + private fun getSendingFooterText(): TextReference { + val feeUM = feeUM as? FeeUM.Content + val amountUM = amountUM as? AmountState.Data + + if (feeUM == null || amountUM == null) return TextReference.EMPTY + + val fee = (feeUM.feeSelectorUM as? FeeSelectorUM.Content)?.selectedFee + val fiatAmountValue = amountUM.amountTextField.fiatAmount.value + val fiatFeeValue = feeUM.rate?.let { fee?.amount?.value?.multiply(it) } + + val fiatSendingValue = if (isSubtracted) { + fiatAmountValue + } else { + if (feeUM.isFeeConvertibleToFiat) { + fiatFeeValue?.let { fiatAmountValue?.plus(it) } + } else { + fiatAmountValue + } + } + + val fiatSending = fiatSendingValue.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + val fiatFee = formatFiatFee( + amount = fee?.amount, + isFeeConvertibleToFiat = feeUM.isFeeConvertibleToFiat, + isFeeApproximate = feeUM.isFeeApproximate, + ) + + return if (feeUM.isTronToken && fee is Fee.Tron) { + getTokenFeeSendingText( + fee = fee, + fiatFee = fiatFee, + fiatSending = fiatSending, + ) + } else { + resourceReference( + id = if (feeUM.isFeeConvertibleToFiat) { + R.string.send_summary_transaction_description + } else { + R.string.send_summary_transaction_description_no_fiat_fee + }, + formatArgs = wrappedList(fiatSending, fiatFee), + ) + } + } + + private fun formatFiatFee(amount: Amount?, isFeeConvertibleToFiat: Boolean, isFeeApproximate: Boolean): String { + return if (isFeeConvertibleToFiat) { + amount?.value.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + } else { + amount?.value.format { + crypto( + decimals = amount?.decimals ?: 0, + symbol = amount?.currencySymbol.orEmpty(), + ).fee( + canBeLower = isFeeApproximate, + ) + } + } + } + + private fun getTokenFeeSendingText(fee: Fee.Tron, fiatFee: String, fiatSending: String): TextReference { + val suffix = when { + fee.remainingEnergy == 0L -> { + resourceReference( + R.string.send_summary_transaction_description_suffix_including, + wrappedList(fiatFee), + ) + } + fee.feeEnergy <= fee.remainingEnergy -> { + resourceReference( + R.string.send_summary_transaction_description_suffix_fee_covered, + wrappedList(fee.feeEnergy), + ) + } + else -> { + resourceReference( + R.string.send_summary_transaction_description_suffix_fee_reduced, + wrappedList(fee.remainingEnergy), + ) + } + } + val prefix = resourceReference( + R.string.send_summary_transaction_description_prefix, + wrappedList(fiatSending), + ) + + return combinedReference(prefix, COMMA_SEPARATOR, suffix) + } + + companion object { + private val COMMA_SEPARATOR = stringReference(", ") + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmSendingStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmSendingStateTransformer.kt new file mode 100644 index 0000000000..e632f7b86f --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmSendingStateTransformer.kt @@ -0,0 +1,19 @@ +package com.tangem.features.send.v2.send.confirm.model.transformers + +import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM +import com.tangem.features.send.v2.send.ui.state.SendUM +import com.tangem.utils.transformer.Transformer + +internal class SendConfirmSendingStateTransformer( + val isSending: Boolean, +) : Transformer { + override fun transform(prevState: SendUM): SendUM { + val confirmUM = prevState.confirmUM as? ConfirmUM.Content ?: return prevState + return prevState.copy( + confirmUM = confirmUM.copy( + isPrimaryButtonEnabled = !isSending, + isSending = isSending, + ), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmSentStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmSentStateTransformer.kt new file mode 100644 index 0000000000..c08c566fc2 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmSentStateTransformer.kt @@ -0,0 +1,20 @@ +package com.tangem.features.send.v2.send.confirm.model.transformers + +import com.tangem.blockchain.common.TransactionData +import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM +import com.tangem.features.send.v2.send.ui.state.SendUM +import com.tangem.utils.transformer.Transformer + +internal class SendConfirmSentStateTransformer( + val txData: TransactionData.Uncompiled, + val txUrl: String, +) : Transformer { + override fun transform(prevState: SendUM): SendUM { + return prevState.copy( + confirmUM = ConfirmUM.Success( + transactionDate = txData.date?.timeInMillis ?: System.currentTimeMillis(), + txUrl = txUrl, + ), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt new file mode 100644 index 0000000000..0b7ea0f46f --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt @@ -0,0 +1,55 @@ +package com.tangem.features.send.v2.send.confirm.model.transformers + +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents +import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM +import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList + +internal class SendConfirmationNotificationsTransformer( + private val feeUM: FeeUM, + private val analyticsEventHandler: AnalyticsEventHandler, + private val cryptoCurrency: CryptoCurrency, +) : Transformer { + override fun transform(prevState: ConfirmUM): ConfirmUM { + val state = prevState as? ConfirmUM.Content ?: return prevState + val feeUM = feeUM as? FeeUM.Content ?: return prevState + return state.copy( + notifications = buildList { + addTooHighNotification(feeUM.feeSelectorUM) + addTooLowNotification(feeUM) + }.toPersistentList(), + ) + } + + private fun MutableList.addTooLowNotification(feeUM: FeeUM.Content) { + val feeSelectorUM = feeUM.feeSelectorUM as? FeeSelectorUM.Content ?: return + val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return + val minimumValue = multipleFees.minimum.amount.value ?: return + val customAmount = feeSelectorUM.customValues.firstOrNull() ?: return + val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) + if (feeSelectorUM.selectedType == FeeType.Custom && minimumValue > customValue) { + add(NotificationUM.Warning.FeeTooLow) + analyticsEventHandler.send( + SendAnalyticEvents.NoticeTransactionDelays(cryptoCurrency.symbol), + ) + } + } + + private fun MutableList.addTooHighNotification(feeSelectorUM: FeeSelectorUM) { + if (feeSelectorUM !is FeeSelectorUM.Content) return + + val (isFeeTooHigh, diff) = checkIfFeeTooHigh(feeSelectorUM) + if (isFeeTooHigh) { + add(NotificationUM.Warning.TooHigh(diff)) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt new file mode 100644 index 0000000000..9b602a5c66 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt @@ -0,0 +1,194 @@ +package com.tangem.features.send.v2.send.confirm.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.Keyboard +import com.tangem.core.ui.components.keyboardAsState +import com.tangem.core.ui.components.transactions.TransactionDoneTitle +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM +import com.tangem.features.send.v2.send.ui.state.SendUM +import com.tangem.features.send.v2.subcomponents.amount.SendAmountBlockComponent +import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent +import com.tangem.features.send.v2.subcomponents.notifications +import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.delay + +private const val TAP_HELP_KEY = "TAP_HELP_KEY" +private const val BLOCKS_KEY = "BLOCKS_KEY" +private const val TAP_HELP_ANIMATION_DELAY = 500L + +@Suppress("LongParameterList") +@Composable +internal fun SendConfirmContent( + sendUM: SendUM, + // destinationBlockComponent: SendDestinationBlockComponent, + amountBlockComponent: SendAmountBlockComponent, + feeBlockComponent: SendFeeBlockComponent, + notificationsComponent: NotificationsComponent, + notificationsUM: ImmutableList, +) { + val confirmUM = sendUM.confirmUM as? ConfirmUM.Content + + Column { + LazyColumn( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + blocks( + uiState = sendUM, + // destinationBlockComponent = destinationBlockComponent, + amountBlockComponent = amountBlockComponent, + feeBlockComponent = feeBlockComponent, + ) + if (confirmUM != null) { + tapHelp(isDisplay = confirmUM.showTapHelp) + with(notificationsComponent) { + content( + state = notificationsUM, + isClickDisabled = confirmUM.isSending, + ) + } + notifications( + notifications = confirmUM.notifications, + isClickDisabled = confirmUM.isSending, + ) + } + } + SendingText(confirmUM = confirmUM) + } +} + +@Composable +private fun SendingText(confirmUM: ConfirmUM.Content?, modifier: Modifier = Modifier) { + var isVisibleProxy by remember { mutableStateOf(confirmUM != null) } + val keyboard by keyboardAsState() + + // the text should appear when the keyboard is closed + LaunchedEffect(confirmUM != null, keyboard) { + if (confirmUM != null && keyboard is Keyboard.Opened) { + return@LaunchedEffect + } + isVisibleProxy = confirmUM != null + } + + AnimatedVisibility( + visible = isVisibleProxy, + modifier = modifier, + enter = slideInVertically() + fadeIn(), + exit = fadeOut(tween(durationMillis = 300)), + label = "Animate show sending state text", + ) { + val wrappedConfirmUM = remember(this) { requireNotNull(confirmUM?.sendingFooter) } + Text( + text = wrappedConfirmUM.resolveAnnotatedReference(), + textAlign = TextAlign.Center, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + ) + } +} + +private fun LazyListScope.blocks( + uiState: SendUM, + // destinationBlockComponent: SendDestinationBlockComponent, + amountBlockComponent: SendAmountBlockComponent, + feeBlockComponent: SendFeeBlockComponent, +) { + item(key = BLOCKS_KEY) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + AnimatedVisibility( + visible = uiState.confirmUM is ConfirmUM.Success, + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), + ) { + val wrappedConfirmUM = remember(this) { uiState.confirmUM as ConfirmUM.Success } + TransactionDoneTitle( + title = resourceReference(R.string.sent_transaction_sent_title), + subtitle = resourceReference( + R.string.send_date_format, + wrappedList( + wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter), + wrappedConfirmUM.transactionDate.toTimeFormat(), + ), + ), + modifier = Modifier.padding(vertical = 12.dp), + ) + } + // todo + // destinationBlockComponent.Content(modifier = Modifier) + amountBlockComponent.Content(modifier = Modifier) + feeBlockComponent.Content(modifier = Modifier) + } + } +} + +private fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modifier) { + item(key = TAP_HELP_KEY) { + var wrappedIsDisplay by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = isDisplay) { + delay(TAP_HELP_ANIMATION_DELAY) + wrappedIsDisplay = isDisplay + } + + if (wrappedIsDisplay) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxWidth() + .animateItem() + .padding(top = TangemTheme.dimens.spacing20), + ) { + val background = TangemTheme.colors.button.secondary + Icon( + painter = painterResource(id = R.drawable.ic_send_hint_shape_12), + tint = TangemTheme.colors.button.secondary, + contentDescription = null, + modifier = Modifier, + ) + Text( + text = stringResourceSafe(id = R.string.send_summary_tap_hint), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(background) + .padding( + horizontal = TangemTheme.dimens.spacing14, + vertical = TangemTheme.dimens.spacing12, + ), + ) + } + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/state/ConfirmUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/state/ConfirmUM.kt new file mode 100644 index 0000000000..ae6bc45add --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/state/ConfirmUM.kt @@ -0,0 +1,31 @@ +package com.tangem.features.send.v2.send.confirm.ui.state + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class ConfirmUM { + + abstract val isPrimaryButtonEnabled: Boolean + + data class Content( + override val isPrimaryButtonEnabled: Boolean = false, + val isSending: Boolean, + val showTapHelp: Boolean, + val sendingFooter: TextReference, + val notifications: ImmutableList, + ) : ConfirmUM() + + data class Success( + val transactionDate: Long, + val txUrl: String, + ) : ConfirmUM() { + override val isPrimaryButtonEnabled: Boolean = true + } + + data object Empty : ConfirmUM() { + override val isPrimaryButtonEnabled: Boolean = false + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index b122c64efd..93df96b047 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -7,6 +7,8 @@ import com.tangem.core.decompose.model.Model import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.send.confirm.SendConfirmComponent +import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent @@ -28,7 +30,8 @@ internal class SendModel @Inject constructor( ) : Model(), SendDestinationComponent.ModelCallback, SendAmountComponent.ModelCallback, - SendFeeComponent.ModelCallback { + SendFeeComponent.ModelCallback, + SendConfirmComponent.ModelCallback { private val _uiState = MutableStateFlow(initialState()) val uiState = _uiState.asStateFlow() @@ -39,6 +42,10 @@ internal class SendModel @Inject constructor( var appCurrency: AppCurrency = AppCurrency.Default var predefinedAmountValue: String? = null + override fun onResult(sendUM: SendUM) { + _uiState.value = sendUM + } + override fun onDestinationResult(destinationUM: DestinationUM) { _uiState.update { it.copy(destinationUM = destinationUM) } } @@ -55,5 +62,6 @@ internal class SendModel @Inject constructor( amountUM = AmountState.Empty(), destinationUM = DestinationUM.Empty(), feeUM = FeeUM.Empty(), + confirmUM = ConfirmUM.Empty, ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt index 49a4f918a7..13fc77a18f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.v2.send.ui.state import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM @@ -8,4 +9,5 @@ internal data class SendUM( val amountUM: AmountState, val destinationUM: DestinationUM, val feeUM: FeeUM, + val confirmUM: ConfirmUM, ) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt index b5a67be3a4..26c543ba8d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt @@ -25,6 +25,6 @@ internal class SendFeeComponent( } interface ModelCallback { - fun onFeeResult(state: FeeUM) + fun onFeeResult(feeUM: FeeUM) } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt index 84e193f53d..9ff6f21955 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt @@ -55,7 +55,7 @@ import javax.inject.Inject @Suppress("LongParameterList") @Stable @ModelScoped -class NotificationsModel @Inject constructor( +internal class NotificationsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val appRouter: AppRouter, diff --git a/features/send-v2/impl/src/main/res/drawable/ic_send_hint_shape_12.xml b/features/send-v2/impl/src/main/res/drawable/ic_send_hint_shape_12.xml new file mode 100644 index 0000000000..5a336de030 --- /dev/null +++ b/features/send-v2/impl/src/main/res/drawable/ic_send_hint_shape_12.xml @@ -0,0 +1,9 @@ + + + From 377f2cd6fdceeca49427c9ce0ddc23d653a45777 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 31 Mar 2025 11:09:21 +0400 Subject: [PATCH 067/138] Updated on 2026-08-14 --- .../ResponseCryptoCurrenciesFactory.kt | 8 ++ .../multi/DefaultMultiNetworkStatusFetcher.kt | 46 +++++++ .../DefaultSingleNetworkStatusFetcher.kt | 114 +++++++++++++++++ .../DefaultMultiNetworkStatusFetcherTest.kt | 119 ++++++++++++++++++ .../DefaultSingleNetworkStatusFetcherTest.kt | 88 +++++++++++++ .../data/tokens/utils/NetworkStatusFactory.kt | 2 +- .../multi/MultiNetworkStatusFetcher.kt | 15 +++ .../single/SingleNetworkStatusFetcher.kt | 15 +++ 8 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt create mode 100644 data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt create mode 100644 data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt create mode 100644 domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusFetcher.kt create mode 100644 domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt index 24e9f844e5..a593d255a9 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt @@ -31,6 +31,14 @@ class ResponseCryptoCurrenciesFactory( .toList() } + fun createCurrencies(tokens: List, scanResponse: ScanResponse): List { + return tokens + .asSequence() + .mapNotNull { createCurrency(it, scanResponse) } + .distinctBy(CryptoCurrency::id) + .toList() + } + fun createCurrency(responseToken: UserTokensResponse.Token, scanResponse: ScanResponse): CryptoCurrency? { var blockchain = Blockchain.fromNetworkId(responseToken.networkId) if (blockchain == null || blockchain == Blockchain.Unknown) { diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt new file mode 100644 index 0000000000..71d51a5793 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt @@ -0,0 +1,46 @@ +package com.tangem.data.networks.multi + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.networks.single.SingleNetworkStatusFetcher +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import javax.inject.Inject + +/** + * Default implementation of [MultiNetworkStatusFetcher] + * + * @property singleNetworkStatusFetcher single network status fetcher + * +[REDACTED_AUTHOR] + */ +internal class DefaultMultiNetworkStatusFetcher @Inject constructor( + private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, +) : MultiNetworkStatusFetcher { + + override suspend fun invoke(params: MultiNetworkStatusFetcher.Params): Either = either { + val result = coroutineScope { + params.networks + .map { + async { + singleNetworkStatusFetcher( + params = SingleNetworkStatusFetcher.Params( + userWalletId = params.userWalletId, + network = it, + ), + ) + } + } + .awaitAll() + } + + val failedResult = result.firstOrNull { it.isLeft() } + + ensure(failedResult == null) { + IllegalStateException("Failed to fetch network statuses") + } + } +} \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt new file mode 100644 index 0000000000..9da7afbce4 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt @@ -0,0 +1,114 @@ +package com.tangem.data.networks.single + +import arrow.core.Either +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory +import com.tangem.data.tokens.utils.NetworkStatusFactory +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.networks.single.SingleNetworkStatusFetcher +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +/** + * Default implementation of [SingleNetworkStatusFetcher] + * + * @param excludedBlockchains excluded blockchains + * @property walletManagersFacade wallet managers facade + * @property networksStatusesStore networks statuses store + * @property userWalletsStore user wallets store + * @property appPreferencesStore app preferences store + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +internal class DefaultSingleNetworkStatusFetcher @Inject constructor( + excludedBlockchains: ExcludedBlockchains, + private val walletManagersFacade: WalletManagersFacade, + private val networksStatusesStore: NetworksStatusesStoreV2, + private val userWalletsStore: UserWalletsStore, + private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : SingleNetworkStatusFetcher { + + private val demoConfig = DemoConfig() + private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig, excludedBlockchains) + private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) + private val networkStatusFactory = NetworkStatusFactory() + + override suspend fun invoke(params: SingleNetworkStatusFetcher.Params): Either = Either.catch { + networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network) + + val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) + val networkCurrencies = createCurrencies(userWallet = userWallet, network = params.network) + + val result = withContext(dispatchers.io) { + walletManagersFacade.update( + userWalletId = params.userWalletId, + network = params.network, + extraTokens = networkCurrencies + .filterIsInstance() + .toSet(), + ) + } + + val status = networkStatusFactory.createNetworkStatus( + network = params.network, + result = result, + currencies = networkCurrencies.toSet(), + ) + + networksStatusesStore.storeActual(userWalletId = params.userWalletId, value = status) + } + .onLeft { + Timber.e("Failed to fetch network status for $params: $it") + networksStatusesStore.storeError(userWalletId = params.userWalletId, network = params.network) + } + + private suspend fun createCurrencies(userWallet: UserWallet, network: Network): List { + val blockchain = Blockchain.fromNetworkId(networkId = network.backendId) + + // multi-currency wallet + if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, network = network) + + // check if the blockchain of single-currency wallet is the same as network + val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() + if (cardBlockchain != blockchain) return emptyList() + + // single-currency wallet with token (NODL) + if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + return cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + } + + // single-currency wallet + return cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf) + } + + private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, network: Network): List { + val response = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue), + ) ?: return emptyList() + + return responseCurrenciesFactory.createCurrencies( + tokens = response.tokens.filter { + it.networkId == network.backendId && it.derivationPath == network.derivationPath.value + }, + scanResponse = userWallet.scanResponse, + ) + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt new file mode 100644 index 0000000000..f74995432d --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt @@ -0,0 +1,119 @@ +package com.tangem.data.networks.multi + +import arrow.core.left +import arrow.core.right +import com.google.common.truth.Truth +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.networks.single.SingleNetworkStatusFetcher +import com.tangem.domain.wallets.models.UserWalletId +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultMultiNetworkStatusFetcherTest { + + private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher = mockk() + + private val fetcher = DefaultMultiNetworkStatusFetcher(singleNetworkStatusFetcher = singleNetworkStatusFetcher) + + @Test + fun `fetch networks statuses successfully`() = runTest { + val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) + + val ethParams = SingleNetworkStatusFetcher.Params( + userWalletId = userWalletId, + network = ethereumAndStellar.first(), + ) + + val stellarParams = SingleNetworkStatusFetcher.Params( + userWalletId = userWalletId, + network = ethereumAndStellar.last(), + ) + + coEvery { singleNetworkStatusFetcher(ethParams) } returns Unit.right() + coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right() + + val actual = fetcher(params) + + coVerify { + singleNetworkStatusFetcher(ethParams) + singleNetworkStatusFetcher(stellarParams) + } + + Truth.assertThat(actual.isRight()).isTrue() + } + + @Test + fun `fetch networks statuses failure if one of them fails`() = runTest { + val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) + + val ethParams = SingleNetworkStatusFetcher.Params( + userWalletId = userWalletId, + network = ethereumAndStellar.first(), + ) + + val stellarParams = SingleNetworkStatusFetcher.Params( + userWalletId = userWalletId, + network = ethereumAndStellar.last(), + ) + + val ethException = IllegalStateException("eth") + coEvery { singleNetworkStatusFetcher(ethParams) } returns ethException.left() + coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right() + + val actual = fetcher(params) + + coVerify { + singleNetworkStatusFetcher(ethParams) + singleNetworkStatusFetcher(stellarParams) + } + + val expected = IllegalStateException("Failed to fetch network statuses") + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + + @Test + fun `fetch networks statuses failure if all of them fails`() = runTest { + val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar) + + val ethParams = SingleNetworkStatusFetcher.Params( + userWalletId = userWalletId, + network = ethereumAndStellar.first(), + ) + + val stellarParams = SingleNetworkStatusFetcher.Params( + userWalletId = userWalletId, + network = ethereumAndStellar.last(), + ) + + coEvery { singleNetworkStatusFetcher(ethParams) } returns IllegalStateException("eth").left() + coEvery { singleNetworkStatusFetcher(stellarParams) } returns IllegalStateException("stellar").left() + + val actual = fetcher(params) + + coVerify { + singleNetworkStatusFetcher(ethParams) + singleNetworkStatusFetcher(stellarParams) + } + + val expected = IllegalStateException("Failed to fetch network statuses") + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + + private companion object { + val userWalletId = UserWalletId("011") + val ethereumAndStellar = MockCryptoCurrencyFactory().ethereumAndStellar.map { it.network }.toSet() + } +} \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt new file mode 100644 index 0000000000..b448cddeec --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt @@ -0,0 +1,88 @@ +package com.tangem.data.networks.single + +import com.google.common.truth.Truth +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.networks.single.SingleNetworkStatusFetcher +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.Ordering +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultSingleNetworkStatusFetcherTest { + + private val walletManagersFacade: WalletManagersFacade = mockk(relaxed = true) + private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxed = true) + private val userWalletsStore: UserWalletsStore = mockk(relaxed = true) + + private val fetcher = DefaultSingleNetworkStatusFetcher( + excludedBlockchains = mockk(relaxed = true), + walletManagersFacade = walletManagersFacade, + networksStatusesStore = networksStatusesStore, + userWalletsStore = userWalletsStore, + appPreferencesStore = mockk(relaxed = true), + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `fetch network status successfully`() = runTest { + val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network) + + val result = UpdateWalletManagerResult.MissedDerivation + coEvery { walletManagersFacade.update(userWalletId, network, emptySet()) } returns result + + val actual = fetcher(params) + + coVerify(ordering = Ordering.SEQUENCE) { + networksStatusesStore.refresh(userWalletId = userWalletId, network = network) + userWalletsStore.getSyncStrict(key = userWalletId) + walletManagersFacade.update(userWalletId, network, emptySet()) + networksStatusesStore.storeActual( + userWalletId = userWalletId, + value = NetworkStatus(network, NetworkStatus.MissedDerivation), + ) + } + + Truth.assertThat(actual.isRight()).isTrue() + } + + @Test + fun `fetch network status failure`() = runTest { + val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network) + + val exception = IllegalStateException() + coEvery { userWalletsStore.getSyncStrict(key = userWalletId) } throws exception + + val actual = fetcher(params) + + coVerify(ordering = Ordering.SEQUENCE) { + networksStatusesStore.refresh(userWalletId = userWalletId, network = network) + userWalletsStore.getSyncStrict(key = userWalletId) + networksStatusesStore.storeError(userWalletId = userWalletId, network = network) + } + + coVerify(inverse = true) { + walletManagersFacade.update(userWalletId = any(), network = any(), extraTokens = any()) + networksStatusesStore.storeActual(userWalletId = any(), value = any()) + } + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isEqualTo(exception) + } + + private companion object { + val userWalletId = UserWalletId("011") + val network = MockCryptoCurrencyFactory().ethereum.network + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt index 3ccfb526a8..5f781ffb46 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt @@ -9,7 +9,7 @@ import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import timber.log.Timber -internal class NetworkStatusFactory { +class NetworkStatusFactory { fun createNetworkStatus( network: Network, diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusFetcher.kt b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusFetcher.kt new file mode 100644 index 0000000000..0710aa80d3 --- /dev/null +++ b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusFetcher.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.networks.multi + +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Fetcher of network status [Network] for wallet with [UserWalletId] + * +[REDACTED_AUTHOR] + */ +interface MultiNetworkStatusFetcher : FlowFetcher { + + data class Params(val userWalletId: UserWalletId, val networks: Set) +} \ No newline at end of file diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt new file mode 100644 index 0000000000..791e66ea05 --- /dev/null +++ b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.networks.single + +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Fetcher of network status [Network] for wallet with [UserWalletId] + * +[REDACTED_AUTHOR] + */ +interface SingleNetworkStatusFetcher : FlowFetcher { + + data class Params(val userWalletId: UserWalletId, val network: Network) +} \ No newline at end of file From ded577faffd9e5361bd67139b08d91d4d152a476 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 31 Mar 2025 18:44:49 +0300 Subject: [PATCH 068/138] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 4 +- .../com/tangem/tap/routing/RootContent.kt | 46 ++++++++----- .../component/impl/DefaultRoutingComponent.kt | 10 +-- .../tangem/tap/routing/utils/ChildFactory.kt | 69 +++++++++---------- .../tangem/tap/routing/utils/RouteMappers.kt | 4 +- .../com/tangem/core/ui/res/TangemTheme.kt | 5 ++ .../presentation/wallet/ui/WalletScreen.kt | 7 +- 7 files changed, 77 insertions(+), 68 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 60676fa1f7..6186775651 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -489,7 +489,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac * `android:configChanges="uiMode"` is set in the manifest. * */ - updateAppBackground() + if (routingFeatureToggles.isNavigationRefactoringEnabled.not()) { + updateAppBackground() + } } private fun updateAppBackground() { diff --git a/app/src/main/java/com/tangem/tap/routing/RootContent.kt b/app/src/main/java/com/tangem/tap/routing/RootContent.kt index 669e7f44a3..f320d18a1c 100644 --- a/app/src/main/java/com/tangem/tap/routing/RootContent.kt +++ b/app/src/main/java/com/tangem/tap/routing/RootContent.kt @@ -2,28 +2,36 @@ package com.tangem.tap.routing import android.app.Activity import android.os.Bundle +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat.startActivity +import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.* import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.value.Value import com.tangem.common.routing.AppRoute import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.message.EventMessageEffect +import com.tangem.core.ui.res.LocalIsNavigationRefactoringEnabled import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.tap.routing.component.RoutingComponent +@OptIn(ExperimentalDecomposeApi::class) @Composable internal fun RootContent( - stack: ChildStack, + stack: Value>, uiDependencies: UiDependencies, modifier: Modifier = Modifier, ) { @@ -33,21 +41,15 @@ internal fun RootContent( activity = context as Activity, uiDependencies = uiDependencies, ) { - val snackbarHostState = LocalSnackbarHostState.current + CompositionLocalProvider( + LocalIsNavigationRefactoringEnabled provides true, + ) { + val snackbarHostState = LocalSnackbarHostState.current - Scaffold( - modifier = modifier, - containerColor = TangemTheme.colors.background.primary, - snackbarHost = { - TangemSnackbarHost( - modifier = Modifier.padding(all = 16.dp), - hostState = snackbarHostState, - ) - }, - contentWindowInsets = WindowInsetsZero, - content = { paddingValues -> + Box(Modifier.background(TangemTheme.colors.background.primary)) { Children( - modifier = Modifier.padding(paddingValues), + modifier = modifier, + animation = stackAnimation(fade()), stack = stack, ) { child -> when (val instance = child.instance) { @@ -63,8 +65,16 @@ internal fun RootContent( -> error("Unsupported child: $instance") } } - }, - ) + + TangemSnackbarHost( + modifier = Modifier + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .padding(all = 16.dp), + hostState = snackbarHostState, + ) + } + } EventMessageEffect() } diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index cafffe14d3..759951e25e 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -4,12 +4,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.value.Value import com.arkivanov.decompose.value.subscribe -import com.arkivanov.essenty.backhandler.BackCallback import com.arkivanov.essenty.lifecycle.doOnDestroy import com.google.android.material.snackbar.Snackbar import com.tangem.common.routing.AppRoute @@ -43,19 +41,15 @@ internal class DefaultRoutingComponent @AssistedInject constructor( AppComponentContext by context, SnackbarHandler { - private val backCallback = BackCallback(priority = Int.MIN_VALUE, onBack = router::pop) - override val stack: Value> = childStack( source = navigationProvider.getOrCreateTyped(), initialStack = { getInitialStackOrInit() }, serializer = null, // AppRoute.serializer(), // Disabled until Nav refactoring completes - handleBackButton = false, + handleBackButton = routingFeatureToggles.isNavigationRefactoringEnabled, childFactory = ::child, ) init { - backHandler.register(backCallback) - lifecycle.doOnDestroy { childFactory.doOnDestroy() } @@ -77,8 +71,6 @@ internal class DefaultRoutingComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { - val stack by this.stack.subscribeAsState() - RootContent( modifier = modifier, stack = stack, diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 732fe9d4d0..6fbc0a8a83 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -87,31 +87,28 @@ internal class ChildFactory @Inject constructor( fun createChild(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child { return if (routingFeatureToggles.isNavigationRefactoringEnabled) { - createChildNew(route, contextFactory) + createChildNew(route, contextFactory(route)) } else { createChildLegacy(route, contextFactory) } } @Suppress("LongMethod", "CyclomaticComplexMethod") - private fun createChildNew(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child { - componentContexts[route] = contextFactory(route) - - // region Child creation + private fun createChildNew(route: AppRoute, context: AppComponentContext): Child { return when (route) { is AppRoute.Initial -> { Child.Initial } is AppRoute.Details -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = DetailsComponent.Params(route.userWalletId), componentFactory = detailsComponentFactory, ) } is AppRoute.Disclaimer -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = DisclaimerComponent.Params(route.isTosAccepted), componentFactory = disclaimerComponentFactory, ) @@ -124,14 +121,14 @@ internal class ChildFactory @Inject constructor( } createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = ManageTokensComponent.Params(route.userWalletId, source), componentFactory = manageTokensComponentFactory, ) } is AppRoute.Welcome -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = WelcomeComponent.Params( intent = route.intent, ), @@ -143,14 +140,14 @@ internal class ChildFactory @Inject constructor( } is AppRoute.WalletSettings -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = WalletSettingsComponent.Params(route.userWalletId), componentFactory = walletSettingsComponentFactory, ) } is AppRoute.MarketsTokenDetails -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = MarketsTokenDetailsComponent.Params( token = route.token, appCurrency = route.appCurrency, @@ -167,7 +164,7 @@ internal class ChildFactory @Inject constructor( } is AppRoute.Onramp -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = OnrampComponent.Params( userWalletId = route.userWalletId, cryptoCurrency = route.currency, @@ -178,35 +175,35 @@ internal class ChildFactory @Inject constructor( } is AppRoute.OnrampSuccess -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = OnrampSuccessComponent.Params(route.externalTxId), componentFactory = onrampSuccessComponentFactory, ) } is AppRoute.BuyCrypto -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = BuyCryptoComponent.Params(userWalletId = route.userWalletId), componentFactory = buyCryptoComponentFactory, ) } is AppRoute.SellCrypto -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = SellCryptoComponent.Params(userWalletId = route.userWalletId), componentFactory = sellCryptoComponentFactory, ) } is AppRoute.SwapCrypto -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = SwapSelectTokensComponent.Params(userWalletId = route.userWalletId), componentFactory = swapSelectTokensComponentFactory, ) } is AppRoute.Onboarding -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = OnboardingEntryComponent.Params( scanResponse = route.scanResponse, mode = when (route.mode) { @@ -222,7 +219,7 @@ internal class ChildFactory @Inject constructor( } is AppRoute.Stories -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = StoriesComponent.Params( storyId = route.storyId, nextScreen = route.nextScreen, @@ -233,7 +230,7 @@ internal class ChildFactory @Inject constructor( } is AppRoute.CurrencyDetails -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = TokenDetailsComponent.Params( userWalletId = route.userWalletId, currency = route.currency, @@ -243,7 +240,7 @@ internal class ChildFactory @Inject constructor( } is AppRoute.Staking -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = StakingComponent.Params( userWalletId = route.userWalletId, cryptoCurrencyId = route.cryptoCurrencyId, @@ -254,7 +251,7 @@ internal class ChildFactory @Inject constructor( } is AppRoute.Swap -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = SwapComponent.Params( currencyFrom = route.currencyFrom, currencyTo = route.currencyTo, @@ -268,7 +265,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.Send -> { if (sendFeatureToggles.isSendV2Enabled) { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = com.tangem.features.send.v2.api.SendComponent.Params( userWalletId = route.userWalletId, currency = route.currency, @@ -281,7 +278,7 @@ internal class ChildFactory @Inject constructor( ) } else { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = SendComponent.Params( userWalletId = route.userWalletId, currency = route.currency, @@ -296,7 +293,7 @@ internal class ChildFactory @Inject constructor( } is AppRoute.Home -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = Unit, componentFactory = homeComponentFactory, ) @@ -304,13 +301,13 @@ internal class ChildFactory @Inject constructor( is AppRoute.WalletConnectSessions -> { if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = Unit, componentFactory = redesignedWalletConnectComponentFactory, ) } else { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = Unit, componentFactory = walletConnectComponentFactory, ) @@ -322,7 +319,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.QrScanning.Source.WalletConnect -> SourceType.WALLET_CONNECT } createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = QrScanningComponent.Params( source = source, networkName = (route.source as? AppRoute.QrScanning.Source.Send)?.networkName, @@ -332,35 +329,35 @@ internal class ChildFactory @Inject constructor( } is AppRoute.AccessCodeRecovery -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = Unit, componentFactory = accessCodeRecoveryComponentFactory, ) } is AppRoute.CardSettings -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = CardSettingsComponent.Params(userWalletId = route.userWalletId), componentFactory = cardSettingsComponentFactory, ) } is AppRoute.AppCurrencySelector -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = Unit, componentFactory = appCurrencySelectorComponentFactory, ) } is AppRoute.AppSettings -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = Unit, componentFactory = appSettingsComponentFactory, ) } is AppRoute.DetailsSecurity -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = SecurityModeComponent.Params( userWalletId = route.userWalletId, ), @@ -369,7 +366,7 @@ internal class ChildFactory @Inject constructor( } is AppRoute.ResetToFactory -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = ResetCardComponent.Params( userWalletId = route.userWalletId, cardId = route.cardId, @@ -381,21 +378,21 @@ internal class ChildFactory @Inject constructor( } is AppRoute.ReferralProgram -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = ReferralComponent.Params(route.userWalletId), componentFactory = referralComponentFactory, ) } is AppRoute.PushNotification -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = Unit, componentFactory = pushNotificationsComponentFactory, ) } is AppRoute.Wallet -> { createComponentChild( - contextProvider = contextProvider(route, contextFactory), + context = context, params = Unit, componentFactory = walletComponentFactory, ) diff --git a/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt b/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt index e7aa49f4d9..f20d2b096b 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt @@ -40,11 +40,9 @@ internal fun > createComponentChild( - contextProvider: Provider, + context: AppComponentContext, params: P, componentFactory: F, ): Child { - val context = contextProvider() - return Child.ComposableComponent(componentFactory.create(context, params)) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index de31c28cc4..fd2a88637f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -312,6 +312,11 @@ val LocalBladeAnimation = staticCompositionLocalOf { error("No MainBottomSheetColor provided") } +// TODO remove this after migration to new navigation +val LocalIsNavigationRefactoringEnabled = staticCompositionLocalOf { + false +} + /** * Determines whether the dark theme should be used based on the given [AppThemeMode]. * diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index ff7362b3c2..28aa1a4a5e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -59,6 +59,7 @@ import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalIsNavigationRefactoringEnabled import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme @@ -427,7 +428,11 @@ private inline fun BaseScaffoldWithMarkets( .padding(bottom = 24.dp) .fillMaxWidth(fraction = 0.7f), isVisible = state.showMarketsOnboarding, - availableHeight = maxHeight - statusBarHeight - bottomBarHeight, + availableHeight = if (LocalIsNavigationRefactoringEnabled.current) { + maxHeight + } else { + maxHeight - statusBarHeight - bottomBarHeight + }, bottomSheetState = bottomSheetState, ) } From aea7f5f41586a10179b4fc91ffeeb17d2e90e0b3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Apr 2025 09:06:11 +0300 Subject: [PATCH 069/138] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../tasks/visa/VisaCardActivationTask.kt | 18 +++--- .../visa/VisaCustomerWalletApproveTask.kt | 18 +++--- .../tap/domain/visa/VisaCardScanHandler.kt | 34 ++++------- .../domain/visa/VisaCardScanHandlerError.kt | 9 --- core/error/build.gradle.kts | 12 +--- core/error/ext/.gitignore | 1 + core/error/ext/build.gradle.kts | 16 +++++ .../com/tangem/core/error/ext/Extensions.kt | 27 +++++++++ .../kotlin/com/tangem/core/error/Utils.kt | 14 ----- domain/legacy/build.gradle.kts | 1 + .../common/visa/VisaWalletPublicKeyUtility.kt | 59 +++++++++---------- domain/visa/models/build.gradle.kts | 1 + .../com/tangem/domain/visa/error/VisaError.kt | 51 ++++++++++++++++ .../domain/visa/model/VisaActivationError.kt | 10 ---- features/onboarding-v2/impl/build.gradle.kts | 1 + .../model/OnboardingVisaAccessCodeModel.kt | 25 +++++--- .../model/OnboardingVisaApproveModel.kt | 24 +++++--- .../model/OnboardingVisaInProgressModel.kt | 9 ++- .../model/OnboardingVisaWelcomeModel.kt | 24 +++++--- settings.gradle.kts | 1 + 21 files changed, 220 insertions(+), 136 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandlerError.kt create mode 100644 core/error/ext/.gitignore create mode 100644 core/error/ext/build.gradle.kts create mode 100644 core/error/ext/src/main/kotlin/com/tangem/core/error/ext/Extensions.kt delete mode 100644 core/error/src/main/kotlin/com/tangem/core/error/Utils.kt create mode 100644 domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt delete mode 100644 domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f95653d523..4102dcfb14 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -122,6 +122,7 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.decompose) implementation(projects.core.deepLinks) + implementation(projects.core.error.ext) implementation(projects.libs.crypto) implementation(projects.libs.auth) implementation(projects.libs.blockchainSdk) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt index 78ea74f626..93b23d3999 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -9,11 +9,13 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CompletionCallback +import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.common.map import com.tangem.common.timemeasure.RealtimeMonotonicTimeSource +import com.tangem.core.error.ext.tangemError import com.tangem.crypto.CryptoUtils import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.datasource.local.visa.VisaOTPStorage @@ -22,6 +24,8 @@ import com.tangem.datasource.local.visa.hasSavedOTP import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.visa.error.VisaActivationError +import com.tangem.domain.visa.error.VisaAuthorizationAPIError import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository @@ -68,7 +72,7 @@ class VisaCardActivationTask @AssistedInject constructor( val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) if (card.cardId != activationInput.cardId) { - return CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.WrongCard.message)) + return CompletionResult.Failure(VisaActivationError.WrongCard.tangemError) } val visaActivationRepository = visaActivationRepositoryFactory.create( @@ -149,7 +153,7 @@ class VisaCardActivationTask @AssistedInject constructor( private suspend fun SessionContext.getDataToSign( signedChallenge: VisaAuthSignedChallenge, - ): Either = either { + ): Either = either { catch( block = { val tokens = visaAuthRepository.getAccessTokens(signedChallenge) @@ -158,13 +162,13 @@ class VisaCardActivationTask @AssistedInject constructor( val remoteState = visaActivationRepository.getActivationRemoteState() if (remoteState !is VisaActivationRemoteState.CardWalletSignatureRequired) { - raise(TangemSdkError.Underlying(VisaActivationError.WrongRemoteState.message)) + raise(VisaActivationError.WrongRemoteState.tangemError) } visaActivationRepository.getCardWalletAcceptanceData(remoteState.request) }, catch = { - raise(TangemSdkError.Underlying("Underlying network error: ${it.message ?: ""}")) + raise(VisaAuthorizationAPIError.tangemError) }, ) } @@ -246,10 +250,10 @@ class VisaCardActivationTask @AssistedInject constructor( ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) val derivedPublicKey = wallet.derivedKeys[VisaUtilities.visaDefaultDerivationPath] - ?: return CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.MissingWallet.message)) + ?: return CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError) val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey) - .getOrElse { return CompletionResult.Failure(TangemSdkError.Underlying(it.message)) } + .getOrElse { return CompletionResult.Failure(it.tangemError) } .value val task = SignHashCommand( @@ -291,7 +295,7 @@ class VisaCardActivationTask @AssistedInject constructor( ): CompletionResult { val otp = otpStorage.getOTP(cardId) ?: run { createOTP() - otpStorage.getOTP(cardId) ?: return CompletionResult.Failure(TangemSdkError.Underlying("OTP not found")) + otpStorage.getOTP(cardId) ?: return CompletionResult.Failure(VisaActivationError.MissingRootOTP.tangemError) } val signedActivationData = dataToSign.sign( diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index 778c08b933..18ec47b7fb 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -11,6 +11,7 @@ import com.tangem.common.core.CompletionCallback import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString +import com.tangem.core.error.ext.tangemError import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.common.util.derivationStyleProvider @@ -18,7 +19,7 @@ import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.visa.model.VisaActivationError +import com.tangem.domain.visa.error.VisaActivationError import com.tangem.domain.visa.model.VisaDataForApprove import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet import com.tangem.domain.visa.model.sign @@ -37,6 +38,7 @@ class VisaCustomerWalletApproveTask( } if (VisaUtilities.isVisaCard(card.firmwareVersion.doubleValue, card.batchId)) { + // TODO TVF-21 callback(CompletionResult.Failure(TangemSdkError.Underlying("Can't use Visa card for approve"))) return } @@ -44,7 +46,7 @@ class VisaCustomerWalletApproveTask( if (visaDataForApprove.customerWalletCardId != null && card.cardId != visaDataForApprove.customerWalletCardId) { callback( CompletionResult.Failure( - TangemSdkError.Underlying("Use tangem wallet specified during visa registration"), + TangemSdkError.Underlying("Use tangem wallet specified during visa registration"), // TODO TVF-21 ), ) return @@ -74,16 +76,12 @@ class VisaCustomerWalletApproveTask( } val derivationPath = VisaUtilities.visaDefaultDerivationPath(derivationStyle) ?: run { - callback( - CompletionResult.Failure( - TangemSdkError.Underlying("Failed to generate derivation path with provided derivation style"), - ), - ) + callback(CompletionResult.Failure(VisaActivationError.FailedToCreateAddress.tangemError)) return } val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { - callback(CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.MissingWallet.message))) + callback(CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError)) return } @@ -123,7 +121,7 @@ class VisaCustomerWalletApproveTask( ) validationResult.onLeft { - callback(CompletionResult.Failure(TangemSdkError.Underlying(it.message))) + callback(CompletionResult.Failure(it.tangemError)) return } @@ -144,7 +142,7 @@ class VisaCustomerWalletApproveTask( targetAddress = visaDataForApprove.targetAddress, card = CardDTO(card), ).getOrElse { - callback(CompletionResult.Failure(TangemSdkError.Underlying(it.message))) + callback(CompletionResult.Failure(it.tangemError)) return } diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt index 4b6dd922b9..ad20167f85 100644 --- a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt +++ b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt @@ -8,11 +8,15 @@ import com.tangem.common.core.CardSession import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString +import com.tangem.core.error.ext.tangemError import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility +import com.tangem.domain.visa.error.VisaActivationError +import com.tangem.domain.visa.error.VisaAuthorizationAPIError +import com.tangem.domain.visa.error.VisaCardScanError import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository @@ -71,9 +75,7 @@ internal class VisaCardScanHandler @Inject constructor( val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run { Timber.e("Failed to create derivation path while first scan") - return CompletionResult.Failure( - TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToCreateDerivationPath.errorDescription), - ) + return CompletionResult.Failure(VisaCardScanError.FailedToCreateDerivationPath.tangemError) } val derivationTask = DeriveWalletPublicKeyTask(wallet.publicKey, derivationPath) @@ -104,32 +106,24 @@ internal class VisaCardScanHandler @Inject constructor( val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run { Timber.e("Failed to create derivation path while handling wallet authorization") - return CompletionResult.Failure( - TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToCreateDerivationPath.errorDescription), - ) + return CompletionResult.Failure(VisaCardScanError.FailedToCreateDerivationPath.tangemError) } val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { Timber.e("Failed to find extended public key while handling wallet authorization") - return CompletionResult.Failure( - TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToFindDerivedWalletKey.errorDescription), - ) + return CompletionResult.Failure(VisaCardScanError.FailedToFindWallet.tangemError) } val extendedPublicKey = wallet.derivedKeys[derivationPath] ?: run { Timber.e("Failed to find extended public key while handling wallet authorization") - return CompletionResult.Failure( - TangemSdkError.Underlying(VisaCardScanHandlerError.FailedToFindDerivedWalletKey.errorDescription), - ) + return CompletionResult.Failure(VisaCardScanError.FailedToFindDerivedWalletKey.tangemError) } val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(extendedPublicKey.publicKey) .getOrElse { - return CompletionResult.Failure( - TangemSdkError.Underlying("Cannot generate address on Visa curve"), - ) + return CompletionResult.Failure(it.tangemError) } Timber.i("Requesting challenge for wallet authorization") @@ -200,7 +194,7 @@ internal class VisaCardScanHandler @Inject constructor( ) }.getOrElse { Timber.e("Failed to get challenge for Card authorization. Plain error: ${it.message}") - return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: "Unknown error")) + return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError) } Timber.i("Received challenge to sign: ${challengeResponse.challenge}") @@ -230,11 +224,7 @@ internal class VisaCardScanHandler @Inject constructor( ) }.getOrElse { Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}") - return CompletionResult.Failure( - TangemSdkError.Underlying( - customMessage = it.message ?: "Unknown error", - ), - ) + return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError) } visaAuthTokenStorage.store( @@ -251,7 +241,7 @@ internal class VisaCardScanHandler @Inject constructor( } if (error != null) { - return CompletionResult.Failure(TangemSdkError.Underlying(error.message)) + return CompletionResult.Failure(error.tangemError) } val activationInput = VisaActivationInput( diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandlerError.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandlerError.kt deleted file mode 100644 index d507ac7b14..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandlerError.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.domain.visa - -private const val COMMON_DESCRIPTION = "Error occurred. Please contact support." - -internal enum class VisaCardScanHandlerError(val errorDescription: String) { - FailedToCreateDerivationPath(COMMON_DESCRIPTION), - FailedToFindWallet(COMMON_DESCRIPTION), - FailedToFindDerivedWalletKey(COMMON_DESCRIPTION), -} \ No newline at end of file diff --git a/core/error/build.gradle.kts b/core/error/build.gradle.kts index 59a90763a4..7ff7fb7522 100644 --- a/core/error/build.gradle.kts +++ b/core/error/build.gradle.kts @@ -1,14 +1,4 @@ plugins { - alias(deps.plugins.android.library) - alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.jvm) id("configuration") -} - -android { - namespace = "com.tangem.core.error" -} - -dependencies { - implementation(tangemDeps.card.core) - implementation(tangemDeps.blockchain) } \ No newline at end of file diff --git a/core/error/ext/.gitignore b/core/error/ext/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/core/error/ext/.gitignore @@ -0,0 +1 @@ +/build diff --git a/core/error/ext/build.gradle.kts b/core/error/ext/build.gradle.kts new file mode 100644 index 0000000000..f6e50d5bd6 --- /dev/null +++ b/core/error/ext/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.core.error.ext" +} + +dependencies { + api(projects.core.error) + + implementation(tangemDeps.card.core) + implementation(tangemDeps.blockchain) +} \ No newline at end of file diff --git a/core/error/ext/src/main/kotlin/com/tangem/core/error/ext/Extensions.kt b/core/error/ext/src/main/kotlin/com/tangem/core/error/ext/Extensions.kt new file mode 100644 index 0000000000..587b53931c --- /dev/null +++ b/core/error/ext/src/main/kotlin/com/tangem/core/error/ext/Extensions.kt @@ -0,0 +1,27 @@ +package com.tangem.core.error.ext + +import com.tangem.common.core.TangemSdkError +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.common.core.TangemError +import com.tangem.core.error.UniversalError + +val TangemError.universalError: UniversalError + get() = when (this) { + is TangemSdkError -> object : UniversalError { + override val errorCode: Int = 101000000 + code + } + else -> object : UniversalError { + override val errorCode: Int = code + } + } + +val BlockchainSdkError.universalError: UniversalError + get() = object : UniversalError { + override val errorCode: Int = 102000000 + code + } + +val UniversalError.tangemError: TangemError + get() = object : TangemError(errorCode) { + override var customMessage: String = errorCode.toString() + override val messageResId: Int? = null + } \ No newline at end of file diff --git a/core/error/src/main/kotlin/com/tangem/core/error/Utils.kt b/core/error/src/main/kotlin/com/tangem/core/error/Utils.kt deleted file mode 100644 index daf75b0558..0000000000 --- a/core/error/src/main/kotlin/com/tangem/core/error/Utils.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.core.error - -import com.tangem.common.core.TangemSdkError -import com.tangem.blockchain.common.BlockchainSdkError - -val TangemSdkError.universalError: UniversalError - get() = object : UniversalError { - override val errorCode: Int = 101000000 + code - } - -val BlockchainSdkError.universalError: UniversalError - get() = object : UniversalError { - override val errorCode: Int = 102000000 + code - } \ No newline at end of file diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index 11e080b895..fc7fa7889b 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -12,6 +12,7 @@ android { dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) + implementation(projects.core.error) implementation(projects.common) implementation(projects.libs.auth) implementation(projects.libs.blockchainSdk) diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt index e7500baf4f..2bc3ecbc4a 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt @@ -8,29 +8,33 @@ import com.tangem.blockchain.common.address.AddressType import com.tangem.common.card.EllipticCurve import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.visa.error.VisaActivationError object VisaWalletPublicKeyUtility { - fun validateExtendedPublicKey(targetAddress: String, extendedPublicKey: ExtendedPublicKey): Either = - either { - validatePublicKey( - targetAddress = targetAddress, - publicKey = extendedPublicKey.publicKey, - ).bind() - } - - fun findKeyWithoutDerivation(targetAddress: String, card: CardDTO): Either = either { - val wallet = findWalletOnSecp256k1(card).bind() - + fun validateExtendedPublicKey( + targetAddress: String, + extendedPublicKey: ExtendedPublicKey, + ): Either = either { validatePublicKey( targetAddress = targetAddress, - publicKey = wallet.publicKey, + publicKey = extendedPublicKey.publicKey, ).bind() - - wallet.publicKey } - fun generateAddressOnSecp256k1(walletPublicKey: ByteArray): Either = either { + fun findKeyWithoutDerivation(targetAddress: String, card: CardDTO): Either = + either { + val wallet = findWalletOnSecp256k1(card).bind() + + validatePublicKey( + targetAddress = targetAddress, + publicKey = wallet.publicKey, + ).bind() + + wallet.publicKey + } + + fun generateAddressOnSecp256k1(walletPublicKey: ByteArray): Either = either { val addresses = catch( block = { VisaUtilities.visaBlockchain.makeAddresses( @@ -39,27 +43,22 @@ object VisaWalletPublicKeyUtility { curve = EllipticCurve.Secp256k1, ) }, - catch = { raise(Error.FailedToCreateAddress) }, + catch = { raise(VisaActivationError.FailedToCreateAddress) }, ) - addresses.firstOrNull { it.type == AddressType.Default } ?: raise(Error.FailedToCreateAddress) + addresses.firstOrNull { it.type == AddressType.Default } ?: raise(VisaActivationError.FailedToCreateAddress) } - private fun findWalletOnSecp256k1(card: CardDTO): Either = either { - card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: raise(Error.MissingWalletOnTargetCurve) + private fun findWalletOnSecp256k1(card: CardDTO): Either = either { + card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: raise(VisaActivationError.MissingWallet) } - private fun validatePublicKey(targetAddress: String, publicKey: ByteArray): Either = either { - val address = generateAddressOnSecp256k1(publicKey).bind() + private fun validatePublicKey(targetAddress: String, publicKey: ByteArray): Either = + either { + val address = generateAddressOnSecp256k1(publicKey).bind() - if (address.value != targetAddress) { - raise(Error.AddressNotMatched) + if (address.value != targetAddress) { + raise(VisaActivationError.AddressNotMatched) + } } - } - - enum class Error(val message: String) { - AddressNotMatched("ValidationError: Address not matched"), - FailedToCreateAddress("ValidationError: Failed to create address"), - MissingWalletOnTargetCurve("ValidationError: Missing wallet on target curve"), - } } \ No newline at end of file diff --git a/domain/visa/models/build.gradle.kts b/domain/visa/models/build.gradle.kts index b2df1fba1f..0aafe4291a 100644 --- a/domain/visa/models/build.gradle.kts +++ b/domain/visa/models/build.gradle.kts @@ -10,4 +10,5 @@ dependencies { ksp(deps.moshi.kotlin.codegen) implementation(deps.moshi.adapters) implementation(deps.kotlin.serialization) + implementation(projects.core.error) } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt new file mode 100644 index 0000000000..f73861c111 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt @@ -0,0 +1,51 @@ +@file:Suppress("MagicNumber") + +package com.tangem.domain.visa.error + +import com.tangem.core.error.UniversalError + +/** + * Each error code must follow this format: xxxyyyzzz where + * xxx - Feature code + * yyy - Subsystem code + * zzz - Specific error code + * If you need to add new subsystem add it to list below incrementing last code. + * `Subsystems`: + * `001` - Common API + * `002` - First card scan error + * `003` - Activation + * `004` - Authorization API + */ +object VisaError : UniversalError { + override val errorCode: Int = 104000000 +} + +object VisaAPIError : UniversalError { + override val errorCode: Int = 104001000 +} + +enum class VisaCardScanError( + override val errorCode: Int, +) : UniversalError { + FailedToCreateDerivationPath(104002001), + FailedToFindWallet(104002002), + FailedToFindDerivedWalletKey(104002003), +} + +enum class VisaActivationError( + override val errorCode: Int, +) : UniversalError { + BlockedForActivation(104003001), + InvalidActivationState(104003002), + WrongCard(104003003), + WrongRemoteState(104003004), + MissingWallet(104003005), + MissingRootOTP(104003006), + FailedToCreateAddress(104003007), + AddressNotMatched(104003008), + InconsistentRemoteState(104003009), +} + +object VisaAuthorizationAPIError : UniversalError { + override val errorCode: Int = 104004000 +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt deleted file mode 100644 index ad826a2747..0000000000 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.domain.visa.model - -enum class VisaActivationError(val message: String) { - BlockedForActivation("Card is blocked for activation"), - InvalidActivationState("Invalid activation state"), - WrongCard("Wrong card tapped"), - WrongRemoteState("Wrong remote state"), - MissingWallet("Missing wallet"), - MissingRootOTP("Missing root OTP"), -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index f0b9fcacbb..c557f4a7a8 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(projects.core.decompose) implementation(projects.core.navigation) implementation(projects.core.datasource) + implementation(projects.core.error.ext) /** Common */ implementation(projects.common.ui) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt index 086d02041d..71c5adec28 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt @@ -7,6 +7,11 @@ import com.tangem.common.extensions.toHexString 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.error.ext.universalError +import com.tangem.core.ui.utils.showErrorDialog +import com.tangem.domain.visa.error.VisaAPIError +import com.tangem.domain.visa.error.VisaAuthorizationAPIError import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.domain.visa.model.VisaCardId import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest @@ -34,6 +39,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( @Suppress("UnusedPrivateMember") private val tangemSdkManager: TangemSdkManager, private val visaAuthRepository: VisaAuthRepository, + private val uiMessageSender: UiMessageSender, ) : Model() { private val params: OnboardingVisaAccessCodeComponent.Config = paramsContainer.require() @@ -138,7 +144,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( ) }.getOrElse { loading(false) - // TODO show alert + uiMessageSender.showErrorDialog(VisaAuthorizationAPIError) return@launch } @@ -148,17 +154,22 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( authorizationChallenge = challengeToSign, ), activationInput = activationStatus.activationInput, - ) as? CompletionResult.Success ?: run { - loading(false) - // TODO show alert - return@launch + ) + + val resultData = when (result) { + is CompletionResult.Failure -> { + loading(false) + uiMessageSender.showErrorDialog(result.error.universalError) + return@launch + } + is CompletionResult.Success -> result.data } runCatching { - visaActivationRepository.activateCard(result.data.signedActivationData) + visaActivationRepository.activateCard(resultData.signedActivationData) }.onFailure { loading(false) - // TODO show alert + uiMessageSender.showErrorDialog(VisaAPIError) return@launch } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt index 396a8039b2..a4803e73d0 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/model/OnboardingVisaApproveModel.kt @@ -6,6 +6,10 @@ import com.tangem.common.extensions.toHexString 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.error.ext.universalError +import com.tangem.core.ui.utils.showErrorDialog +import com.tangem.domain.visa.error.VisaAPIError import com.tangem.domain.visa.model.VisaCardId import com.tangem.domain.visa.model.VisaDataForApprove import com.tangem.domain.visa.repository.VisaActivationRepository @@ -27,6 +31,7 @@ internal class OnboardingVisaApproveModel @Inject constructor( visaActivationRepositoryFactory: VisaActivationRepository.Factory, override val dispatchers: CoroutineDispatcherProvider, private val tangemSdkManager: TangemSdkManager, + private val uiMessageSender: UiMessageSender, ) : Model() { private val params = paramsContainer.require() @@ -56,7 +61,7 @@ internal class OnboardingVisaApproveModel @Inject constructor( visaActivationRepository.getCustomerWalletAcceptanceData(params.preparationDataForApprove.request) }.getOrElse { loading(false) - // TODO show dialog + uiMessageSender.showErrorDialog(VisaAPIError) return@launch } @@ -66,17 +71,22 @@ internal class OnboardingVisaApproveModel @Inject constructor( targetAddress = params.preparationDataForApprove.customerWalletAddress, dataToSign = dataToSign, ), - ) as? CompletionResult.Success ?: run { - loading(false) - // TODO show dialog - return@launch + ) + + val resultData = when (result) { + is CompletionResult.Failure -> { + loading(false) + uiMessageSender.showErrorDialog(result.error.universalError) + return@launch + } + is CompletionResult.Success -> result.data } runCatching { - visaActivationRepository.approveByCustomerWallet(result.data) + visaActivationRepository.approveByCustomerWallet(resultData) }.onFailure { loading(false) - // TODO show dialog + uiMessageSender.showErrorDialog(VisaAPIError) return@launch } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt index 57f424606e..b8d608e1d3 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -5,9 +5,13 @@ import com.tangem.common.extensions.toHexString 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.utils.showErrorDialog import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.datasource.local.visa.VisaOTPStorage import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.visa.error.VisaActivationError +import com.tangem.domain.visa.error.VisaAuthorizationAPIError import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository @@ -36,6 +40,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( private val otpStorage: VisaOTPStorage, private val userWalletBuilderFactory: UserWalletBuilder.Factory, private val userWalletsListManager: UserWalletsListManager, + private val uiMessageSender: UiMessageSender, ) : Model() { private val params = paramsContainer.require() @@ -58,7 +63,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( is VisaActivationRemoteState.CardWalletSignatureRequired, VisaActivationRemoteState.BlockedForActivation, -> { - // TODO show alert inconsistent state + uiMessageSender.showErrorDialog(VisaActivationError.InconsistentRemoteState) return@launch } is VisaActivationRemoteState.CustomerWalletSignatureRequired, @@ -92,7 +97,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( val newTokens = runCatching { visaAuthRepository.refreshAccessTokens(authTokens.refreshToken) }.getOrElse { - // TODO show alert + uiMessageSender.showErrorDialog(VisaAuthorizationAPIError) return } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt index 82a7092b99..db3f28bd1a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt @@ -5,6 +5,10 @@ import com.tangem.common.extensions.toHexString 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.error.ext.universalError +import com.tangem.core.ui.utils.showErrorDialog +import com.tangem.domain.visa.error.VisaAPIError import com.tangem.domain.visa.model.VisaCardId import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest import com.tangem.domain.visa.repository.VisaActivationRepository @@ -28,6 +32,7 @@ internal class OnboardingVisaWelcomeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val tangemSdkManager: TangemSdkManager, private val visaActivationRepositoryFactory: VisaActivationRepository.Factory, + private val uiMessageSender: UiMessageSender, ) : Model() { private val params = paramsContainer.require() @@ -68,24 +73,29 @@ internal class OnboardingVisaWelcomeModel @Inject constructor( visaActivationRepository.getCardWalletAcceptanceData(params.dataToSignRequest) }.getOrElse { loading(false) - // TODO show alert + uiMessageSender.showErrorDialog(VisaAPIError) return@launch } val result = tangemSdkManager.activateVisaCard( mode = VisaCardActivationTaskMode.SignOnly(dataToSignByCardWallet = dataToSignByCardWallet), activationInput = params.activationInput, - ) as? CompletionResult.Success ?: run { - loading(false) - // TODO show alert - return@launch + ) + + val resultData = when (result) { + is CompletionResult.Failure -> { + loading(false) + uiMessageSender.showErrorDialog(result.error.universalError) + return@launch + } + is CompletionResult.Success -> result.data } runCatching { - visaActivationRepository.activateCard(result.data.signedActivationData) + visaActivationRepository.activateCard(resultData.signedActivationData) }.onFailure { loading(false) - // TODO show alert + uiMessageSender.showErrorDialog(VisaAPIError) return@launch } diff --git a/settings.gradle.kts b/settings.gradle.kts index b4efe097b4..cbcae1d8bb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -149,6 +149,7 @@ include(":core:deep-links:global") include(":core:decompose") include(":core:pagination") include(":core:error") +include(":core:error:ext") // endregion Core modules // region Common modules From c83ce2ada3e3a3413a40cdee6ade976e8ee831bd Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Apr 2025 13:10:07 +0500 Subject: [PATCH 070/138] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 9 + .../tangem/tap/routing/utils/ChildFactory.kt | 14 ++ .../com/tangem/common/routing/AppRoute.kt | 5 + .../local/nft/DefaultNFTRuntimeStore.kt | 38 ++-- .../datasource/local/nft/NFTRuntimeStore.kt | 4 +- .../nft/converter/NFTSdkAssetConverter.kt | 9 +- .../converter/NFTSdkCollectionConverter.kt | 11 + .../ui/components/fields}/InputManager.kt | 4 +- .../tangem/data/nft/DefaultNFTRepository.kt | 204 ++++++++++++++++-- .../DefaultWalletManagersFacade.kt | 17 +- .../walletmanager/WalletManagersFacade.kt | 9 +- .../com/tangem/domain/nft/models/NFTAsset.kt | 60 +++--- .../tangem/domain/nft/models/NFTCollection.kt | 13 +- .../domain/nft/models/NFTCollections.kt | 27 +++ .../tangem/domain/nft/models/NFTSalePrice.kt | 6 +- .../nft/FetchNFTCollectionAssetsUseCase.kt | 15 ++ .../domain/nft/GetNFTCollectionsUseCase.kt | 2 +- .../domain/nft/repository/NFTRepository.kt | 3 + features/nft/api/build.gradle.kts | 1 + .../nft/component/NFTCollectionsComponent.kt | 14 ++ features/nft/impl/build.gradle.kts | 3 + .../tangem/features/nft/NFTFeatureModule.kt | 20 ++ .../DefaultNFTCollectionsComponent.kt | 37 ++++ .../entity/NFTCollectionAssetsListUM.kt | 12 +- .../nft/collections/entity/NFTCollectionUM.kt | 1 + .../entity/NFTCollectionsStateUM.kt | 6 + .../entity/NFTCollectionsWarningUM.kt | 5 +- ...hangeCollectionExpandedStateTransformer.kt | 34 +++ .../transformer/ToggleSearchBarTransformer.kt | 22 ++ .../transformer/UpdateDataStateTransformer.kt | 134 ++++++++++++ .../UpdateSearchQueryTransformer.kt | 22 ++ .../collections/model/NFTCollectionsModel.kt | 138 ++++++++++++ .../nft/collections/ui/NFTCollection.kt | 21 +- .../nft/collections/ui/NFTCollectionAsset.kt | 16 +- .../nft/collections/ui/NFTCollections.kt | 49 +++++ .../collections/ui/NFTCollectionsContent.kt | 197 ++++++++++------- .../collections/ui/NFTCollectionsFailed.kt | 2 +- features/onramp/impl/build.gradle.kts | 1 + .../main/model/OnrampMainComponentModel.kt | 2 +- .../model/OnrampSelectCountryModel.kt | 2 +- .../model/OnrampSelectCurrencyModel.kt | 2 +- .../model/AvailableSwapPairsModel.kt | 6 +- .../tokenlist/model/OnrampTokenListModel.kt | 2 +- .../intents/WalletContentClickIntents.kt | 7 + .../common/preview/WalletScreenPreviewData.kt | 1 + .../router/DefaultWalletRouter.kt | 4 + .../presentation/router/InnerWalletRouter.kt | 3 + .../wallet/state/model/WalletNFTItemUM.kt | 5 +- .../SetNFTCollectionsTransformer.kt | 45 ++-- .../subscribers/WalletNFTListSubscriber.kt | 12 +- .../presentation/wallet/ui/WalletScreen.kt | 1 - .../wallet/ui/components/WalletNFTItem.kt | 20 +- .../MultiCurrencyNFTCollections.kt | 3 +- 53 files changed, 1062 insertions(+), 238 deletions(-) rename {features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils => core/ui/src/main/java/com/tangem/core/ui/components/fields}/InputManager.kt (88%) create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionAssetsUseCase.kt create mode 100644 features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTCollectionsComponent.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/DefaultNFTCollectionsComponent.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsStateUM.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ChangeCollectionExpandedStateTransformer.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ToggleSearchBarTransformer.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateSearchQueryTransformer.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollections.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index 039fa951ff..f9972a842d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -1,5 +1,6 @@ package com.tangem.tap.di.domain +import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase import com.tangem.domain.nft.FetchNFTCollectionsUseCase import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.nft.repository.NFTRepository @@ -37,4 +38,12 @@ internal object NFTDomainModule { nftRepository = nftRepository, ) } + + @Provides + @Singleton + fun providesFetchNFTCollectionAssetsUseCase(nftRepository: NFTRepository): FetchNFTCollectionAssetsUseCase { + return FetchNFTCollectionAssetsUseCase( + nftRepository = nftRepository, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 6fbc0a8a83..b68f419f3f 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -12,6 +12,7 @@ import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.markets.details.MarketsTokenDetailsComponent +import com.tangem.features.nft.component.NFTCollectionsComponent import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent import com.tangem.features.onramp.component.* import com.tangem.features.pushnotifications.api.PushNotificationsComponent @@ -80,6 +81,7 @@ internal class ChildFactory @Inject constructor( private val sendComponentFactoryV2: com.tangem.features.send.v2.api.SendComponent.Factory, private val sendFeatureToggles: SendFeatureToggles, private val redesignedWalletConnectComponentFactory: RedisegnedWalletConnectComponent.Factory, + private val nftCollectionsComponentFactory: NFTCollectionsComponent.Factory, private val testerRouter: TesterRouter, private val routingFeatureToggles: RoutingFeatureToggles, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, @@ -397,6 +399,12 @@ internal class ChildFactory @Inject constructor( componentFactory = walletComponentFactory, ) } + is AppRoute.NFTCollections -> + createComponentChild( + context = context, + params = NFTCollectionsComponent.Params(userWalletId = route.userWalletId), + componentFactory = nftCollectionsComponentFactory, + ) is AppRoute.OnboardingNote, is AppRoute.SaveWallet, is AppRoute.OnboardingOther, @@ -729,6 +737,12 @@ internal class ChildFactory @Inject constructor( componentFactory = storiesComponentFactory, ) } + is AppRoute.NFTCollections -> + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = NFTCollectionsComponent.Params(userWalletId = route.userWalletId), + componentFactory = nftCollectionsComponentFactory, + ) } // endregion } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 6619d29662..60e5d50f16 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -277,4 +277,9 @@ sealed class AppRoute(val path: String) : Route { val nextScreen: AppRoute, val screenSource: String, ) : AppRoute(path = "/stories$storyId") + + @Serializable + data class NFTCollections( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/nft_collections/${userWalletId.stringValue}") } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt index 753597e44a..cf693aab39 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt @@ -42,7 +42,7 @@ internal class DefaultNFTRuntimeStore( ) } - override fun getAsset(collectionId: NFTCollection.Identifier, assetId: NFTAsset.Identifier): Flow = + override fun getAsset(collectionId: NFTCollection.Identifier, assetId: NFTAsset.Identifier): Flow = collectionsRuntimeStore .get() .combine(getSalePrice(assetId)) { collectionsData, price -> @@ -50,13 +50,17 @@ internal class DefaultNFTRuntimeStore( .getCollection(collectionId) ?.getAsset(assetId) ?.mergeWithPrice(price) - ?: NFTAsset.Error(assetId) } override fun getSalePrice(assetId: NFTAsset.Identifier): Flow = pricesRuntimeStore .get() .map { it[assetId] ?: NFTSalePrice.Empty(assetId) } + override suspend fun getSalePriceSync(assetId: NFTAsset.Identifier): NFTSalePrice = pricesRuntimeStore + .getSyncOrNull() + ?.let { it[assetId] } + ?: NFTSalePrice.Empty(assetId) + override suspend fun saveCollections(collections: NFTCollections) { collectionsRuntimeStore.store(collections) } @@ -72,8 +76,13 @@ internal class DefaultNFTRuntimeStore( ?.collections ?.firstOrNull { it.id == collectionId } - private fun NFTCollection.getAsset(assetId: NFTAsset.Identifier): NFTAsset? = - assets.firstOrNull { it.id == assetId } + private fun NFTCollection.getAsset(assetId: NFTAsset.Identifier): NFTAsset? = when (val assets = assets) { + is NFTCollection.Assets.Empty, + is NFTCollection.Assets.Loading, + is NFTCollection.Assets.Failed, + -> null + is NFTCollection.Assets.Value -> assets.items.firstOrNull { it.id == assetId } + } private fun NFTCollections.mergeWithPrices(prices: Map): NFTCollections = when (val content = this.content) { @@ -89,18 +98,23 @@ internal class DefaultNFTRuntimeStore( copy( collections = this.collections?.map { data -> data.copy( - assets = data.assets.map { asset -> - asset.mergeWithPrice(prices[asset.id] ?: NFTSalePrice.Empty(asset.id)) + assets = when (val assets = data.assets) { + is NFTCollection.Assets.Empty, + is NFTCollection.Assets.Loading, + is NFTCollection.Assets.Failed, + -> assets + is NFTCollection.Assets.Value -> assets.copy( + items = assets.items.map { asset -> + asset.mergeWithPrice(prices[asset.id] ?: NFTSalePrice.Empty(asset.id)) + }, + ) }, ) }, source = this.source, ) - private fun NFTAsset.mergeWithPrice(price: NFTSalePrice): NFTAsset = when (this) { - is NFTAsset.Error -> this - is NFTAsset.Value -> copy( - salePrice = price, - ) - } + private fun NFTAsset.mergeWithPrice(price: NFTSalePrice): NFTAsset = copy( + salePrice = price, + ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTRuntimeStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTRuntimeStore.kt index e9b27f5d0c..8c211d1f8e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTRuntimeStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTRuntimeStore.kt @@ -16,7 +16,9 @@ interface NFTRuntimeStore { fun getAsset(collectionId: NFTCollection.Identifier, assetId: NFTAsset.Identifier): Flow - fun getSalePrice(assetId: NFTAsset.Identifier): Flow + fun getSalePrice(assetId: NFTAsset.Identifier): Flow + + suspend fun getSalePriceSync(assetId: NFTAsset.Identifier): NFTSalePrice suspend fun saveCollections(collections: NFTCollections) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt index add09e2c0a..db95b8ce76 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt @@ -12,7 +12,7 @@ object NFTSdkAssetConverter : Converter, NFTAsset> { val (network, asset) = value val assetId = NFTSdkAssetIdentifierConverter.convert(asset.identifier) val collectionId = NFTSdkCollectionIdentifierConverter.convert(asset.collectionIdentifier) - return NFTAsset.Value( + return NFTAsset( id = assetId, collectionId = collectionId, network = network, @@ -25,23 +25,22 @@ object NFTSdkAssetConverter : Converter, NFTAsset> { assetId = assetId, value = it.value, symbol = it.symbol, - source = StatusSource.CACHE, ) } ?: NFTSalePrice.Empty(assetId = assetId), rarity = asset.rarity?.let { - NFTAsset.Value.Rarity( + NFTAsset.Rarity( rank = it.rank, label = it.label, ) }, media = asset.media?.let { - NFTAsset.Value.Media( + NFTAsset.Media( url = it.url, mimetype = it.mimetype, ) }, traits = asset.traits.map { - NFTAsset.Value.Trait( + NFTAsset.Trait( name = it.name, value = it.value, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt index ed9a5ed3ec..603e003ccb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt @@ -1,5 +1,6 @@ package com.tangem.datasource.local.nft.converter +import com.tangem.domain.models.StatusSource import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.tokens.model.Network @@ -23,6 +24,16 @@ object NFTSdkCollectionConverter : Converter, NF } .filter { it.id !is NFTAsset.Identifier.Unknown + } + .let { + if (it.isEmpty()) { + NFTCollection.Assets.Empty + } else { + NFTCollection.Assets.Value( + items = it, + source = StatusSource.CACHE, + ) + } }, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/InputManager.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/InputManager.kt similarity index 88% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/InputManager.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/fields/InputManager.kt index b3add7d7ee..42e959887b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/InputManager.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/InputManager.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.utils +package com.tangem.core.ui.components.fields import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.withDebounce @@ -12,7 +12,7 @@ import javax.inject.Inject * [REDACTED_AUTHOR] */ -internal class InputManager @Inject constructor() { +class InputManager @Inject constructor() { val query: Flow get() = _query diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index 5a89fbe57f..ebf03a5f42 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -9,6 +9,7 @@ import com.tangem.datasource.local.nft.NFTRuntimeStore import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory import com.tangem.datasource.local.nft.converter.NFTSdkAssetIdentifierConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter +import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter import com.tangem.domain.models.StatusSource import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollections @@ -24,9 +25,12 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch +import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject +import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection +@Suppress("LargeClass") internal class DefaultNFTRepository @Inject constructor( private val nftPersistenceStoreFactory: NFTPersistenceStoreFactory, private val nftRuntimeStoreFactory: NFTRuntimeStoreFactory, @@ -34,10 +38,11 @@ internal class DefaultNFTRepository @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, ) : NFTRepository { - private val jobs = mutableMapOf() + private val networkJobs = ConcurrentHashMap() + private val collectionJobs = ConcurrentHashMap() - private val nftRuntimeStores = mutableMapOf() - private val nftPersistenceStores = mutableMapOf() + private val nftRuntimeStores = ConcurrentHashMap() + private val nftPersistenceStores = ConcurrentHashMap() override fun observeCollections(userWalletId: UserWalletId, networks: List): Flow> = flow { emitAll(observeCollectionsInternal(userWalletId, networks)) } @@ -64,15 +69,9 @@ internal class DefaultNFTRepository @Inject constructor( launch(dispatchers.io) { Either.catch { expireCollections(userWalletId, network) - walletManagersFacade.getNFTCollections(userWalletId, network) - }.onLeft { - saveFailedStateInRuntime( - userWalletId = userWalletId, - network = network, - error = it, - ) - }.onRight { - val mergedCollections = it.mergeWithStoredAssets(userWalletId, network) + + val collections = walletManagersFacade.getNFTCollections(userWalletId, network) + val mergedCollections = collections.mergeWithStoredAssets(userWalletId, network) saveCollectionsInRuntime( userWalletId = userWalletId, @@ -84,14 +83,112 @@ internal class DefaultNFTRepository @Inject constructor( network = network, collections = mergedCollections, ) + }.onLeft { + saveFailedStateInRuntime( + userWalletId = userWalletId, + network = network, + error = it, + ) } - }.saveIn(getJobHolder(network)) + }.saveIn(getNetworkJobHolder(network)) } else { null } }.joinAll() } + override suspend fun refreshAssets( + userWalletId: UserWalletId, + network: Network, + collectionId: NFTCollection.Identifier, + ) = coroutineScope { + launch(dispatchers.io) { + Either.catch { + expireAssets(userWalletId, network, collectionId) + + val sdkCollectionId = NFTSdkCollectionIdentifierConverter.convertBack(collectionId) + + val assets = walletManagersFacade.getNFTAssets( + userWalletId = userWalletId, + network = network, + collectionIdentifier = sdkCollectionId, + ) + + assets.forEach { + val assetId = NFTSdkAssetIdentifierConverter.convert(it.identifier) + val price = getNFTRuntimeStore(userWalletId, network).getSalePriceSync(assetId) + if (price is NFTSalePrice.Error) { + refreshSalePrice(userWalletId, network, sdkCollectionId, it.identifier) + } + } + + getNFTPersistenceStore(userWalletId, network) + .getCollectionsSync() + ?.map { + if (it.identifier == sdkCollectionId) { + it.copy(assets = assets) + } else { + it + } + } + ?.let { + saveCollectionsInRuntime( + userWalletId = userWalletId, + network = network, + collections = it, + ) + saveCollectionsInPersistence( + userWalletId = userWalletId, + network = network, + collections = it, + ) + } + }.onLeft { + saveFailedStateInRuntime( + userWalletId = userWalletId, + network = network, + error = it, + ) + } + }.saveIn(getCollectionJobHolder(collectionId)).join() + } + + private suspend fun refreshSalePrice( + userWalletId: UserWalletId, + network: Network, + sdkCollectionId: SdkNFTCollection.Identifier, + sdkAssetId: SdkNFTAsset.Identifier, + ) = coroutineScope { + launch(dispatchers.io) { + val assetId = NFTSdkAssetIdentifierConverter.convert(sdkAssetId) + + Either.catch { + saveSalePriceInRuntime(userWalletId, network, NFTSalePrice.Loading(assetId)) + + val sdkSalePrice = + walletManagersFacade.getNFTSalePrice(userWalletId, network, sdkCollectionId, sdkAssetId) + + val salePrice = if (sdkSalePrice == null) { + NFTSalePrice.Empty(assetId) + } else { + NFTSalePrice.Value( + assetId = assetId, + value = sdkSalePrice.value, + symbol = sdkSalePrice.symbol, + ) + } + + saveSalePriceInRuntime(userWalletId, network, salePrice) + + sdkSalePrice?.let { + saveSalePriceInPersistence(userWalletId, network, sdkAssetId, it) + } + }.onLeft { + saveSalePriceInRuntime(userWalletId, network, NFTSalePrice.Error(assetId)) + } + } + } + private suspend fun expireCollections(userWalletId: UserWalletId, network: Network) { val runtimeStore = getNFTRuntimeStore(userWalletId, network) val expiredCollections = runtimeStore @@ -100,6 +197,23 @@ internal class DefaultNFTRepository @Inject constructor( runtimeStore.saveCollections(expiredCollections) } + private suspend fun expireAssets( + userWalletId: UserWalletId, + network: Network, + collectionId: NFTCollection.Identifier, + ) { + val runtimeStore = getNFTRuntimeStore(userWalletId, network) + val storedCollections = runtimeStore.getCollectionsSync() + val expiredCollections = storedCollections + .changeCollectionAssetsStatusSource(collectionId, StatusSource.CACHE) + .let { + storedCollections.copy( + content = it, + ) + } + runtimeStore.saveCollections(expiredCollections) + } + private suspend fun saveCollectionsInRuntime( userWalletId: UserWalletId, network: Network, @@ -126,7 +240,9 @@ internal class DefaultNFTRepository @Inject constructor( getNFTRuntimeStore(userWalletId, network).let { store -> val storedCollections = store.getCollectionsSync() val content = storedCollections.content - val updatedCollections = if (content is NFTCollections.Content.Collections && content.collections != null) { + val updatedCollections = if (content is NFTCollections.Content.Collections && + !content.collections.isNullOrEmpty() + ) { // if there is any cached collections in store, then mark them as not actual and emit anyway storedCollections.changeStatusSource(StatusSource.ONLY_CACHE) } else { @@ -148,15 +264,35 @@ internal class DefaultNFTRepository @Inject constructor( getNFTPersistenceStore(userWalletId, network).saveCollections(collections) } - private fun getJobHolder(network: Network): JobHolder = jobs[network] ?: run { + private suspend fun saveSalePriceInRuntime(userWalletId: UserWalletId, network: Network, salePrice: NFTSalePrice) { + getNFTRuntimeStore(userWalletId, network).saveSalePrice(salePrice) + } + + private suspend fun saveSalePriceInPersistence( + userWalletId: UserWalletId, + network: Network, + assetId: SdkNFTAsset.Identifier, + salePrice: SdkNFTAsset.SalePrice, + ) { + getNFTPersistenceStore(userWalletId, network).saveSalePrice(assetId, salePrice) + } + + private fun getNetworkJobHolder(network: Network): JobHolder = networkJobs.getOrPut(network) { JobHolder().also { - jobs[network] = it + networkJobs[network] = it } } + private fun getCollectionJobHolder(collectionId: NFTCollection.Identifier): JobHolder = + collectionJobs.getOrPut(collectionId) { + JobHolder().also { + collectionJobs[collectionId] = it + } + } + private fun getNFTPersistenceStore(userWalletId: UserWalletId, network: Network): NFTPersistenceStore { val storeId = (userWalletId to network).formatted() - return nftPersistenceStores[storeId] ?: run { + return nftPersistenceStores.getOrPut(storeId) { nftPersistenceStoreFactory.provide(userWalletId, network).also { nftPersistenceStores[storeId] = it } @@ -165,7 +301,7 @@ internal class DefaultNFTRepository @Inject constructor( private suspend fun getNFTRuntimeStore(userWalletId: UserWalletId, network: Network): NFTRuntimeStore { val storeId = (userWalletId to network).formatted() - return nftRuntimeStores[storeId] ?: run { + return nftRuntimeStores.getOrPut(storeId) { nftRuntimeStoreFactory.provide(network).also { nftRuntimeStores[storeId] = it val storedCollections = getStoredCollections(userWalletId, network) @@ -213,7 +349,6 @@ internal class DefaultNFTRepository @Inject constructor( assetId = assetId, value = price.value, symbol = price.symbol, - source = StatusSource.CACHE, ) } } @@ -227,6 +362,37 @@ internal class DefaultNFTRepository @Inject constructor( }, ) + private fun NFTCollections.changeCollectionAssetsStatusSource( + collectionId: NFTCollection.Identifier, + source: StatusSource, + ) = when (val content = content) { + is NFTCollections.Content.Collections -> + content + .copy( + collections = content + .collections + ?.map { + if (it.id == collectionId) { + it.changeAssetsStatusSource(source) + } else { + it + } + }, + ) + is NFTCollections.Content.Error -> content + } + + private fun NFTCollection.changeAssetsStatusSource(source: StatusSource) = copy( + assets = when (val assets = this.assets) { + is NFTCollection.Assets.Empty -> NFTCollection.Assets.Loading + is NFTCollection.Assets.Loading -> assets + is NFTCollection.Assets.Failed -> assets + is NFTCollection.Assets.Value -> assets.copy( + source = source, + ) + }, + ) + private suspend fun List.mergeWithStoredAssets( userWalletId: UserWalletId, network: Network, diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 5dc33bd1cd..d15b9f68bd 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -667,7 +667,7 @@ class DefaultWalletManagersFacade( return walletManager.getAssets(address, collectionIdentifier) } - override suspend fun getAsset( + override suspend fun getNFTAsset( userWalletId: UserWalletId, network: Network, collectionIdentifier: NFTCollection.Identifier, @@ -682,6 +682,21 @@ class DefaultWalletManagersFacade( return walletManager.getAsset(collectionIdentifier, assetIdentifier) } + override suspend fun getNFTSalePrice( + userWalletId: UserWalletId, + network: Network, + collectionIdentifier: NFTCollection.Identifier, + assetIdentifier: NFTAsset.Identifier, + ): NFTAsset.SalePrice? { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) ?: return null + return walletManager.getSalePrice(collectionIdentifier, assetIdentifier) + } + override suspend fun isAccountInitialized(userWalletId: UserWalletId, network: Network): Boolean { val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network) val initializableAccountWalletManger = walletManager as? InitializableAccount ?: return true diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index e71d19ab0f..b40c497186 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -259,13 +259,20 @@ interface WalletManagersFacade { collectionIdentifier: NFTCollection.Identifier, ): List - suspend fun getAsset( + suspend fun getNFTAsset( userWalletId: UserWalletId, network: Network, collectionIdentifier: NFTCollection.Identifier, assetIdentifier: NFTAsset.Identifier, ): NFTAsset? + suspend fun getNFTSalePrice( + userWalletId: UserWalletId, + network: Network, + collectionIdentifier: NFTCollection.Identifier, + assetIdentifier: NFTAsset.Identifier, + ): NFTAsset.SalePrice? + /** * If wallet manager implements [InitializableAccount] then returns [InitializableAccount.isAccountInitialized] * value. Otherwise always return true diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt index f2cbe202c4..761db519d4 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt @@ -3,43 +3,35 @@ package com.tangem.domain.nft.models import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.Network -sealed class NFTAsset { - abstract val id: Identifier +data class NFTAsset( + val id: Identifier, + val collectionId: NFTCollection.Identifier, + val network: Network, + val contractType: String, + val owner: String?, + val name: String?, + val description: String?, + val salePrice: NFTSalePrice, + val rarity: Rarity?, + val media: Media?, + val traits: List, + val source: StatusSource, +) { - data class Value( - override val id: Identifier, - val collectionId: NFTCollection.Identifier, - val network: Network, - val contractType: String, - val owner: String?, - val name: String?, - val description: String?, - val salePrice: NFTSalePrice, - val rarity: Rarity?, - val media: Media?, - val traits: List, - val source: StatusSource, - ) : NFTAsset() { + data class Media( + val mimetype: String?, + val url: String, + ) - data class Media( - val mimetype: String, - val url: String, - ) + data class Rarity( + val rank: String, + val label: String, + ) - data class Rarity( - val rank: String, - val label: String, - ) - - data class Trait( - val name: String, - val value: String, - ) - } - - data class Error( - override val id: Identifier, - ) : NFTAsset() + data class Trait( + val name: String, + val value: String, + ) sealed class Identifier { data class EVM( diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollection.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollection.kt index 55563ea813..58b5aea439 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollection.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollection.kt @@ -1,5 +1,6 @@ package com.tangem.domain.nft.models +import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.Network data class NFTCollection( @@ -9,8 +10,18 @@ data class NFTCollection( val description: String?, val logoUrl: String?, val count: Int, - val assets: List = emptyList(), + val assets: Assets, ) { + sealed class Assets { + data object Empty : Assets() + data object Loading : Assets() + data object Failed : Assets() + data class Value( + val items: List, + val source: StatusSource, + ) : Assets() + } + sealed class Identifier { data class EVM(val tokenAddress: String) : Identifier() data class TON(val contractAddress: String?) : Identifier() diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollections.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollections.kt index bd97bf4cfe..42eb1cc51e 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollections.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollections.kt @@ -27,4 +27,31 @@ data class NFTCollections( ), ) } +} + +fun List.allCollectionsFailed() = this.all { + it.content is NFTCollections.Content.Error +} + +fun List.anyCollectionFailed() = this.any { + it.content is NFTCollections.Content.Error || + it.content is NFTCollections.Content.Collections && + it.content.source == StatusSource.ONLY_CACHE +} + +fun List.allLoadedCollectionsEmpty() = this + .map { it.content } + .filterIsInstance() + .all { it.collections.isNullOrEmpty() } + +fun List.allCollectionsLoaded() = this.all { + val content = it.content + content is NFTCollections.Content.Collections && + content.source != StatusSource.CACHE +} + +fun List.allCollectionsEmpty() = this.all { + val content = it.content + content is NFTCollections.Content.Collections && + content.collections.isNullOrEmpty() } \ No newline at end of file diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt index 3ef25c436e..26b9c2e17f 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt @@ -1,6 +1,5 @@ package com.tangem.domain.nft.models -import com.tangem.domain.models.StatusSource import java.math.BigDecimal sealed class NFTSalePrice { @@ -10,6 +9,10 @@ sealed class NFTSalePrice { override val assetId: NFTAsset.Identifier, ) : NFTSalePrice() + data class Loading( + override val assetId: NFTAsset.Identifier, + ) : NFTSalePrice() + data class Error( override val assetId: NFTAsset.Identifier, ) : NFTSalePrice() @@ -18,6 +21,5 @@ sealed class NFTSalePrice { override val assetId: NFTAsset.Identifier, val value: BigDecimal, val symbol: String, - val source: StatusSource, ) : NFTSalePrice() } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionAssetsUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionAssetsUseCase.kt new file mode 100644 index 0000000000..66d5f1c780 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionAssetsUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.nft + +import com.tangem.domain.nft.models.NFTCollection +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +class FetchNFTCollectionAssetsUseCase( + private val nftRepository: NFTRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, network: Network, collectionId: NFTCollection.Identifier) { + nftRepository.refreshAssets(userWalletId, network, collectionId) + } +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt index f59d8578b6..6619b45d46 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt @@ -14,7 +14,7 @@ class GetNFTCollectionsUseCase( ) { @OptIn(ExperimentalCoroutinesApi::class) - fun launch(userWalletId: UserWalletId): Flow> = currenciesRepository + operator fun invoke(userWalletId: UserWalletId): Flow> = currenciesRepository .getWalletCurrenciesUpdates(userWalletId) .flatMapLatest { val networks = it diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt index 9a220eb202..7ab39c2bff 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt @@ -1,5 +1,6 @@ package com.tangem.domain.nft.repository +import com.tangem.domain.nft.models.NFTCollection import com.tangem.domain.nft.models.NFTCollections import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId @@ -9,4 +10,6 @@ interface NFTRepository { fun observeCollections(userWalletId: UserWalletId, networks: List): Flow> suspend fun refreshCollections(userWalletId: UserWalletId, networks: List) + + suspend fun refreshAssets(userWalletId: UserWalletId, network: Network, collectionId: NFTCollection.Identifier) } \ No newline at end of file diff --git a/features/nft/api/build.gradle.kts b/features/nft/api/build.gradle.kts index f94a0582c0..eb681efb3a 100644 --- a/features/nft/api/build.gradle.kts +++ b/features/nft/api/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { /* Project - Domain */ implementation(projects.domain.models) + implementation(projects.domain.wallets.models) /* Project - Core */ implementation(projects.core.decompose) diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTCollectionsComponent.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTCollectionsComponent.kt new file mode 100644 index 0000000000..3417b6a1f2 --- /dev/null +++ b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTCollectionsComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.nft.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId + +interface NFTCollectionsComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index 7c051fa34e..a33b3e1ad5 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -27,7 +27,10 @@ dependencies { implementation(projects.core.datasource) /** Domain modules */ + implementation(projects.domain.nft) implementation(projects.domain.nft.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) /** Common */ implementation(projects.common.ui) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/NFTFeatureModule.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/NFTFeatureModule.kt index b456b29b0b..09b06ecd70 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/NFTFeatureModule.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/NFTFeatureModule.kt @@ -1,10 +1,17 @@ package com.tangem.features.nft import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.core.decompose.model.Model +import com.tangem.features.nft.collections.DefaultNFTCollectionsComponent +import com.tangem.features.nft.collections.model.NFTCollectionsModel +import com.tangem.features.nft.component.NFTCollectionsComponent +import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap import javax.inject.Singleton @Module @@ -16,4 +23,17 @@ internal object NFTFeatureModule { fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): NFTFeatureToggles { return DefaultNFTFeatureToggles(featureTogglesManager) } +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface NFTFeatureModuleBinds { + @Binds + @Singleton + fun bindComponentFactory(impl: DefaultNFTCollectionsComponent.Factory): NFTCollectionsComponent.Factory + + @Binds + @IntoMap + @ClassKey(NFTCollectionsModel::class) + fun bindModel(model: NFTCollectionsModel): Model } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/DefaultNFTCollectionsComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/DefaultNFTCollectionsComponent.kt new file mode 100644 index 0000000000..bb82591e48 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/DefaultNFTCollectionsComponent.kt @@ -0,0 +1,37 @@ +package com.tangem.features.nft.collections + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.nft.collections.model.NFTCollectionsModel +import com.tangem.features.nft.collections.ui.NFTCollections +import com.tangem.features.nft.component.NFTCollectionsComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultNFTCollectionsComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: NFTCollectionsComponent.Params, +) : NFTCollectionsComponent, AppComponentContext by context { + + private val model: NFTCollectionsModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + NFTCollections(state, modifier) + } + + @AssistedFactory + interface Factory : NFTCollectionsComponent.Factory { + override fun create( + context: AppComponentContext, + params: NFTCollectionsComponent.Params, + ): DefaultNFTCollectionsComponent + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetsListUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetsListUM.kt index 7b0147b03a..742ae7adce 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetsListUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetsListUM.kt @@ -5,12 +5,8 @@ import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class NFTCollectionAssetsListUM { - data object Collapsed : NFTCollectionAssetsListUM() - - @Immutable - sealed class Expanded : NFTCollectionAssetsListUM() { - data class Loading(val itemsCount: Int) : Expanded() - data class Failed(val onRetryClick: () -> Unit) : Expanded() - data class Content(val items: ImmutableList) : Expanded() - } + data object Init : NFTCollectionAssetsListUM() + data class Loading(val itemsCount: Int) : NFTCollectionAssetsListUM() + data class Failed(val onRetryClick: () -> Unit) : NFTCollectionAssetsListUM() + data class Content(val items: ImmutableList) : NFTCollectionAssetsListUM() } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt index 8e3375906b..e7e24ded3b 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt @@ -10,5 +10,6 @@ internal data class NFTCollectionUM( val logoUrl: String?, val description: TextReference, val assets: NFTCollectionAssetsListUM, + val isExpanded: Boolean, val onExpandClick: () -> Unit, ) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsStateUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsStateUM.kt new file mode 100644 index 0000000000..14267e3120 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsStateUM.kt @@ -0,0 +1,6 @@ +package com.tangem.features.nft.collections.entity + +internal data class NFTCollectionsStateUM( + val onBackClick: () -> Unit, + val content: NFTCollectionsUM, +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsWarningUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsWarningUM.kt index f0bdc22aaa..c2f3e9ab11 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsWarningUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsWarningUM.kt @@ -2,4 +2,7 @@ package com.tangem.features.nft.collections.entity import com.tangem.core.ui.components.notifications.NotificationConfig -internal data class NFTCollectionsWarningUM(val config: NotificationConfig) \ No newline at end of file +internal data class NFTCollectionsWarningUM( + val id: String, + val config: NotificationConfig, +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ChangeCollectionExpandedStateTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ChangeCollectionExpandedStateTransformer.kt new file mode 100644 index 0000000000..867c3d2303 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ChangeCollectionExpandedStateTransformer.kt @@ -0,0 +1,34 @@ +package com.tangem.features.nft.collections.entity.transformer + +import com.tangem.domain.nft.models.NFTCollection +import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM +import com.tangem.features.nft.collections.entity.NFTCollectionsUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList + +internal class ChangeCollectionExpandedStateTransformer( + private val collectionId: NFTCollection.Identifier, + private val onFirstExpanded: () -> Unit, +) : Transformer { + + override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy( + content = when (prevState.content) { + is NFTCollectionsUM.Empty, + is NFTCollectionsUM.Loading, + is NFTCollectionsUM.Failed, + -> prevState.content + is NFTCollectionsUM.Content -> prevState.content.copy( + collections = prevState.content.collections.map { + if (it.id == collectionId.toString()) { + if (!it.isExpanded) { + onFirstExpanded() + } + it.copy(isExpanded = !it.isExpanded) + } else { + it + } + }.toPersistentList(), + ) + }, + ) +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ToggleSearchBarTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ToggleSearchBarTransformer.kt new file mode 100644 index 0000000000..12445f43ce --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ToggleSearchBarTransformer.kt @@ -0,0 +1,22 @@ +package com.tangem.features.nft.collections.entity.transformer + +import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM +import com.tangem.features.nft.collections.entity.NFTCollectionsUM +import com.tangem.utils.transformer.Transformer + +internal class ToggleSearchBarTransformer(private val isActive: Boolean) : Transformer { + + override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy( + content = when (val content = prevState.content) { + is NFTCollectionsUM.Content -> content.copy( + search = content.search.copy( + isActive = isActive, + ), + ) + is NFTCollectionsUM.Empty, + is NFTCollectionsUM.Loading, + is NFTCollectionsUM.Failed, + -> content + }, + ) +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt new file mode 100644 index 0000000000..e5fe0846c9 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateDataStateTransformer.kt @@ -0,0 +1,134 @@ +package com.tangem.features.nft.collections.entity.transformer + +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.nft.models.* +import com.tangem.features.nft.collections.entity.* +import com.tangem.features.nft.impl.R +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList + +@Suppress("LongParameterList") +internal class UpdateDataStateTransformer( + private val nftCollections: List, + private val searchQuery: String, + private val onReceiveClick: () -> Unit, + private val onRetryClick: () -> Unit, + private val onExpandCollectionClick: (NFTCollection) -> Unit, + private val onRetryAssetsClick: (NFTCollection) -> Unit, + private val onAssetClick: (NFTAsset) -> Unit, + private val initialSearchBarFactory: () -> SearchBarUM, +) : Transformer { + + override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy( + content = when { + nftCollections.allCollectionsFailed() -> + NFTCollectionsUM.Failed(onRetryClick, onReceiveClick) + nftCollections.anyCollectionFailed() && nftCollections.allLoadedCollectionsEmpty() -> + NFTCollectionsUM.Failed(onRetryClick, onReceiveClick) + nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() -> + NFTCollectionsUM.Empty(onReceiveClick) + !nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() -> + NFTCollectionsUM.Loading(onReceiveClick) + else -> { + NFTCollectionsUM.Content( + search = if (prevState.content is NFTCollectionsUM.Content) { + prevState.content.search.copy( + query = searchQuery, + ) + } else { + initialSearchBarFactory() + }, + collections = nftCollections + .map { it.content } + .asSequence() + .filterIsInstance() + .map { it.collections.orEmpty().transform(prevState, searchQuery) } + .flatten() + .toPersistentList(), + warnings = transformNotifications(), + onReceiveClick = onReceiveClick, + ) + } + }, + ) + + private fun List.transform( + state: NFTCollectionsStateUM, + query: String, + ): ImmutableList = mapNotNull { + if (query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true) { + NFTCollectionUM( + id = it.id.toString(), + networkIconId = getActiveIconRes(it.network.id.value), + name = it.name.orEmpty(), + description = TextReference.PluralRes( + R.plurals.nft_collections_count, + it.count, + wrappedList(it.count), + ), + logoUrl = it.logoUrl, + assets = it.transformAssets(), + onExpandClick = { + onExpandCollectionClick(it) + }, + isExpanded = it.isExpanded(state), + ) + } else { + null + } + }.toPersistentList() + + private fun transformNotifications(): ImmutableList = buildList { + if (nftCollections.anyCollectionFailed()) { + add( + NFTCollectionsWarningUM( + id = "loading troubles", + config = NotificationConfig( + title = TextReference.Res(R.string.nft_collections_warning_title), + subtitle = TextReference.Res(R.string.nft_collections_warning_subtitle), + iconResId = R.drawable.ic_alert_triangle_20, + ), + ), + ) + } + }.toPersistentList() + + private fun NFTCollection.transformAssets(): NFTCollectionAssetsListUM = when (val assets = this.assets) { + is NFTCollection.Assets.Empty -> NFTCollectionAssetsListUM.Init + is NFTCollection.Assets.Loading -> NFTCollectionAssetsListUM.Loading(count) + is NFTCollection.Assets.Failed -> NFTCollectionAssetsListUM.Failed { onRetryAssetsClick(this) } + is NFTCollection.Assets.Value -> NFTCollectionAssetsListUM.Content( + items = assets + .items + .map { it.transform() } + .toPersistentList(), + ) + } + + private fun NFTAsset.transform(): NFTCollectionAssetUM = NFTCollectionAssetUM( + id = id.toString(), + name = name.orEmpty(), + imageUrl = media?.url, + price = when (val salePrice = salePrice) { + is NFTSalePrice.Empty -> NFTSalePriceUM.Failed + is NFTSalePrice.Loading -> NFTSalePriceUM.Loading + is NFTSalePrice.Error -> NFTSalePriceUM.Failed + is NFTSalePrice.Value -> NFTSalePriceUM.Content(salePrice.value.toString()) + }, + onItemClick = { + onAssetClick(this) + }, + ) + + private fun NFTCollection.isExpanded(state: NFTCollectionsStateUM): Boolean = + (state.content as? NFTCollectionsUM.Content) + ?.collections + ?.firstOrNull { it.id == id.toString() } + ?.isExpanded + ?: false +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateSearchQueryTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateSearchQueryTransformer.kt new file mode 100644 index 0000000000..e2dd4173fa --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/UpdateSearchQueryTransformer.kt @@ -0,0 +1,22 @@ +package com.tangem.features.nft.collections.entity.transformer + +import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM +import com.tangem.features.nft.collections.entity.NFTCollectionsUM +import com.tangem.utils.transformer.Transformer + +internal class UpdateSearchQueryTransformer(private val newQuery: String) : Transformer { + + override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy( + content = when (val content = prevState.content) { + is NFTCollectionsUM.Content -> content.copy( + search = content.search.copy( + query = newQuery, + ), + ) + is NFTCollectionsUM.Empty, + is NFTCollectionsUM.Loading, + is NFTCollectionsUM.Failed, + -> content + }, + ) +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt new file mode 100644 index 0000000000..5a206e8c95 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt @@ -0,0 +1,138 @@ +package com.tangem.features.nft.collections.model + +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.navigation.Router +import com.tangem.core.ui.components.fields.InputManager +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase +import com.tangem.domain.nft.GetNFTCollectionsUseCase +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.nft.models.NFTCollection +import com.tangem.features.nft.collections.entity.* +import com.tangem.features.nft.collections.entity.transformer.ChangeCollectionExpandedStateTransformer +import com.tangem.features.nft.collections.entity.transformer.ToggleSearchBarTransformer +import com.tangem.features.nft.collections.entity.transformer.UpdateDataStateTransformer +import com.tangem.features.nft.collections.entity.transformer.UpdateSearchQueryTransformer +import com.tangem.features.nft.component.NFTCollectionsComponent +import com.tangem.features.nft.impl.R +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +@ModelScoped +internal class NFTCollectionsModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val searchManager: InputManager, + private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, + private val fetchNFTCollectionAssetsUseCase: FetchNFTCollectionAssetsUseCase, + paramsContainer: ParamsContainer, +) : Model() { + + val state: StateFlow get() = _state + + private val _state = MutableStateFlow( + value = NFTCollectionsStateUM( + onBackClick = ::navigateBack, + content = NFTCollectionsUM.Loading(::onReceiveClick), + ), + ) + + private val params: NFTCollectionsComponent.Params = paramsContainer.require() + + init { + subscribeToNFTCollections() + } + + private fun subscribeToNFTCollections() { + combine( + flow = getNFTCollectionsUseCase(params.userWalletId), + flow2 = searchManager.query.distinctUntilChanged(), + ) { nftCollections, query -> + _state.update { + UpdateDataStateTransformer( + nftCollections = nftCollections, + searchQuery = query, + onReceiveClick = ::onReceiveClick, + onRetryClick = ::onRetryClick, + onExpandCollectionClick = ::onExpandCollectionClick, + onRetryAssetsClick = ::onRetryAssetsClick, + onAssetClick = ::onAssetClick, + initialSearchBarFactory = ::getInitialSearchBar, + ).transform(it) + } + } + .launchIn(modelScope) + } + + private fun onSearchQueryChange(newQuery: String) { + modelScope.launch { + _state.update { UpdateSearchQueryTransformer(newQuery).transform(it) } + + searchManager.update(newQuery) + } + } + + private fun getInitialSearchBar(): SearchBarUM = SearchBarUM( + placeholderText = resourceReference(R.string.common_search), + query = "", + isActive = false, + onQueryChange = ::onSearchQueryChange, + onActiveChange = ::toggleSearchBar, + ) + + private fun toggleSearchBar(isActive: Boolean) { + _state.update { + ToggleSearchBarTransformer(isActive).transform(it) + } + } + + private fun onExpandCollectionClick(collection: NFTCollection) { + _state.update { + ChangeCollectionExpandedStateTransformer( + collectionId = collection.id, + onFirstExpanded = { onFirstExpanded(collection) }, + ).transform(it) + } + } + + private fun onFirstExpanded(collection: NFTCollection) { + loadCollectionAssets(collection) + } + + private fun onRetryAssetsClick(collection: NFTCollection) { + loadCollectionAssets(collection) + } + + private fun onRetryClick() { + // TODO refresh all + } + + @Suppress("UnusedPrivateMember") + private fun onAssetClick(asset: NFTAsset) { + // TODO move to details + } + + private fun onReceiveClick() { + // TODO move to receive + } + + private fun navigateBack() { + router.pop() + } + + private fun loadCollectionAssets(collection: NFTCollection) { + modelScope.launch { + fetchNFTCollectionAssetsUseCase( + userWalletId = params.userWalletId, + network = collection.network, + collectionId = collection.id, + ) + } + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt index 32f7159f78..ffaf0c94b1 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt @@ -13,12 +13,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.currency.icon.CurrencyIconTopBadge import com.tangem.core.ui.extensions.TextReference @@ -28,13 +31,14 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.nft.collections.entity.NFTCollectionAssetsListUM import com.tangem.features.nft.collections.entity.NFTCollectionUM import com.tangem.features.nft.impl.R +import kotlinx.collections.immutable.persistentListOf private const val CHEVRON_ROTATION_EXPANDED = 180f private const val CHEVRON_ROTATION_COLLAPSED = 0f @Composable internal fun NFTCollection(state: NFTCollectionUM, modifier: Modifier = Modifier) { - val isExpanded = state.assets is NFTCollectionAssetsListUM.Expanded + val isExpanded = state.isExpanded Column( modifier = modifier, @@ -87,8 +91,12 @@ private fun Logo(state: NFTCollectionUM) { SubcomposeAsyncImage( modifier = Modifier .align(Alignment.CenterStart) - .size(TangemTheme.dimens.size36), - model = state.logoUrl, + .size(TangemTheme.dimens.size36) + .clip(TangemTheme.shapes.roundedCorners8), + model = ImageRequest.Builder(LocalContext.current) + .data(state.logoUrl) + .crossfade(true) + .build(), loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, @@ -99,6 +107,7 @@ private fun Logo(state: NFTCollectionUM) { .background(TangemTheme.colors.field.primary), ) }, + contentScale = ContentScale.Crop, contentDescription = null, ) CurrencyIconTopBadge( @@ -160,7 +169,8 @@ private class NFTCollectionProvider : CollectionPreviewParameterProvider + AnimatedContent( + targetState = state.content, + contentKey = { it::class }, + label = "NFT Collections", + ) { + val contentModifier = Modifier + .padding(innerPadding) + .fillMaxSize() + when (val content = it) { + is NFTCollectionsUM.Content -> NFTCollectionsContent(content, contentModifier) + is NFTCollectionsUM.Empty -> NFTCollectionsEmpty(content, contentModifier) + is NFTCollectionsUM.Failed -> NFTCollectionsFailed(content, contentModifier) + is NFTCollectionsUM.Loading -> Unit + } + } + }, + ) +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt index 6a1b002cf4..e1301e1146 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt @@ -1,12 +1,16 @@ package com.tangem.features.nft.collections.ui import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -24,17 +28,12 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.nft.collections.entity.* -import com.tangem.features.nft.collections.entity.NFTCollectionAssetUM -import com.tangem.features.nft.collections.entity.NFTCollectionAssetsListUM -import com.tangem.features.nft.collections.entity.NFTCollectionUM -import com.tangem.features.nft.collections.entity.NFTCollectionsUM -import com.tangem.features.nft.collections.entity.NFTSalePriceUM import com.tangem.features.nft.impl.R import kotlinx.collections.immutable.persistentListOf @Suppress("LongMethod") @Composable -internal fun NFTCollectionsContent(state: NFTCollectionsUM.Content, modifier: Modifier = Modifier) { +internal fun NFTCollectionsContent(content: NFTCollectionsUM.Content, modifier: Modifier = Modifier) { val listState = rememberLazyListState() Box( @@ -48,29 +47,34 @@ internal fun NFTCollectionsContent(state: NFTCollectionsUM.Content, modifier: Mo ), ) { Column( - modifier = Modifier, + modifier = Modifier + .fillMaxWidth(), ) { SearchBar( - state = state.search, + state = content.search, colors = TangemSearchBarDefaults.secondaryTextFieldColors, ) - state.warnings.fastForEach { - NFTCollectionWarning( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing16), - state = it, - ) + content.warnings.fastForEach { + key(it.id) { + NFTCollectionWarning( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing16), + state = it, + ) + } } LazyColumn( modifier = Modifier + .fillMaxWidth() .padding( top = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing60, ) .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.primary), state = listState, ) { - state.collections.fastForEach { collection -> + content.collections.fastForEach { collection -> item(key = collection.id) { NFTCollection( modifier = Modifier.fillMaxWidth(), @@ -78,23 +82,26 @@ internal fun NFTCollectionsContent(state: NFTCollectionsUM.Content, modifier: Mo ) } when (val assets = collection.assets) { - is NFTCollectionAssetsListUM.Collapsed -> Unit - is NFTCollectionAssetsListUM.Expanded.Loading -> { + is NFTCollectionAssetsListUM.Init -> Unit + is NFTCollectionAssetsListUM.Loading -> { assetsListLoading( collectionId = collection.id, content = assets, + expanded = collection.isExpanded, ) } - is NFTCollectionAssetsListUM.Expanded.Failed -> { + is NFTCollectionAssetsListUM.Failed -> { assetsListFailed( collectionId = collection.id, content = assets, + expanded = collection.isExpanded, ) } - is NFTCollectionAssetsListUM.Expanded.Content -> { + is NFTCollectionAssetsListUM.Content -> { assetsListContent( collectionId = collection.id, content = assets, + expanded = collection.isExpanded, ) } } @@ -106,14 +113,15 @@ internal fun NFTCollectionsContent(state: NFTCollectionsUM.Content, modifier: Mo .fillMaxWidth() .align(Alignment.BottomCenter), text = stringResourceSafe(R.string.nft_collections_receive), - onClick = { }, + onClick = content.onReceiveClick, ) } } private fun LazyListScope.assetsListLoading( collectionId: String, - content: NFTCollectionAssetsListUM.Expanded.Loading, + content: NFTCollectionAssetsListUM.Loading, + expanded: Boolean, ) { val itemsCount = content.itemsCount val rowCount = (itemsCount + 1) / 2 @@ -121,60 +129,71 @@ private fun LazyListScope.assetsListLoading( item( key = "loading_${collectionId}_$rowIndex", ) { - val paddingValues = PaddingValues( - start = TangemTheme.dimens.spacing6, - top = TangemTheme.dimens.spacing6, - end = TangemTheme.dimens.spacing6, - bottom = TangemTheme.dimens.spacing20, - ) - Row( - modifier = Modifier - .padding(TangemTheme.dimens.spacing6) - .animateItem(), - ) { - NFTCollectionAssetLoading( - modifier = Modifier - .weight(1f) - .padding(paddingValues), + AnimatedVisibility(visible = expanded) { + val paddingValues = PaddingValues( + start = TangemTheme.dimens.spacing6, + top = TangemTheme.dimens.spacing6, + end = TangemTheme.dimens.spacing6, + bottom = TangemTheme.dimens.spacing20, ) - if (rowIndex == rowCount - 1 && itemsCount % 2 != 0) { - Box( - modifier = Modifier - .weight(1f) - .padding(paddingValues), - ) - } else { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing6) + .animateContentSize(), + ) { NFTCollectionAssetLoading( modifier = Modifier .weight(1f) .padding(paddingValues), ) + if (rowIndex == rowCount - 1 && itemsCount % 2 != 0) { + Box( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + ) + } else { + NFTCollectionAssetLoading( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + ) + } } } } } } -private fun LazyListScope.assetsListFailed(collectionId: String, content: NFTCollectionAssetsListUM.Expanded.Failed) { +private fun LazyListScope.assetsListFailed( + collectionId: String, + content: NFTCollectionAssetsListUM.Failed, + expanded: Boolean, +) { item( key = "failed_$collectionId", ) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(TangemTheme.dimens.size142), - contentAlignment = Alignment.Center, - ) { - UnableToLoadData( - onRetryClick = content.onRetryClick, - ) + AnimatedVisibility(visible = expanded) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size142) + .animateContentSize(), + contentAlignment = Alignment.Center, + ) { + UnableToLoadData( + onRetryClick = content.onRetryClick, + ) + } } } } private fun LazyListScope.assetsListContent( collectionId: String, - content: NFTCollectionAssetsListUM.Expanded.Content, + content: NFTCollectionAssetsListUM.Content, + expanded: Boolean, ) { val items = content.items val itemsCount = items.size @@ -185,36 +204,43 @@ private fun LazyListScope.assetsListContent( item( key = "content_${collectionId}_${item1.id}_${item2?.id}", ) { - val paddingValues = PaddingValues( - start = TangemTheme.dimens.spacing6, - top = TangemTheme.dimens.spacing6, - end = TangemTheme.dimens.spacing6, - bottom = TangemTheme.dimens.spacing20, - ) - Row( - modifier = Modifier - .padding(TangemTheme.dimens.spacing6) - .animateItem(), - ) { - NFTCollectionAsset( - modifier = Modifier - .weight(1f) - .padding(paddingValues), - state = item1, + AnimatedVisibility(visible = expanded) { + val paddingValues = PaddingValues( + start = TangemTheme.dimens.spacing6, + top = TangemTheme.dimens.spacing6, + end = TangemTheme.dimens.spacing6, + bottom = TangemTheme.dimens.spacing20, ) - if (item2 == null) { - Box( - modifier = Modifier - .weight(1f) - .padding(paddingValues), - ) - } else { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing6) + .animateContentSize(), + ) { NFTCollectionAsset( modifier = Modifier .weight(1f) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .clickable { item1.onItemClick() } .padding(paddingValues), - state = item2, + state = item1, ) + if (item2 == null) { + Box( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + ) + } else { + NFTCollectionAsset( + modifier = Modifier + .weight(1f) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .clickable { item2.onItemClick() } + .padding(paddingValues), + state = item2, + ) + } } } } @@ -228,7 +254,7 @@ private fun LazyListScope.assetsListContent( private fun Preview_NFTCollectionsContent() { TangemThemePreview { NFTCollectionsContent( - state = NFTCollectionsUM.Content( + content = NFTCollectionsUM.Content( search = SearchBarUM( placeholderText = resourceReference(R.string.common_search), query = "", @@ -243,7 +269,8 @@ private fun Preview_NFTCollectionsContent() { logoUrl = "", networkIconId = R.drawable.img_eth_22, description = TextReference.Str("3 items"), - assets = NFTCollectionAssetsListUM.Collapsed, + assets = NFTCollectionAssetsListUM.Content(persistentListOf()), + isExpanded = false, onExpandClick = { }, ), NFTCollectionUM( @@ -252,9 +279,10 @@ private fun Preview_NFTCollectionsContent() { logoUrl = "", networkIconId = R.drawable.img_eth_22, description = TextReference.Str("3 items"), - assets = NFTCollectionAssetsListUM.Expanded.Loading( + assets = NFTCollectionAssetsListUM.Loading( itemsCount = 1, ), + isExpanded = true, onExpandClick = { }, ), NFTCollectionUM( @@ -263,9 +291,10 @@ private fun Preview_NFTCollectionsContent() { logoUrl = "", networkIconId = R.drawable.img_eth_22, description = TextReference.Str("3 items"), - assets = NFTCollectionAssetsListUM.Expanded.Failed( + assets = NFTCollectionAssetsListUM.Failed( onRetryClick = { }, ), + isExpanded = true, onExpandClick = { }, ), NFTCollectionUM( @@ -274,7 +303,7 @@ private fun Preview_NFTCollectionsContent() { logoUrl = "", networkIconId = R.drawable.img_eth_22, description = TextReference.Str("3 items"), - assets = NFTCollectionAssetsListUM.Expanded.Content( + assets = NFTCollectionAssetsListUM.Content( items = persistentListOf( NFTCollectionAssetUM( id = "item1", @@ -299,11 +328,13 @@ private fun Preview_NFTCollectionsContent() { ), ), ), + isExpanded = true, onExpandClick = { }, ), ), warnings = persistentListOf( NFTCollectionsWarningUM( + id = "loading troubles", config = NotificationConfig( title = TextReference.Res(R.string.nft_collections_warning_title), subtitle = TextReference.Res(R.string.nft_collections_warning_subtitle), diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsFailed.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsFailed.kt index f16d193537..d4d99bc548 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsFailed.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsFailed.kt @@ -34,7 +34,7 @@ internal fun NFTCollectionsFailed(state: NFTCollectionsUM.Failed, modifier: Modi .fillMaxWidth() .align(Alignment.BottomCenter), text = stringResourceSafe(R.string.nft_collections_receive), - onClick = { }, + onClick = state.onReceiveClick, ) } } diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index 2df9a81756..b32858e319 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(projects.core.decompose) implementation(projects.core.navigation) implementation(projects.core.ui) + implementation(projects.core.utils) /** Project - Common */ implementation(projects.common.routing) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 63a9b5ce7e..3b86caa30d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -10,6 +10,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.components.fields.InputManager import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.demo.IsDemoCardUseCase @@ -25,7 +26,6 @@ import com.tangem.features.onramp.main.entity.* import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory import com.tangem.features.onramp.providers.entity.SelectProviderResult -import com.tangem.features.onramp.utils.InputManager import com.tangem.features.onramp.utils.sendOnrampErrorEvent import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt index e7a8f8dedf..8d0835efff 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt @@ -4,6 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler 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.ui.components.fields.InputManager import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.onramp.FetchOnrampCountriesUseCase @@ -20,7 +21,6 @@ import com.tangem.features.onramp.selectcountry.entity.CountryListUMController import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountryItemsErrorTransformer import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountryItemsLoadingTransformer import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountryItemsTransformer -import com.tangem.features.onramp.utils.InputManager import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer import com.tangem.features.onramp.utils.sendOnrampErrorEvent diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt index 93d64b7bca..1162910ed0 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt @@ -4,6 +4,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler 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.ui.components.fields.InputManager import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.onramp.FetchOnrampCurrenciesUseCase @@ -20,7 +21,6 @@ import com.tangem.features.onramp.selectcurrency.entity.CurrencyListController import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurrencyItemsErrorTransformer import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurrencyItemsLoadingTransformer import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurrencyItemsTransformer -import com.tangem.features.onramp.utils.InputManager import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer import com.tangem.features.onramp.utils.sendOnrampErrorEvent diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index d31bb7513d..09f41bc29d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -3,6 +3,7 @@ package com.tangem.features.onramp.swap.availablepairs.model import arrow.core.getOrElse import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.components.fields.InputManager import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList @@ -31,7 +32,6 @@ import com.tangem.features.onramp.tokenlist.entity.TokenListUMController import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer -import com.tangem.features.onramp.utils.InputManager import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer @@ -63,7 +63,7 @@ internal class AvailableSwapPairsModel @Inject constructor( private val availablePairsByNetworkFlow = MutableStateFlow>(emptyMap()) init { - initializeSearchBardCallbacks() + initializeSearchBarCallbacks() subscribeOnUpdateState() subscribeOnAvailablePairsUpdates() @@ -82,7 +82,7 @@ internal class AvailableSwapPairsModel @Inject constructor( .shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1) } - private fun initializeSearchBardCallbacks() { + private fun initializeSearchBarCallbacks() { tokenListUMController.update( transformer = UpdateSearchBarCallbacksTransformer( onQueryChange = ::onSearchQueryChange, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index 76809b1485..666236c87f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -4,6 +4,7 @@ import arrow.core.getOrElse import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.components.fields.InputManager import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -28,7 +29,6 @@ import com.tangem.features.onramp.tokenlist.entity.TokenListUMController import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer -import com.tangem.features.onramp.utils.InputManager import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index 90fdd5b760..79fd4f1a7f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -12,6 +12,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory import com.tangem.feature.wallet.presentation.wallet.domain.unwrap @@ -55,6 +56,8 @@ internal interface WalletContentClickIntents { fun onConfirmDisposeExpressStatus() fun onDisposeExpressStatus() + + fun onNFTClick(userWalletId: UserWalletId) } @Suppress("LongParameterList") @@ -235,4 +238,8 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } stateHolder.update(CloseBottomSheetTransformer(userWalletId)) } + + override fun onNFTClick(userWalletId: UserWalletId) { + router.openNFTCollectionsScreen(userWalletId) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index b8cf556b05..08937391da 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -125,6 +125,7 @@ internal object WalletScreenPreviewData { collectionsCount = 1, assetsCount = 3, isFlickering = false, + onItemClick = { }, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 05a688f302..f6305b826f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -115,4 +115,8 @@ internal class DefaultWalletRouter @Inject constructor( override fun openScanFailedDialog(onTryAgain: () -> Unit) { reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN, onTryAgain)) } + + override fun openNFTCollectionsScreen(userWalletId: UserWalletId) { + router.push(AppRoute.NFTCollections(userWalletId)) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 9ec77cb66c..4e49fb9f70 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -56,4 +56,7 @@ internal interface InnerWalletRouter { /** Open scan failed dialog */ fun openScanFailedDialog(onTryAgain: () -> Unit) + + /** Open NFT collections screen */ + fun openNFTCollectionsScreen(userWalletId: UserWalletId) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNFTItemUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNFTItemUM.kt index 1d2dd2b8ba..f4406d5dc5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNFTItemUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNFTItemUM.kt @@ -10,7 +10,9 @@ sealed class WalletNFTItemUM { data object Loading : WalletNFTItemUM() - data object Empty : WalletNFTItemUM() + data class Empty( + val onItemClick: () -> Unit, + ) : WalletNFTItemUM() data object Failed : WalletNFTItemUM() @@ -19,6 +21,7 @@ sealed class WalletNFTItemUM { val collectionsCount: Int, val assetsCount: Int, val isFlickering: Boolean, + val onItemClick: () -> Unit, ) : WalletNFTItemUM() { @Immutable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt index f713b64077..9c5fcb1479 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.StatusSource -import com.tangem.domain.nft.models.NFTCollections +import com.tangem.domain.nft.models.* import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState @@ -10,16 +10,21 @@ import kotlinx.collections.immutable.toPersistentList internal class SetNFTCollectionsTransformer( userWalletId: UserWalletId, private val nftCollections: List, + private val onItemClick: () -> Unit, ) : WalletStateTransformer(userWalletId) { override fun transform(prevState: WalletState): WalletState = when (prevState) { is WalletState.MultiCurrency.Content -> prevState.copy( nftState = when { - allCollectionsFailed() -> WalletNFTItemUM.Failed - anyCollectionFailed() && allLoadedCollectionsEmpty() -> WalletNFTItemUM.Failed - allCollectionsLoaded() && allCollectionsEmpty() -> WalletNFTItemUM.Empty - !allCollectionsLoaded() && allCollectionsEmpty() -> WalletNFTItemUM.Loading - else -> createContentNFTItemUM() + nftCollections.allCollectionsFailed() -> + WalletNFTItemUM.Failed + nftCollections.anyCollectionFailed() && nftCollections.allLoadedCollectionsEmpty() -> + WalletNFTItemUM.Failed + nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() -> + WalletNFTItemUM.Empty(onItemClick) + !nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() -> + WalletNFTItemUM.Loading + else -> createContentNFTItemUM(onItemClick) }, ) is WalletState.SingleCurrency.Content, @@ -31,7 +36,7 @@ internal class SetNFTCollectionsTransformer( -> prevState } - private fun createContentNFTItemUM(): WalletNFTItemUM.Content { + private fun createContentNFTItemUM(onItemClick: () -> Unit): WalletNFTItemUM.Content { val collectionsContent = nftCollections .map { it.content } .filterIsInstance() @@ -61,34 +66,10 @@ internal class SetNFTCollectionsTransformer( assetsCount = collections .sumOf { it.count }, isFlickering = isFlickering, + onItemClick = onItemClick, ) } - private fun allCollectionsFailed() = nftCollections.all { - it.content is NFTCollections.Content.Error - } - - private fun anyCollectionFailed() = nftCollections.any { - it.content is NFTCollections.Content.Error - } - - private fun allLoadedCollectionsEmpty() = nftCollections - .map { it.content } - .filterIsInstance() - .all { it.collections.isNullOrEmpty() } - - private fun allCollectionsLoaded() = nftCollections.all { - val content = it.content - content is NFTCollections.Content.Collections && - content.source != StatusSource.CACHE - } - - private fun allCollectionsEmpty() = nftCollections.all { - val content = it.content - content is NFTCollections.Content.Collections && - content.collections.isNullOrEmpty() - } - companion object { private const val NFT_COLLECTIONS_MAX_PREVIEWS_COUNT = 4 } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt index 476770ce55..67a36a2935 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt @@ -11,13 +11,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* -@Suppress("UnusedPrivateMember") internal class WalletNFTListSubscriber( private val userWallet: UserWallet, private val stateHolder: WalletStateController, private val walletsRepository: WalletsRepository, private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, - clickIntents: WalletClickIntents, + private val clickIntents: WalletClickIntents, ) : WalletSubscriber() { @OptIn(ExperimentalCoroutinesApi::class) @@ -27,8 +26,7 @@ internal class WalletNFTListSubscriber( .flatMapLatest { nftEnabled -> // if NFT is enabled for this wallet, then start observing changes from store and apply transformer if need if (nftEnabled) { - getNFTCollectionsUseCase - .launch(userWallet.walletId) + getNFTCollectionsUseCase(userWallet.walletId) .shareIn( scope = coroutineScope, started = SharingStarted.WhileSubscribed(), @@ -36,7 +34,11 @@ internal class WalletNFTListSubscriber( ) .onEach { stateHolder.update( - SetNFTCollectionsTransformer(userWallet.walletId, it), + SetNFTCollectionsTransformer( + userWalletId = userWallet.walletId, + nftCollections = it, + onItemClick = { clickIntents.onNFTClick(userWallet.walletId) }, + ), ) } } else { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 28aa1a4a5e..4dcbc3b0a9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -713,7 +713,6 @@ internal fun LazyListScope.nftCollections(state: WalletState, itemModifier: Modi nftCollections( modifier = itemModifier, state = it.nftState, - onClick = {}, ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt index 7f2a8515b7..079f2876e1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt @@ -34,19 +34,19 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @Composable -internal fun WalletNFTItem(state: WalletNFTItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = { }) { +internal fun WalletNFTItem(state: WalletNFTItemUM, modifier: Modifier = Modifier) { when (state) { is WalletNFTItemUM.Hidden -> Unit is WalletNFTItemUM.Empty -> WalletNFTItemEmpty( modifier = modifier, - onClick = onClick, + onClick = state.onItemClick, ) is WalletNFTItemUM.Failed -> WalletNFTItemFailed(modifier = modifier) is WalletNFTItemUM.Loading -> WalletNFTItemLoading(modifier = modifier) is WalletNFTItemUM.Content -> WalletNFTItemContent( state = state, - onClick = onClick, + onClick = state.onItemClick, modifier = modifier, ) } @@ -396,16 +396,15 @@ private fun RowContentContainer( @Composable private fun Preview_WalletNFTItem(@PreviewParameter(WalletNFTItemProvider::class) state: WalletNFTItemUM) { TangemThemePreview { - WalletNFTItem( - state = state, - onClick = {}, - ) + WalletNFTItem(state = state) } } private class WalletNFTItemProvider : CollectionPreviewParameterProvider( collection = listOf( - WalletNFTItemUM.Empty, + WalletNFTItemUM.Empty( + onItemClick = { }, + ), WalletNFTItemUM.Loading, WalletNFTItemUM.Failed, WalletNFTItemUM.Content( @@ -415,6 +414,7 @@ private class WalletNFTItemProvider : CollectionPreviewParameterProvider Unit, modifier: Modifier = Modifier) { +internal fun LazyListScope.nftCollections(state: WalletNFTItemUM, modifier: Modifier = Modifier) { item(key = NFT_COLLECTIONS_CONTENT_TYPE, contentType = NFT_COLLECTIONS_CONTENT_TYPE) { WalletNFTItem( modifier = modifier, state = state, - onClick = onClick, ) } } \ No newline at end of file From b10bd5f3f9b11955e08a7d2217f8e2194b4d621c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Apr 2025 09:10:46 +0000 Subject: [PATCH 071/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 32be17375b..21d426e758 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "release-app_5.22-997" +tangemBlockchainSdk = "develop-990" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.22-440" +tangemCardSdk = "develop-445" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From e8f2ed85176a7c7dbe08faf4aa2207b200282182 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Apr 2025 16:21:50 +0500 Subject: [PATCH 072/138] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 48 +++++---- .../tangem/tap/routing/utils/ChildFactory.kt | 14 +++ .../com/tangem/common/routing/AppRoute.kt | 5 + core/res/src/main/res/values/strings.xml | 3 + .../tangem/data/nft/DefaultNFTRepository.kt | 2 + domain/nft/build.gradle.kts | 2 + .../nft/FilterNFTAvailableNetworksUseCase.kt | 17 +++ .../nft/GetNFTAvailableNetworksUseCase.kt | 22 ++++ .../domain/nft/repository/NFTRepository.kt | 2 + .../nft/component/NFTReceiveComponent.kt | 14 +++ .../tangem/features/nft/NFTFeatureModule.kt | 18 +++- .../collections/model/NFTCollectionsModel.kt | 10 +- .../nft/receive/DefaultNFTReceiveComponent.kt | 37 +++++++ .../receive/entity/NFTReceiveNetworksUM.kt | 9 -- .../nft/receive/entity/NFTReceiveUM.kt | 23 ++++ .../transformer/ToggleSearchBarTransformer.kt | 13 +++ .../transformer/UpdateDataStateTransformer.kt | 34 ++++++ .../UpdateSearchQueryTransformer.kt | 13 +++ .../nft/receive/model/NFTReceiveModel.kt | 100 ++++++++++++++++++ .../features/nft/receive/ui/NFTReceive.kt | 66 ++++++++++++ ...tworks.kt => NFTReceiveNetworksContent.kt} | 74 +++++-------- .../nft/receive/ui/NFTReceiveNetworksEmpty.kt | 18 ++++ 22 files changed, 463 insertions(+), 81 deletions(-) create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/FilterNFTAvailableNetworksUseCase.kt create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTAvailableNetworksUseCase.kt create mode 100644 features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTReceiveComponent.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/DefaultNFTReceiveComponent.kt delete mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveNetworksUM.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveUM.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/ToggleSearchBarTransformer.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/UpdateDataStateTransformer.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/UpdateSearchQueryTransformer.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt rename features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/{NFTReceiveNetworks.kt => NFTReceiveNetworksContent.kt} (59%) create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksEmpty.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index f9972a842d..b9cfccbc58 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -1,10 +1,9 @@ package com.tangem.tap.di.domain -import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase -import com.tangem.domain.nft.FetchNFTCollectionsUseCase -import com.tangem.domain.nft.GetNFTCollectionsUseCase +import com.tangem.domain.nft.* import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,30 +19,43 @@ internal object NFTDomainModule { fun providesGetNFTCollectionsUseCase( currenciesRepository: CurrenciesRepository, nftRepository: NFTRepository, - ): GetNFTCollectionsUseCase { - return GetNFTCollectionsUseCase( - currenciesRepository = currenciesRepository, - nftRepository = nftRepository, - ) - } + ): GetNFTCollectionsUseCase = GetNFTCollectionsUseCase( + currenciesRepository = currenciesRepository, + nftRepository = nftRepository, + ) @Provides @Singleton fun providesFetchNFTCollectionsUseCase( currenciesRepository: CurrenciesRepository, nftRepository: NFTRepository, - ): FetchNFTCollectionsUseCase { - return FetchNFTCollectionsUseCase( - currenciesRepository = currenciesRepository, - nftRepository = nftRepository, - ) - } + ): FetchNFTCollectionsUseCase = FetchNFTCollectionsUseCase( + currenciesRepository = currenciesRepository, + nftRepository = nftRepository, + ) @Provides @Singleton - fun providesFetchNFTCollectionAssetsUseCase(nftRepository: NFTRepository): FetchNFTCollectionAssetsUseCase { - return FetchNFTCollectionAssetsUseCase( + fun providesFetchNFTCollectionAssetsUseCase(nftRepository: NFTRepository): FetchNFTCollectionAssetsUseCase = + FetchNFTCollectionAssetsUseCase( nftRepository = nftRepository, ) - } + + @Provides + @Singleton + fun providesGetNFTAvailableNetworksUseCase( + nftRepository: NFTRepository, + currenciesRepository: CurrenciesRepository, + ): GetNFTAvailableNetworksUseCase = GetNFTAvailableNetworksUseCase( + currenciesRepository = currenciesRepository, + nftRepository = nftRepository, + ) + + @Provides + @Singleton + fun providesFilterNFTAvailableNetworksUseCase( + dispatchers: CoroutineDispatcherProvider, + ): FilterNFTAvailableNetworksUseCase = FilterNFTAvailableNetworksUseCase( + dispatchers = dispatchers, + ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index b68f419f3f..dd951b0a53 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -13,6 +13,7 @@ import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.nft.component.NFTCollectionsComponent +import com.tangem.features.nft.component.NFTReceiveComponent import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent import com.tangem.features.onramp.component.* import com.tangem.features.pushnotifications.api.PushNotificationsComponent @@ -82,6 +83,7 @@ internal class ChildFactory @Inject constructor( private val sendFeatureToggles: SendFeatureToggles, private val redesignedWalletConnectComponentFactory: RedisegnedWalletConnectComponent.Factory, private val nftCollectionsComponentFactory: NFTCollectionsComponent.Factory, + private val nftReceiveComponentFactory: NFTReceiveComponent.Factory, private val testerRouter: TesterRouter, private val routingFeatureToggles: RoutingFeatureToggles, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, @@ -405,6 +407,12 @@ internal class ChildFactory @Inject constructor( params = NFTCollectionsComponent.Params(userWalletId = route.userWalletId), componentFactory = nftCollectionsComponentFactory, ) + is AppRoute.NFTReceive -> + createComponentChild( + context = context, + params = NFTReceiveComponent.Params(userWalletId = route.userWalletId), + componentFactory = nftReceiveComponentFactory, + ) is AppRoute.OnboardingNote, is AppRoute.SaveWallet, is AppRoute.OnboardingOther, @@ -743,6 +751,12 @@ internal class ChildFactory @Inject constructor( params = NFTCollectionsComponent.Params(userWalletId = route.userWalletId), componentFactory = nftCollectionsComponentFactory, ) + is AppRoute.NFTReceive -> + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = NFTReceiveComponent.Params(userWalletId = route.userWalletId), + componentFactory = nftReceiveComponentFactory, + ) } // endregion } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 60e5d50f16..2a77d9fd2b 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -282,4 +282,9 @@ sealed class AppRoute(val path: String) : Route { data class NFTCollections( val userWalletId: UserWalletId, ) : AppRoute(path = "/nft_collections/${userWalletId.stringValue}") + + @Serializable + data class NFTReceive( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/nft_receive/${userWalletId.stringValue}") } \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 9966613b6c..22cf05d7e5 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -510,6 +510,9 @@ NFT collections Some data may not load Temporary loading problems + No results. Please try another request. + Receive NFT + To My wallet %1$d NFTs in %2$d collection Tap here to receive first NFT NFT collections diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index ebf03a5f42..197249ec85 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -153,6 +153,8 @@ internal class DefaultNFTRepository @Inject constructor( }.saveIn(getCollectionJobHolder(collectionId)).join() } + override suspend fun isNFTSupported(network: Network): Boolean = network.canHandleNFTs() + private suspend fun refreshSalePrice( userWalletId: UserWalletId, network: Network, diff --git a/domain/nft/build.gradle.kts b/domain/nft/build.gradle.kts index 2159e615a7..b5ce316d58 100644 --- a/domain/nft/build.gradle.kts +++ b/domain/nft/build.gradle.kts @@ -13,6 +13,8 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.coroutines) + implementation(projects.core.utils) + implementation(projects.domain.core) implementation(projects.domain.models) implementation(projects.domain.nft.models) diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FilterNFTAvailableNetworksUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FilterNFTAvailableNetworksUseCase.kt new file mode 100644 index 0000000000..3308232789 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FilterNFTAvailableNetworksUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.nft + +import com.tangem.domain.tokens.model.Network +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +class FilterNFTAvailableNetworksUseCase( + private val dispatchers: CoroutineDispatcherProvider, +) { + suspend operator fun invoke(networks: List, searchQuery: String): List = + withContext(dispatchers.default) { + networks.filter { + it.name.contains(searchQuery, ignoreCase = true) || + it.currencySymbol.contains(searchQuery, ignoreCase = true) + } + } +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTAvailableNetworksUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTAvailableNetworksUseCase.kt new file mode 100644 index 0000000000..203f12e012 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTAvailableNetworksUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.nft + +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetNFTAvailableNetworksUseCase( + private val currenciesRepository: CurrenciesRepository, + private val nftRepository: NFTRepository, +) { + operator fun invoke(userWalletId: UserWalletId): Flow> = currenciesRepository + .getWalletCurrenciesUpdates(userWalletId) + .map { cryptoCurrencies -> + cryptoCurrencies + .map { cryptoCurrency -> cryptoCurrency.network } + .distinct() + .filter { nftRepository.isNFTSupported(it) } + } +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt index 7ab39c2bff..5d811e9a39 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/repository/NFTRepository.kt @@ -12,4 +12,6 @@ interface NFTRepository { suspend fun refreshCollections(userWalletId: UserWalletId, networks: List) suspend fun refreshAssets(userWalletId: UserWalletId, network: Network, collectionId: NFTCollection.Identifier) + + suspend fun isNFTSupported(network: Network): Boolean } \ No newline at end of file diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTReceiveComponent.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTReceiveComponent.kt new file mode 100644 index 0000000000..9184afa340 --- /dev/null +++ b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTReceiveComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.nft.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId + +interface NFTReceiveComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/NFTFeatureModule.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/NFTFeatureModule.kt index 09b06ecd70..91a6bbd3d5 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/NFTFeatureModule.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/NFTFeatureModule.kt @@ -5,6 +5,9 @@ import com.tangem.core.decompose.model.Model import com.tangem.features.nft.collections.DefaultNFTCollectionsComponent import com.tangem.features.nft.collections.model.NFTCollectionsModel import com.tangem.features.nft.component.NFTCollectionsComponent +import com.tangem.features.nft.component.NFTReceiveComponent +import com.tangem.features.nft.receive.DefaultNFTReceiveComponent +import com.tangem.features.nft.receive.model.NFTReceiveModel import dagger.Binds import dagger.Module import dagger.Provides @@ -30,10 +33,21 @@ internal object NFTFeatureModule { internal interface NFTFeatureModuleBinds { @Binds @Singleton - fun bindComponentFactory(impl: DefaultNFTCollectionsComponent.Factory): NFTCollectionsComponent.Factory + fun bindNFTCollectionsComponentFactory( + impl: DefaultNFTCollectionsComponent.Factory, + ): NFTCollectionsComponent.Factory @Binds @IntoMap @ClassKey(NFTCollectionsModel::class) - fun bindModel(model: NFTCollectionsModel): Model + fun bindNFTCollectionsModel(model: NFTCollectionsModel): Model + + @Binds + @Singleton + fun bindNFTReceiveComponentFactory(impl: DefaultNFTReceiveComponent.Factory): NFTReceiveComponent.Factory + + @Binds + @IntoMap + @ClassKey(NFTReceiveModel::class) + fun bindNFTReceiveModel(model: NFTReceiveModel): Model } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt index 5a206e8c95..aa8431196e 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.nft.collections.model +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -11,7 +12,8 @@ import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTCollection -import com.tangem.features.nft.collections.entity.* +import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM +import com.tangem.features.nft.collections.entity.NFTCollectionsUM import com.tangem.features.nft.collections.entity.transformer.ChangeCollectionExpandedStateTransformer import com.tangem.features.nft.collections.entity.transformer.ToggleSearchBarTransformer import com.tangem.features.nft.collections.entity.transformer.UpdateDataStateTransformer @@ -119,7 +121,11 @@ internal class NFTCollectionsModel @Inject constructor( } private fun onReceiveClick() { - // TODO move to receive + router.push( + AppRoute.NFTReceive( + userWalletId = params.userWalletId, + ), + ) } private fun navigateBack() { diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/DefaultNFTReceiveComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/DefaultNFTReceiveComponent.kt new file mode 100644 index 0000000000..f9651f18e3 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/DefaultNFTReceiveComponent.kt @@ -0,0 +1,37 @@ +package com.tangem.features.nft.receive + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.nft.component.NFTReceiveComponent +import com.tangem.features.nft.receive.model.NFTReceiveModel +import com.tangem.features.nft.receive.ui.NFTReceive +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultNFTReceiveComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: NFTReceiveComponent.Params, +) : NFTReceiveComponent, AppComponentContext by context { + + private val model: NFTReceiveModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + NFTReceive(state, modifier) + } + + @AssistedFactory + interface Factory : NFTReceiveComponent.Factory { + override fun create( + context: AppComponentContext, + params: NFTReceiveComponent.Params, + ): DefaultNFTReceiveComponent + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveNetworksUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveNetworksUM.kt deleted file mode 100644 index 91abfb75ef..0000000000 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveNetworksUM.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.features.nft.receive.entity - -import com.tangem.core.ui.components.fields.entity.SearchBarUM -import kotlinx.collections.immutable.ImmutableList - -internal data class NFTReceiveNetworksUM( - val searchBar: SearchBarUM, - val networks: ImmutableList, -) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveUM.kt new file mode 100644 index 0000000000..0764b2f074 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveUM.kt @@ -0,0 +1,23 @@ +package com.tangem.features.nft.receive.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import kotlinx.collections.immutable.ImmutableList + +internal data class NFTReceiveUM( + val onBackClick: () -> Unit, + val search: SearchBarUM, + val networks: Networks, + val bottomSheetConfig: TangemBottomSheetConfig?, +) { + + @Immutable + sealed class Networks { + data object Empty : Networks() + + data class Content( + val items: ImmutableList, + ) : Networks() + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/ToggleSearchBarTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/ToggleSearchBarTransformer.kt new file mode 100644 index 0000000000..16b51e66a9 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/ToggleSearchBarTransformer.kt @@ -0,0 +1,13 @@ +package com.tangem.features.nft.receive.entity.transformer + +import com.tangem.features.nft.receive.entity.NFTReceiveUM +import com.tangem.utils.transformer.Transformer + +internal class ToggleSearchBarTransformer(private val isActive: Boolean) : Transformer { + + override fun transform(prevState: NFTReceiveUM): NFTReceiveUM = prevState.copy( + search = prevState.search.copy( + isActive = isActive, + ), + ) +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/UpdateDataStateTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/UpdateDataStateTransformer.kt new file mode 100644 index 0000000000..560bf018f7 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/UpdateDataStateTransformer.kt @@ -0,0 +1,34 @@ +package com.tangem.features.nft.receive.entity.transformer + +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.domain.tokens.model.Network +import com.tangem.features.nft.receive.entity.NFTNetworkUM +import com.tangem.features.nft.receive.entity.NFTReceiveUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList + +internal class UpdateDataStateTransformer( + private val networks: List, + private val onNetworkClick: (Network) -> Unit, +) : Transformer { + + override fun transform(prevState: NFTReceiveUM): NFTReceiveUM = prevState.copy( + networks = when { + networks.isEmpty() -> NFTReceiveUM.Networks.Empty + else -> NFTReceiveUM.Networks.Content( + items = networks + .map { it.transform() } + .toPersistentList(), + ) + }, + ) + + private fun Network.transform(): NFTNetworkUM = NFTNetworkUM( + id = id.value, + iconRes = getActiveIconRes(id.value), + name = name, + onItemClick = { + onNetworkClick(this) + }, + ) +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/UpdateSearchQueryTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/UpdateSearchQueryTransformer.kt new file mode 100644 index 0000000000..6e2d305b3d --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/UpdateSearchQueryTransformer.kt @@ -0,0 +1,13 @@ +package com.tangem.features.nft.receive.entity.transformer + +import com.tangem.features.nft.receive.entity.NFTReceiveUM +import com.tangem.utils.transformer.Transformer + +internal class UpdateSearchQueryTransformer(private val newQuery: String) : Transformer { + + override fun transform(prevState: NFTReceiveUM): NFTReceiveUM = prevState.copy( + search = prevState.search.copy( + query = newQuery, + ), + ) +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt new file mode 100644 index 0000000000..2e1b50b3c7 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt @@ -0,0 +1,100 @@ +package com.tangem.features.nft.receive.model + +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.navigation.Router +import com.tangem.core.ui.components.fields.InputManager +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.nft.FilterNFTAvailableNetworksUseCase +import com.tangem.domain.nft.GetNFTAvailableNetworksUseCase +import com.tangem.domain.tokens.model.Network +import com.tangem.features.nft.component.NFTReceiveComponent +import com.tangem.features.nft.impl.R +import com.tangem.features.nft.receive.entity.NFTReceiveUM +import com.tangem.features.nft.receive.entity.transformer.ToggleSearchBarTransformer +import com.tangem.features.nft.receive.entity.transformer.UpdateDataStateTransformer +import com.tangem.features.nft.receive.entity.transformer.UpdateSearchQueryTransformer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ModelScoped +internal class NFTReceiveModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val searchManager: InputManager, + private val getNFTAvailableNetworksUseCase: GetNFTAvailableNetworksUseCase, + private val filterNFTAvailableNetworksUseCase: FilterNFTAvailableNetworksUseCase, + paramsContainer: ParamsContainer, +) : Model() { + + val state: StateFlow get() = _state + + private val _state = MutableStateFlow( + value = NFTReceiveUM( + onBackClick = ::navigateBack, + search = getInitialSearchBar(), + networks = NFTReceiveUM.Networks.Content(persistentListOf()), + bottomSheetConfig = null, + ), + ) + + private val params: NFTReceiveComponent.Params = paramsContainer.require() + + init { + subscribeToNFTAvailableNetworks() + } + + private fun subscribeToNFTAvailableNetworks() { + combine( + flow = getNFTAvailableNetworksUseCase(params.userWalletId), + flow2 = searchManager.query.distinctUntilChanged(), + ) { networks, query -> + filterNFTAvailableNetworksUseCase(networks, query) + } + .onEach { filteredNetworks -> + _state.update { + UpdateDataStateTransformer( + networks = filteredNetworks, + onNetworkClick = ::onNetworkClick, + ).transform(it) + } + } + .launchIn(modelScope) + } + + private fun onSearchQueryChange(newQuery: String) { + modelScope.launch { + _state.update { UpdateSearchQueryTransformer(newQuery).transform(it) } + + searchManager.update(newQuery) + } + } + + private fun getInitialSearchBar(): SearchBarUM = SearchBarUM( + placeholderText = resourceReference(R.string.common_search), + query = "", + isActive = false, + onQueryChange = ::onSearchQueryChange, + onActiveChange = ::toggleSearchBar, + ) + + private fun toggleSearchBar(isActive: Boolean) { + _state.update { + ToggleSearchBarTransformer(isActive).transform(it) + } + } + + @Suppress("UnusedPrivateMember") + private fun onNetworkClick(network: Network) { + // TODO show receive bottom sheet + } + + private fun navigateBack() { + router.pop() + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt new file mode 100644 index 0000000000..326d77da63 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt @@ -0,0 +1,66 @@ +package com.tangem.features.nft.receive.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.components.fields.TangemSearchBarDefaults +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.nft.impl.R +import com.tangem.features.nft.receive.entity.NFTReceiveUM + +@Composable +internal fun NFTReceive(state: NFTReceiveUM, modifier: Modifier = Modifier) { + BackHandler(onBack = state.onBackClick) + + Scaffold( + modifier = modifier, + containerColor = TangemTheme.colors.background.secondary, + topBar = { + TangemTopAppBar( + modifier = Modifier.statusBarsPadding(), + startButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_close_24, + onIconClicked = state.onBackClick, + ), + title = stringResourceSafe(id = R.string.nft_receive_title), + subtitle = stringResourceSafe(id = R.string.nft_receive_subtitle), + ) + }, + content = { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + ) { + SearchBar( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + state = state.search, + colors = TangemSearchBarDefaults.secondaryTextFieldColors, + ) + + val contentModifier = Modifier + .padding(TangemTheme.dimens.spacing16) + + AnimatedContent( + targetState = state.networks, + contentKey = { it::class }, + label = "NFT Receive", + ) { + when (val networks = it) { + is NFTReceiveUM.Networks.Content -> NFTReceiveNetworksContent(networks, contentModifier) + is NFTReceiveUM.Networks.Empty -> NFTReceiveNetworksEmpty(contentModifier) + } + } + } + }, + ) +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworks.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksContent.kt similarity index 59% rename from features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworks.kt rename to features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksContent.kt index e51fb169bb..23fab1ef15 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworks.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksContent.kt @@ -1,9 +1,8 @@ package com.tangem.features.nft.receive.ui import android.content.res.Configuration +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -14,57 +13,39 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.fields.SearchBar -import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.nft.impl.R import com.tangem.features.nft.receive.entity.NFTNetworkUM -import com.tangem.features.nft.receive.entity.NFTReceiveNetworksUM +import com.tangem.features.nft.receive.entity.NFTReceiveUM import kotlinx.collections.immutable.persistentListOf @Composable -internal fun NFTReceiveNetworks(state: NFTReceiveNetworksUM, modifier: Modifier = Modifier) { +internal fun NFTReceiveNetworksContent(state: NFTReceiveUM.Networks.Content, modifier: Modifier = Modifier) { val listState = rememberLazyListState() - Column( + LazyColumn( modifier = modifier - .fillMaxSize() - .background(TangemTheme.colors.background.secondary) - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.primary) + .animateContentSize(), + state = listState, ) { - SearchBar( - state = state.searchBar, - // TODO fix colors after merging [REDACTED_TASK_KEY] - ) - - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .padding( - vertical = TangemTheme.dimens.spacing16, - ) - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.primary), - state = listState, + item( + key = "title", ) { - item( - key = "title", - ) { - Title() - } - items( - items = state.networks, - key = NFTNetworkUM::id, - ) { network -> - NFTNetwork(state = network) - } + Title() + } + items( + items = state.items, + key = NFTNetworkUM::id, + ) { network -> + NFTNetwork( + state = network, + modifier = Modifier.animateItem(), + ) } } } @@ -91,16 +72,9 @@ private fun Title(modifier: Modifier = Modifier) { @Composable private fun Preview_NFTReceiveNetworksUM() { TangemThemePreview { - NFTReceiveNetworks( - state = NFTReceiveNetworksUM( - searchBar = SearchBarUM( - placeholderText = resourceReference(R.string.common_search), - query = "", - onQueryChange = { }, - isActive = false, - onActiveChange = { }, - ), - networks = persistentListOf( + NFTReceiveNetworksContent( + state = NFTReceiveUM.Networks.Content( + items = persistentListOf( NFTNetworkUM( id = "item1", name = "Ethereum", diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksEmpty.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksEmpty.kt new file mode 100644 index 0000000000..cebaa5a91d --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksEmpty.kt @@ -0,0 +1,18 @@ +package com.tangem.features.nft.receive.ui + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.nft.impl.R + +@Composable +internal fun NFTReceiveNetworksEmpty(modifier: Modifier = Modifier) { + Text( + modifier = modifier, + text = stringResourceSafe(id = R.string.nft_empty_search), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) +} \ No newline at end of file From 6246065cabbfe827fa2b6e85e3f9de35847eaeb3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Apr 2025 15:00:11 +0300 Subject: [PATCH 073/138] Updated on 2026-08-14 --- .../tasks/visa/VisaCardActivationTask.kt | 31 ++++++++++++++++--- .../data/visa/MockVisaActivationRepository.kt | 8 ++++- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt index 93b23d3999..890ce4ba9b 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -16,7 +16,7 @@ import com.tangem.common.extensions.toHexString import com.tangem.common.map import com.tangem.common.timemeasure.RealtimeMonotonicTimeSource import com.tangem.core.error.ext.tangemError -import com.tangem.crypto.CryptoUtils +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.datasource.local.visa.VisaOTPStorage import com.tangem.datasource.local.visa.VisaOtpData @@ -31,6 +31,7 @@ import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.operations.GenerateOTPCommand import com.tangem.operations.attestation.AttestCardKeyCommand +import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.sign.SignHashCommand import com.tangem.operations.sign.SignHashResponse @@ -56,6 +57,8 @@ class VisaCardActivationTask @AssistedInject constructor( private val visaActivationRepositoryFactory: VisaActivationRepository.Factory, ) : CardSessionRunnable { + override val allowsRequestAccessCodeFromRepository: Boolean = false + private class SessionContext( val visaActivationRepository: VisaActivationRepository, val cardId: String, @@ -105,7 +108,7 @@ class VisaCardActivationTask @AssistedInject constructor( private suspend fun SessionContext.signAuthorizationChallenge( challengeToSign: VisaAuthChallenge.Card, ): CompletionResult { - val attestationCommand = AttestCardKeyCommand(challenge = CryptoUtils.generateRandomBytes(length = 16)) + val attestationCommand = AttestCardKeyCommand(challenge = challengeToSign.challenge.hexToBytes()) val timedResult = RealtimeMonotonicTimeSource.measureTimedValue { suspendCancellableCoroutine { continuation -> attestationCommand.run(session = session) { attestationResponse -> @@ -249,8 +252,14 @@ class VisaCardActivationTask @AssistedInject constructor( card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - val derivedPublicKey = wallet.derivedKeys[VisaUtilities.visaDefaultDerivationPath] - ?: return CompletionResult.Failure(VisaActivationError.MissingWallet.tangemError) + val derivedPublicKey = when (val deriveKeyResult = deriveKey(wallet.publicKey)) { + is CompletionResult.Failure -> { + return CompletionResult.Failure(deriveKeyResult.error) + } + is CompletionResult.Success -> { + deriveKeyResult.data + } + } val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey) .getOrElse { return CompletionResult.Failure(it.tangemError) } @@ -288,6 +297,20 @@ class VisaCardActivationTask @AssistedInject constructor( } } + private suspend fun SessionContext.deriveKey(publicKey: ByteArray): CompletionResult { + val derivationPath = VisaUtilities.visaDefaultDerivationPath + ?: return CompletionResult.Failure(VisaActivationError.FailedToCreateAddress.tangemError) + + val derivationTask = DeriveWalletPublicKeyTask(publicKey, derivationPath) + val derivationTaskResult = suspendCancellableCoroutine { continuation -> + derivationTask.run(session) { result -> + continuation.resume(result) + } + } + + return derivationTaskResult + } + private suspend fun SessionContext.handleSignedData( dataToSign: VisaDataToSignByCardWallet, walletAddress: String, diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/MockVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/MockVisaActivationRepository.kt index ed49e8187b..5a33fb6a70 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/MockVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/MockVisaActivationRepository.kt @@ -14,7 +14,13 @@ class MockVisaActivationRepository @AssistedInject constructor( ) : VisaActivationRepository { override suspend fun getActivationRemoteState(): VisaActivationRemoteState { - return VisaActivationRemoteState.PaymentAccountDeploying + return VisaActivationRemoteState.CardWalletSignatureRequired( + request = VisaCardWalletDataToSignRequest( + orderId = "orderId", + customerId = "customerId", + customerWalletAddress = "customerWalletAddress", + ), + ) } override suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState { From 62a0ccb7d3d27596ceadf22edd0c93838779d2cd Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Apr 2025 17:17:51 +0500 Subject: [PATCH 074/138] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 9 ++ domain/manage-tokens/build.gradle.kts | 6 + .../GetDistinctManagedCurrenciesUseCase.kt | 26 ++++ ...GetDistinctManagedCurrenciesUseCaseTest.kt | 112 ++++++++++++++++++ .../utils/list/ManageTokensListManager.kt | 64 ++++------ 5 files changed, 173 insertions(+), 44 deletions(-) create mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetDistinctManagedCurrenciesUseCase.kt create mode 100644 domain/manage-tokens/src/test/kotlin/com/tangem/domain/managetokens/GetDistinctManagedCurrenciesUseCaseTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index 8d32208885..6734b93464 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -9,6 +9,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -108,4 +109,12 @@ internal object ManageTokensDomainModule { fun provideCheckCurrencyUnsupportedUseCase(repository: ManageTokensRepository): CheckCurrencyUnsupportedUseCase { return CheckCurrencyUnsupportedUseCase(repository) } + + @Provides + @Singleton + fun provideDistinctManagedCurrenciesTokenUseCase( + coroutineDispatchersProvider: CoroutineDispatcherProvider, + ): GetDistinctManagedCurrenciesUseCase { + return GetDistinctManagedCurrenciesUseCase(coroutineDispatchersProvider) + } } \ No newline at end of file diff --git a/domain/manage-tokens/build.gradle.kts b/domain/manage-tokens/build.gradle.kts index 958503009f..da0b3d8595 100644 --- a/domain/manage-tokens/build.gradle.kts +++ b/domain/manage-tokens/build.gradle.kts @@ -22,4 +22,10 @@ dependencies { /* Core */ api(projects.core.pagination) + testImplementation(projects.core.pagination) + + /* Tests */ + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetDistinctManagedCurrenciesUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetDistinctManagedCurrenciesUseCase.kt new file mode 100644 index 0000000000..605a11db0f --- /dev/null +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetDistinctManagedCurrenciesUseCase.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.managetokens + +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.pagination.Batch +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +class GetDistinctManagedCurrenciesUseCase( + private val coroutineDispatchersProvider: CoroutineDispatcherProvider, +) { + + // FIXME: Add interception functionality to BatchFlow state and call this on domain + // [REDACTED_JIRA] + suspend operator fun invoke( + batches: List>>, + ): List>> = withContext(coroutineDispatchersProvider.default) { + val seenIds = mutableSetOf() + + batches.map { batch -> + val filtered = batch.data.filter { currency -> + seenIds.add(currency.id) + } + batch.copy(data = filtered) + } + } +} \ No newline at end of file diff --git a/domain/manage-tokens/src/test/kotlin/com/tangem/domain/managetokens/GetDistinctManagedCurrenciesUseCaseTest.kt b/domain/manage-tokens/src/test/kotlin/com/tangem/domain/managetokens/GetDistinctManagedCurrenciesUseCaseTest.kt new file mode 100644 index 0000000000..69c9a4370c --- /dev/null +++ b/domain/manage-tokens/src/test/kotlin/com/tangem/domain/managetokens/GetDistinctManagedCurrenciesUseCaseTest.kt @@ -0,0 +1,112 @@ +package com.tangem.domain.managetokens + +import com.tangem.domain.managetokens.model.ManagedCryptoCurrency +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import com.tangem.pagination.Batch +import org.junit.Test +import java.util.UUID +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest + +class GetDistinctManagedTokensUseCaseTest { + + private val useCase = GetDistinctManagedCurrenciesUseCase(TestingCoroutineDispatcherProvider()) + + @Test + fun `GIVEN generated batch with multiple duplicates WHEN invoke THEN batches with unique manage currencies`() = + runTest { + // GIVEN + val totalBatches = 10 + val batchSize = 20 + val duplicatePerBatch = 5 + val uniquePerBatch = batchSize - duplicatePerBatch + val globalTokens = mutableListOf() + val batches = mutableListOf>>() + + repeat(totalBatches) { batchIndex -> + val duplicates = if (batchIndex == 0) { + emptyList() + } else { + globalTokens.take(duplicatePerBatch) + } + + val newTokens = List(uniquePerBatch) { + val token = testToken(id = "token_${batchIndex}_$it") + globalTokens += token + token + } + + val batch = Batch(batchIndex, duplicates + newTokens) + batches += batch + } + + // WHEN + val result = useCase(batches) + + // THEN + val seenIds = mutableSetOf() + + result.forEach { batch -> + batch.data.forEach { token -> + assertThat(seenIds).doesNotContain(token.id) + seenIds.add(token.id) + } + } + + val totalTokens = result.sumOf { it.data.size } + assertThat(totalTokens).isEqualTo(totalBatches * (batchSize - duplicatePerBatch)) + } + + @Test + fun `GIVEN static batch with multiple duplicates WHEN invoke THEN batches with unique manage currencies`() = + runTest { + // GIVEN + val token1 = testToken(id = "A", name = "Delta") + val token2 = testToken(id = "B", name = "Delta") + val token3 = testToken(id = "C", name = "Delta") + val token4 = testToken(id = "D", name = "Delta") + val token5 = testToken(id = "E", name = "Delta") + val token6 = testToken(id = "F", name = "Delta") + val token7 = testToken(id = "G", name = "Delta") + val token8 = testToken(id = "H", name = "Delta") + val token9 = testToken(id = "I", name = "Delta") + val token10 = testToken(id = "J", name = "Delta") + + val batches = listOf( + Batch(0, listOf(token1, token2, token3, token4, token5)), + Batch(1, listOf(token1, token7, token8, token9, token6)), + Batch(2, listOf(token2, token8, token8, token6, token10)), + ) + + // WHEN + val result = useCase(batches) + + // THEN + val seenIds = mutableSetOf() + + result.forEach { batch -> + batch.data.forEach { token -> + assertThat(seenIds).doesNotContain(token.id) + seenIds.add(token.id) + } + } + + val totalTokens = result.sumOf { it.data.size } + assertThat(totalTokens).isEqualTo(10) + } +} + +fun testToken( + id: String = UUID.randomUUID().toString(), + name: String = "Test Token", + symbol: String = "TTK", +): ManagedCryptoCurrency { + return ManagedCryptoCurrency.Token( + id = ManagedCryptoCurrency.ID(id), + name = name, + symbol = symbol, + iconUrl = "null", + availableNetworks = emptyList(), + addedIn = emptySet(), + ) +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index 7dde531506..d9052b18b2 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -1,7 +1,5 @@ package com.tangem.features.managetokens.utils.list -import androidx.compose.ui.util.fastForEachIndexed -import androidx.compose.ui.util.fastMap import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped @@ -10,10 +8,7 @@ import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase -import com.tangem.domain.managetokens.CheckHasLinkedTokensUseCase -import com.tangem.domain.managetokens.GetManagedTokensUseCase -import com.tangem.domain.managetokens.RemoveCustomManagedCryptoCurrencyUseCase +import com.tangem.domain.managetokens.* import com.tangem.domain.managetokens.model.* import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId @@ -43,6 +38,7 @@ import javax.inject.Inject @ModelScoped internal class ManageTokensListManager @Inject constructor( private val getManagedTokensUseCase: GetManagedTokensUseCase, + private val getDistinctManagedTokensUseCase: GetDistinctManagedCurrenciesUseCase, private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase, private val removeCustomCurrencyUseCase: RemoveCustomManagedCryptoCurrencyUseCase, private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, @@ -166,47 +162,27 @@ internal class ManageTokensListManager @Inject constructor( return } - state.update { state -> - val newBatches = distinctCurrencies(batchListState.data) - val currentBatches = state.currencyBatches + scope.launch { + state.update { state -> + val newBatches = getDistinctManagedTokensUseCase(batchListState.data) + val currentBatches = state.currencyBatches - // Distinct until changed - if (newBatches.size == currentBatches.size && - newBatches.map { it.key } == currentBatches.map { it.key } && - newBatches.flatMap { it.data } == currentBatches.flatMap { it.data } - ) { - return - } - - val canEditItems = userWalletId != null - state.copy( - userWalletId = userWalletId, - currencyBatches = newBatches, - uiBatches = uiManager.createOrUpdateUiBatches(newBatches, canEditItems), - canEditItems = canEditItems, - ) - } - } - - // FIXME: Add interception functionality to BatchFlow state and do this on domain - // [REDACTED_JIRA] - private fun distinctCurrencies( - batches: List>>, - ): List>> { - val allCurrenciesIds = mutableListOf() - - return batches.fastMap { batch -> - val batchCurrencies = batch.data.toMutableList() - - batch.data.fastForEachIndexed { index, currency -> - if (currency.id in allCurrenciesIds) { - batchCurrencies.removeAt(index) - } else { - allCurrenciesIds.add(currency.id) + // Distinct until changed + if (newBatches.size == currentBatches.size && + newBatches.map { it.key } == currentBatches.map { it.key } && + newBatches.flatMap { it.data } == currentBatches.flatMap { it.data } + ) { + return@launch } - } - batch.copy(data = batchCurrencies) + val canEditItems = userWalletId != null + state.copy( + userWalletId = userWalletId, + currencyBatches = newBatches, + uiBatches = uiManager.createOrUpdateUiBatches(newBatches, canEditItems), + canEditItems = canEditItems, + ) + } } } From ac6f73170e1816532d882562a9fd8b80f983bafd Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Apr 2025 09:22:16 +0300 Subject: [PATCH 075/138] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/routing/RootContent.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/routing/RootContent.kt b/app/src/main/java/com/tangem/tap/routing/RootContent.kt index f320d18a1c..d0c69a7abe 100644 --- a/app/src/main/java/com/tangem/tap/routing/RootContent.kt +++ b/app/src/main/java/com/tangem/tap/routing/RootContent.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -59,7 +60,9 @@ internal fun RootContent( } is RoutingComponent.Child.LegacyIntent -> { // TODO: Remove and use it's own router: [REDACTED_JIRA] - startActivity(context, instance.intent, Bundle.EMPTY) + LaunchedEffect(instance) { + startActivity(context, instance.intent, Bundle.EMPTY) + } } is RoutingComponent.Child.LegacyFragment, -> error("Unsupported child: $instance") From bdcd8f2ce8c7d9a8f2e2b378f126f24279bf583d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Apr 2025 14:24:40 +0700 Subject: [PATCH 076/138] Updated on 2026-08-14 --- .../app/TangemWcBlockchainHelper.kt | 2 +- .../DefaultLegacyWalletConnectRepository.kt | 1 + .../DefaultWalletConnectSessionsRepository.kt | 4 +- .../di/WalletConnectInteractorModule.kt | 1 + .../domain/LegacyWalletConnectRepository.kt | 1 + .../domain/WalletConnectInteractor.kt | 3 + .../domain/WcBlockchainHelper.kt | 2 +- .../domain/WcSessionRequestConverter.kt | 1 + .../domain/models/WalletConnectEvents.kt | 1 + core/datasource/build.gradle.kts | 1 + .../datasource/di/WalletConnectModule.kt | 45 ++++++++++ .../DefaultWalletConnectStore.kt | 23 +++++ .../local/walletconnect/WalletConnectStore.kt | 17 ++++ .../di/WalletConnectDataModule.kt | 18 +++- .../pair/DefaultWcPairUseCase.kt | 2 +- .../sessions/DefaultWcSessionsManager.kt | 88 +++++++++++++++---- .../data/walletconnect/utils/ToOurModel.kt | 14 +++ domain/wallet-connect/models/build.gradle.kts | 5 ++ .../walletconnect/model/WcSessionDTO.kt | 10 +++ .../walletconnect/model/legacy}/Account.kt | 2 +- .../walletconnect/model/legacy}/Session.kt | 2 +- .../WalletConnectSessionsRepository.kt | 4 +- .../model/sdkcopy/WcSdkSession.kt | 1 + .../repository/WcSessionsManager.kt | 4 +- features/walletconnect/impl/build.gradle.kts | 2 + 25 files changed, 224 insertions(+), 30 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/WalletConnectModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/DefaultWalletConnectStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/WalletConnectStore.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt rename {app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models => domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy}/Account.kt (84%) rename {app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models => domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy}/Session.kt (80%) rename {app/src/main/java/com/tangem/tap/domain/walletconnect2/domain => domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy}/WalletConnectSessionsRepository.kt (67%) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt index 95046dfdb8..b0e2c44579 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt @@ -4,7 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper -import com.tangem.tap.domain.walletconnect2.domain.models.Account +import com.tangem.domain.walletconnect.model.legacy.Account internal class TangemWcBlockchainHelper : WcBlockchainHelper { diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 3d1c5dc93f..9b88770588 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -9,6 +9,7 @@ import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.pair.unsupportedDApps +import com.tangem.domain.walletconnect.model.legacy.Account import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectSessionsRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectSessionsRepository.kt index 77b531934a..108492f2de 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectSessionsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultWalletConnectSessionsRepository.kt @@ -4,8 +4,8 @@ import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types import com.tangem.datasource.files.FileReader -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository -import com.tangem.tap.domain.walletconnect2.domain.models.Session +import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository +import com.tangem.domain.walletconnect.model.legacy.Session import timber.log.Timber internal class DefaultWalletConnectSessionsRepository( diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt index 3df4ed99be..3e47ce122d 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt @@ -7,6 +7,7 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.files.FileReader import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt index 289a2664b5..59d5954e00 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt @@ -1,5 +1,6 @@ package com.tangem.tap.domain.walletconnect2.domain +import com.tangem.domain.walletconnect.model.legacy.Account import com.tangem.tap.domain.walletconnect2.domain.models.* import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index 3d11b576d0..d8d5a989ae 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -6,6 +6,9 @@ import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletconnect.model.legacy.Account +import com.tangem.domain.walletconnect.model.legacy.Session +import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt index 91771ac6e1..74241e4cf6 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt @@ -1,7 +1,7 @@ package com.tangem.tap.domain.walletconnect2.domain import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.domain.walletconnect2.domain.models.Account +import com.tangem.domain.walletconnect.model.legacy.Account interface WcBlockchainHelper { fun chainIdToNetworkIdOrNull(chainId: String): String? diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt index 909120438b..16eaf90e60 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt @@ -1,5 +1,6 @@ package com.tangem.tap.domain.walletconnect2.domain +import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect2.domain.models.BnbData import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt index 85e6e06f5b..1298ede44d 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt @@ -1,5 +1,6 @@ package com.tangem.tap.domain.walletconnect2.domain.models +import com.tangem.domain.walletconnect.model.legacy.Account import com.tangem.tap.domain.walletconnect2.domain.WcRequest import java.net.URI diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 3e2fdba3e3..0efd84702a 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(projects.domain.onramp.models) implementation(projects.domain.models) implementation(projects.domain.nft.models) + implementation(projects.domain.walletConnect.models) /** Tangem libraries */ implementation(tangemDeps.blockchain) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/WalletConnectModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/WalletConnectModule.kt new file mode 100644 index 0000000000..4c5f149013 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/WalletConnectModule.kt @@ -0,0 +1,45 @@ +package com.tangem.datasource.di + +import android.content.Context +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.squareup.moshi.Moshi +import com.tangem.datasource.local.walletconnect.DefaultWalletConnectStore +import com.tangem.datasource.local.walletconnect.WalletConnectStore +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.datasource.utils.setTypes +import com.tangem.domain.walletconnect.model.WcSessionDTO +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object WalletConnectModule { + + @Provides + @Singleton + fun provideWalletConnectStore( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + dispatchers: CoroutineDispatcherProvider, + ): WalletConnectStore { + return DefaultWalletConnectStore( + persistenceStore = DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = setTypes(), + defaultValue = emptySet(), + ), + produceFile = { context.dataStoreFile(fileName = "wallet_connect_sessions") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/DefaultWalletConnectStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/DefaultWalletConnectStore.kt new file mode 100644 index 0000000000..57e97f18d7 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/DefaultWalletConnectStore.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.local.walletconnect + +import androidx.datastore.core.DataStore +import com.tangem.domain.walletconnect.model.WcSessionDTO +import kotlinx.coroutines.flow.Flow + +internal typealias WcSessionCollection = Set + +class DefaultWalletConnectStore( + private val persistenceStore: DataStore, +) : WalletConnectStore { + + override val sessions: Flow + get() = persistenceStore.data + + override suspend fun saveSessions(sessions: WcSessionCollection) { + persistenceStore.updateData { data -> data.plus(sessions) } + } + + override suspend fun removeSessions(sessions: WcSessionCollection) { + persistenceStore.updateData { data -> data.minus(sessions) } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/WalletConnectStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/WalletConnectStore.kt new file mode 100644 index 0000000000..4f3a8397b8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/WalletConnectStore.kt @@ -0,0 +1,17 @@ +package com.tangem.datasource.local.walletconnect + +import com.tangem.domain.walletconnect.model.WcSessionDTO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first + +interface WalletConnectStore { + + val sessions: Flow + suspend fun findSessionByTopic(topic: String) = sessions.first().find { it.topic == topic } + + suspend fun saveSessions(sessions: WcSessionCollection) + suspend fun saveSession(session: WcSessionDTO) = saveSessions(setOf(session)) + + suspend fun removeSessions(sessions: WcSessionCollection) + suspend fun removeSession(session: WcSessionDTO) = removeSessions(setOf(session)) +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index 57dd79d196..edab0d12af 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -18,8 +18,10 @@ import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletconnect.model.WcMethod +import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletconnect.repository.WalletConnectRepository import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.request.WcRequestService @@ -82,7 +84,21 @@ internal object WalletConnectDataModule { @Provides @Singleton - fun defaultWcSessionsManager(): DefaultWcSessionsManager = DefaultWcSessionsManager() + fun defaultWcSessionsManager( + store: WalletConnectStore, + dispatchers: CoroutineDispatcherProvider, + legacyStore: WalletConnectSessionsRepository, + getWallets: GetWalletsUseCase, + ): DefaultWcSessionsManager { + val scope = CoroutineScope(SupervisorJob() + dispatchers.io) + return DefaultWcSessionsManager( + store = store, + dispatchers = dispatchers, + legacyStore = legacyStore, + getWallets = getWallets, + scope = scope, + ) + } @Provides @Singleton diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index f574bcd29e..590b9acbf6 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -96,7 +96,7 @@ internal class DefaultWcPairUseCase( is Wallet.Model.SettledSessionResponse.Result -> { val newSession = settledSession.session.toDomain(sessionForApprove.walletId) - sessionsManager.saveSessions(sessionForApprove.walletId, newSession) + sessionsManager.saveSession(sessionForApprove.walletId, newSession) newSession.right() } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index cfd0a023d0..32df16baba 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -1,38 +1,92 @@ package com.tangem.data.walletconnect.sessions import com.reown.walletkit.client.Wallet +import com.reown.walletkit.client.WalletKit import com.tangem.data.walletconnect.utils.WcSdkObserver +import com.tangem.data.walletconnect.utils.toOurModel +import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.WcSessionDTO +import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.distinctUntilChanged +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber -internal class DefaultWcSessionsManager : WcSessionsManager, WcSdkObserver { +internal class DefaultWcSessionsManager constructor( + private val store: WalletConnectStore, + private val legacyStore: WalletConnectSessionsRepository, + private val getWallets: GetWalletsUseCase, + private val dispatchers: CoroutineDispatcherProvider, + private val scope: CoroutineScope, +) : WcSessionsManager, WcSdkObserver { - private val _sessions = MutableSharedFlow>>( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - override val sessions get() = _sessions.distinctUntilChanged() + override val sessions: Flow>> + get() = store.sessions + .onEach(::migrateLegacyStore) + .map(::associateWithSdk) + .distinctUntilChanged() + .flowOn(dispatchers.io) - override suspend fun saveSessions(userWalletId: UserWalletId, session: WcSession) { - TODO("Not yet implemented") + override suspend fun saveSession(userWalletId: UserWalletId, session: WcSession) { + store.saveSession(WcSessionDTO(session.sdkModel.topic, session.userWalletId)) } - override suspend fun removeSessions(userWalletId: UserWalletId, session: WcSession) { - TODO("Not yet implemented") + override suspend fun removeSession(userWalletId: UserWalletId, session: WcSession) { + store.removeSession(WcSessionDTO(session.sdkModel.topic, session.userWalletId)) } - override suspend fun findSessionByTopic(topic: String): WcSession? { - TODO("Not yet implemented") + override suspend fun findSessionByTopic(topic: String): WcSession? = withContext(dispatchers.io) { + val storedSessions = store.findSessionByTopic(topic) ?: return@withContext null + val sdkSession = WalletKit.getActiveSessionByTopic(topic) ?: return@withContext null + WcSession(userWalletId = storedSessions.walletId, sdkModel = sdkSession.toOurModel()) } override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { - // Triggered when the session is deleted by the peer + if (sessionDelete !is Wallet.Model.SessionDelete.Success) return Timber.i("onSessionDelete: $sessionDelete") - // todo (wc) find session, delete and update sessions Flow + scope.launch { + val storedSessions = store.findSessionByTopic(sessionDelete.topic) ?: return@launch + store.removeSession(storedSessions) + } + } + + private suspend fun migrateLegacyStore(inNewStoreSessions: Set) { + val walletIds = getWallets.invokeSync().mapTo(mutableSetOf()) { it.walletId } + val inLegacyStoreSessions = walletIds + .map { walletId -> + flow { emit(legacyStore.loadSessions(walletId.stringValue).map { WcSessionDTO(it.topic, walletId) }) } + } + .merge() + .first() + + val mustSaveInNewStore = inLegacyStoreSessions.subtract(inNewStoreSessions) + if (mustSaveInNewStore.isNotEmpty()) store.saveSessions(mustSaveInNewStore) + } + + private suspend fun associateWithSdk(storeSessions: Set): Map> { + val sdkSessions = WalletKit.getListOfActiveSessions() + val wcSessions = sdkSessions.mapNotNull { sdkSession -> + val storedSessions = storeSessions.find { it.topic == sdkSession.topic } + ?: return@mapNotNull null + WcSession(userWalletId = storedSessions.walletId, sdkModel = sdkSession.toOurModel()) + } + + val unknownStoredSessions = storeSessions + .filterNot { dto -> wcSessions.any { it.sdkModel.topic == dto.topic } } + + if (unknownStoredSessions.isNotEmpty()) { + unknownStoredSessions.forEach { unknown -> + legacyStore.removeSession(unknown.walletId.stringValue, unknown.topic) + } + store.removeSessions(unknownStoredSessions.toSet()) + } + + return wcSessions.groupBy { it.userWalletId } } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt index 5da50ba3c4..de7c1ff78d 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt @@ -1,12 +1,15 @@ package com.tangem.data.walletconnect.utils +import com.reown.android.Core import com.reown.walletkit.client.Wallet +import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest.JSONRPCRequest internal fun Wallet.Model.Session.toOurModel(): WcSdkSession = WcSdkSession( topic = topic, + appMetaData = metaData.toOurModel(), ) internal fun Wallet.Model.SessionRequest.toOurModel(): WcSdkSessionRequest = WcSdkSessionRequest( @@ -19,4 +22,15 @@ internal fun Wallet.Model.SessionRequest.JSONRPCRequest.toOurModel(): JSONRPCReq id = this.id, method = this.method, params = this.params, +) + +internal fun Core.Model.AppMetaData?.toOurModel(): WcAppMetaData = WcAppMetaData( + name = this?.name.orEmpty(), + description = this?.description.orEmpty(), + url = this?.url.orEmpty(), + icons = this?.icons.orEmpty(), + redirect = this?.redirect, + appLink = this?.appLink, + linkMode = this?.linkMode ?: false, + verifyUrl = this?.verifyUrl, ) \ No newline at end of file diff --git a/domain/wallet-connect/models/build.gradle.kts b/domain/wallet-connect/models/build.gradle.kts index 47fbf86d0b..c471057d7c 100644 --- a/domain/wallet-connect/models/build.gradle.kts +++ b/domain/wallet-connect/models/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.ksp) id("configuration") } dependencies { @@ -7,4 +8,8 @@ dependencies { /* Domain */ implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) + + /* Other */ + implementation(deps.moshi) + ksp(deps.moshi.kotlin.codegen) } \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt new file mode 100644 index 0000000000..63a946b9c6 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.walletconnect.model + +import com.squareup.moshi.JsonClass +import com.tangem.domain.wallets.models.UserWalletId + +@JsonClass(generateAdapter = true) +data class WcSessionDTO( + val topic: String, + val walletId: UserWalletId, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Account.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/Account.kt similarity index 84% rename from app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Account.kt rename to domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/Account.kt index 48114a57c4..8c370b9fbd 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Account.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/Account.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.walletconnect2.domain.models +package com.tangem.domain.walletconnect.model.legacy import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Session.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/Session.kt similarity index 80% rename from app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Session.kt rename to domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/Session.kt index 6950d29486..5ca9f80ed4 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/Session.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/Session.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.walletconnect2.domain.models +package com.tangem.domain.walletconnect.model.legacy import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectSessionsRepository.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/WalletConnectSessionsRepository.kt similarity index 67% rename from app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectSessionsRepository.kt rename to domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/WalletConnectSessionsRepository.kt index 395b928f86..dce7f7f080 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectSessionsRepository.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/legacy/WalletConnectSessionsRepository.kt @@ -1,6 +1,4 @@ -package com.tangem.tap.domain.walletconnect2.domain - -import com.tangem.tap.domain.walletconnect2.domain.models.Session +package com.tangem.domain.walletconnect.model.legacy interface WalletConnectSessionsRepository { suspend fun loadSessions(userWallet: String): List diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt index bd88f8ea98..9b2d6c45b7 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt @@ -5,4 +5,5 @@ package com.tangem.domain.walletconnect.model.sdkcopy */ data class WcSdkSession( val topic: String, + val appMetaData: WcAppMetaData, ) \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt index 4d2d837da0..7deafa02b2 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.Flow interface WcSessionsManager { val sessions: Flow>> - suspend fun saveSessions(userWalletId: UserWalletId, session: WcSession) - suspend fun removeSessions(userWalletId: UserWalletId, session: WcSession) + suspend fun saveSession(userWalletId: UserWalletId, session: WcSession) + suspend fun removeSession(userWalletId: UserWalletId, session: WcSession) suspend fun findSessionByTopic(topic: String): WcSession? } \ No newline at end of file diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index 0fb71c954e..e85bbfc291 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -13,6 +13,8 @@ android { dependencies { implementation(projects.features.walletconnect.api) + implementation(projects.domain.walletConnect) + implementation(projects.domain.walletConnect.models) /** Core */ implementation(projects.core.configToggles) From 10d6cc7beeb1dd486d622681ab1db1417195cd44 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Apr 2025 16:02:30 +0500 Subject: [PATCH 077/138] Updated on 2026-08-14 --- .../buttons/small/TangemIconButton.kt | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt new file mode 100644 index 0000000000..39a042f707 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt @@ -0,0 +1,72 @@ +package com.tangem.core.ui.components.buttons.small + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +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.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Small button icon + * + * @param iconRes icon resource + * @param onClick lambda be invoked when action component is clicked + * @param modifier composable modifier + * @param shape icon button shape + * @param background background color + * @param iconTint icon color + * + * [Show in Figma](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=4105-1439&t=nnYBX1qCZmUNhBDf-4) + */ +@Composable +fun TangemIconButton( + @DrawableRes iconRes: Int, + onClick: () -> Unit, + modifier: Modifier = Modifier, + shape: RoundedCornerShape = RoundedCornerShape(24.dp), + background: Color = TangemTheme.colors.button.secondary, + iconTint: Color = TangemTheme.colors.icon.secondary, +) { + IconButton( + onClick = onClick, + modifier = modifier + .clip(shape) + .background(background) + .size(24.dp), + ) { + Icon( + painter = rememberVectorPainter(ImageVector.vectorResource(iconRes)), + contentDescription = "", + tint = iconTint, + modifier = Modifier.size(16.dp), + ) + } +} + +// region Preview +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemIconButton_Preview() { + TangemThemePreview { + TangemIconButton( + iconRes = R.drawable.ic_close_24, + onClick = {}, + ) + } +} +// endregion \ No newline at end of file From 304dfb320541d17f5e3bd1accfb1f236a7726685 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Apr 2025 16:04:01 +0500 Subject: [PATCH 078/138] Updated on 2026-08-14 --- common/ui/build.gradle.kts | 1 + .../chooseaddress/ChooseAddressBottomSheet.kt | 4 +- .../ChooseAddressBottomSheetConfig.kt | 4 +- .../GiveTxPermisssionBottomSheet.kt | 2 +- .../ui/bottomsheet/receive}/AddressMappers.kt | 2 +- .../ui/bottomsheet/receive}/AddressModel.kt | 2 +- .../receive}/TokenReceiveBottomSheet.kt | 4 +- .../receive}/TokenReceiveBottomSheetConfig.kt | 2 +- .../expressStatus/ExpressStatusBottomSheet.kt | 2 +- core/ui/build.gradle.kts | 7 ++ .../ModalBottomSheetWithBackHandling.kt | 88 +++++++++++++++++++ .../message/MessageBottomSheet.kt | 2 +- .../{ => sheet}/TangemBottomSheet.kt | 88 ++----------------- .../TangemBottomSheetDraggableHeader.kt | 2 +- .../{ => sheet}/TangemBottomSheetTitle.kt | 2 +- .../impl/DefaultAskBiometryComponent.kt | 2 +- .../ui/AddCustomTokenBottomSheet.kt | 2 +- .../ui/components/ExchangesBottomSheet.kt | 2 +- .../impl/ui/components/InfoBottomSheet.kt | 4 +- .../ui/components/SecurityScoreBottomSheet.kt | 4 +- .../impl/model/TokenActionsHandler.kt | 4 +- .../impl/ui/AddToPortfolioBottomSheet.kt | 2 +- .../markets/portfolio/impl/ui/MyPortfolio.kt | 2 +- .../impl/ui/TokenActionsBottomSheet.kt | 4 +- .../impl/ui/WalletSelectorBottomSheet.kt | 2 +- .../MarketsListSortByBottomSheet.kt | 2 +- .../accesscode/ui/MultiWalletAccessCodeBS.kt | 2 +- .../ui/PassphraseInfoBottomSheet.kt | 2 +- .../topup/model/OnboardingNoteTopUpModel.kt | 4 +- .../topup/ui/OnboardingNoteTopUpScreen.kt | 2 +- .../v2/twin/impl/model/OnboardingTwinModel.kt | 4 +- .../v2/twin/impl/ui/OnboardingTwin.kt | 2 +- .../wallet2/ui/PassphraseInfoBottomSheet.kt | 2 +- .../ui/ConfirmResidencyBottomSheet.kt | 2 +- .../ui/OnrampAddToPortfolioBottomSheet.kt | 2 +- .../ui/SelectPaymentMethodBottomSheet.kt | 2 +- .../providers/ui/SelectProviderBottomSheet.kt | 2 +- .../ui/SelectCountryBottomSheet.kt | 2 +- .../ui/SelectCurrencyBottomSheet.kt | 2 +- .../QrScanningCameraDeniedBottomSheet.kt | 2 +- .../StakingActionSelectorBottomSheet.kt | 4 +- .../ui/bottomsheet/StakingInfoBottomSheet.kt | 4 +- .../feature/swap/ui/ChooseFeeBottomSheet.kt | 2 +- .../swap/ui/ChooseProviderBottomSheet.kt | 2 +- .../model/TokenDetailsClickIntents.kt | 2 +- .../tokendetails/model/TokenDetailsModel.kt | 4 +- .../state/factory/TokenDetailsStateFactory.kt | 6 +- .../tokendetails/ui/TokenDetailsScreen.kt | 8 +- .../express/ExpressStatusBottomSheet.kt | 2 +- .../WalletCurrencyActionsClickIntents.kt | 8 +- .../wallet/state/WalletStateController.kt | 1 - .../presentation/wallet/ui/WalletScreen.kt | 8 +- .../PushNotificationsBottomSheet.kt | 2 +- .../ui/components/TokenActionsBottomSheet.kt | 2 +- .../ui/components/common/WalletBottomSheet.kt | 2 +- .../visa/BalancesAndLimitsBottomSheet.kt | 2 +- .../visa/VisaTxDetailsBottomSheet.kt | 2 +- 57 files changed, 180 insertions(+), 155 deletions(-) rename {core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets => common/ui/src/main/java/com/tangem/common/ui/bottomsheet}/chooseaddress/ChooseAddressBottomSheet.kt (89%) rename {core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets => common/ui/src/main/java/com/tangem/common/ui/bottomsheet}/chooseaddress/ChooseAddressBottomSheetConfig.kt (67%) rename {core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive => common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive}/AddressMappers.kt (96%) rename {core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive => common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive}/AddressModel.kt (76%) rename {core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive => common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive}/TokenReceiveBottomSheet.kt (98%) rename {core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive => common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive}/TokenReceiveBottomSheetConfig.kt (87%) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/internal/ModalBottomSheetWithBackHandling.kt rename core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/{ => sheet}/TangemBottomSheet.kt (68%) rename core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/{ => sheet}/TangemBottomSheetDraggableHeader.kt (95%) rename core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/{ => sheet}/TangemBottomSheetTitle.kt (97%) diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index 42a053ad9b..fa468a6c9a 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { /** Project - Common */ implementation(projects.core.ui) implementation(projects.core.utils) + implementation(projects.core.res) implementation(projects.libs.crypto) /** Project - Domain */ diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheet.kt similarity index 89% rename from core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheet.kt rename to common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheet.kt index 098950828e..4054fddac7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheet.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.bottomsheets.chooseaddress +package com.tangem.common.ui.bottomsheet.chooseaddress import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.ui.R import com.tangem.core.ui.components.SimpleSettingsRow -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheetConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheetConfig.kt similarity index 67% rename from core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheetConfig.kt rename to common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheetConfig.kt index 2bbbe6c79e..cd34d703e9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/chooseaddress/ChooseAddressBottomSheetConfig.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheetConfig.kt @@ -1,7 +1,7 @@ -package com.tangem.core.ui.components.bottomsheets.chooseaddress +package com.tangem.common.ui.bottomsheet.chooseaddress +import com.tangem.common.ui.bottomsheet.receive.AddressModel import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import kotlinx.collections.immutable.ImmutableList class ChooseAddressBottomSheetConfig( 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 4291b55a7f..9476ea0cad 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 @@ -26,7 +26,7 @@ import com.tangem.common.ui.R import com.tangem.common.ui.bottomsheet.permission.state.* import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.inputrow.InputRowDefault diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressMappers.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressMappers.kt similarity index 96% rename from core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressMappers.kt rename to common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressMappers.kt index dfd23f44c2..e5d3092126 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressMappers.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressMappers.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.bottomsheets.tokenreceive +package com.tangem.common.ui.bottomsheet.receive import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressModel.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressModel.kt similarity index 76% rename from core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressModel.kt rename to common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressModel.kt index 8571fdfa35..b520e6ff03 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressModel.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressModel.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.bottomsheets.tokenreceive +package com.tangem.common.ui.bottomsheet.receive import com.tangem.core.ui.extensions.TextReference diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheet.kt similarity index 98% rename from core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt rename to common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheet.kt index d6f38a3567..a212153bfa 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheet.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.bottomsheets.tokenreceive +package com.tangem.common.ui.bottomsheet.receive import android.content.res.Configuration import androidx.compose.animation.animateColorAsState @@ -30,7 +30,7 @@ import androidx.compose.ui.unit.dp import com.tangem.core.res.getStringSafe import com.tangem.core.ui.R import com.tangem.core.ui.components.SecondaryButtonIconStart -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheetConfig.kt similarity index 87% rename from core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt rename to common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheetConfig.kt index eee9fdb3bd..463dc32fc4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheetConfig.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.bottomsheets.tokenreceive +package com.tangem.common.ui.bottomsheet.receive import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import kotlinx.collections.immutable.ImmutableList diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBottomSheet.kt index 98c0bfce00..2dc8a4cf7a 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBottomSheet.kt @@ -2,7 +2,7 @@ package com.tangem.common.ui.expressStatus import androidx.compose.runtime.Composable import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.res.TangemTheme diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index d6b5e0c90f..78fc52949f 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -6,6 +6,13 @@ plugins { android { namespace = "com.tangem.core.ui" + + packaging { + resources { + // To build and run composable preview + merges += "paymentrequest.proto" + } + } } dependencies { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/internal/ModalBottomSheetWithBackHandling.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/internal/ModalBottomSheetWithBackHandling.kt new file mode 100644 index 0000000000..b4df773674 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/internal/ModalBottomSheetWithBackHandling.kt @@ -0,0 +1,88 @@ +package com.tangem.core.ui.components.bottomsheets.internal + +import androidx.activity.compose.BackHandler +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.input.key.* +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ModalBottomSheetWithBackHandling( + onDismissRequest: () -> Unit, + onBack: (() -> Unit), + modifier: Modifier = Modifier, + sheetState: SheetState = rememberModalBottomSheetState(), + sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth, + shape: Shape = BottomSheetDefaults.ExpandedShape, + containerColor: Color = BottomSheetDefaults.ContainerColor, + contentColor: Color = contentColorFor(containerColor), + tonalElevation: Dp = 0.dp, + scrimColor: Color = BottomSheetDefaults.ScrimColor, + dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() }, + contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets }, + content: @Composable ColumnScope.() -> Unit, +) { + BackHandler(enabled = sheetState.targetValue != SheetValue.Hidden) { + onBack() + } + + val requester = remember { FocusRequester() } + val backPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current + + ModalBottomSheet( + onDismissRequest = onDismissRequest, + modifier = modifier + .focusRequester(requester) + .focusable() + .onPreviewKeyEvent { + if (it.key == Key.Back && it.type == KeyEventType.KeyUp && !it.nativeKeyEvent.isCanceled) { + backPressedDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() + return@onPreviewKeyEvent true + } + return@onPreviewKeyEvent false + }, + sheetState = sheetState, + sheetMaxWidth = sheetMaxWidth, + shape = shape, + containerColor = containerColor, + contentColor = contentColor, + tonalElevation = tonalElevation, + scrimColor = scrimColor, + dragHandle = dragHandle, + contentWindowInsets = contentWindowInsets, + properties = ModalBottomSheetDefaults.properties( + // Set false otherwise the onPreviewKeyEvent doesn't work at all. + // The functionality of shouldDismissOnBackPress is achieved by the BackHandler. + shouldDismissOnBackPress = false, + ), + content = content, + ) + + LaunchedEffect(Unit) { + delay(timeMillis = 200) + requester.requestFocus() + } +} + +@OptIn(ExperimentalMaterial3Api::class) +suspend fun SheetState.collapse(onCollapsed: () -> Unit) { + coroutineScope { + launch { hide() }.invokeOnCompletion { onCollapsed() } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheet.kt index f0f478d567..9ecae2ab38 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheet.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.extensions.resolveReference diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt similarity index 68% rename from core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt index c433f996ea..177cd551ad 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt @@ -1,29 +1,25 @@ -package com.tangem.core.ui.components.bottomsheets +package com.tangem.core.ui.components.bottomsheets.sheet -import androidx.activity.compose.BackHandler -import androidx.activity.compose.LocalOnBackPressedDispatcherOwner -import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.* -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue.Expanded +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.input.key.* import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp 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.internal.ModalBottomSheetWithBackHandling +import com.tangem.core.ui.components.bottomsheets.internal.collapse import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.WindowInsetsZero -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch /** * Bottom sheet with [content], [titleText] and optional [titleAction]. @@ -209,70 +205,4 @@ inline fun BasicBottomSheet( content = bsContent, ) } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ModalBottomSheetWithBackHandling( - onDismissRequest: () -> Unit, - onBack: (() -> Unit), - modifier: Modifier = Modifier, - sheetState: SheetState = rememberModalBottomSheetState(), - sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth, - shape: Shape = BottomSheetDefaults.ExpandedShape, - containerColor: Color = BottomSheetDefaults.ContainerColor, - contentColor: Color = contentColorFor(containerColor), - tonalElevation: Dp = 0.dp, - scrimColor: Color = BottomSheetDefaults.ScrimColor, - dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() }, - contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets }, - content: @Composable ColumnScope.() -> Unit, -) { - BackHandler(enabled = sheetState.targetValue != SheetValue.Hidden) { - onBack() - } - - val requester = remember { FocusRequester() } - val backPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current - - ModalBottomSheet( - onDismissRequest = onDismissRequest, - modifier = modifier - .focusRequester(requester) - .focusable() - .onPreviewKeyEvent { - if (it.key == Key.Back && it.type == KeyEventType.KeyUp && !it.nativeKeyEvent.isCanceled) { - backPressedDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() - return@onPreviewKeyEvent true - } - return@onPreviewKeyEvent false - }, - sheetState = sheetState, - sheetMaxWidth = sheetMaxWidth, - shape = shape, - containerColor = containerColor, - contentColor = contentColor, - tonalElevation = tonalElevation, - scrimColor = scrimColor, - dragHandle = dragHandle, - contentWindowInsets = contentWindowInsets, - properties = ModalBottomSheetDefaults.properties( - // Set false otherwise the onPreviewKeyEvent doesn't work at all. - // The functionality of shouldDismissOnBackPress is achieved by the BackHandler. - shouldDismissOnBackPress = false, - ), - content = content, - ) - - LaunchedEffect(Unit) { - delay(timeMillis = 200) - requester.requestFocus() - } -} - -@OptIn(ExperimentalMaterial3Api::class) -suspend fun SheetState.collapse(onCollapsed: () -> Unit) { - coroutineScope { - launch { hide() }.invokeOnCompletion { onCollapsed() } - } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetDraggableHeader.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetDraggableHeader.kt similarity index 95% rename from core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetDraggableHeader.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetDraggableHeader.kt index 37bff1013a..66b0ae6033 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetDraggableHeader.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetDraggableHeader.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.bottomsheets +package com.tangem.core.ui.components.bottomsheets.sheet import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetTitle.kt similarity index 97% rename from core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetTitle.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetTitle.kt index 65e7c87924..8ca60f6218 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheetTitle.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.bottomsheets +package com.tangem.core.ui.components.bottomsheets.sheet import android.content.res.Configuration import androidx.compose.runtime.Composable diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/DefaultAskBiometryComponent.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/DefaultAskBiometryComponent.kt index f76042e8fb..81ca241043 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/DefaultAskBiometryComponent.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/DefaultAskBiometryComponent.kt @@ -9,7 +9,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.essenty.instancekeeper.getOrCreateSimple import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.features.biometry.AskBiometryComponent diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt index 80ed6c4a56..c7d87cd784 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt index 4c7bd46d6c..8135d641d2 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt @@ -19,7 +19,7 @@ import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.audits.AuditLabelUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt index 23e16539b1..8081d1a0b8 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt @@ -7,9 +7,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt index 391e6b8969..e6ef8e94b6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt @@ -23,9 +23,9 @@ import androidx.compose.ui.util.fastForEachIndexed import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.resolveReference diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index 663a22a12f..1e67919846 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -6,8 +6,8 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig +import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.SnackbarMessage diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt index 262b9fca39..fdfbb03db2 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt @@ -26,7 +26,7 @@ import com.tangem.common.ui.userwallet.UserWalletItem import com.tangem.core.ui.components.* import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt index 811b350c65..615366fd0e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -18,7 +18,7 @@ import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SmallButtonShimmer import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt index 8a09c1758a..8cb81049e5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt @@ -9,9 +9,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.SimpleSettingsRow -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt index a9fdbdaef9..615c95ba9f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt @@ -18,7 +18,7 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.TangemBlockCardColors -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListSortByBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListSortByBottomSheet.kt index 9f08d614c4..d862db0b01 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListSortByBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListSortByBottomSheet.kt @@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.inputrow.InputRowChecked import com.tangem.core.ui.components.inputrow.inner.DividerContainer diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeBS.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeBS.kt index 2e7c8e89c6..c7b634eeb7 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeBS.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeBS.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.stringResourceSafe diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/PassphraseInfoBottomSheet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/PassphraseInfoBottomSheet.kt index 99c65af89b..a01530b02b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/PassphraseInfoBottomSheet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/PassphraseInfoBottomSheet.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.stringResourceSafe diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt index 28a0eda74b..959dcbe305 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt @@ -8,8 +8,8 @@ import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig +import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.card.repository.CardRepository diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt index e8dbf64055..1ae01521ea 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index 1fb3811bcf..0c7a4c59a2 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -15,8 +15,8 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig +import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList import com.tangem.core.ui.format.bigdecimal.crypto diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt index c4535a2f95..01799c96cc 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt @@ -20,7 +20,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemAnimations import com.tangem.core.ui.res.TangemTheme diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt index cddeb6277c..a2912a2666 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ui/ConfirmResidencyBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ui/ConfirmResidencyBottomSheet.kt index 4fedef0994..55250b545a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ui/ConfirmResidencyBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ui/ConfirmResidencyBottomSheet.kt @@ -17,7 +17,7 @@ import coil.request.ImageRequest import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.resolveReference diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/ui/OnrampAddToPortfolioBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/ui/OnrampAddToPortfolioBottomSheet.kt index 253dd52eed..c3e18ec36c 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/ui/OnrampAddToPortfolioBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/ui/OnrampAddToPortfolioBottomSheet.kt @@ -1,7 +1,7 @@ package com.tangem.features.onramp.hottokens.portfolio.ui import androidx.compose.runtime.Composable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.res.TangemTheme diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt index 27096e2eb8..e58782f5a5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt @@ -11,7 +11,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt index 39cc85083b..6f74ef7c95 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.util.fastForEach import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.TextReference diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt index 224e1c4168..aaea4ccb7e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.text.style.TextOverflow import coil.compose.AsyncImage import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.buttons.SecondarySmallButton diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt index 472d72efcb..8bcf48db89 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/ui/SelectCurrencyBottomSheet.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.util.fastForEach import coil.compose.AsyncImage import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.buttons.SecondarySmallButton diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt index dd1f555379..ca29304e81 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.SimpleSettingsRow -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt index 64c371054c..9a1c63bc5b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingActionSelectorBottomSheet.kt @@ -10,9 +10,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt index af0ceabcc0..ebc0b6ab11 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt @@ -9,9 +9,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index 13dc36fbac..836783751f 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.* diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index e8078c5bee..1b45b356a8 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt index c716c21445..eeaaa1174a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt @@ -1,6 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.model -import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel +import com.tangem.common.ui.bottomsheet.receive.AddressModel import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 6e167e2833..6ca825bee9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -17,8 +17,8 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.share.ShareManager import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel -import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels +import com.tangem.common.ui.bottomsheet.receive.AddressModel +import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index b69153ce42..980eac8c0f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -4,9 +4,9 @@ import androidx.paging.PagingData import arrow.core.Either import com.tangem.common.ui.tokens.getUnavailabilityReasonText import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels +import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig +import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index f315753e17..03b631022d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -18,10 +18,10 @@ import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.expressTransactionsItems -import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet -import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet -import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheet +import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.marketprice.MarketPriceBlockState diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheet.kt index 0bf0416539..360c82a53a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheet.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.OnrampStatusBottomSheetContent import com.tangem.common.ui.expressStatus.state.* -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index 6e1ec6a43d..ce71b37760 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -11,10 +11,10 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.navigation.share.ShareManager import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel -import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels +import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig +import com.tangem.common.ui.bottomsheet.receive.AddressModel +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig +import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.WrappedList diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index e9771676b7..cf9c56b44c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.state import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.event.consumedEvent import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 4dcbc3b0a9..01de3ee5da 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -47,10 +47,10 @@ import com.tangem.common.ui.expressStatus.expressTransactionsItems import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet -import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet -import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheet +import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.components.rememberIsKeyboardVisible import com.tangem.core.ui.components.sheetscaffold.* diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt index 4bbb305824..67fec39547 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt @@ -11,7 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.showcase.ShowcaseContent import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt index 5e4db0ab08..ea93bb097c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.SimpleSettingsRow -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.getDefaultRowColors import com.tangem.core.ui.components.getWarningRowColors diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt index 23d7179e34..ff2d45177d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt @@ -16,7 +16,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconStart import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SecondaryButtonIconStart -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt index d9296dab81..426ce24da0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt index 4511cf32ef..228fef3f7d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.SpacerW12 -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme From 93f6d5f2cce3164cfe09309bf116491b2763ea71 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Apr 2025 16:04:47 +0500 Subject: [PATCH 079/138] Updated on 2026-08-14 --- .../modal/TangemModalBottomSheet.kt | 262 ++++++++++++++++++ .../modal/TangemModalBottomSheetTitle.kt | 131 +++++++++ 2 files changed, 393 insertions(+) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetTitle.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt new file mode 100644 index 0000000000..f97a4f5daf --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt @@ -0,0 +1,262 @@ +package com.tangem.core.ui.components.bottomsheets.modal + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.material3.SheetValue.Expanded +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.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +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.SpacerH24 +import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.internal.ModalBottomSheetWithBackHandling +import com.tangem.core.ui.components.bottomsheets.internal.collapse +import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.WindowInsetsZero + +const val MODAL_SHEET_MAX_HEIGHT = 0.8f + +/** + * Modal bottom sheet with [content] and optional [title]. + * + * Maximum height of sheet is 80% screen height + * + * [Show in Figma](https://www.figma.com/design/09KKG4ZVuFDZhj8WLv5rGJ/%F0%9F%9A%A7-App-experience?node-id=3254-61208&t=vixa7id6ggALcxfF-4) + */ +@Composable +inline fun TangemModalBottomSheet( + config: TangemBottomSheetConfig, + containerColor: Color = TangemTheme.colors.background.primary, + skipPartiallyExpanded: Boolean = true, + noinline onBack: (() -> Unit)? = null, + crossinline title: @Composable BoxScope.(T) -> Unit = {}, + crossinline content: @Composable ColumnScope.(T) -> Unit, +) { + val isAlwaysVisible = LocalBottomSheetAlwaysVisible.current + + if (isAlwaysVisible) { + PreviewModalBottomSheet( + config = config, + containerColor = containerColor, + title = title, + content = content, + skipPartiallyExpanded = skipPartiallyExpanded, + ) + } else { + DefaultModalBottomSheet( + config = config, + containerColor = containerColor, + title = title, + content = content, + onBack = onBack, + skipPartiallyExpanded = skipPartiallyExpanded, + ) + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +inline fun DefaultModalBottomSheet( + config: TangemBottomSheetConfig, + containerColor: Color, + skipPartiallyExpanded: Boolean = true, + noinline onBack: (() -> Unit)? = null, + crossinline title: @Composable (BoxScope.(T) -> Unit), + crossinline content: @Composable (ColumnScope.(T) -> Unit), +) { + var isVisible by remember { mutableStateOf(value = config.isShown) } + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded) + + if (isVisible && config.content is T) { + BasicModalBottomSheet( + config = config, + sheetState = sheetState, + containerColor = containerColor, + title = title, + onBack = onBack, + content = content, + ) + } + + LaunchedEffect(key1 = config.isShown) { + if (config.isShown) { + isVisible = true + } else { + sheetState.collapse { isVisible = false } + } + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +inline fun PreviewModalBottomSheet( + config: TangemBottomSheetConfig, + containerColor: Color, + skipPartiallyExpanded: Boolean = true, + crossinline title: @Composable (BoxScope.(T) -> Unit), + crossinline content: @Composable (ColumnScope.(T) -> Unit), +) { + BasicModalBottomSheet( + config = config, + sheetState = SheetState( + skipPartiallyExpanded = skipPartiallyExpanded, + initialValue = Expanded, + density = LocalDensity.current, + ), + onBack = null, + containerColor = containerColor, + title = title, + content = content, + ) +} + +@Suppress("LongParameterList") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +inline fun BasicModalBottomSheet( + config: TangemBottomSheetConfig, + sheetState: SheetState, + containerColor: Color, + noinline onBack: (() -> Unit)? = null, + crossinline title: @Composable (BoxScope.(T) -> Unit), + crossinline content: @Composable (ColumnScope.(T) -> Unit), + modifier: Modifier = Modifier, +) { + val model = config.content as? T ?: return + + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + val bsContent: @Composable ColumnScope.() -> Unit = { + val maxHeight = LocalConfiguration.current.screenHeightDp * MODAL_SHEET_MAX_HEIGHT + + Column( + modifier = Modifier + .systemBarsPadding() + .padding(horizontal = 8.dp, vertical = 8.dp) + .clip(TangemTheme.shapes.roundedCornersLarge) + .background(containerColor) + .heightIn(max = maxHeight.dp) + .padding(bottom = bottomBarHeight) + .fillMaxWidth(), + ) { + Box(modifier = Modifier.fillMaxWidth()) { + title(model) + } + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + ) { + content(model) + } + } + } + + if (onBack != null) { + ModalBottomSheetWithBackHandling( + modifier = modifier, + onDismissRequest = config.onDismissRequest, + sheetState = sheetState, + containerColor = Color.Transparent, + shape = TangemTheme.shapes.roundedCornersLarge, + contentWindowInsets = { WindowInsetsZero }, + onBack = onBack, + dragHandle = null, + content = bsContent, + ) + } else { + ModalBottomSheet( + modifier = modifier, + onDismissRequest = config.onDismissRequest, + sheetState = sheetState, + containerColor = Color.Transparent, + shape = TangemTheme.shapes.roundedCornersLarge, + contentWindowInsets = { WindowInsetsZero }, + dragHandle = null, + content = bsContent, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 800) +@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemModalBottomSheet_Preview() { + TangemThemePreview { + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = TangemBottomSheetConfigContentPreviewConfig(), + ), + title = { + TangemModalBottomSheetTitle( + endIconRes = R.drawable.ic_close_24, + onEndClick = {}, + ) + }, + content = { + Column( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(100)) + .background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f)) + .padding(12.dp), + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_alert_24), + ), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + SpacerH24() + Text( + text = "Unsuported networks", + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + SpacerH8() + Text( + text = "Tangem does not currently support a required network by React App.", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + SpacerH(48.dp) + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = "Go it", + onClick = {}, + ) + } + }, + ) + } +} + +private class TangemBottomSheetConfigContentPreviewConfig : TangemBottomSheetConfigContent +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetTitle.kt new file mode 100644 index 0000000000..7029f1c2e0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetTitle.kt @@ -0,0 +1,131 @@ +package com.tangem.core.ui.components.bottomsheets.modal + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +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.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.core.ui.R +import com.tangem.core.ui.components.buttons.small.TangemIconButton +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Title component for [TangemModalBottomSheet] with [TangemIconButton] for buttons. + */ +@Composable +fun TangemModalBottomSheetTitle( + modifier: Modifier = Modifier, + title: TextReference? = null, + @DrawableRes startIconRes: Int? = null, + onStartClick: (() -> Unit)? = null, + @DrawableRes endIconRes: Int? = null, + onEndClick: (() -> Unit)? = null, +) { + Box( + modifier = modifier + .fillMaxWidth() + .height(56.dp), + ) { + if (startIconRes != null && onStartClick != null) { + TangemIconButton( + iconRes = startIconRes, + onClick = onStartClick, + modifier = Modifier + .padding(16.dp) + .align(Alignment.CenterStart), + ) + } + if (title != null) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.align(Alignment.Center), + ) + } + if (endIconRes != null && onEndClick != null) { + TangemIconButton( + iconRes = endIconRes, + onClick = onEndClick, + modifier = Modifier + .padding(16.dp) + .align(Alignment.CenterEnd), + ) + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_TangemModalBottomSheetTitle( + @PreviewParameter(TangemModalBottomSheetTitleProvider::class) params: TangemModalBottomSheetTitleData, +) { + TangemThemePreview { + TangemModalBottomSheetTitle( + title = params.title, + startIconRes = params.startIconRes, + onStartClick = params.onStartClick, + endIconRes = params.endIconRes, + onEndClick = params.onEndClick, + modifier = Modifier.background(TangemTheme.colors.background.secondary), + ) + } +} + +private data class TangemModalBottomSheetTitleData( + val title: TextReference?, + val startIconRes: Int?, + val onStartClick: (() -> Unit)?, + val endIconRes: Int?, + val onEndClick: (() -> Unit)?, +) + +private class TangemModalBottomSheetTitleProvider : PreviewParameterProvider { + override val values = sequenceOf( + TangemModalBottomSheetTitleData( + title = resourceReference(R.string.wallet_title), + startIconRes = R.drawable.ic_back_24, + onStartClick = {}, + endIconRes = null, + onEndClick = null, + ), + TangemModalBottomSheetTitleData( + title = resourceReference(R.string.wallet_title), + startIconRes = null, + onStartClick = null, + endIconRes = R.drawable.ic_close_24, + onEndClick = {}, + ), + TangemModalBottomSheetTitleData( + title = resourceReference(R.string.wallet_title), + startIconRes = R.drawable.ic_back_24, + onStartClick = {}, + endIconRes = R.drawable.ic_close_24, + onEndClick = {}, + ), + TangemModalBottomSheetTitleData( + title = null, + startIconRes = R.drawable.ic_back_24, + onStartClick = {}, + endIconRes = R.drawable.ic_close_24, + onEndClick = {}, + ), + ) +} +// endregion Preview \ No newline at end of file From e4477a24024dd03d71639ee041229957c00dda69 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Apr 2025 11:16:52 +0300 Subject: [PATCH 080/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 21d426e758..254de504ec 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,5 +1,5 @@ [versions] -tangemBlockchainSdk = "develop-990" +tangemBlockchainSdk = "develop-1000" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-445" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 57684bfeef03bbb474102e27a02dee0f159accd0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 31 Mar 2025 15:30:48 +0500 Subject: [PATCH 081/138] Updated on 2026-08-14 --- common/ui/build.gradle.kts | 1 + .../ui/tokens/TokenItemStateConverter.kt | 38 +++++------------- data/tokens/build.gradle.kts | 1 + .../DefaultCurrencyChecksRepository.kt | 8 +++- domain/staking/build.gradle.kts | 1 + .../staking/model/stakekit/YieldBalance.kt | 25 +----------- .../domain/staking/utils/YieldBalanceExt.kt | 39 +++++++++++++++++++ .../TokenListFiatBalanceOperations.kt | 32 ++++++--------- .../operations/TokenListSortingOperations.kt | 11 ++---- .../OnrampTokenItemStateConverterFactory.kt | 4 +- .../impl/presentation/model/StakingModel.kt | 1 + .../converters/YieldBalancesConverter.kt | 6 +-- ...TokenDetailsBalanceSelectStateConverter.kt | 16 +++----- .../TokenDetailsLoadedBalanceConverter.kt | 20 +++++----- .../TokenDetailsStakingInfoConverter.kt | 4 +- .../tokendetails/state/utils/BalanceUtils.kt | 8 +--- features/wallet/impl/build.gradle.kts | 1 + .../CryptoCurrencyToDraggableItemConverter.kt | 9 ++--- 18 files changed, 105 insertions(+), 120 deletions(-) create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index fa468a6c9a..4cbc5cccfb 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.legacy) implementation(projects.domain.staking.models) + implementation(projects.domain.staking) implementation(projects.domain.tokens.models) implementation(projects.domain.transaction.models) implementation(projects.domain.wallets.models) diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index c33c364110..4a8dc44eab 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -14,8 +14,8 @@ import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.isZero @@ -121,36 +121,27 @@ class TokenItemStateConverter( companion object { - fun CryptoCurrencyStatus.getFormattedFiatAmount(appCurrency: AppCurrency, includeStaking: Boolean): String { + fun CryptoCurrencyStatus.getFormattedFiatAmount(appCurrency: AppCurrency): String { val fiatAmount = value.fiatAmount ?: return DASH_SIGN - val totalAmount = if (includeStaking) { - val fiatYieldBalance = value.fiatRate?.times(getStakedBalance()).orZero() - - fiatAmount.plus(fiatYieldBalance) - } else { - fiatAmount - } + val fiatYieldBalance = value.fiatRate?.times(getStakedBalance()).orZero() + val totalAmount = fiatAmount.plus(fiatYieldBalance) return totalAmount.format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) } } - fun CryptoCurrencyStatus.getFormattedCryptoAmount(includeStaking: Boolean): String { + fun CryptoCurrencyStatus.getFormattedCryptoAmount(): String { val cryptoAmount = value.amount ?: return DASH_SIGN - val totalAmount = if (includeStaking) { - cryptoAmount.plus(getStakedBalance()) - } else { - cryptoAmount - } + val totalAmount = cryptoAmount.plus(getStakedBalance()) return totalAmount.format { crypto(currency) } } - private fun CryptoCurrencyStatus.getStakedBalance() = - (value.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance().orZero() + private fun CryptoCurrencyStatus.getStakedBalance() = (value.yieldBalance as? YieldBalance.Data) + ?.getTotalWithRewardsStakingBalance(blockchainId = currency.network.id.value).orZero() private fun createTitleState(currencyStatus: CryptoCurrencyStatus): TokenItemState.TitleState { return when (val value = currencyStatus.value) { @@ -200,11 +191,7 @@ class TokenItemStateConverter( is CryptoCurrencyStatus.NoAccount, -> { TokenItemState.Subtitle2State.TextContent( - text = status.getFormattedCryptoAmount( - includeStaking = BlockchainUtils.isIncludeStakingTotalBalance( - status.currency.network.id.value, - ), - ), + text = status.getFormattedCryptoAmount(), isFlickering = status.value.isFlickering(), ) } @@ -227,12 +214,7 @@ class TokenItemStateConverter( is CryptoCurrencyStatus.NoAccount, -> { TokenItemState.FiatAmountState.Content( - text = status.getFormattedFiatAmount( - appCurrency = appCurrency, - includeStaking = BlockchainUtils.isIncludeStakingTotalBalance( - status.currency.network.id.value, - ), - ), + text = status.getFormattedFiatAmount(appCurrency = appCurrency), isFlickering = status.value.isFlickering(), icons = buildList { if (!status.getStakedBalance().isZero()) { diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index 499154d785..8eb2673b05 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) implementation(projects.domain.staking.models) + implementation(projects.domain.staking) /** Project - Data */ implementation(projects.core.datasource) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt index f72dea5b6d..b79cc3bc30 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt @@ -7,6 +7,7 @@ import com.tangem.blockchain.common.ReserveAmountProvider import com.tangem.blockchain.common.UtxoAmountLimitProvider import com.tangem.data.tokens.converters.UtxoConverter import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.utils.getTotalStakingBalance import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.CurrencyAmount @@ -18,6 +19,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.isZero +import com.tangem.utils.extensions.orZero import kotlinx.coroutines.withContext import java.math.BigDecimal @@ -132,8 +134,10 @@ internal class DefaultCurrencyChecksRepository( ): CryptoCurrencyWarning.Rent? { val rentData = walletManagersFacade.getRentInfo(userWalletId, currencyStatus.currency.network) ?: return null val balanceValue = currencyStatus.value as? CryptoCurrencyStatus.Loaded ?: return null - val stakingTotalBalance = - (balanceValue.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance() ?: BigDecimal.ZERO + val stakingBalance = balanceValue.yieldBalance as? YieldBalance.Data + val stakingTotalBalance = stakingBalance?.getTotalStakingBalance( + blockchainId = currencyStatus.currency.network.id.value, + ).orZero() return when { balanceValue.amount.isZero() && stakingTotalBalance.isZero() -> null balanceValue.amount < rentData.exemptionAmount && stakingTotalBalance.isZero() -> { diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index d87b8d595b..45a68f97f1 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -27,4 +27,5 @@ dependencies { implementation(tangemDeps.blockchain) { exclude(module = "joda-time") } + implementation(projects.libs.crypto) } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index 8856f70ef3..ce016aee04 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -15,30 +15,7 @@ sealed class YieldBalance { override val address: String, val balance: YieldBalanceItem, val source: StatusSource, - ) : YieldBalance() { - fun getTotalWithRewardsStakingBalance(): BigDecimal { - return balance.items.sumOf { it.amount } - } - - fun getTotalStakingBalance(): BigDecimal { - return balance.items - .filterNot { it.type == BalanceType.REWARDS } - .sumOf { it.amount } - } - - fun getRewardStakingBalance(): BigDecimal { - return balance.items - .filter { it.type == BalanceType.REWARDS } - .sumOf { it.amount } - } - - fun getValidatorsCount(): Int { - return balance.items - .filterNot { it.validatorAddress.isNullOrBlank() } - .distinctBy { it.validatorAddress } - .size - } - } + ) : YieldBalance() data class Empty( override val integrationId: String?, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt b/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt new file mode 100644 index 0000000000..fc7a759b50 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/utils/YieldBalanceExt.kt @@ -0,0 +1,39 @@ +package com.tangem.domain.staking.utils + +import com.tangem.domain.staking.model.stakekit.BalanceType +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.lib.crypto.BlockchainUtils +import java.math.BigDecimal + +fun YieldBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String): BigDecimal { + return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) { + balance.items.sumOf { it.amount } + } else { + getRewardStakingBalance() + } +} + +fun YieldBalance.Data.getTotalStakingBalance(blockchainId: String): BigDecimal { + return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) { + balance.items + .filterNot { it.type == BalanceType.REWARDS } + .sumOf { it.amount } + } else { + balance.items + .filterNot { it.type == BalanceType.REWARDS } + .sumOf { it.amount } - getRewardStakingBalance() + } +} + +fun YieldBalance.Data.getRewardStakingBalance(): BigDecimal { + return balance.items + .filter { it.type == BalanceType.REWARDS } + .sumOf { it.amount } +} + +fun YieldBalance.Data.getValidatorsCount(): Int { + return balance.items + .filterNot { it.validatorAddress.isNullOrBlank() } + .distinctBy { it.validatorAddress } + .size +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt index 2a655fba4c..1bb9ceb2f4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt @@ -4,10 +4,10 @@ import arrow.core.NonEmptyList import com.tangem.domain.models.StatusSource import com.tangem.domain.models.getResultStatusSource import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.lib.crypto.BlockchainUtils.isIncludeStakingTotalBalance import com.tangem.utils.extensions.orZero import java.math.BigDecimal @@ -22,8 +22,7 @@ internal class TokenListFiatBalanceOperations( if (isAnyTokenLoading) return fiatBalance for (token in currencies) { - val networkId = token.currency.network.id.value - val includeStakingBalance = isIncludeStakingTotalBalance(networkId) + val blockchainId = token.currency.network.id.value when (val status = token.value) { is CryptoCurrencyStatus.Loading -> { fiatBalance = TotalFiatBalance.Loading @@ -38,7 +37,7 @@ internal class TokenListFiatBalanceOperations( is CryptoCurrencyStatus.Unreachable, is CryptoCurrencyStatus.NoAmount, -> { - if (BlockchainUtils.isIncludeToBalanceOnError(networkId)) { + if (BlockchainUtils.isIncludeToBalanceOnError(blockchainId)) { fiatBalance = recalculateNoAccountBalance(status, fiatBalance) } else { fiatBalance = TotalFiatBalance.Failed @@ -49,10 +48,10 @@ internal class TokenListFiatBalanceOperations( fiatBalance = recalculateNoAccountBalance(status, fiatBalance) } is CryptoCurrencyStatus.Loaded -> { - fiatBalance = recalculateBalance(status, fiatBalance, includeStakingBalance) + fiatBalance = recalculateBalance(status, fiatBalance, blockchainId) } is CryptoCurrencyStatus.Custom -> { - fiatBalance = recalculateBalance(status, fiatBalance, includeStakingBalance) + fiatBalance = recalculateBalance(status, fiatBalance, blockchainId) } } } @@ -77,16 +76,12 @@ internal class TokenListFiatBalanceOperations( private fun recalculateBalance( status: CryptoCurrencyStatus.Loaded, currentBalance: TotalFiatBalance, - includeStakingBalance: Boolean, + blockchainId: String, ): TotalFiatBalance { return with(currentBalance) { val yieldBalance = status.yieldBalance as? YieldBalance.Data - val stakingBalance = yieldBalance?.getTotalWithRewardsStakingBalance().orZero() - val fiatStakingBalance = if (includeStakingBalance) { - status.fiatRate.times(stakingBalance) - } else { - BigDecimal.ZERO - } + val stakingBalance = yieldBalance?.getTotalWithRewardsStakingBalance(blockchainId).orZero() + val fiatStakingBalance = status.fiatRate.times(stakingBalance) (this as? TotalFiatBalance.Loaded)?.copy( amount = this.amount + status.fiatAmount + fiatStakingBalance, ) ?: TotalFiatBalance.Loaded( @@ -100,16 +95,13 @@ internal class TokenListFiatBalanceOperations( private fun recalculateBalance( status: CryptoCurrencyStatus.Custom, currentBalance: TotalFiatBalance, - includeStakingBalance: Boolean, + blockchainId: String, ): TotalFiatBalance { return with(currentBalance) { val isTokenAmountCanBeSummarized = status.fiatAmount != null - val yieldBalance = (status.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance().orZero() - val fiatYieldBalance = if (includeStakingBalance) { - status.fiatRate?.times(yieldBalance).orZero() - } else { - BigDecimal.ZERO - } + val yieldBalance = (status.yieldBalance as? YieldBalance.Data) + ?.getTotalWithRewardsStakingBalance(blockchainId).orZero() + val fiatYieldBalance = status.fiatRate?.times(yieldBalance).orZero() (this as? TotalFiatBalance.Loaded)?.copy( amount = this.amount + status.fiatAmount.orZero() + fiatYieldBalance, isAllAmountsSummarized = isTokenAmountCanBeSummarized, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt index fd6ff690a3..f884cd6662 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt @@ -8,8 +8,8 @@ import arrow.core.raise.ensure import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptyListOrNull import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.model.* -import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.extensions.orZero import java.math.BigDecimal @@ -99,13 +99,8 @@ internal class TokenListSortingOperations( private fun CryptoCurrencyStatus.getTotalBalance(): BigDecimal { val yieldBalance = value.yieldBalance as? YieldBalance.Data - val totalYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance().orZero() - val totalFiatYieldBalance = if (BlockchainUtils.isIncludeStakingTotalBalance(currency.network.id.value)) { - totalYieldBalance.multiply(value.fiatRate.orZero()) - } else { - BigDecimal.ZERO - } - + val totalYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance(currency.network.id.value).orZero() + val totalFiatYieldBalance = totalYieldBalance.multiply(value.fiatRate.orZero()) return value.fiatAmount?.plus(totalFiatYieldBalance).orZero() } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt index 0491aa226a..b54f7d0386 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt @@ -68,7 +68,7 @@ internal object OnrampTokenItemStateConverterFactory { is CryptoCurrencyStatus.NoAccount, -> { TokenItemState.Subtitle2State.TextContent( - text = status.getFormattedCryptoAmount(includeStaking = false), + text = status.getFormattedCryptoAmount(), isFlickering = status.value.isFlickering(), ) } @@ -92,7 +92,7 @@ internal object OnrampTokenItemStateConverterFactory { is CryptoCurrencyStatus.NoAccount, -> { TokenItemState.FiatAmountState.TextContent( - text = status.getFormattedFiatAmount(appCurrency = appCurrency, includeStaking = false), + text = status.getFormattedFiatAmount(appCurrency = appCurrency), isAvailable = isAvailable, isFlickering = status.value.isFlickering(), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 9b6c29105e..9a9798fba7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -36,6 +36,7 @@ import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction +import com.tangem.domain.staking.utils.getValidatorsCount import com.tangem.domain.tokens.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 02a6356ad2..e663307922 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.staking.utils.getRewardStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.YieldReward @@ -31,11 +32,10 @@ internal class YieldBalancesConverter( val appCurrency = appCurrencyProvider() val cryptoCurrency = cryptoCurrencyStatus.currency - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance + val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data val balanceToShowItems = balancesToShowProvider() - return if (yieldBalance is YieldBalance.Data || balanceToShowItems.any { it.isPending }) { - val yieldBalance = yieldBalance as? YieldBalance.Data + return if (yieldBalance != null || balanceToShowItems.any { it.isPending }) { val cryptoRewardsValue = yieldBalance?.getRewardStakingBalance() val fiatRate = cryptoCurrencyStatus.value.fiatRate diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt index 3425cedb3d..c9a852d86d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt @@ -5,10 +5,10 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance -import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import java.math.BigDecimal @@ -30,10 +30,10 @@ internal class TokenDetailsBalanceSelectStateConverter( } val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data - val stakingCryptoAmount = yieldBalance?.getTotalWithRewardsStakingBalance() + val stakingCryptoAmount = yieldBalance?.getTotalWithRewardsStakingBalance( + cryptoCurrencyStatus.currency.network.id.value, + ) val stakingFiatAmount = stakingCryptoAmount?.let { cryptoCurrencyStatus.value.fiatRate?.multiply(it) } - val includeStakingTotalBalance = - BlockchainUtils.isIncludeStakingTotalBalance(cryptoCurrencyStatus.currency.network.id.value) copy( tokenBalanceBlockState = if (tokenBalanceBlockState is TokenDetailsBalanceBlockState.Content) { tokenBalanceBlockState.copy( @@ -43,13 +43,11 @@ internal class TokenDetailsBalanceSelectStateConverter( stakingFiatAmount = stakingFiatAmount, selectedBalanceType = value.type, appCurrency = appCurrencyProvider(), - includeStaking = includeStakingTotalBalance, ), displayCryptoBalance = formatCryptoAmount( status = cryptoCurrencyStatus, stakingCryptoAmount = stakingCryptoAmount, selectedBalanceType = value.type, - includeStaking = includeStakingTotalBalance, ), ) } else { @@ -64,9 +62,8 @@ internal class TokenDetailsBalanceSelectStateConverter( stakingFiatAmount: BigDecimal?, selectedBalanceType: BalanceType, appCurrency: AppCurrency, - includeStaking: Boolean, ): String { - val fiatAmount = status.fiatAmount?.getBalance(selectedBalanceType, stakingFiatAmount, includeStaking) + val fiatAmount = status.fiatAmount?.getBalance(selectedBalanceType, stakingFiatAmount) return fiatAmount.format { fiat( @@ -80,9 +77,8 @@ internal class TokenDetailsBalanceSelectStateConverter( status: CryptoCurrencyStatus, stakingCryptoAmount: BigDecimal?, selectedBalanceType: BalanceType, - includeStaking: Boolean, ): String { - val amount = status.value.amount?.getBalance(selectedBalanceType, stakingCryptoAmount, includeStaking) + val amount = status.value.amount?.getBalance(selectedBalanceType, stakingCryptoAmount) return amount.format { crypto(status.currency) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 12a86802f4..04fff27647 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -10,14 +10,16 @@ import com.tangem.core.ui.format.bigdecimal.* import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents -import com.tangem.feature.tokendetails.presentation.tokendetails.state.* +import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance -import com.tangem.lib.crypto.BlockchainUtils.isIncludeStakingTotalBalance import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter @@ -84,10 +86,12 @@ internal class TokenDetailsLoadedBalanceConverter( currentState: TokenDetailsBalanceBlockState, status: CryptoCurrencyStatus, ): TokenDetailsBalanceBlockState { - val stakingCryptoAmount = (status.value.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance() + val stakingCryptoAmount = + (status.value.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance( + status.currency.network.id.value, + ) val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } val isBalanceSelectorEnabled = !stakingCryptoAmount.isNullOrZero() - val includeStakingTotalBalance = isIncludeStakingTotalBalance(status.currency.network.id.value) return when (status.value) { is CryptoCurrencyStatus.NoQuote, is CryptoCurrencyStatus.Loaded, @@ -102,13 +106,11 @@ internal class TokenDetailsLoadedBalanceConverter( stakingFiatAmount, currentState.selectedBalanceType, appCurrencyProvider(), - includeStakingTotalBalance, ), displayCryptoBalance = formatCryptoAmount( status, stakingCryptoAmount, currentState.selectedBalanceType, - includeStakingTotalBalance, ), balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig, onBalanceSelect = clickIntents::onBalanceSelect, @@ -192,10 +194,9 @@ internal class TokenDetailsLoadedBalanceConverter( stakingFiatAmount: BigDecimal?, selectedBalanceType: BalanceType, appCurrency: AppCurrency, - includeStaking: Boolean, ): String { val fiatAmount = status.fiatAmount ?: return DASH_SIGN - val totalAmount = fiatAmount.getBalance(selectedBalanceType, stakingFiatAmount, includeStaking) + val totalAmount = fiatAmount.getBalance(selectedBalanceType, stakingFiatAmount) return totalAmount.format { fiat( @@ -209,10 +210,9 @@ internal class TokenDetailsLoadedBalanceConverter( status: CryptoCurrencyStatus, stakingCryptoAmount: BigDecimal?, selectedBalanceType: BalanceType, - includeStaking: Boolean, ): String { val amount = status.value.amount ?: return DASH_SIGN - val totalAmount = amount.getBalance(selectedBalanceType, stakingCryptoAmount, includeStaking) + val totalAmount = amount.getBalance(selectedBalanceType, stakingCryptoAmount) return totalAmount.format { crypto(status.currency) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt index 72d2697d5e..0f402c7631 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt @@ -13,6 +13,8 @@ import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.utils.getRewardStakingBalance +import com.tangem.domain.staking.utils.getTotalStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState @@ -55,7 +57,7 @@ internal class TokenDetailsStakingInfoConverter( private fun getStakingInfoBlock(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM? { val yieldBalance = status.value.yieldBalance as? YieldBalance.Data - val stakingCryptoAmount = yieldBalance?.getTotalStakingBalance() + val stakingCryptoAmount = yieldBalance?.getTotalStakingBalance(status.currency.network.id.value) val pendingBalances = yieldBalance?.balance?.items ?: emptyList() val iconState = state.tokenInfoBlockState.iconState diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt index 25b4f3ecc1..fe714ff2ca 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt @@ -3,12 +3,8 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.utils import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType import java.math.BigDecimal -internal fun BigDecimal.getBalance( - selectedBalanceType: BalanceType, - stakingAmount: BigDecimal?, - includeStaking: Boolean, -): BigDecimal { - return if (selectedBalanceType == BalanceType.ALL && stakingAmount != null && includeStaking) { +internal fun BigDecimal.getBalance(selectedBalanceType: BalanceType, stakingAmount: BigDecimal?): BigDecimal { + return if (selectedBalanceType == BalanceType.ALL && stakingAmount != null) { this.plus(stakingAmount) } else { this diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 8416888824..595cdad4de 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -84,6 +84,7 @@ dependencies { implementation(projects.domain.analytics) implementation(projects.domain.visa) implementation(projects.domain.staking.models) + implementation(projects.domain.staking) implementation(projects.domain.markets.models) implementation(projects.domain.feedback) implementation(projects.domain.onramp.models) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index c7302dd54a..a8124c5b6a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -8,11 +8,11 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId -import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.orZero @@ -65,11 +65,8 @@ internal class CryptoCurrencyToDraggableItemConverter( private fun getFormattedFiatAmount(currency: CryptoCurrencyStatus, appCurrency: AppCurrency): String { val yieldBalance = currency.value.yieldBalance as? YieldBalance.Data val fiatRate = currency.value.fiatRate ?: BigDecimal.ZERO - val fiatYieldBalance = if (BlockchainUtils.isIncludeStakingTotalBalance(currency.currency.network.id.value)) { - yieldBalance?.getTotalWithRewardsStakingBalance()?.multiply(fiatRate).orZero() - } else { - BigDecimal.ZERO - } + val fiatYieldBalance = yieldBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.id.value) + ?.multiply(fiatRate).orZero() val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN return (fiatAmount + fiatYieldBalance).format { fiat(appCurrency.code, appCurrency.symbol) } From 7a9bea464d4ee8cdfee038d388e35d1c6971e8b9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Apr 2025 13:53:30 +0300 Subject: [PATCH 082/138] Updated on 2026-08-14 --- .../com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt index d6e075bb8a..8a8f3e30d3 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -32,9 +32,7 @@ internal class WalletManagerFactoryCreator @Inject constructor( config = config, blockchainProviderTypes = blockchainProviderTypes, accountCreator = accountCreator, - featureToggles = BlockchainFeatureToggles( - isEthereumEIP1559Enabled = true, - ), + featureToggles = BlockchainFeatureToggles(), blockchainDataStorage = blockchainDataStorage, loggers = listOf(blockchainSDKLogger), ) From 7a45703d68e9aef2409d1da59372fc915e180060 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Apr 2025 16:14:21 +0500 Subject: [PATCH 083/138] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../exchangeServices/moonpay/MoonpayBlockchainMapping.kt | 1 + .../com/tangem/data/common/currency/NetworkOperations.kt | 1 + .../tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt | 1 + .../com/tangem/domain/common/configs/Wallet2CardConfig.kt | 2 ++ .../tangem/domain/common/configs/Wallet2CardConfigTest.kt | 2 ++ .../com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 1 + .../main/java/com/tangem/blockchainsdk/utils/Blockchain.kt | 5 +++++ 8 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 8e5de701e0..29f73d909e 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 8e5de701e02a2d48a68e32cd4783549dde01983e +Subproject commit 29f73d909e14cb3760118b197b1929a6088bc40e diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt index a0f0c7f157..235cf6be4c 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt @@ -157,4 +157,5 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? Scroll, ScrollTestnet -> null ZkLinkNova, ZkLinkNovaTestnet -> null KaspaTestnet -> null + Pepecoin, PepecoinTestnet -> null } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt index a0fde030cc..7e0a654891 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/NetworkOperations.kt @@ -301,6 +301,7 @@ private fun Blockchain.getSupportedTransactionExtras(): Network.TransactionExtra Blockchain.ApeChain, Blockchain.ApeChainTestnet, Blockchain.Scroll, Blockchain.ScrollTestnet, Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet, + Blockchain.Pepecoin, Blockchain.PepecoinTestnet, -> Network.TransactionExtrasType.NONE // endregion } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt index aed9a68d2a..2291dbc4c6 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt @@ -157,5 +157,6 @@ public val Blockchain.mercuryoNetwork: String? Blockchain.Scroll, Blockchain.ScrollTestnet -> null Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet -> null Blockchain.KaspaTestnet -> null + Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> null } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt index aa0a5e23e6..b9fbfffbf3 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/configs/Wallet2CardConfig.kt @@ -205,6 +205,8 @@ data object Wallet2CardConfig : CardConfig { Blockchain.ScrollTestnet -> EllipticCurve.Secp256k1 Blockchain.ZkLinkNova -> EllipticCurve.Secp256k1 Blockchain.ZkLinkNovaTestnet -> EllipticCurve.Secp256k1 + Blockchain.Pepecoin -> EllipticCurve.Secp256k1 + Blockchain.PepecoinTestnet -> EllipticCurve.Secp256k1 } } } \ No newline at end of file diff --git a/domain/legacy/src/test/java/com/tangem/domain/common/configs/Wallet2CardConfigTest.kt b/domain/legacy/src/test/java/com/tangem/domain/common/configs/Wallet2CardConfigTest.kt index d80b9be554..b97e91d9d1 100644 --- a/domain/legacy/src/test/java/com/tangem/domain/common/configs/Wallet2CardConfigTest.kt +++ b/domain/legacy/src/test/java/com/tangem/domain/common/configs/Wallet2CardConfigTest.kt @@ -162,6 +162,8 @@ class Wallet2CardConfigTest { Blockchain.ScrollTestnet to EllipticCurve.Secp256k1, Blockchain.ZkLinkNova to EllipticCurve.Secp256k1, Blockchain.ZkLinkNovaTestnet to EllipticCurve.Secp256k1, + Blockchain.Pepecoin to EllipticCurve.Secp256k1, + Blockchain.PepecoinTestnet to EllipticCurve.Secp256k1, ) @Test diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 3079435400..08e7d43af8 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -991,6 +991,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( ApeChain, ApeChainTestnet, Scroll, ScrollTestnet, ZkLinkNova, ZkLinkNovaTestnet, + Pepecoin, PepecoinTestnet, -> Fee.Common(feeAmount) // endregion } diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index 094ed851ec..484b6f89ee 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -163,6 +163,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "scroll/test" -> Blockchain.ScrollTestnet "zklink" -> Blockchain.ZkLinkNova "zklink/test" -> Blockchain.ZkLinkNovaTestnet + "pepecoin" -> Blockchain.Pepecoin + "pepecoin/test" -> Blockchain.PepecoinTestnet else -> null } } @@ -323,6 +325,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.ScrollTestnet -> "scroll/test" Blockchain.ZkLinkNova -> "zklink" Blockchain.ZkLinkNovaTestnet -> "zklink/test" + Blockchain.Pepecoin -> "pepecoin" + Blockchain.PepecoinTestnet -> "pepecoin/test" } } @@ -425,6 +429,7 @@ fun Blockchain.toCoinId(): String { Blockchain.ApeChain, Blockchain.ApeChainTestnet -> "apecoin" Blockchain.Scroll, Blockchain.ScrollTestnet -> "scroll" Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet -> "zklink" + Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> "pepecoin-network" } } From c1d4bf79cbf304a7a8cb343f671093c3c88902cd Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Apr 2025 14:20:37 +0500 Subject: [PATCH 084/138] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 14 +++++++ common/routing/build.gradle.kts | 1 + .../com/tangem/common/routing/AppRoute.kt | 7 ++++ domain/networks/.gitignore | 1 + domain/nft/models/build.gradle.kts | 5 ++- .../com/tangem/domain/nft/models/NFTAsset.kt | 23 ++++++++++-- .../tangem/domain/nft/models/NFTCollection.kt | 16 ++++++++ .../tangem/domain/nft/models/NFTSalePrice.kt | 10 ++++- features/nft/api/build.gradle.kts | 1 + .../nft/component/NFTDetailsComponent.kt | 16 ++++++++ .../tangem/features/nft/NFTFeatureModule.kt | 12 ++++++ .../nft/details/DefaultNFTDetailsComponent.kt | 37 +++++++++++++++++++ .../nft/details/entity/NFTDetailsUM.kt | 5 +++ .../nft/details/model/NFTDetailsModel.kt | 34 +++++++++++++++++ .../features/nft/details/ui/NFTDetails.kt | 12 ++++++ 15 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 domain/networks/.gitignore create mode 100644 features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsComponent.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/DefaultNFTDetailsComponent.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 7d2a6727f7..3c0ac636bb 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -13,6 +13,7 @@ import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.nft.component.NFTCollectionsComponent +import com.tangem.features.nft.component.NFTDetailsComponent import com.tangem.features.nft.component.NFTReceiveComponent import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent import com.tangem.features.onramp.component.* @@ -84,6 +85,7 @@ internal class ChildFactory @Inject constructor( private val redesignedWalletConnectComponentFactory: RedisegnedWalletConnectComponent.Factory, private val nftCollectionsComponentFactory: NFTCollectionsComponent.Factory, private val nftReceiveComponentFactory: NFTReceiveComponent.Factory, + private val nftDetailsComponentFactory: NFTDetailsComponent.Factory, private val testerRouter: TesterRouter, private val routingFeatureToggles: RoutingFeatureToggles, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, @@ -415,6 +417,12 @@ internal class ChildFactory @Inject constructor( params = NFTReceiveComponent.Params(userWalletId = route.userWalletId), componentFactory = nftReceiveComponentFactory, ) + is AppRoute.NFTDetails -> + createComponentChild( + context = context, + params = NFTDetailsComponent.Params(userWalletId = route.userWalletId, nftAsset = route.nftAsset), + componentFactory = nftDetailsComponentFactory, + ) is AppRoute.OnboardingNote, is AppRoute.SaveWallet, is AppRoute.OnboardingOther, @@ -761,6 +769,12 @@ internal class ChildFactory @Inject constructor( params = NFTReceiveComponent.Params(userWalletId = route.userWalletId), componentFactory = nftReceiveComponentFactory, ) + is AppRoute.NFTDetails -> + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = NFTDetailsComponent.Params(userWalletId = route.userWalletId, nftAsset = route.nftAsset), + componentFactory = nftDetailsComponentFactory, + ) } // endregion } diff --git a/common/routing/build.gradle.kts b/common/routing/build.gradle.kts index 4fd88cfb46..285041a42a 100644 --- a/common/routing/build.gradle.kts +++ b/common/routing/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(projects.domain.markets.models) implementation(projects.domain.onramp.models) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.nft.models) /* Libs - Other */ api(deps.kotlin.serialization) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 5e2f642f8b..cc4bda0a08 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -8,6 +8,7 @@ import com.tangem.core.decompose.navigation.Route import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId @@ -288,4 +289,10 @@ sealed class AppRoute(val path: String) : Route { data class NFTReceive( val userWalletId: UserWalletId, ) : AppRoute(path = "/nft_receive/${userWalletId.stringValue}") + + @Serializable + data class NFTDetails( + val userWalletId: UserWalletId, + val nftAsset: NFTAsset, + ) : AppRoute(path = "/nft_details/${userWalletId.stringValue}/${nftAsset.collectionId}/${nftAsset.id.stringValue}") } \ No newline at end of file diff --git a/domain/networks/.gitignore b/domain/networks/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/networks/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/nft/models/build.gradle.kts b/domain/nft/models/build.gradle.kts index 92208c0a6b..ce4aec98eb 100644 --- a/domain/nft/models/build.gradle.kts +++ b/domain/nft/models/build.gradle.kts @@ -8,7 +8,10 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.coroutines) + implementation(projects.domain.core) implementation(projects.domain.models) - implementation(projects.domain.tokens.models) + + /* Utils */ + implementation(deps.kotlin.serialization) } \ No newline at end of file diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt index 761db519d4..3e2558f928 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTAsset.kt @@ -2,7 +2,9 @@ package com.tangem.domain.nft.models import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.Network +import kotlinx.serialization.Serializable +@Serializable data class NFTAsset( val id: Identifier, val collectionId: NFTCollection.Identifier, @@ -18,29 +20,44 @@ data class NFTAsset( val source: StatusSource, ) { + @Serializable data class Media( val mimetype: String?, val url: String, ) + @Serializable data class Rarity( val rank: String, val label: String, ) + @Serializable data class Trait( val name: String, val value: String, ) + @Serializable sealed class Identifier { + abstract val stringValue: String + + @Serializable data class EVM( val tokenId: String, val tokenAddress: String, - ) : Identifier() + ) : Identifier() { + override val stringValue: String = "${tokenAddress}_$tokenId" + } - data class TON(val tokenAddress: String) : Identifier() + @Serializable + data class TON(val tokenAddress: String) : Identifier() { + override val stringValue: String = tokenAddress + } - data object Unknown : Identifier() + @Serializable + data object Unknown : Identifier() { + override val stringValue: String = "" + } } } \ No newline at end of file diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollection.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollection.kt index 58b5aea439..d75d39c6de 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollection.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollection.kt @@ -2,7 +2,9 @@ package com.tangem.domain.nft.models import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.Network +import kotlinx.serialization.Serializable +@Serializable data class NFTCollection( val id: Identifier, val network: Network, @@ -12,19 +14,33 @@ data class NFTCollection( val count: Int, val assets: Assets, ) { + @Serializable sealed class Assets { + @Serializable data object Empty : Assets() + + @Serializable data object Loading : Assets() + + @Serializable data object Failed : Assets() + + @Serializable data class Value( val items: List, val source: StatusSource, ) : Assets() } + @Serializable sealed class Identifier { + @Serializable data class EVM(val tokenAddress: String) : Identifier() + + @Serializable data class TON(val contractAddress: String?) : Identifier() + + @Serializable data object Unknown : Identifier() } } \ No newline at end of file diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt index 26b9c2e17f..52cc43d072 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTSalePrice.kt @@ -1,25 +1,31 @@ package com.tangem.domain.nft.models -import java.math.BigDecimal +import com.tangem.domain.core.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable +@Serializable sealed class NFTSalePrice { abstract val assetId: NFTAsset.Identifier + @Serializable data class Empty( override val assetId: NFTAsset.Identifier, ) : NFTSalePrice() + @Serializable data class Loading( override val assetId: NFTAsset.Identifier, ) : NFTSalePrice() + @Serializable data class Error( override val assetId: NFTAsset.Identifier, ) : NFTSalePrice() + @Serializable data class Value( override val assetId: NFTAsset.Identifier, - val value: BigDecimal, + val value: SerializedBigDecimal, val symbol: String, ) : NFTSalePrice() } \ No newline at end of file diff --git a/features/nft/api/build.gradle.kts b/features/nft/api/build.gradle.kts index eb681efb3a..d9217b0692 100644 --- a/features/nft/api/build.gradle.kts +++ b/features/nft/api/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { /* Project - Domain */ implementation(projects.domain.models) + implementation(projects.domain.nft.models) implementation(projects.domain.wallets.models) /* Project - Core */ diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsComponent.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsComponent.kt new file mode 100644 index 0000000000..0f43e3a58f --- /dev/null +++ b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsComponent.kt @@ -0,0 +1,16 @@ +package com.tangem.features.nft.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.wallets.models.UserWalletId + +interface NFTDetailsComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + val nftAsset: NFTAsset, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/NFTFeatureModule.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/NFTFeatureModule.kt index 91a6bbd3d5..75f083f217 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/NFTFeatureModule.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/NFTFeatureModule.kt @@ -5,7 +5,10 @@ import com.tangem.core.decompose.model.Model import com.tangem.features.nft.collections.DefaultNFTCollectionsComponent import com.tangem.features.nft.collections.model.NFTCollectionsModel import com.tangem.features.nft.component.NFTCollectionsComponent +import com.tangem.features.nft.component.NFTDetailsComponent import com.tangem.features.nft.component.NFTReceiveComponent +import com.tangem.features.nft.details.DefaultNFTDetailsComponent +import com.tangem.features.nft.details.model.NFTDetailsModel import com.tangem.features.nft.receive.DefaultNFTReceiveComponent import com.tangem.features.nft.receive.model.NFTReceiveModel import dagger.Binds @@ -50,4 +53,13 @@ internal interface NFTFeatureModuleBinds { @IntoMap @ClassKey(NFTReceiveModel::class) fun bindNFTReceiveModel(model: NFTReceiveModel): Model + + @Binds + @Singleton + fun bindNFTDetailsComponentFactory(impl: DefaultNFTDetailsComponent.Factory): NFTDetailsComponent.Factory + + @Binds + @IntoMap + @ClassKey(NFTDetailsModel::class) + fun bindNFTDetailsModel(model: NFTDetailsModel): Model } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/DefaultNFTDetailsComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/DefaultNFTDetailsComponent.kt new file mode 100644 index 0000000000..bb220d9f9f --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/DefaultNFTDetailsComponent.kt @@ -0,0 +1,37 @@ +package com.tangem.features.nft.details + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.nft.component.NFTDetailsComponent +import com.tangem.features.nft.details.model.NFTDetailsModel +import com.tangem.features.nft.details.ui.NFTDetails +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultNFTDetailsComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: NFTDetailsComponent.Params, +) : NFTDetailsComponent, AppComponentContext by context { + + private val model: NFTDetailsModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + NFTDetails(state, modifier) + } + + @AssistedFactory + interface Factory : NFTDetailsComponent.Factory { + override fun create( + context: AppComponentContext, + params: NFTDetailsComponent.Params, + ): DefaultNFTDetailsComponent + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt new file mode 100644 index 0000000000..d024abdcff --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt @@ -0,0 +1,5 @@ +package com.tangem.features.nft.details.entity + +internal data class NFTDetailsUM( + val onBackClick: () -> Unit, +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt new file mode 100644 index 0000000000..5c7e9d20e4 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt @@ -0,0 +1,34 @@ +package com.tangem.features.nft.details.model + +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.navigation.Router +import com.tangem.features.nft.component.NFTDetailsComponent +import com.tangem.features.nft.details.entity.NFTDetailsUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@ModelScoped +internal class NFTDetailsModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + paramsContainer: ParamsContainer, +) : Model() { + + val state: StateFlow get() = _state + + private val _state = MutableStateFlow( + value = NFTDetailsUM( + onBackClick = ::navigateBack, + ), + ) + + @Suppress("UnusedPrivateMember") + private val params: NFTDetailsComponent.Params = paramsContainer.require() + + private fun navigateBack() { + router.pop() + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt new file mode 100644 index 0000000000..b40e1ab39c --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt @@ -0,0 +1,12 @@ +package com.tangem.features.nft.details.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.features.nft.details.entity.NFTDetailsUM + +@Suppress("UnusedPrivateMember") +@Composable +internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) { + BackHandler(onBack = state.onBackClick) +} \ No newline at end of file From 799c132a5bb2bc87d03ed9689db8a6ebade5e8b6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 31 Mar 2025 15:10:05 +0400 Subject: [PATCH 085/138] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../tap/di/domain/TokensDomainModule.kt | 24 +++- .../configs/feature_toggles_config.json | 4 +- .../di/NetworksStatusesStoreModule.kt | 28 ++-- .../networks/di/NetworkStatusFetcherModule.kt | 24 ++++ .../di/NetworkStatusProducerFactoryModule.kt | 28 ++++ .../di/NetworkStatusSupplierModule.kt | 56 ++++++++ .../repository/DefaultNetworksRepository.kt | 17 +-- domain/tokens/build.gradle.kts | 2 + .../CachedCurrenciesStatusesOperations.kt | 123 +++++++++++++----- .../tokens/repository/NetworksRepository.kt | 11 -- ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 4 + .../repository/MockNetworksRepository.kt | 9 +- 13 files changed, 252 insertions(+), 80 deletions(-) create mode 100644 data/networks/src/main/java/com/tangem/data/networks/di/NetworkStatusFetcherModule.kt create mode 100644 data/networks/src/main/java/com/tangem/data/networks/di/NetworkStatusProducerFactoryModule.kt create mode 100644 data/networks/src/main/java/com/tangem/data/networks/di/NetworkStatusSupplierModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4102dcfb14..b36d773b95 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -108,6 +108,7 @@ dependencies { implementation(projects.domain.onramp) implementation(projects.domain.promo) implementation(projects.domain.promo.models) + implementation(projects.domain.networks) implementation(projects.common) implementation(projects.common.routing) @@ -150,6 +151,7 @@ dependencies { implementation(projects.data.manageTokens) implementation(projects.data.nft) implementation(projects.data.onramp) + implementation(projects.data.networks) /** Features */ implementation(projects.features.onboarding) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 248192b95d..fc92ca427e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -2,6 +2,8 @@ package com.tangem.tap.di.domain import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.promo.PromoRepository import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.* @@ -47,7 +49,12 @@ internal object TokensDomainModule { networksRepository: NetworksRepository, stakingRepository: StakingRepository, ): FetchTokenListUseCase { - return FetchTokenListUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository) + return FetchTokenListUseCase( + currenciesRepository = currenciesRepository, + networksRepository = networksRepository, + quotesRepository = quotesRepository, + stakingRepository = stakingRepository, + ) } @Provides @@ -151,7 +158,12 @@ internal object TokensDomainModule { networksRepository: NetworksRepository, stakingRepository: StakingRepository, ): FetchCurrencyStatusUseCase { - return FetchCurrencyStatusUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository) + return FetchCurrencyStatusUseCase( + currenciesRepository, + networksRepository, + quotesRepository, + stakingRepository, + ) } @Provides @@ -349,12 +361,16 @@ internal object TokensDomainModule { quotesRepository: QuotesRepository, networksRepository: NetworksRepository, stakingRepository: StakingRepository, + singleNetworkStatusSupplier: SingleNetworkStatusSupplier, + multiNetworkStatusFetcher: MultiNetworkStatusFetcher, ): BaseCurrenciesStatusesOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, stakingRepository = stakingRepository, + singleNetworkStatusSupplier = singleNetworkStatusSupplier, + multiNetworkStatusFetcher = multiNetworkStatusFetcher, tokensFeatureToggles = tokensFeatureToggles, ) } @@ -367,12 +383,16 @@ internal object TokensDomainModule { quotesRepository: QuotesRepository, networksRepository: NetworksRepository, stakingRepository: StakingRepository, + singleNetworkStatusSupplier: SingleNetworkStatusSupplier, + multiNetworkStatusFetcher: MultiNetworkStatusFetcher, ): BaseCurrencyStatusOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, stakingRepository = stakingRepository, + singleNetworkStatusSupplier = singleNetworkStatusSupplier, + multiNetworkStatusFetcher = multiNetworkStatusFetcher, tokensFeatureToggles = tokensFeatureToggles, ) } 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 046b05e60e..34b5b0126e 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 @@ -45,7 +45,7 @@ }, { "name": "NEW_ATTESTATION_ENABLED", - "version": "undefined" + "version": "5.23.0" }, { "name": "TWIN_REFACTORING_ENABLED", @@ -61,6 +61,6 @@ }, { "name": "NETWORKS_LOADING_REFACTORING_ENABLED", - "version": "undefined" + "version": "5.23.0" } ] diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworksStatusesStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworksStatusesStoreModule.kt index 62010f5c24..b258b16a24 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworksStatusesStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworksStatusesStoreModule.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.di import android.content.Context +import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi @@ -25,22 +26,29 @@ import kotlinx.coroutines.SupervisorJob internal object NetworksStatusesStoreModule { @Provides - fun provideNetworksStatusesStore( + fun providePersistenceNetworksStatusesStore( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, dispatchers: CoroutineDispatcherProvider, + ): DataStore>> { + return DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = mapWithStringKeyTypes(valueTypes = setTypes()), + defaultValue = emptyMap(), + ), + produceFile = { context.dataStoreFile(fileName = "networks_statuses") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ) + } + + @Provides + fun provideNetworksStatusesStore( + persistenceNetworksStatusesStore: DataStore>>, ): NetworksStatusesStore { return DefaultNetworksStatusesStore( runtimeDataStore = RuntimeDataStore(), - persistenceDataStore = DataStoreFactory.create( - serializer = MoshiDataStoreSerializer( - moshi = moshi, - types = mapWithStringKeyTypes(valueTypes = setTypes()), - defaultValue = emptyMap(), - ), - produceFile = { context.dataStoreFile(fileName = "networks_statuses") }, - scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), - ), + persistenceDataStore = persistenceNetworksStatusesStore, ) } } \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/di/NetworkStatusFetcherModule.kt b/data/networks/src/main/java/com/tangem/data/networks/di/NetworkStatusFetcherModule.kt new file mode 100644 index 0000000000..85eb5b2848 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/di/NetworkStatusFetcherModule.kt @@ -0,0 +1,24 @@ +package com.tangem.data.networks.di + +import com.tangem.data.networks.multi.DefaultMultiNetworkStatusFetcher +import com.tangem.data.networks.single.DefaultSingleNetworkStatusFetcher +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.networks.single.SingleNetworkStatusFetcher +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface NetworkStatusFetcherModule { + + @Binds + @Singleton + fun bindSingleNetworkStatusFetcher(impl: DefaultSingleNetworkStatusFetcher): SingleNetworkStatusFetcher + + @Binds + @Singleton + fun bindMultiNetworkStatusFetcher(impl: DefaultMultiNetworkStatusFetcher): MultiNetworkStatusFetcher +} \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/di/NetworkStatusProducerFactoryModule.kt b/data/networks/src/main/java/com/tangem/data/networks/di/NetworkStatusProducerFactoryModule.kt new file mode 100644 index 0000000000..e029ba4c86 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/di/NetworkStatusProducerFactoryModule.kt @@ -0,0 +1,28 @@ +package com.tangem.data.networks.di + +import com.tangem.data.networks.multi.DefaultMultiNetworkStatusProducer +import com.tangem.data.networks.single.DefaultSingleNetworkStatusProducer +import com.tangem.domain.networks.multi.MultiNetworkStatusProducer +import com.tangem.domain.networks.single.SingleNetworkStatusProducer +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface NetworkStatusProducerFactoryModule { + + @Binds + @Singleton + fun bindSingleNetworkStatusProducerFactory( + impl: DefaultSingleNetworkStatusProducer.Factory, + ): SingleNetworkStatusProducer.Factory + + @Binds + @Singleton + fun bindMultiNetworkStatusProducerFactory( + impl: DefaultMultiNetworkStatusProducer.Factory, + ): MultiNetworkStatusProducer.Factory +} \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/di/NetworkStatusSupplierModule.kt b/data/networks/src/main/java/com/tangem/data/networks/di/NetworkStatusSupplierModule.kt new file mode 100644 index 0000000000..aea068fa04 --- /dev/null +++ b/data/networks/src/main/java/com/tangem/data/networks/di/NetworkStatusSupplierModule.kt @@ -0,0 +1,56 @@ +package com.tangem.data.networks.di + +import androidx.datastore.core.DataStore +import com.tangem.data.networks.store.DefaultNetworksStatusesStoreV2 +import com.tangem.data.networks.store.NetworksStatusesStoreV2 +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.networks.multi.MultiNetworkStatusProducer +import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier +import com.tangem.domain.networks.single.SingleNetworkStatusProducer +import com.tangem.domain.networks.single.SingleNetworkStatusSupplier +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object NetworkStatusSupplierModule { + + @Provides + @Singleton + fun provideNetworksStatusesStoreV2( + persistenceNetworksStatusesStore: DataStore>>, + dispatchers: CoroutineDispatcherProvider, + ): NetworksStatusesStoreV2 { + return DefaultNetworksStatusesStoreV2( + runtimeStore = RuntimeSharedStore(), + persistenceDataStore = persistenceNetworksStatusesStore, + dispatchers = dispatchers, + ) + } + + @Provides + @Singleton + fun provideSingleNetworkStatusSupplier(factory: SingleNetworkStatusProducer.Factory): SingleNetworkStatusSupplier { + return object : SingleNetworkStatusSupplier( + factory = factory, + keyCreator = { + "single_network_status_${it.userWalletId.stringValue}_${it.network.id.value}_" + + it.network.derivationPath.value + }, + ) {} + } + + @Provides + @Singleton + fun provideMultiNetworkStatusSupplier(factory: MultiNetworkStatusProducer.Factory): MultiNetworkStatusSupplier { + return object : MultiNetworkStatusSupplier( + factory = factory, + keyCreator = { "multi_networks_statuses_${it.userWalletId.stringValue}" }, + ) {} + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index 85fce53032..b85018c34b 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -26,7 +26,9 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn import timber.log.Timber @Suppress("LongParameterList") @@ -60,19 +62,6 @@ internal class DefaultNetworksRepository( } } - override fun getNetworkStatusesUpdatesLegacy( - userWalletId: UserWalletId, - networks: Set, - ): Flow> = channelFlow { - networksStatusesStore.get(userWalletId) - .onEach(::send) - .launchIn(scope = this + dispatchers.io) - - withContext(dispatchers.io) { - fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false) - } - } - override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) { val currencies = getCurrencies(userWalletId, networks) withContext(dispatchers.io) { diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 9382289ad6..6ba910ceae 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(projects.features.swap.domain.models) implementation(projects.domain.promo.models) implementation(projects.domain.promo) + implementation(projects.domain.networks) /** Project - Api */ implementation(projects.features.send.api) @@ -50,6 +51,7 @@ dependencies { /** Tests */ testImplementation(deps.test.junit) testImplementation(deps.test.coroutine) + testImplementation(deps.test.mockk) testImplementation(tangemDeps.blockchain) { exclude(module = "joda-time") } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index cab4aff110..b4cf9ec8c4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens.operations import arrow.core.* +import arrow.core.raise.either import arrow.core.raise.recover import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow @@ -9,6 +10,9 @@ import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.networks.single.SingleNetworkStatusProducer +import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository @@ -21,15 +25,19 @@ import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.utils.extractAddress import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +@Suppress("LongParameterList") class CachedCurrenciesStatusesOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, - @Suppress("UnusedPrivateMember") private val tokensFeatureToggles: TokensFeatureToggles, + private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, + private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + private val tokensFeatureToggles: TokensFeatureToggles, ) : BaseCurrenciesStatusesOperations, BaseCurrencyStatusOperations(currenciesRepository, quotesRepository, networksRepository, stakingRepository) { @@ -145,20 +153,26 @@ class CachedCurrenciesStatusesOperations( networks: Set, currenciesIds: Set, currencies: List, - ): Either { - return coroutineScope { - Either.catch { - awaitAll( - async { networksRepository.fetchNetworkStatuses(userWalletId, networks) }, - async { - val rawCurrenciesIds = currenciesIds.mapNotNullTo(mutableSetOf()) { it.rawCurrencyId } - quotesRepository.fetchQuotes(rawCurrenciesIds) - }, - async { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies) }, - ) - } - .map { } + ): Either = either { + coroutineScope { + awaitAll( + async { + if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { + multiNetworkStatusFetcher( + params = MultiNetworkStatusFetcher.Params(userWalletId, networks), + ) + } else { + networksRepository.fetchNetworkStatuses(userWalletId, networks) + } + }, + async { + val rawCurrenciesIds = currenciesIds.mapNotNullTo(mutableSetOf()) { it.rawCurrencyId } + quotesRepository.fetchQuotes(rawCurrenciesIds) + }, + async { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies) }, + ) } + .map { } } private fun createCurrenciesStatuses( @@ -251,31 +265,44 @@ class CachedCurrenciesStatusesOperations( userWalletId: UserWalletId, network: Network, ): EitherFlow> { - return networksRepository.getNetworkStatusesUpdates(userWalletId, setOf(network)) - .map, Either>> { it.right() } - .retryWhen { cause, _ -> - emit(Error.DataError(cause).left()) - // adding delay before retry to avoid spam when flow restarted - delay(RETRY_DELAY) - true - } - .distinctUntilChanged() - .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } + return if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { + singleNetworkStatusSupplier( + params = SingleNetworkStatusProducer.Params(userWalletId = userWalletId, network = network), + ) + .map>> { setOf(it).right() } + .distinctUntilChanged() + .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } + } else { + networksRepository.getNetworkStatusesUpdates(userWalletId, setOf(network)) + .map, Either>> { it.right() } + .retryWhen { cause, _ -> + emit(Error.DataError(cause).left()) + // adding delay before retry to avoid spam when flow restarted + delay(RETRY_DELAY) + true + } + .distinctUntilChanged() + .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } + } } private fun getNetworksStatuses( userWalletId: UserWalletId, networks: NonEmptySet, ): EitherFlow> { - return networksRepository.getNetworkStatusesUpdates(userWalletId, networks) - .map, Either>> { it.right() } - .retryWhen { cause, _ -> - emit(TokenListError.DataError(cause).left()) - // adding delay before retry to avoid spam when flow restarted - delay(RETRY_DELAY) - true - } - .distinctUntilChanged() + return if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { + getNetworkStatusesUpdates(userWalletId, networks) + } else { + networksRepository.getNetworkStatusesUpdates(userWalletId, networks) + .map, Either>> { it.right() } + .retryWhen { cause, _ -> + emit(TokenListError.DataError(cause).left()) + // adding delay before retry to avoid spam when flow restarted + delay(RETRY_DELAY) + true + } + .distinctUntilChanged() + } } private fun getYieldBalances( @@ -293,6 +320,36 @@ class CachedCurrenciesStatusesOperations( .distinctUntilChanged() } + // temporary code because token list is built using networks list + private fun getNetworkStatusesUpdates( + userWalletId: UserWalletId, + networks: NonEmptySet, + ): EitherFlow> { + return channelFlow { + val state = MutableStateFlow(emptySet()) + + networks.onEach { + launch { + singleNetworkStatusSupplier( + params = SingleNetworkStatusProducer.Params(userWalletId = userWalletId, network = it), + ) + .onEach { status -> + state.update { loadedStatuses -> + loadedStatuses.addOrReplace(status) { it.network == status.network } + } + } + .launchIn(scope = this) + } + } + + state + .onEach(::send) + .launchIn(scope = this) + } + .map, Either>> { it.right() } + .distinctUntilChanged() + } + companion object { internal const val RETRY_DELAY = 2000L } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index 7b799da0f0..1b66ccde06 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -35,17 +35,6 @@ interface NetworksRepository { * */ suspend fun fetchNetworkStatuses(userWalletId: UserWalletId, networks: Set, refresh: Boolean = false) - /** - * Retrieves updates of network statuses of specified blockchain networks for a specific user wallet. - * - * Loads remote network statuses if they have expired. - * - * @param userWalletId The unique identifier of the user wallet. - * @param networks A set of network which statuses are to be retrieved. - * @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. - */ - fun getNetworkStatusesUpdatesLegacy(userWalletId: UserWalletId, networks: Set): Flow> - /** * Fetches pending transactions for given network * diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index d86abc8442..975da996c8 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -17,6 +17,7 @@ import com.tangem.domain.tokens.repository.MockQuotesRepository import com.tangem.domain.tokens.repository.MockStakingRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.mockk import junit.framework.TestCase.assertEquals import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first @@ -176,9 +177,12 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { quotesRepository = MockQuotesRepository(quotes), networksRepository = MockNetworksRepository(statuses), stakingRepository = MockStakingRepository(), + tokensFeatureToggles = object : TokensFeatureToggles { override val isNetworksLoadingRefactoringEnabled: Boolean = false }, + singleNetworkStatusSupplier = mockk(), + multiNetworkStatusFetcher = mockk(), ), dispatchers = dispatchers, ) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt index 956bfa3dac..c7866234bf 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -26,13 +26,6 @@ internal class MockNetworksRepository( /* no-op */ } - override fun getNetworkStatusesUpdatesLegacy( - userWalletId: UserWalletId, - networks: Set, - ): Flow> { - return statuses.map { it.getOrElse { e -> throw e } } - } - override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) { // no-op } @@ -42,7 +35,7 @@ internal class MockNetworksRepository( networks: Set, refresh: Boolean, ): Set { - return getNetworkStatusesUpdatesLegacy(userWalletId, networks).first() + return statuses.map { it.getOrElse { e -> throw e } }.first() } override fun isNeedToCreateAccountWithoutReserve(network: Network) = false From 6c16caedba28f3e21f303df2a3dd71476fb8294b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Apr 2025 14:42:30 +0400 Subject: [PATCH 086/138] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 6 ++ .../tap/di/domain/MarketsDomainModule.kt | 6 ++ .../tap/di/domain/TokensDomainModule.kt | 41 +++++++- .../tap/di/domain/TransactionDomainModule.kt | 6 ++ .../di/NetworksStatusesStoreModule.kt | 3 + .../tangem/domain/common/CardTypesResolver.kt | 2 + .../domain/common/TangemCardTypesResolver.kt | 2 + domain/manage-tokens/build.gradle.kts | 3 +- .../managetokens/SaveManagedTokensUseCase.kt | 23 ++++- domain/markets/build.gradle.kts | 9 +- .../domain/markets/SaveMarketTokensUseCase.kt | 24 ++++- .../tokens/AddCryptoCurrenciesUseCase.kt | 28 ++++-- .../tokens/FetchCardTokenListUseCase.kt | 18 +++- .../tokens/FetchCurrencyStatusUseCase.kt | 18 +++- .../domain/tokens/FetchTokenListUseCase.kt | 21 +++- .../UpdateDelayedNetworkStatusUseCase.kt | 18 +++- .../BaseCurrencyStatusOperations.kt | 95 ++++++++++++------- .../CachedCurrenciesStatusesOperations.kt | 12 ++- ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 1 + domain/transaction/build.gradle.kts | 1 + .../usecase/AssociateAssetUseCase.kt | 26 ++++- .../wallet/child/wallet/model/WalletModel.kt | 7 ++ 22 files changed, 288 insertions(+), 82 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index 6734b93464..f6467cc073 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -4,7 +4,9 @@ import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.managetokens.* import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository @@ -70,6 +72,8 @@ internal object ManageTokensDomainModule { derivationsRepository: DerivationsRepository, stakingRepository: StakingRepository, quotesRepository: QuotesRepository, + multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + tokensFeatureToggles: TokensFeatureToggles, ): SaveManagedTokensUseCase { return SaveManagedTokensUseCase( customTokensRepository = customTokensRepository, @@ -79,6 +83,8 @@ internal object ManageTokensDomainModule { derivationsRepository = derivationsRepository, stakingRepository = stakingRepository, quotesRepository = quotesRepository, + multiNetworkStatusFetcher = multiNetworkStatusFetcher, + tokensFeatureToggles = tokensFeatureToggles, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 72b1e2451c..3a568db5e6 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -4,7 +4,9 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository @@ -60,6 +62,8 @@ object MarketsDomainModule { networksRepository: NetworksRepository, stakingRepository: StakingRepository, quotesRepository: QuotesRepository, + multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + tokensFeatureToggles: TokensFeatureToggles, ): SaveMarketTokensUseCase { return SaveMarketTokensUseCase( derivationsRepository = derivationsRepository, @@ -68,6 +72,8 @@ object MarketsDomainModule { networksRepository = networksRepository, stakingRepository = stakingRepository, quotesRepository = quotesRepository, + multiNetworkStatusFetcher = multiNetworkStatusFetcher, + tokensFeatureToggles = tokensFeatureToggles, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index fc92ca427e..237b83807e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -3,6 +3,8 @@ package com.tangem.tap.di.domain import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier +import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.promo.PromoRepository import com.tangem.domain.staking.repositories.StakingRepository @@ -32,12 +34,16 @@ internal object TokensDomainModule { networksRepository: NetworksRepository, stakingRepository: StakingRepository, quotesRepository: QuotesRepository, + multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + tokensFeatureToggles: TokensFeatureToggles, ): AddCryptoCurrenciesUseCase { return AddCryptoCurrenciesUseCase( currenciesRepository = currenciesRepository, networksRepository = networksRepository, stakingRepository = stakingRepository, quotesRepository = quotesRepository, + multiNetworkStatusFetcher = multiNetworkStatusFetcher, + tokensFeatureToggles = tokensFeatureToggles, ) } @@ -48,12 +54,16 @@ internal object TokensDomainModule { quotesRepository: QuotesRepository, networksRepository: NetworksRepository, stakingRepository: StakingRepository, + multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + tokensFeatureToggles: TokensFeatureToggles, ): FetchTokenListUseCase { return FetchTokenListUseCase( currenciesRepository = currenciesRepository, networksRepository = networksRepository, quotesRepository = quotesRepository, stakingRepository = stakingRepository, + multiNetworkStatusFetcher = multiNetworkStatusFetcher, + tokensFeatureToggles = tokensFeatureToggles, ) } @@ -157,12 +167,16 @@ internal object TokensDomainModule { quotesRepository: QuotesRepository, networksRepository: NetworksRepository, stakingRepository: StakingRepository, + singleNetworkStatusFetcher: SingleNetworkStatusFetcher, + tokensFeatureToggles: TokensFeatureToggles, ): FetchCurrencyStatusUseCase { return FetchCurrencyStatusUseCase( - currenciesRepository, - networksRepository, - quotesRepository, - stakingRepository, + currenciesRepository = currenciesRepository, + networksRepository = networksRepository, + quotesRepository = quotesRepository, + stakingRepository = stakingRepository, + singleNetworkStatusFetcher = singleNetworkStatusFetcher, + tokensFeatureToggles = tokensFeatureToggles, ) } @@ -173,8 +187,17 @@ internal object TokensDomainModule { quotesRepository: QuotesRepository, networksRepository: NetworksRepository, stakingRepository: StakingRepository, + multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + tokensFeatureToggles: TokensFeatureToggles, ): FetchCardTokenListUseCase { - return FetchCardTokenListUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository) + return FetchCardTokenListUseCase( + currenciesRepository = currenciesRepository, + networksRepository = networksRepository, + quotesRepository = quotesRepository, + stakingRepository = stakingRepository, + multiNetworkStatusFetcher = multiNetworkStatusFetcher, + tokensFeatureToggles = tokensFeatureToggles, + ) } @Provides @@ -284,9 +307,13 @@ internal object TokensDomainModule { @Singleton fun provideUpdateDelayedCurrencyStatusUseCase( networksRepository: NetworksRepository, + singleNetworkStatusFetcher: SingleNetworkStatusFetcher, + tokensFeatureToggles: TokensFeatureToggles, ): UpdateDelayedNetworkStatusUseCase { return UpdateDelayedNetworkStatusUseCase( networksRepository = networksRepository, + singleNetworkStatusFetcher = singleNetworkStatusFetcher, + tokensFeatureToggles = tokensFeatureToggles, ) } @@ -362,6 +389,7 @@ internal object TokensDomainModule { networksRepository: NetworksRepository, stakingRepository: StakingRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, + multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, ): BaseCurrenciesStatusesOperations { return CachedCurrenciesStatusesOperations( @@ -370,6 +398,7 @@ internal object TokensDomainModule { networksRepository = networksRepository, stakingRepository = stakingRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, + multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusFetcher = multiNetworkStatusFetcher, tokensFeatureToggles = tokensFeatureToggles, ) @@ -384,6 +413,7 @@ internal object TokensDomainModule { networksRepository: NetworksRepository, stakingRepository: StakingRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, + multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, ): BaseCurrencyStatusOperations { return CachedCurrenciesStatusesOperations( @@ -392,6 +422,7 @@ internal object TokensDomainModule { networksRepository = networksRepository, stakingRepository = stakingRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, + multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusFetcher = multiNetworkStatusFetcher, tokensFeatureToggles = tokensFeatureToggles, ) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 507a284a76..a693b4963e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -2,6 +2,8 @@ package com.tangem.tap.di.domain import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.networks.single.SingleNetworkStatusSupplier +import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.transaction.FeeRepository @@ -49,12 +51,16 @@ internal object TransactionDomainModule { walletManagersFacade: WalletManagersFacade, currenciesRepository: CurrenciesRepository, networksRepository: NetworksRepository, + singleNetworkStatusSupplier: SingleNetworkStatusSupplier, + tokensFeatureToggles: TokensFeatureToggles, ): AssociateAssetUseCase { return AssociateAssetUseCase( cardSdkConfigRepository = cardSdkConfigRepository, walletManagersFacade = walletManagersFacade, currenciesRepository = currenciesRepository, networksRepository = networksRepository, + singleNetworkStatusSupplier = singleNetworkStatusSupplier, + tokensFeatureToggles = tokensFeatureToggles, ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworksStatusesStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworksStatusesStoreModule.kt index b258b16a24..f0cabaf025 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworksStatusesStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworksStatusesStoreModule.kt @@ -20,11 +20,13 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) internal object NetworksStatusesStoreModule { + @Singleton @Provides fun providePersistenceNetworksStatusesStore( @NetworkMoshi moshi: Moshi, @@ -42,6 +44,7 @@ internal object NetworksStatusesStoreModule { ) } + @Singleton @Provides fun provideNetworksStatusesStore( persistenceNetworksStatusesStore: DataStore>>, diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt index 29ce08c107..a42c4a5b8d 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt @@ -26,6 +26,8 @@ interface CardTypesResolver { fun isDevKit(): Boolean + fun isSingleWallet(): Boolean + fun isSingleWalletWithToken(): Boolean fun isMultiwalletAllowed(): Boolean diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index 46035bd37d..5b997342b9 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -50,6 +50,8 @@ internal class TangemCardTypesResolver( override fun isDevKit(): Boolean = card.batchId == DEV_KIT_CARD_BATCH_ID + override fun isSingleWallet(): Boolean = !isMultiwalletAllowed() && !isSingleWalletWithToken() && !isVisaWallet() + override fun isSingleWalletWithToken(): Boolean = walletData?.token != null && !isMultiwalletAllowed() override fun isMultiwalletAllowed(): Boolean { diff --git a/domain/manage-tokens/build.gradle.kts b/domain/manage-tokens/build.gradle.kts index da0b3d8595..ef5facece8 100644 --- a/domain/manage-tokens/build.gradle.kts +++ b/domain/manage-tokens/build.gradle.kts @@ -11,8 +11,9 @@ android { dependencies { /* Domain */ - api(projects.domain.manageTokens.models) api(projects.domain.core) + api(projects.domain.manageTokens.models) + api(projects.domain.networks) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) implementation(projects.domain.staking) diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt index 1cec10d0c1..9f78e90712 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt @@ -5,7 +5,9 @@ import arrow.core.flatten import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -23,6 +25,8 @@ class SaveManagedTokensUseCase( private val derivationsRepository: DerivationsRepository, private val stakingRepository: StakingRepository, private val quotesRepository: QuotesRepository, + private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + private val tokensFeatureToggles: TokensFeatureToggles, ) { suspend operator fun invoke( @@ -92,11 +96,20 @@ class SaveManagedTokensUseCase( val networkToUpdate = currenciesToAdd.map { it.network } .subtract(existingCurrencies.map { it.network }.toSet()) - networksRepository.getNetworkStatusesSync( - userWalletId = userWalletId, - networks = networksToUpdate + networkToUpdate, - refresh = true, - ) + if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { + multiNetworkStatusFetcher( + MultiNetworkStatusFetcher.Params( + userWalletId = userWalletId, + networks = networksToUpdate + networkToUpdate, + ), + ) + } else { + networksRepository.getNetworkStatusesSync( + userWalletId = userWalletId, + networks = networksToUpdate + networkToUpdate, + refresh = true, + ) + } } private suspend fun refreshUpdatedYieldBalances( diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts index f5a22e3d18..6ba3a16199 100644 --- a/domain/markets/build.gradle.kts +++ b/domain/markets/build.gradle.kts @@ -15,12 +15,13 @@ dependencies { api(projects.domain.appCurrency.models) api(projects.domain.card) api(projects.domain.core) - api(projects.domain.markets.models) - api(projects.domain.wallets.models) - api(projects.domain.models) api(projects.domain.legacy) - api(projects.domain.wallets) + api(projects.domain.markets.models) + api(projects.domain.models) + api(projects.domain.networks) api(projects.domain.staking) + api(projects.domain.wallets) + api(projects.domain.wallets.models) implementation(projects.domain.tokens.models) implementation(projects.domain.tokens) diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt index 352fb55d85..4d086677b5 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt @@ -3,7 +3,9 @@ package com.tangem.domain.markets import arrow.core.Either import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -20,6 +22,7 @@ import com.tangem.domain.wallets.models.UserWalletId * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") class SaveMarketTokensUseCase( private val derivationsRepository: DerivationsRepository, private val marketsTokenRepository: MarketsTokenRepository, @@ -27,6 +30,8 @@ class SaveMarketTokensUseCase( private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, private val quotesRepository: QuotesRepository, + private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + private val tokensFeatureToggles: TokensFeatureToggles, ) { suspend operator fun invoke( @@ -73,11 +78,20 @@ class SaveMarketTokensUseCase( } private suspend fun refreshUpdatedNetworks(userWalletId: UserWalletId, addedCurrencies: List) { - networksRepository.getNetworkStatusesSync( - userWalletId = userWalletId, - networks = addedCurrencies.map(CryptoCurrency::network).toSet(), - refresh = true, - ) + if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { + multiNetworkStatusFetcher( + MultiNetworkStatusFetcher.Params( + userWalletId = userWalletId, + networks = addedCurrencies.map(CryptoCurrency::network).toSet(), + ), + ) + } else { + networksRepository.getNetworkStatusesSync( + userWalletId = userWalletId, + networks = addedCurrencies.map(CryptoCurrency::network).toSet(), + refresh = true, + ) + } } private suspend fun refreshUpdatedYieldBalances( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt index 370a29ee8e..2b30de2785 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt @@ -5,6 +5,7 @@ import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.toNonEmptyListOrNull +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -25,6 +26,8 @@ class AddCryptoCurrenciesUseCase( private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, private val quotesRepository: QuotesRepository, + private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + private val tokensFeatureToggles: TokensFeatureToggles, ) { /** @@ -132,16 +135,25 @@ class AddCryptoCurrenciesUseCase( val networkToUpdate = currenciesToAdd.map { it.network } .subtract(existingCurrencies.map { it.network }.toSet()) - catch( - { - networksRepository.getNetworkStatusesSync( + if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { + multiNetworkStatusFetcher( + MultiNetworkStatusFetcher.Params( userWalletId = userWalletId, networks = networksToUpdate + networkToUpdate, - refresh = true, - ) - }, - ) { - raise(it) + ), + ) + } else { + catch( + { + networksRepository.getNetworkStatusesSync( + userWalletId = userWalletId, + networks = networksToUpdate + networkToUpdate, + refresh = true, + ) + }, + ) { + raise(it) + } } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt index 9bac51630e..2098e11d7b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency @@ -21,6 +22,8 @@ class FetchCardTokenListUseCase( private val networksRepository: NetworksRepository, private val quotesRepository: QuotesRepository, private val stakingRepository: StakingRepository, + private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + private val tokensFeatureToggles: TokensFeatureToggles, ) { suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either { @@ -73,10 +76,17 @@ class FetchCardTokenListUseCase( networks: Set, refresh: Boolean, ) { - catch( - block = { networksRepository.getNetworkStatusesSync(userWalletId, networks, refresh) }, - catch = { raise(TokenListError.DataError(it)) }, - ) + if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { + multiNetworkStatusFetcher( + MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = networks), + ) + .mapLeft { TokenListError.DataError(it) } + } else { + catch( + block = { networksRepository.getNetworkStatusesSync(userWalletId, networks, refresh) }, + catch = { raise(TokenListError.DataError(it)) }, + ) + } } private suspend fun fetchQuotes(currenciesIds: Set, refresh: Boolean) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index a6455893a6..fa725b8c75 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either +import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency @@ -31,6 +32,8 @@ class FetchCurrencyStatusUseCase( private val networksRepository: NetworksRepository, private val quotesRepository: QuotesRepository, private val stakingRepository: StakingRepository, + private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, + private val tokensFeatureToggles: TokensFeatureToggles, ) { /** @@ -116,10 +119,17 @@ class FetchCurrencyStatusUseCase( network: Network, refresh: Boolean, ) { - catch( - block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(network), refresh) }, - ) { - raise(CurrencyStatusError.DataError(it)) + if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { + singleNetworkStatusFetcher( + params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network), + ) + .mapLeft { CurrencyStatusError.DataError(it) } + } else { + catch( + block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(network), refresh) }, + ) { + raise(CurrencyStatusError.DataError(it)) + } } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt index 408c4e81ca..5344fecfd0 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt @@ -6,6 +6,7 @@ import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptyListOrNull +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency @@ -33,6 +34,8 @@ class FetchTokenListUseCase( private val networksRepository: NetworksRepository, private val quotesRepository: QuotesRepository, private val stakingRepository: StakingRepository, + private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, + private val tokensFeatureToggles: TokensFeatureToggles, ) { /** @@ -104,10 +107,20 @@ class FetchTokenListUseCase( networks: Set, refresh: Boolean, ) { - catch( - block = { networksRepository.getNetworkStatusesSync(userWalletId, networks, refresh) }, - ) { - raise(TokenListError.DataError(it)) + if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { + if (refresh) { + multiNetworkStatusFetcher( + params = MultiNetworkStatusFetcher.Params(userWalletId, networks), + ) + .mapLeft { TokenListError.DataError(it) } + .bind() + } + } else { + catch( + block = { networksRepository.getNetworkStatusesSync(userWalletId, networks, refresh) }, + ) { + raise(TokenListError.DataError(it)) + } } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt index 26d641497a..5be8ae95ef 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/UpdateDelayedNetworkStatusUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either +import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.NetworksRepository @@ -20,6 +21,8 @@ import kotlinx.coroutines.delay class UpdateDelayedNetworkStatusUseCase( private val networksRepository: NetworksRepository, + private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, + private val tokensFeatureToggles: TokensFeatureToggles, ) { /** @@ -47,10 +50,17 @@ class UpdateDelayedNetworkStatusUseCase( network: Network, refresh: Boolean, ) { - catch( - block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(network), refresh) }, - ) { - raise(CurrencyStatusError.DataError(it)) + if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { + singleNetworkStatusFetcher( + params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network), + ) + .mapLeft { CurrencyStatusError.DataError(it) } + } else { + catch( + block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(network), refresh) }, + ) { + raise(CurrencyStatusError.DataError(it)) + } } } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index f765bd2dec..ac008a8ac8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -6,9 +6,14 @@ import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.raise.recover import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.networks.multi.MultiNetworkStatusProducer +import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier +import com.tangem.domain.networks.single.SingleNetworkStatusProducer +import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -17,7 +22,6 @@ import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch /** * Base operations for working with currency status @@ -29,12 +33,15 @@ import kotlinx.coroutines.launch * [REDACTED_AUTHOR] */ -@Suppress("LargeClass") +@Suppress("LargeClass", "LongParameterList") abstract class BaseCurrencyStatusOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, + private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, + private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, + private val tokensFeatureToggles: TokensFeatureToggles, ) { protected val currencyStatusProxyCreator = CurrencyStatusProxyCreator(stakingRepository) @@ -102,26 +109,13 @@ abstract class BaseCurrencyStatusOperations( val yieldBalanceFlow = getYieldBalance(userWalletId = userWalletId, cryptoCurrency = currency) - return channelFlow { - launch { - fetchComponents( - userWalletId = userWalletId, - networks = nonEmptySetOf(currency.network), - currenciesIds = nonEmptySetOf(currency.id), - currencies = nonEmptyListOf(currency), - ) - } - - combine(quoteFlow, statusFlow, yieldBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeYieldBalance -> - currencyStatusProxyCreator.createCurrencyStatus( - currency = currency, - maybeQuote = maybeQuote, - maybeNetworkStatus = maybeNetworkStatus, - maybeYieldBalance = maybeYieldBalance, - ) - } - .onEach(::send) - .launchIn(this) + return combine(quoteFlow, statusFlow, yieldBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeYieldBalance -> + currencyStatusProxyCreator.createCurrencyStatus( + currency = currency, + maybeQuote = maybeQuote, + maybeNetworkStatus = maybeNetworkStatus, + maybeYieldBalance = maybeYieldBalance, + ) } } @@ -179,12 +173,25 @@ abstract class BaseCurrencyStatusOperations( } val quote = cryptoCurrencyId.rawCurrencyId?.let { quotesRepository.getQuoteSync(it) }?.right() ?: Error.EmptyQuotes.left() - val networkStatuses = + + val networkStatuses = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { + singleNetworkStatusSupplier( + params = SingleNetworkStatusProducer.Params( + userWalletId = userWalletId, + network = currency.network, + ), + ) + .firstOrNull() + .right() + } else { networksRepository.getNetworkStatusesSync( userWalletId = userWalletId, networks = setOf(currency.network), refresh = false, - ).firstOrNull { it.network == currency.network }.right() + ) + .firstOrNull { it.network == currency.network } + .right() + } val yieldBalances = getYieldBalanceSync(userWalletId, currency) @@ -231,8 +238,17 @@ abstract class BaseCurrencyStatusOperations( val (networks, currenciesIds) = getIds(nonEmptyCurrencies) val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet() val quotes = quotesRepository.getQuotesSync(rawIds, false).right() - val networkStatuses = + + val networkStatuses = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { + multiNetworkStatusSupplier( + params = MultiNetworkStatusProducer.Params(userWalletId = userWalletId), + ) + .firstOrNull() + .orEmpty() + .right() + } else { networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right() + } val yieldBalances = getYieldBalancesSync(userWalletId, nonEmptyCurrencies) return currencyStatusProxyCreator.createCurrenciesStatuses( @@ -259,14 +275,27 @@ abstract class BaseCurrencyStatusOperations( }, catch = { Error.DataError(it).left() }, ) - val networkStatus = catch( - block = { - networksRepository.getNetworkStatusesSync(userWalletId, setOf(currency.network)) - .firstOrNull { it.network == currency.network } - .right() - }, - catch = { Error.DataError(it).left() }, - ) + + val networkStatus = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { + singleNetworkStatusSupplier( + params = SingleNetworkStatusProducer.Params( + userWalletId = userWalletId, + network = currency.network, + ), + ) + .firstOrNull() + .right() + } else { + catch( + block = { + networksRepository.getNetworkStatusesSync(userWalletId, setOf(currency.network)) + .firstOrNull { it.network == currency.network } + .right() + }, + catch = { Error.DataError(it).left() }, + ) + } + val yieldBalances = getYieldBalanceSync(userWalletId, currency) return currencyStatusProxyCreator.createCurrencyStatus( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index b4cf9ec8c4..b43e518a08 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -11,6 +11,7 @@ import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusProducer import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.staking.model.stakekit.YieldBalance @@ -36,10 +37,19 @@ class CachedCurrenciesStatusesOperations( private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, + multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) : BaseCurrenciesStatusesOperations, - BaseCurrencyStatusOperations(currenciesRepository, quotesRepository, networksRepository, stakingRepository) { + BaseCurrencyStatusOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + stakingRepository = stakingRepository, + multiNetworkStatusSupplier = multiNetworkStatusSupplier, + singleNetworkStatusSupplier = singleNetworkStatusSupplier, + tokensFeatureToggles = tokensFeatureToggles, + ) { override fun getCurrenciesStatuses( userWalletId: UserWalletId, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index 975da996c8..3e5e99c917 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -183,6 +183,7 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { }, singleNetworkStatusSupplier = mockk(), multiNetworkStatusFetcher = mockk(), + multiNetworkStatusSupplier = mockk(), ), dispatchers = dispatchers, ) diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index 154228ce40..dbae9c5373 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -32,4 +32,5 @@ dependencies { implementation(projects.domain.transaction.models) implementation(projects.domain.demo) implementation(projects.domain.card) + api(projects.domain.networks) } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt index b910f1c01c..3df20fa08d 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/AssociateAssetUseCase.kt @@ -5,6 +5,9 @@ import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.blockchain.extensions.SimpleResult import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.networks.single.SingleNetworkStatusProducer +import com.tangem.domain.networks.single.SingleNetworkStatusSupplier +import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus import com.tangem.domain.tokens.model.NetworkStatus @@ -14,12 +17,15 @@ import com.tangem.domain.transaction.error.AssociateAssetError import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.isNullOrZero +import kotlinx.coroutines.flow.firstOrNull class AssociateAssetUseCase( private val cardSdkConfigRepository: CardSdkConfigRepository, private val walletManagersFacade: WalletManagersFacade, private val currenciesRepository: CurrenciesRepository, private val networksRepository: NetworksRepository, + private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, + private val tokensFeatureToggles: TokensFeatureToggles, ) { suspend operator fun invoke( @@ -53,13 +59,25 @@ class AssociateAssetUseCase( } private suspend fun isBalanceZero(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean { - val networkStatus = networksRepository.getNetworkStatusesSync( - userWalletId = userWalletId, - networks = setOf(currency.network), - ).find { it.network == currency.network } + val networkStatus = if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { + singleNetworkStatusSupplier( + params = SingleNetworkStatusProducer.Params( + userWalletId = userWalletId, + network = currency.network, + ), + ) + .firstOrNull() + } else { + networksRepository.getNetworkStatusesSync( + userWalletId = userWalletId, + networks = setOf(currency.network), + ).find { it.network == currency.network } + } + val networkCoinAmountStatus = (networkStatus?.value as? NetworkStatus.Verified) ?.amounts ?.get(currency.id) + return networkCoinAmountStatus is CryptoCurrencyAmountStatus.Loaded && networkCoinAmountStatus.value.isNullOrZero() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 80175b7ddf..5359f89098 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -10,8 +10,10 @@ import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.nft.FetchNFTCollectionsUseCase import com.tangem.domain.settings.* +import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.repository.WalletsRepository @@ -79,6 +81,7 @@ internal class WalletModel @Inject constructor( private val analyticsEventsHandler: AnalyticsEventHandler, private val fetchNFTCollectionsUseCase: FetchNFTCollectionsUseCase, private val walletsRepository: WalletsRepository, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -353,6 +356,10 @@ internal class WalletModel @Inject constructor( coroutineScope = modelScope, ) + if (action.selectedWallet.scanResponse.cardTypesResolver.isSingleWallet()) { + fetchCurrencyStatusUseCase(userWalletId = action.selectedWallet.walletId) + } + if (action.wallets.size > 1 && isWalletsScrollPreviewEnabled()) { withContext(dispatchers.io) { delay(timeMillis = 1_800) } From 3bbdbbb5a303bc6860deb314c8fb5b510b598213 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Apr 2025 01:41:05 +0500 Subject: [PATCH 087/138] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 8 +++ .../ChooseAddressBottomSheetConfig.kt | 19 ++++++- .../ui/bottomsheet/receive/AddressMappers.kt | 57 +++++++++++++++---- .../ui/bottomsheet/receive/AddressModel.kt | 1 + .../receive/TokenReceiveBottomSheet.kt | 30 +++++----- .../receive/TokenReceiveBottomSheetConfig.kt | 43 +++++++++++++- core/res/src/main/res/values/strings.xml | 2 + .../domain/nft/GetNFTNetworkStatusUseCase.kt | 15 +++++ .../impl/model/TokenActionsHandler.kt | 12 ++-- .../ShowReceiveBottomSheetTransformer.kt | 32 +++++++++++ .../nft/receive/model/NFTReceiveModel.kt | 51 ++++++++++++++++- .../features/nft/receive/ui/NFTReceive.kt | 16 ++++++ .../topup/model/OnboardingNoteTopUpModel.kt | 16 +++--- .../v2/twin/impl/model/OnboardingTwinModel.kt | 14 ++--- .../state/factory/TokenDetailsStateFactory.kt | 18 ++++-- .../WalletCurrencyActionsClickIntents.kt | 25 +++++--- 16 files changed, 286 insertions(+), 73 deletions(-) create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworkStatusUseCase.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/ShowReceiveBottomSheetTransformer.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index b9cfccbc58..a8cb6b030d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -3,6 +3,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.nft.* import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -58,4 +59,11 @@ internal object NFTDomainModule { ): FilterNFTAvailableNetworksUseCase = FilterNFTAvailableNetworksUseCase( dispatchers = dispatchers, ) + + @Provides + @Singleton + fun providesGetNFTNetworkStatusUseCase(networksRepository: NetworksRepository): GetNFTNetworkStatusUseCase = + GetNFTNetworkStatusUseCase( + networksRepository = networksRepository, + ) } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheetConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheetConfig.kt index cd34d703e9..becc21a447 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheetConfig.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/chooseaddress/ChooseAddressBottomSheetConfig.kt @@ -1,10 +1,27 @@ package com.tangem.common.ui.bottomsheet.chooseaddress import com.tangem.common.ui.bottomsheet.receive.AddressModel +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig +import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkAddress import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList class ChooseAddressBottomSheetConfig( val addressModels: ImmutableList, val onClick: (AddressModel) -> Unit, -) : TangemBottomSheetConfigContent \ No newline at end of file +) : TangemBottomSheetConfigContent { + constructor( + asset: TokenReceiveBottomSheetConfig.Asset, + network: Network, + networkAddress: NetworkAddress, + onClick: (AddressModel) -> Unit, + ) : this( + addressModels = networkAddress.availableAddresses + .mapToAddressModels(asset, network) + .toImmutableList(), + onClick = onClick, + ) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressMappers.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressMappers.kt index e5d3092126..42f67f45d9 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressMappers.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressMappers.kt @@ -1,6 +1,9 @@ package com.tangem.common.ui.bottomsheet.receive +import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -12,18 +15,48 @@ private const val DEL_ADDRESS_NAME = "Main" private const val LEGACY_ADDRESS_NAME = "Legacy" private const val DEFAULT_ADDRESS_NAME = "Default" -fun Set.mapToAddressModels(currency: CryptoCurrency): List = this - .sortedBy { it.type.ordinal } - .map { address -> - AddressModel( - displayName = currency.network.getAddressDisplayName(address.type), - value = address.value, - type = when (address.type) { - NetworkAddress.Address.Type.Primary -> AddressModel.Type.Default - NetworkAddress.Address.Type.Secondary -> AddressModel.Type.Legacy - }, - ) - } +fun Set.mapToAddressModels(cryptoCurrency: CryptoCurrency) = mapToAddressModels( + stringReference("${cryptoCurrency.name} (${cryptoCurrency.symbol})"), + cryptoCurrency.network, +) + +fun Set.mapToAddressModels( + asset: TokenReceiveBottomSheetConfig.Asset, + network: Network, +): List = when (asset) { + is TokenReceiveBottomSheetConfig.Asset.Currency -> mapToAddressModels( + stringReference("${asset.name} (${asset.symbol})"), + network, + ) + is TokenReceiveBottomSheetConfig.Asset.NFT -> mapToAddressModels( + resourceReference(R.string.common_nft), + network, + ) +} + +private fun Set.mapToAddressModels(name: TextReference, network: Network): List = + this + .sortedBy { it.type.ordinal } + .map { address -> + val displayName = network.getAddressDisplayName(address.type) + AddressModel( + displayName = displayName, + fullName = if (this.size < 2) { + name + } else { + combinedReference( + displayName, + stringReference(" "), + name, + ) + }, + value = address.value, + type = when (address.type) { + NetworkAddress.Address.Type.Primary -> AddressModel.Type.Default + NetworkAddress.Address.Type.Secondary -> AddressModel.Type.Legacy + }, + ) + } private fun Network.getAddressDisplayName(addressType: NetworkAddress.Address.Type): TextReference { return when (id.value) { diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressModel.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressModel.kt index b520e6ff03..847cc47d2b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressModel.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/AddressModel.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.extensions.TextReference data class AddressModel( val displayName: TextReference, + val fullName: TextReference, val value: String, val type: Type, ) { diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheet.kt index a212153bfa..f6024e6f93 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheet.kt @@ -67,7 +67,7 @@ private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfi QrCodeContent(content = content, onAddressChange = { selectedAddress = it }) Info( - currency = content.symbol, + symbol = content.asset.displaySymbol, network = content.network, showMemoDisclaimer = content.showMemoDisclaimer, ) @@ -82,7 +82,7 @@ private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfi } @Composable -private fun Info(currency: String, network: String, showMemoDisclaimer: Boolean, modifier: Modifier = Modifier) { +private fun Info(symbol: TextReference, network: String, showMemoDisclaimer: Boolean, modifier: Modifier = Modifier) { Column( modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally, @@ -104,7 +104,7 @@ private fun Info(currency: String, network: String, showMemoDisclaimer: Boolean, config = NotificationConfig( title = resourceReference( R.string.receive_bottom_sheet_warning_title, - wrappedList(currency, network), + wrappedList(symbol.resolveReference(), network), ), subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description), iconResId = R.drawable.ic_alert_circle_24, @@ -189,9 +189,8 @@ private fun QrCodePage(content: TokenReceiveBottomSheetConfig, qrCodePainter: Pa ) { Text( text = stringResourceSafe( - R.string.receive_bottom_sheet_warning_message, - getName(content = content, index = currentIndex), - content.symbol, + R.string.receive_bottom_sheet_warning_message_compact, + content.addresses[currentIndex].fullName.resolveReference(), content.network, ), color = TangemTheme.colors.text.primary1, @@ -215,15 +214,6 @@ private fun QrCodePage(content: TokenReceiveBottomSheetConfig, qrCodePainter: Pa } } -@Composable -private fun getName(content: TokenReceiveBottomSheetConfig, index: Int): String { - return if (content.addresses.size < 2) { - content.name - } else { - "${content.addresses[index].displayName.resolveReference()} ${content.name}" - } -} - @Composable private fun Buttons( snackbarHostState: SnackbarHostState, @@ -291,12 +281,18 @@ private fun Preview_TokenReceiveBottomSheet( private class TokenReceiveBottomSheetConfigPreviewProvider : PreviewParameterProvider { val address = AddressModel( displayName = stringReference("Address 1"), + fullName = combinedReference( + stringReference("Address 1"), + stringReference(" Stellar"), + ), value = "0xe5178c7d4d0e861ed2e9414e045b501226b0de8d", type = AddressModel.Type.Default, ) private val baseConfig = TokenReceiveBottomSheetConfig( - name = "Stellar", - symbol = "XLM", + asset = TokenReceiveBottomSheetConfig.Asset.Currency( + name = "Stellar", + symbol = "XLM", + ), network = "Ethereum", addresses = persistentListOf(address), showMemoDisclaimer = false, diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheetConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheetConfig.kt index 463dc32fc4..382d977c74 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheetConfig.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/receive/TokenReceiveBottomSheetConfig.kt @@ -1,14 +1,51 @@ package com.tangem.common.ui.bottomsheet.receive +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkAddress import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList data class TokenReceiveBottomSheetConfig( - val name: String, - val symbol: String, + val asset: Asset, val network: String, val addresses: ImmutableList, val showMemoDisclaimer: Boolean, val onCopyClick: (String) -> Unit, val onShareClick: (String) -> Unit, -) : TangemBottomSheetConfigContent \ No newline at end of file +) : TangemBottomSheetConfigContent { + + constructor( + asset: Asset, + network: Network, + networkAddress: NetworkAddress, + showMemoDisclaimer: Boolean, + onCopyClick: (String) -> Unit, + onShareClick: (String) -> Unit, + ) : this( + asset = asset, + network = network.name, + addresses = networkAddress.availableAddresses + .mapToAddressModels(asset, network) + .toImmutableList(), + showMemoDisclaimer = showMemoDisclaimer, + onCopyClick = onCopyClick, + onShareClick = onShareClick, + ) + + @Immutable + sealed class Asset { + abstract val displaySymbol: TextReference + data class Currency(val name: String, val symbol: String) : Asset() { + override val displaySymbol = stringReference(symbol) + } + data object NFT : Asset() { + override val displaySymbol = resourceReference(R.string.common_nft) + } + } +} \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 22cf05d7e5..0eba9dda01 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -116,6 +116,7 @@ Create %1$s (%2$s) Custom + NFT %d day %d days @@ -649,6 +650,7 @@ Camera access denied No memo required %1$s (%2$s) on %3$s network + %1$s on %2$s network Sending any other currency will result in its irreversible loss. Send only %s to this address. Sending any other currency will result in its irreversible loss. Send only %1$s on the %2$s network diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworkStatusUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworkStatusUseCase.kt new file mode 100644 index 0000000000..4d0771cde1 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworkStatusUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.nft + +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.wallets.models.UserWalletId + +class GetNFTNetworkStatusUseCase( + private val networksRepository: NetworksRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): NetworkStatus? = networksRepository + .getNetworkStatusesSync(userWalletId, setOf(network), false) + .firstOrNull { it.network == network } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index 1e67919846..61cae107f4 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -102,12 +102,12 @@ internal class TokenActionsHandler @AssistedInject constructor( } }, content = TokenReceiveBottomSheetConfig( - name = currency.name, - symbol = currency.symbol, - network = currency.network.name, - addresses = networkAddress.availableAddresses - .mapToAddressModels(currency) - .toImmutableList(), + asset = TokenReceiveBottomSheetConfig.Asset.Currency( + name = currency.name, + symbol = currency.symbol, + ), + network = currency.network, + networkAddress = networkAddress, showMemoDisclaimer = currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, onCopyClick = {}, onShareClick = {}, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/ShowReceiveBottomSheetTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/ShowReceiveBottomSheetTransformer.kt new file mode 100644 index 0000000000..4fc0f0de2d --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/ShowReceiveBottomSheetTransformer.kt @@ -0,0 +1,32 @@ +package com.tangem.features.nft.receive.entity.transformer + +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.features.nft.receive.entity.NFTReceiveUM +import com.tangem.utils.transformer.Transformer + +internal class ShowReceiveBottomSheetTransformer( + private val network: Network, + private val networkAddress: NetworkAddress, + private val onDismissBottomSheet: () -> Unit, + private val onCopyClick: (String) -> Unit, + private val onShareClick: (String) -> Unit, +) : Transformer { + + override fun transform(prevState: NFTReceiveUM): NFTReceiveUM = prevState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = onDismissBottomSheet, + content = TokenReceiveBottomSheetConfig( + asset = TokenReceiveBottomSheetConfig.Asset.NFT, + network = network, + networkAddress = networkAddress, + showMemoDisclaimer = network.transactionExtrasType != Network.TransactionExtrasType.NONE, + onCopyClick = onCopyClick, + onShareClick = onShareClick, + ), + ), + ) +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt index 2e1b50b3c7..dc0eefd9b3 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/model/NFTReceiveModel.kt @@ -4,15 +4,20 @@ 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.navigation.Router +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.fields.InputManager import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.nft.FilterNFTAvailableNetworksUseCase import com.tangem.domain.nft.GetNFTAvailableNetworksUseCase +import com.tangem.domain.nft.GetNFTNetworkStatusUseCase import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.features.nft.component.NFTReceiveComponent import com.tangem.features.nft.impl.R import com.tangem.features.nft.receive.entity.NFTReceiveUM +import com.tangem.features.nft.receive.entity.transformer.ShowReceiveBottomSheetTransformer import com.tangem.features.nft.receive.entity.transformer.ToggleSearchBarTransformer import com.tangem.features.nft.receive.entity.transformer.UpdateDataStateTransformer import com.tangem.features.nft.receive.entity.transformer.UpdateSearchQueryTransformer @@ -22,6 +27,7 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class NFTReceiveModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, @@ -29,6 +35,9 @@ internal class NFTReceiveModel @Inject constructor( private val searchManager: InputManager, private val getNFTAvailableNetworksUseCase: GetNFTAvailableNetworksUseCase, private val filterNFTAvailableNetworksUseCase: FilterNFTAvailableNetworksUseCase, + private val getNFTNetworkStatusUseCase: GetNFTNetworkStatusUseCase, + private val clipboardManager: ClipboardManager, + private val shareManager: ShareManager, paramsContainer: ParamsContainer, ) : Model() { @@ -89,9 +98,47 @@ internal class NFTReceiveModel @Inject constructor( } } - @Suppress("UnusedPrivateMember") + private fun onReceiveBottomSheetDismiss() { + _state.update { + it.copy( + bottomSheetConfig = it.bottomSheetConfig?.copy(isShown = false), + ) + } + } + private fun onNetworkClick(network: Network) { - // TODO show receive bottom sheet + modelScope.launch { + val networkStatus = getNFTNetworkStatusUseCase.invoke( + userWalletId = params.userWalletId, + network = network, + ) ?: return@launch + + when (val value = networkStatus.value) { + is NetworkStatus.Verified -> { + _state.update { + ShowReceiveBottomSheetTransformer( + network = network, + networkAddress = value.address, + onDismissBottomSheet = ::onReceiveBottomSheetDismiss, + onCopyClick = ::onCopyClick, + onShareClick = ::onShareClick, + ).transform(it) + } + } + is NetworkStatus.MissedDerivation, + is NetworkStatus.NoAccount, + is NetworkStatus.Unreachable, + -> Unit + } + } + } + + private fun onCopyClick(text: String) { + clipboardManager.setText(text = text, isSensitive = true) + } + + private fun onShareClick(text: String) { + shareManager.shareText(text = text) } private fun navigateBack() { diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt index 326d77da63..69acaeef22 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt @@ -6,8 +6,11 @@ import androidx.compose.foundation.layout.* import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.TangemSearchBarDefaults import com.tangem.core.ui.extensions.stringResourceSafe @@ -63,4 +66,17 @@ internal fun NFTReceive(state: NFTReceiveUM, modifier: Modifier = Modifier) { } }, ) + + ShowBottomSheet(state.bottomSheetConfig) +} + +@Composable +private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { + if (bottomSheetConfig != null) { + when (bottomSheetConfig.content) { + is TokenReceiveBottomSheetConfig -> { + TokenReceiveBottomSheet(config = bottomSheetConfig) + } + } + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt index 959dcbe305..f51e0f6df5 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.note.impl.child.topup.model +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig import com.tangem.core.analytics.Analytics import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -8,8 +9,6 @@ import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig -import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.card.repository.CardRepository @@ -30,7 +29,6 @@ import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTop import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.isPositive -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -223,12 +221,12 @@ internal class OnboardingNoteTopUpModel @Inject constructor( isShown = true, onDismissRequest = uiState.value.onDismissBottomSheet, content = TokenReceiveBottomSheetConfig( - name = currencyStatus.currency.name, - symbol = currencyStatus.currency.symbol, - network = currencyStatus.currency.network.name, - addresses = networkAddress.availableAddresses - .mapToAddressModels(currencyStatus.currency) - .toImmutableList(), + asset = TokenReceiveBottomSheetConfig.Asset.Currency( + name = currencyStatus.currency.name, + symbol = currencyStatus.currency.symbol, + ), + network = currencyStatus.currency.network, + networkAddress = networkAddress, showMemoDisclaimer = currencyStatus.currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, onCopyClick = { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index 0c7a4c59a2..bfbd7e56c8 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -16,7 +16,6 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig -import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList import com.tangem.core.ui.format.bigdecimal.crypto @@ -51,7 +50,6 @@ import com.tangem.sdk.extensions.localizedDescriptionRes import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.firstOrNull @@ -370,12 +368,12 @@ internal class OnboardingTwinModel @Inject constructor( } }, content = TokenReceiveBottomSheetConfig( - name = currency.name, - symbol = currency.symbol, - network = currency.network.name, - addresses = networkAddress.availableAddresses - .mapToAddressModels(currency) - .toImmutableList(), + asset = TokenReceiveBottomSheetConfig.Asset.Currency( + name = currency.name, + symbol = currency.symbol, + ), + network = currency.network, + networkAddress = networkAddress, showMemoDisclaimer = currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, onCopyClick = {}, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 980eac8c0f..04b691db1a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -6,7 +6,6 @@ import com.tangem.common.ui.tokens.getUnavailabilityReasonText import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig -import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState @@ -267,10 +266,12 @@ internal class TokenDetailsStateFactory( isShown = true, onDismissRequest = clickIntents::onDismissBottomSheet, content = TokenReceiveBottomSheetConfig( - name = currency.name, - symbol = currency.symbol, - network = currency.network.name, - addresses = networkAddress.availableAddresses.mapToAddressModels(currency).toImmutableList(), + asset = TokenReceiveBottomSheetConfig.Asset.Currency( + name = currency.name, + symbol = currency.symbol, + ), + network = currency.network, + networkAddress = networkAddress, showMemoDisclaimer = currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, onCopyClick = onCopyClick, onShareClick = onShareClick, @@ -288,7 +289,12 @@ internal class TokenDetailsStateFactory( isShown = true, onDismissRequest = clickIntents::onDismissBottomSheet, content = ChooseAddressBottomSheetConfig( - addressModels = networkAddress.availableAddresses.mapToAddressModels(currency).toImmutableList(), + asset = TokenReceiveBottomSheetConfig.Asset.Currency( + name = currency.name, + symbol = currency.symbol, + ), + network = currency.network, + networkAddress = networkAddress, onClick = clickIntents::onAddressTypeSelected, ), ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index ce71b37760..9068f9a7f1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -169,7 +169,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( stateHolder.showBottomSheet( createReceiveBottomSheetContent( currency = cryptoCurrencyStatus.currency, - addresses = cryptoCurrencyStatus.value.networkAddress?.availableAddresses ?: return, + addresses = cryptoCurrencyStatus.value.networkAddress ?: return, ), userWalletId, ) @@ -189,13 +189,15 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private fun createReceiveBottomSheetContent( currency: CryptoCurrency, - addresses: Set, + addresses: NetworkAddress, ): TangemBottomSheetConfigContent { return TokenReceiveBottomSheetConfig( - name = currency.name, - symbol = currency.symbol, - network = currency.network.name, - addresses = addresses.mapToAddressModels(currency).toImmutableList(), + asset = TokenReceiveBottomSheetConfig.Asset.Currency( + name = currency.name, + symbol = currency.symbol, + ), + network = currency.network, + networkAddress = addresses, showMemoDisclaimer = currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, onCopyClick = { analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) @@ -488,7 +490,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( when (val addresses = currencyStatus.value.networkAddress) { is NetworkAddress.Selectable -> { - showChooseAddressBottomSheet(userWalletId, addresses.availableAddresses, currencyStatus.currency) + showChooseAddressBottomSheet(userWalletId, addresses, currencyStatus.currency) } is NetworkAddress.Single -> { router.openUrl( @@ -506,12 +508,17 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private fun showChooseAddressBottomSheet( userWalletId: UserWalletId, - addresses: Set, + addresses: NetworkAddress, currency: CryptoCurrency, ) { stateHolder.showBottomSheet( ChooseAddressBottomSheetConfig( - addressModels = addresses.mapToAddressModels(currency).toImmutableList(), + asset = TokenReceiveBottomSheetConfig.Asset.Currency( + name = currency.name, + symbol = currency.symbol, + ), + network = currency.network, + networkAddress = addresses, onClick = { onAddressTypeSelected( userWalletId = userWalletId, From 877909e9b79f8c8913d83904e7226de683a48dfc Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Apr 2025 08:07:17 +0500 Subject: [PATCH 088/138] Updated on 2026-08-14 --- .../AddStakingNotificationsTransformer.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index 2ac8c6e2ef..f301a048e9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -98,7 +98,7 @@ internal class AddStakingNotificationsTransformer( onReload = prevState.clickIntents::getFee, feeValue = feeValue, ) - addStakingErrorNotifications(stakingError = stakingError) + addStakingErrorNotifications(stakingError = stakingError, onReload = prevState.clickIntents::getFee) // warnings addWarningNotifications( prevState = prevState, @@ -134,7 +134,10 @@ internal class AddStakingNotificationsTransformer( ) } - private fun MutableList.addStakingErrorNotifications(stakingError: StakingError?) { + private fun MutableList.addStakingErrorNotifications( + stakingError: StakingError?, + onReload: () -> Unit, + ) { when (stakingError) { is StakingError.StakeKitApiError -> { if (stakingError.message != StakingErrors.MinimumAmountNotReachedError.message) { @@ -144,9 +147,7 @@ internal class AddStakingNotificationsTransformer( } } null -> Unit - else -> add( - StakingNotification.Error.Common(subtitle = stringReference(stakingError.toString())), - ) + else -> add(NotificationUM.Warning.NetworkFeeUnreachable(onReload)) } } From de6683b57ac873fac53677a2b7e34286718827d4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Apr 2025 15:51:03 +0700 Subject: [PATCH 089/138] Updated on 2026-08-14 --- .../DefaultWalletConnectStore.kt | 2 +- .../network/ethereum/WcEthNetwork.kt | 1 - .../pair/DefaultWcPairUseCase.kt | 14 ++++---- .../request/DefaultWcRequestService.kt | 6 ++-- .../sessions/DefaultWcSessionsManager.kt | 6 ++-- .../utils/JSONRPCRequestConverter.kt | 15 ++++++++ .../data/walletconnect/utils/ToOurModel.kt | 36 ------------------- .../utils/WcAppMetaDataConverter.kt | 33 +++++++++++++++++ .../utils/WcSdkSessionConverter.kt | 15 ++++++++ .../utils/WcSdkSessionRequestConverter.kt | 16 +++++++++ .../usecase/pair/WcPairUseCase.kt | 5 ++- 11 files changed, 94 insertions(+), 55 deletions(-) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/JSONRPCRequestConverter.kt delete mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcAppMetaDataConverter.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkSessionConverter.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkSessionRequestConverter.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/DefaultWalletConnectStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/DefaultWalletConnectStore.kt index 57e97f18d7..79da8620de 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/DefaultWalletConnectStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/walletconnect/DefaultWalletConnectStore.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.Flow internal typealias WcSessionCollection = Set -class DefaultWalletConnectStore( +internal class DefaultWalletConnectStore( private val persistenceStore: DataStore, ) : WalletConnectStore { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index 0ce30801eb..69407681e0 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -17,7 +17,6 @@ import com.tangem.domain.walletconnect.usecase.ethereum.WcEthMethod.SignMessage import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow -@Suppress("UnusedPrivateClass") // todo(wc) remove later internal class WcEthNetwork( private val moshi: Moshi, private val respondService: WcRespondService, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index 590b9acbf6..65f7b110ee 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -6,7 +6,7 @@ import arrow.core.right import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.data.walletconnect.utils.WcSdkObserver -import com.tangem.data.walletconnect.utils.toOurModel +import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionApprove @@ -15,7 +15,6 @@ import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.usecase.pair.WcPairState import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase -import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow @@ -28,7 +27,6 @@ import kotlin.coroutines.resume val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox") -@Suppress("UnusedPrivateMember") // todo(wc) remove after add mapping internal class DefaultWcPairUseCase( private val sessionsManager: WcSessionsManager, private val associateNetworksDelegate: AssociateNetworksDelegate, @@ -40,8 +38,8 @@ internal class DefaultWcPairUseCase( Channel>(Channel.BUFFERED) private val onSessionSettleResponse = Channel(Channel.BUFFERED) - override fun pairFlow(uri: String, source: WcPairUseCase.Source, selectedWallet: UserWallet): Flow = - flow { + override fun pairFlow(uri: String, source: WcPairUseCase.Source): Flow { + return flow { emit(WcPairState.Loading) // call sdk.pair and wait result, finish flow on error @@ -63,7 +61,7 @@ internal class DefaultWcPairUseCase( return@flow } - val proposalState = buildProposalState(sdkSessionProposal, verifyContext) + val proposalState = buildProposalState(sdkSessionProposal) .fold(ifLeft = { WcPairState.Error(it) }, ifRight = { it }) emit(proposalState) @@ -104,6 +102,7 @@ internal class DefaultWcPairUseCase( ) emit(WcPairState.Approving.Result(sessionForApprove, either)) } + } override fun approve(sessionForApprove: WcSessionApprove) { onCallTerminalAction.trySend(TerminalAction.Approve(sessionForApprove)) @@ -188,7 +187,6 @@ internal class DefaultWcPairUseCase( private suspend fun buildProposalState( sessionProposal: Wallet.Model.SessionProposal, - verifyContext: Wallet.Model.VerifyContext, ): Either = runCatching { val proposalNetwork = associateNetworksDelegate.associate(sessionProposal) val appMetaData = WcAppMetaData( @@ -213,7 +211,7 @@ internal class DefaultWcPairUseCase( private fun Wallet.Model.Session.toDomain(walletId: UserWalletId): WcSession = WcSession( userWalletId = walletId, - sdkModel = this.toOurModel(), + sdkModel = WcSdkSessionConverter.convert(this), ) private sealed interface TerminalAction { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt index f69f2f4563..99f06b1f7a 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt @@ -2,7 +2,7 @@ package com.tangem.data.walletconnect.request import com.reown.walletkit.client.Wallet import com.tangem.data.walletconnect.utils.WcSdkObserver -import com.tangem.data.walletconnect.utils.toOurModel +import com.tangem.data.walletconnect.utils.WcSdkSessionRequestConverter import com.tangem.domain.walletconnect.model.WcMethod import com.tangem.domain.walletconnect.model.WcRequest import com.tangem.domain.walletconnect.repository.WcSessionsManager @@ -16,7 +16,7 @@ internal class DefaultWcRequestService( private val sessionsManager: WcSessionsManager, private val respondService: WcRespondService, private val requestAdapters: Set>, - private val scope: CoroutineScope, // todo(wc) is ok inject scope? featureScope like Decompose Model scope + private val scope: CoroutineScope, ) : WcRequestService, WcSdkObserver { override val requests: MutableSharedFlow> = MutableSharedFlow() @@ -26,7 +26,7 @@ internal class DefaultWcRequestService( verifyContext: Wallet.Model.VerifyContext, ) { // Triggered when a Dapp sends SessionRequest to sign a transaction or a message - val sr = sessionRequest.toOurModel() + val sr = WcSdkSessionRequestConverter.convert(sessionRequest) val method = sr.request.method val params = sr.request.params scope.launch { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index 32df16baba..2807f9d74c 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -3,7 +3,7 @@ package com.tangem.data.walletconnect.sessions import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.data.walletconnect.utils.WcSdkObserver -import com.tangem.data.walletconnect.utils.toOurModel +import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionDTO @@ -44,7 +44,7 @@ internal class DefaultWcSessionsManager constructor( override suspend fun findSessionByTopic(topic: String): WcSession? = withContext(dispatchers.io) { val storedSessions = store.findSessionByTopic(topic) ?: return@withContext null val sdkSession = WalletKit.getActiveSessionByTopic(topic) ?: return@withContext null - WcSession(userWalletId = storedSessions.walletId, sdkModel = sdkSession.toOurModel()) + WcSession(userWalletId = storedSessions.walletId, sdkModel = WcSdkSessionConverter.convert(sdkSession)) } override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { @@ -74,7 +74,7 @@ internal class DefaultWcSessionsManager constructor( val wcSessions = sdkSessions.mapNotNull { sdkSession -> val storedSessions = storeSessions.find { it.topic == sdkSession.topic } ?: return@mapNotNull null - WcSession(userWalletId = storedSessions.walletId, sdkModel = sdkSession.toOurModel()) + WcSession(userWalletId = storedSessions.walletId, sdkModel = WcSdkSessionConverter.convert(sdkSession)) } val unknownStoredSessions = storeSessions diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/JSONRPCRequestConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/JSONRPCRequestConverter.kt new file mode 100644 index 0000000000..f882b3c3a5 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/JSONRPCRequestConverter.kt @@ -0,0 +1,15 @@ +package com.tangem.data.walletconnect.utils + +import com.reown.walletkit.client.Wallet +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest.JSONRPCRequest +import com.tangem.utils.converter.Converter + +internal object JSONRPCRequestConverter : Converter { + override fun convert(value: Wallet.Model.SessionRequest.JSONRPCRequest): JSONRPCRequest { + return JSONRPCRequest( + id = value.id, + method = value.method, + params = value.params, + ) + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt deleted file mode 100644 index de7c1ff78d..0000000000 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.data.walletconnect.utils - -import com.reown.android.Core -import com.reown.walletkit.client.Wallet -import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData -import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession -import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest -import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest.JSONRPCRequest - -internal fun Wallet.Model.Session.toOurModel(): WcSdkSession = WcSdkSession( - topic = topic, - appMetaData = metaData.toOurModel(), -) - -internal fun Wallet.Model.SessionRequest.toOurModel(): WcSdkSessionRequest = WcSdkSessionRequest( - topic = this.topic, - chainId = this.chainId, - request = this.request.toOurModel(), -) - -internal fun Wallet.Model.SessionRequest.JSONRPCRequest.toOurModel(): JSONRPCRequest = JSONRPCRequest( - id = this.id, - method = this.method, - params = this.params, -) - -internal fun Core.Model.AppMetaData?.toOurModel(): WcAppMetaData = WcAppMetaData( - name = this?.name.orEmpty(), - description = this?.description.orEmpty(), - url = this?.url.orEmpty(), - icons = this?.icons.orEmpty(), - redirect = this?.redirect, - appLink = this?.appLink, - linkMode = this?.linkMode ?: false, - verifyUrl = this?.verifyUrl, -) \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcAppMetaDataConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcAppMetaDataConverter.kt new file mode 100644 index 0000000000..df4fdcaade --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcAppMetaDataConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.data.walletconnect.utils + +import com.reown.android.Core +import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData +import com.tangem.utils.converter.Converter + +internal object WcAppMetaDataConverter : Converter { + + val empty + get() = WcAppMetaData( + name = "", + description = "", + url = "", + icons = listOf(), + redirect = null, + appLink = null, + linkMode = false, + verifyUrl = null, + ) + + override fun convert(value: Core.Model.AppMetaData): WcAppMetaData { + return WcAppMetaData( + name = value.name, + description = value.description, + url = value.url, + icons = value.icons, + redirect = value.redirect, + appLink = value.appLink, + linkMode = value.linkMode, + verifyUrl = value.verifyUrl, + ) + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkSessionConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkSessionConverter.kt new file mode 100644 index 0000000000..0f2e570acd --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkSessionConverter.kt @@ -0,0 +1,15 @@ +package com.tangem.data.walletconnect.utils + +import com.reown.walletkit.client.Wallet +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession +import com.tangem.utils.converter.Converter + +internal object WcSdkSessionConverter : Converter { + + override fun convert(value: Wallet.Model.Session): WcSdkSession { + return WcSdkSession( + topic = value.topic, + appMetaData = value.metaData?.let { WcAppMetaDataConverter.convert(it) } ?: WcAppMetaDataConverter.empty, + ) + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkSessionRequestConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkSessionRequestConverter.kt new file mode 100644 index 0000000000..603699848e --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkSessionRequestConverter.kt @@ -0,0 +1,16 @@ +package com.tangem.data.walletconnect.utils + +import com.reown.walletkit.client.Wallet +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.utils.converter.Converter + +internal object WcSdkSessionRequestConverter : Converter { + + override fun convert(value: Wallet.Model.SessionRequest): WcSdkSessionRequest { + return WcSdkSessionRequest( + topic = value.topic, + chainId = value.chainId, + request = JSONRPCRequestConverter.convert(value.request), + ) + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt index 2600c0d889..2de6663dbd 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt @@ -1,15 +1,14 @@ package com.tangem.domain.walletconnect.usecase.pair import com.tangem.domain.walletconnect.model.WcSessionApprove -import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.Flow interface WcPairUseCase { - fun pairFlow(uri: String, source: Source, selectedWallet: UserWallet): Flow + fun pairFlow(uri: String, source: Source): Flow fun approve(sessionForApprove: WcSessionApprove) fun reject() - enum class Source { QR, DEEPLINK, ETC } + enum class Source { QR, DEEPLINK, CLIPBOARD, ETC } } \ No newline at end of file From 8a81175461a419a1c33db845f9861fed3398f2cc Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Apr 2025 03:02:58 +0500 Subject: [PATCH 090/138] Updated on 2026-08-14 --- .../collections/model/NFTCollectionsModel.kt | 8 +++- .../features/nft/details/entity/NFTAssetUM.kt | 40 +++++++++++++++++++ .../nft/details/entity/NFTDetailsUM.kt | 8 ++++ .../nft/details/model/NFTDetailsModel.kt | 36 ++++++++++++++++- 4 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt index aa8431196e..21618350db 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt @@ -115,9 +115,13 @@ internal class NFTCollectionsModel @Inject constructor( // TODO refresh all } - @Suppress("UnusedPrivateMember") private fun onAssetClick(asset: NFTAsset) { - // TODO move to details + router.push( + AppRoute.NFTDetails( + userWalletId = params.userWalletId, + nftAsset = asset, + ), + ) } private fun onReceiveClick() { diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt new file mode 100644 index 0000000000..e5bea7ed65 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt @@ -0,0 +1,40 @@ +package com.tangem.features.nft.details.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +data class NFTAssetUM( + val name: String, + val salePrice: SalePrice, + val description: String?, + val rarity: Rarity?, + val media: Media?, + val traits: ImmutableList, + val baseInfoItems: ImmutableList, +) { + data class Media( + val mimetype: String?, + val url: String, + ) + + data class Rarity( + val rank: String, + val label: String, + ) + + data class BlockItem( + val title: TextReference, + val titleTextEllipsis: TextEllipsis = TextEllipsis.End, + val value: String, + val valueTextEllipsis: TextEllipsis = TextEllipsis.End, + ) + + @Immutable + sealed class SalePrice { + data object Loading : SalePrice() + data object Empty : SalePrice() + data class Content(val value: String, val fiatValue: String) : SalePrice() + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt index d024abdcff..55df42f2df 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTDetailsUM.kt @@ -1,5 +1,13 @@ package com.tangem.features.nft.details.entity +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig + internal data class NFTDetailsUM( + val nftAsset: NFTAssetUM, val onBackClick: () -> Unit, + val onReadMoreClick: () -> Unit, + val onSeeAllClick: () -> Unit, + val onExploreClick: () -> Unit, + val onSendClick: () -> Unit, + val bottomSheetConfig: TangemBottomSheetConfig?, ) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt index 5c7e9d20e4..19448754bf 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt @@ -5,8 +5,10 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.features.nft.component.NFTDetailsComponent +import com.tangem.features.nft.details.entity.NFTAssetUM import com.tangem.features.nft.details.entity.NFTDetailsUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* import javax.inject.Inject @@ -17,16 +19,46 @@ internal class NFTDetailsModel @Inject constructor( paramsContainer: ParamsContainer, ) : Model() { + @Suppress("UnusedPrivateMember") + private val params: NFTDetailsComponent.Params = paramsContainer.require() + val state: StateFlow get() = _state private val _state = MutableStateFlow( value = NFTDetailsUM( + nftAsset = NFTAssetUM( + name = "", + salePrice = NFTAssetUM.SalePrice.Empty, + description = null, + rarity = null, + media = null, + traits = persistentListOf(), + baseInfoItems = persistentListOf(), + ), onBackClick = ::navigateBack, + onReadMoreClick = ::onReadMoreClick, + onSeeAllClick = ::onSeeAllClick, + onExploreClick = ::onExploreClick, + onSendClick = ::onSendClick, + bottomSheetConfig = null, ), ) - @Suppress("UnusedPrivateMember") - private val params: NFTDetailsComponent.Params = paramsContainer.require() + private fun onReadMoreClick() { + // TODO implement + } + + private fun onSeeAllClick() { + // TODO implement + } + + private fun onExploreClick() { + // TODO implement + } + + private fun onSendClick() { + // TODO implement + } private fun navigateBack() { router.pop() From 95f764c878e91011990bbb4a4ca7609c02f8bc8e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Apr 2025 11:51:19 +0500 Subject: [PATCH 091/138] Updated on 2026-08-14 --- .../features/send/v2/common/NavigationUM.kt | 21 +++ .../v2/common/SendNavigationModelCallback.kt | 5 + .../features/send/v2/di/SendModelModule.kt | 6 + .../v2/send/confirm/model/SendConfirmModel.kt | 124 +++++++++-------- .../send/v2/send/ui/SendNavigationButtons.kt | 128 ++++++++++++++++++ .../send/v2/send/ui/state/ButtonsUM.kt | 25 ++++ .../features/send/v2/send/ui/state/SendUM.kt | 2 + .../amount/SendAmountComponent.kt | 3 +- .../amount/model/SendAmountModel.kt | 69 +++++----- .../destination/SendDestinationComponent.kt | 2 +- .../destination/model/SendDestinationModel.kt | 82 +++++------ .../v2/subcomponents/fee/SendFeeComponent.kt | 5 +- .../subcomponents/fee/model/SendFeeModel.kt | 37 ++--- 13 files changed, 354 insertions(+), 155 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/NavigationUM.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendNavigationModelCallback.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/SendNavigationButtons.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/ButtonsUM.kt diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/NavigationUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/NavigationUM.kt new file mode 100644 index 0000000000..149d4a5a5e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/NavigationUM.kt @@ -0,0 +1,21 @@ +package com.tangem.features.send.v2.common + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.send.v2.send.ui.state.ButtonsUM + +internal sealed class NavigationUM { + data class Content( + val title: TextReference, + val subtitle: TextReference?, + @DrawableRes val backIconRes: Int, + val backIconClick: () -> Unit, + @DrawableRes val additionalIconRes: Int? = null, + val additionalIconClick: (() -> Unit)? = null, + val primaryButton: ButtonsUM.PrimaryButtonUM, + val prevButton: ButtonsUM.PrimaryButtonUM?, + val secondaryPairButtonsUM: ButtonsUM.SecondaryPairButtonsUM?, + ) : NavigationUM() + + data object Empty : NavigationUM() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendNavigationModelCallback.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendNavigationModelCallback.kt new file mode 100644 index 0000000000..1c2c1800c4 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendNavigationModelCallback.kt @@ -0,0 +1,5 @@ +package com.tangem.features.send.v2.common + +internal interface SendNavigationModelCallback { + fun onNavigationResult(navigationUM: NavigationUM) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt index 8bdd0be817..38ff838b69 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt @@ -7,6 +7,7 @@ import com.tangem.features.send.v2.send.model.SendModel import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeModel +import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationsModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -41,4 +42,9 @@ internal interface SendModelModule { @IntoMap @ClassKey(SendConfirmModel::class) fun provideSendConfirmModel(model: SendConfirmModel): Model + + @Binds + @IntoMap + @ClassKey(NotificationsModel::class) + fun provideNotificationsModel(model: NotificationsModel): Model } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 8dd8330471..6f43c416f5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -15,6 +15,9 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener +import com.tangem.features.send.v2.send.ui.state.ButtonsUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase @@ -34,6 +37,8 @@ import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.utils.convertToSdkAmount +import com.tangem.features.send.v2.common.NavigationUM +import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.SendRoute import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource @@ -479,71 +484,70 @@ internal class SendConfirmModel @Inject constructor( } } - fun configConfirmNavigation() { + private fun configConfirmNavigation() { combine( flow = uiState, flow2 = params.currentRoute, transform = { state, route -> state to route }, ).onEach { (state, _) -> - // todo - // val amountUM = state.amountUM as? AmountState.Data - // val confirmUM = state.confirmUM - // params.callback.onResult( - // state.copy( - // navigationUM = NavigationUM.Content( - // title = resourceReference( - // id = R.string.send_summary_title, - // formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name), - // ), - // subtitle = amountUM?.title, - // backIconRes = R.drawable.ic_close_24, - // backIconClick = { - // analyticsEventHandler.send( - // SendAnalyticEvents.CloseButtonClicked( - // source = SendScreenSource.Confirm, - // isFromSummary = true, - // isValid = confirmUM.isPrimaryButtonEnabled, - // ), - // ) - // appRouter.pop() - // }, - // primaryButton = ButtonsUM.PrimaryButtonUM( - // text = when (confirmUM) { - // is ConfirmUM.Success -> resourceReference(R.string.common_close) - // is ConfirmUM.Content -> if (confirmUM.isSending) { - // resourceReference(R.string.send_sending) - // } else { - // resourceReference(R.string.common_send) - // } - // else -> resourceReference(R.string.common_send) - // }, - // iconResId = R.drawable.ic_tangem_24.takeIf { confirmUM is ConfirmUM.Content }, - // isEnabled = confirmUM.isPrimaryButtonEnabled, - // isHapticClick = confirmUM is ConfirmUM.Content && !confirmUM.isSending, - // onClick = { - // when (confirmUM) { - // is ConfirmUM.Success -> appRouter.pop() - // is ConfirmUM.Content -> if (confirmUM.isSending) { - // return@PrimaryButtonUM - // } else { - // onSendClick() - // } - // else -> return@PrimaryButtonUM - // } - // }, - // ), - // prevButton = null, - // secondaryPairButtonsUM = ButtonsUM.SecondaryPairButtonsUM( - // leftText = resourceReference(R.string.common_explore), - // leftIconResId = R.drawable.ic_web_24, - // onLeftClick = ::onExploreClick, - // rightText = resourceReference(R.string.common_share), - // rightIconResId = R.drawable.ic_share_24, - // onRightClick = ::onShareClick, - // ).takeIf { confirmUM is ConfirmUM.Success }, - // ), - // ), - // ) + val amountUM = state.amountUM as? AmountState.Data + val confirmUM = state.confirmUM + params.callback.onResult( + state.copy( + navigationUM = NavigationUM.Content( + title = resourceReference( + id = R.string.send_summary_title, + formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name), + ), + subtitle = amountUM?.title, + backIconRes = R.drawable.ic_close_24, + backIconClick = { + analyticsEventHandler.send( + SendAnalyticEvents.CloseButtonClicked( + source = SendScreenSource.Confirm, + isFromSummary = true, + isValid = confirmUM.isPrimaryButtonEnabled, + ), + ) + appRouter.pop() + }, + primaryButton = ButtonsUM.PrimaryButtonUM( + text = when (confirmUM) { + is ConfirmUM.Success -> resourceReference(R.string.common_close) + is ConfirmUM.Content -> if (confirmUM.isSending) { + resourceReference(R.string.send_sending) + } else { + resourceReference(R.string.common_send) + } + else -> resourceReference(R.string.common_send) + }, + iconResId = R.drawable.ic_tangem_24.takeIf { confirmUM is ConfirmUM.Content }, + isEnabled = confirmUM.isPrimaryButtonEnabled, + isHapticClick = confirmUM is ConfirmUM.Content && !confirmUM.isSending, + onClick = { + when (confirmUM) { + is ConfirmUM.Success -> appRouter.pop() + is ConfirmUM.Content -> if (confirmUM.isSending) { + return@PrimaryButtonUM + } else { + onSendClick() + } + else -> return@PrimaryButtonUM + } + }, + ), + prevButton = null, + secondaryPairButtonsUM = ButtonsUM.SecondaryPairButtonsUM( + leftText = resourceReference(R.string.common_explore), + leftIconResId = R.drawable.ic_web_24, + onLeftClick = ::onExploreClick, + rightText = resourceReference(R.string.common_share), + rightIconResId = R.drawable.ic_share_24, + onRightClick = ::onShareClick, + ).takeIf { confirmUM is ConfirmUM.Success }, + ), + ), + ) }.launchIn(modelScope) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/SendNavigationButtons.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/SendNavigationButtons.kt new file mode 100644 index 0000000000..f808951e95 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/SendNavigationButtons.kt @@ -0,0 +1,128 @@ +package com.tangem.features.send.v2.send.ui + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SecondaryButtonIconStart +import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.features.send.v2.send.ui.state.ButtonsUM +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.common.NavigationUM + +@Composable +internal fun SendNavigationButtons(navigationUM: NavigationUM, modifier: Modifier = Modifier) { + val navigationUM = navigationUM as? NavigationUM.Content ?: return + + Column( + modifier = modifier.padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + SendDoneButtons(navigationUM.secondaryPairButtonsUM) + SendNavigationButton( + navigationUM = navigationUM, + ) + } +} + +@Composable +private fun SendNavigationButton(navigationUM: NavigationUM, modifier: Modifier = Modifier) { + val hapticFeedback = LocalHapticFeedback.current + val navigationUM = navigationUM as? NavigationUM.Content ?: return + val primaryButton = navigationUM.primaryButton + + Row(modifier = modifier) { + AnimatedVisibility( + visible = navigationUM.prevButton != null, + enter = expandHorizontally(expandFrom = Alignment.End), + exit = shrinkHorizontally(shrinkTowards = Alignment.End), + ) { + val wrappedNavigationUM = remember(this) { requireNotNull(navigationUM.prevButton) } + Row { + Icon( + painter = rememberVectorPainter(ImageVector.vectorResource(R.drawable.ic_back_24)), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .background(TangemTheme.colors.button.secondary) + .clickable(onClick = wrappedNavigationUM.onClick) + .padding(12.dp), + ) + SpacerW12() + } + } + TangemButton( + modifier = Modifier.fillMaxWidth(), + text = primaryButton.text.resolveReference(), + icon = primaryButton.iconResId?.let { + TangemButtonIconPosition.End(it) + } ?: TangemButtonIconPosition.None, + enabled = primaryButton.isEnabled, + onClick = { + if (primaryButton.isHapticClick) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + primaryButton.onClick() + }, + showProgress = false, + colors = TangemButtonsDefaults.primaryButtonColors, + textStyle = TangemTheme.typography.subtitle1, + ) + } +} + +@Composable +private fun SendDoneButtons(pairButtonsUM: ButtonsUM.SecondaryPairButtonsUM?, modifier: Modifier = Modifier) { + val hapticFeedback = LocalHapticFeedback.current + + AnimatedVisibility( + visible = pairButtonsUM != null, + modifier = modifier, + enter = slideInVertically().plus(fadeIn()), + exit = slideOutVertically().plus(fadeOut()), + label = "Animate show sent state buttons", + ) { + val wrappedPairButtonsUM = remember(this) { requireNotNull(pairButtonsUM) } + Row(modifier = Modifier.padding(bottom = 12.dp)) { + SecondaryButtonIconStart( + text = wrappedPairButtonsUM.leftText.resolveReference(), + iconResId = wrappedPairButtonsUM.leftIconResId!!, + onClick = wrappedPairButtonsUM.onLeftClick, + modifier = Modifier.weight(1f), + ) + SpacerW12() + SecondaryButtonIconStart( + text = wrappedPairButtonsUM.rightText.resolveReference(), + iconResId = wrappedPairButtonsUM.rightIconResId!!, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + wrappedPairButtonsUM.onRightClick() + }, + modifier = Modifier.weight(1f), + ) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/ButtonsUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/ButtonsUM.kt new file mode 100644 index 0000000000..9b4d716e8a --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/ButtonsUM.kt @@ -0,0 +1,25 @@ +package com.tangem.features.send.v2.send.ui.state + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference + +sealed class ButtonsUM { + + data class PrimaryButtonUM( + val text: TextReference, + val additionalText: TextReference? = null, + @DrawableRes val iconResId: Int? = null, + val isEnabled: Boolean, + val isHapticClick: Boolean = false, + val onClick: () -> Unit, + ) : ButtonsUM() + + data class SecondaryPairButtonsUM( + val leftText: TextReference, + @DrawableRes val leftIconResId: Int? = null, + val onLeftClick: () -> Unit, + val rightText: TextReference, + @DrawableRes val rightIconResId: Int? = null, + val onRightClick: () -> Unit, + ) : ButtonsUM() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt index 13fc77a18f..c36bda9430 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.v2.send.ui.state import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.features.send.v2.common.NavigationUM import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM @@ -10,4 +11,5 @@ internal data class SendUM( val destinationUM: DestinationUM, val feeUM: FeeUM, val confirmUM: ConfirmUM, + val navigationUM: NavigationUM, ) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt index 9cb990495f..6e35e058f5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt @@ -11,6 +11,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.common.SendNavigationModelCallback import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams.AmountParams import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel @@ -35,7 +36,7 @@ internal class SendAmountComponent( ) } - interface ModelCallback { + interface ModelCallback : SendNavigationModelCallback { fun onAmountResult(amountUM: AmountState) } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index 25fa331654..e0588bbc0e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -16,6 +16,8 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase +import com.tangem.features.send.v2.common.NavigationUM +import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.SendRoute import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener @@ -200,40 +202,39 @@ internal class SendAmountModel @Inject constructor( flow = uiState, flow2 = params.currentRoute, transform = { state, route -> state to route }, - ).onEach { (_, _) -> - // todo - // params.callback.onNavigationResult( - // NavigationUM.Content( - // title = resourceReference(R.string.send_amount_label), - // subtitle = null, - // backIconRes = R.drawable.ic_back_24, - // backIconClick = { - // if (route.isEditMode) { - // saveResult() - // } - // router.pop() - // }, - // primaryButton = ButtonsUM.PrimaryButtonUM( - // text = if (route.isEditMode) { - // resourceReference(R.string.common_continue) - // } else { - // resourceReference(R.string.common_next) - // }, - // isEnabled = state.isPrimaryButtonEnabled, - // onClick = ::onAmountNext, - // ), - // prevButton = ButtonsUM.PrimaryButtonUM( - // text = TextReference.EMPTY, - // iconResId = R.drawable.ic_back_24, - // isEnabled = true, - // onClick = { - // saveResult() - // router.pop() - // }, - // ).takeIf { route.isEditMode.not() }, - // secondaryPairButtonsUM = null, - // ), - // ) + ).onEach { (state, route) -> + params.callback.onNavigationResult( + NavigationUM.Content( + title = resourceReference(R.string.send_amount_label), + subtitle = null, + backIconRes = R.drawable.ic_back_24, + backIconClick = { + if (!route.isEditMode) { + saveResult() + } + router.pop() + }, + primaryButton = ButtonsUM.PrimaryButtonUM( + text = if (route.isEditMode) { + resourceReference(R.string.common_continue) + } else { + resourceReference(R.string.common_next) + }, + isEnabled = state.isPrimaryButtonEnabled, + onClick = ::onAmountNext, + ), + prevButton = ButtonsUM.PrimaryButtonUM( + text = TextReference.EMPTY, + iconResId = R.drawable.ic_back_24, + isEnabled = true, + onClick = { + saveResult() + router.pop() + }, + ).takeIf { route.isEditMode.not() }, + secondaryPairButtonsUM = null, + ), + ) }.launchIn(modelScope) } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt index c67e2b6b74..e7a6b44743 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt @@ -40,7 +40,7 @@ internal class SendDestinationComponent( val isEditMode: Boolean, ) - interface ModelCallback { + interface ModelCallback : SendNavigationModelCallback { fun onDestinationResult(destinationUM: DestinationUM) } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 4deb3cfe46..199c1d86ce 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -19,6 +19,8 @@ import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.send.v2.common.NavigationUM +import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.SendRoute import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource @@ -44,6 +46,7 @@ internal class SendDestinationModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val router: Router, + private val appRouter: AppRouter, private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, private val getWalletsUseCase: GetWalletsUseCase, @@ -254,46 +257,45 @@ internal class SendDestinationModel @Inject constructor( flow = uiState, flow2 = params.currentRoute, transform = { state, route -> state to route }, - ).onEach { - // todo - // params.callback.onNavigationResult( - // NavigationUM.Content( - // title = resourceReference(R.string.send_recipient_label), - // subtitle = null, - // backIconRes = if (route.isEditMode) { - // R.drawable.ic_back_24 - // } else { - // R.drawable.ic_close_24 - // }, - // backIconClick = { - // if (route.isEditMode) { - // router.pop() - // } else { - // appRouter.pop() - // } - // }, - // additionalIconRes = R.drawable.ic_qrcode_scan_24, - // additionalIconClick = { - // router.push( - // AppRoute.QrScanning( - // source = AppRoute.QrScanning.Source.SEND, - // networkName = cryptoCurrency.network.name, - // ), - // ) - // }, - // primaryButton = ButtonsUM.PrimaryButtonUM( - // text = if (route.isEditMode) { - // resourceReference(R.string.common_continue) - // } else { - // resourceReference(R.string.common_next) - // }, - // isEnabled = state.isPrimaryButtonEnabled, - // onClick = ::onNextClick, - // ), - // prevButton = null, - // secondaryPairButtonsUM = null, - // ), - // ) + ).onEach { (state, route) -> + params.callback.onNavigationResult( + NavigationUM.Content( + title = resourceReference(R.string.send_recipient_label), + subtitle = null, + backIconRes = if (route.isEditMode) { + R.drawable.ic_back_24 + } else { + R.drawable.ic_close_24 + }, + backIconClick = { + if (route.isEditMode) { + router.pop() + } else { + analyticsEventHandler.send( + SendAnalyticEvents.CloseButtonClicked( + source = SendScreenSource.Address, + isFromSummary = false, + isValid = state.isPrimaryButtonEnabled, + ), + ) + appRouter.pop() + } + }, + additionalIconRes = R.drawable.ic_qrcode_scan_24, + additionalIconClick = ::onQrCodeScanClick, + primaryButton = ButtonsUM.PrimaryButtonUM( + text = if (route.isEditMode) { + resourceReference(R.string.common_continue) + } else { + resourceReference(R.string.common_next) + }, + isEnabled = state.isPrimaryButtonEnabled, + onClick = ::onNextClick, + ), + prevButton = null, + secondaryPairButtonsUM = null, + ), + ) }.launchIn(modelScope) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt index 26c543ba8d..5b7cdce04a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponent.kt @@ -6,9 +6,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.features.send.v2.common.SendNavigationModelCallback import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeModel import com.tangem.features.send.v2.subcomponents.fee.ui.SendFeeContent +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM internal class SendFeeComponent( appComponentContext: AppComponentContext, @@ -24,7 +25,7 @@ internal class SendFeeComponent( SendFeeContent(state = state.value, clickIntents = model) } - interface ModelCallback { + interface ModelCallback : SendNavigationModelCallback { fun onFeeResult(feeUM: FeeUM) } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt index ae53c69895..6d1d982599 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt @@ -8,8 +8,12 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.navigation.url.UrlOpener +import com.tangem.features.send.v2.send.ui.state.ButtonsUM +import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase +import com.tangem.features.send.v2.common.NavigationUM +import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger @@ -261,23 +265,22 @@ internal class SendFeeModel @Inject constructor( flow = uiState, flow2 = params.currentRoute, transform = { state, route -> state to route }, - ).onEach { (_, _) -> - // todo - // params.callback.onNavigationResult( - // NavigationUM.Content( - // title = resourceReference(R.string.common_fee_selector_title), - // subtitle = null, - // backIconRes = R.drawable.ic_back_24, - // backIconClick = router::pop, - // primaryButton = ButtonsUM.PrimaryButtonUM( - // text = resourceReference(R.string.common_continue), - // isEnabled = state.isPrimaryButtonEnabled, - // onClick = ::onNextClick, - // ), - // prevButton = null, - // secondaryPairButtonsUM = null, - // ), - // ) + ).onEach { (state, _) -> + params.callback.onNavigationResult( + NavigationUM.Content( + title = resourceReference(R.string.common_fee_selector_title), + subtitle = null, + backIconRes = R.drawable.ic_back_24, + backIconClick = router::pop, + primaryButton = ButtonsUM.PrimaryButtonUM( + text = resourceReference(R.string.common_continue), + isEnabled = state.isPrimaryButtonEnabled, + onClick = ::onNextClick, + ), + prevButton = null, + secondaryPairButtonsUM = null, + ), + ) }.launchIn(modelScope) } From 9e98ebe77b51eb0d5aecf9d1b2cb0292a82851cf Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Apr 2025 11:54:10 +0500 Subject: [PATCH 092/138] Updated on 2026-08-14 --- .../send/v2/send/DefaultSendComponent.kt | 12 +- .../v2/send/confirm/SendConfirmComponent.kt | 44 +++--- .../v2/send/confirm/ui/SendConfirmContent.kt | 10 +- .../SendDestinationAlertFactory.kt | 28 ++++ .../SendDestinationBlockComponent.kt | 45 ++++++ .../destination/SendDestinationComponent.kt | 24 +--- .../SendDestinationComponentParams.kt | 39 +++++ .../destination/model/SendDestinationModel.kt | 133 +++++++++++++++--- .../SendDestinationInitialStateTransformer.kt | 15 +- .../destination/ui/DestinationBlock.kt | 93 ++++++++++++ 10 files changed, 370 insertions(+), 73 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationAlertFactory.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationBlockComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponentParams.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 3683819133..d26a2ec9c3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -39,8 +39,12 @@ internal class DefaultSendComponent @AssistedInject constructor( popCallback = { onChildBack() }, ) - private val initialRoute = SendRoute.Empty - private val currentRoute = MutableStateFlow(initialRoute) + private val initialRoute = if (params.amount == null) { + SendRoute.Destination(isEditMode = false) + } else { + SendRoute.Empty + } + private val currentRoute = MutableStateFlow(initialRoute) private val model: SendModel = getOrCreateModel(params = params, router = innerRouter) @@ -77,12 +81,12 @@ internal class DefaultSendComponent @AssistedInject constructor( private fun getDestinationComponent(factoryContext: AppComponentContext, route: SendRoute) = SendDestinationComponent( appComponentContext = factoryContext, - params = SendDestinationComponent.Params( + params = SendDestinationComponentParams.DestinationParams( state = model.uiState.value.destinationUM, currentRoute = currentRoute.filterIsInstance(), analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, userWallet = model.userWallet, - cryptoCurrencyStatus = model.cryptoCurrencyStatus, + cryptoCurrency = params.currency, callback = model, isEditMode = route.isEditMode, ), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt index 52c4fd8737..28e8110f7f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt @@ -18,6 +18,8 @@ import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.features.send.v2.subcomponents.amount.SendAmountBlockComponent import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent @@ -33,22 +35,22 @@ internal class SendConfirmComponent( private val blockClickEnableFlow = MutableStateFlow(false) - // private val destinationBlockComponent = - // SendDestinationBlockComponent( - // appComponentContext = child("sendConfirmDestinationBlock"), - // params = SendDestinationComponentParams.DestinationBlockParams( - // state = model.uiState.value.destinationUM, - // analyticsCategoryName = params.analyticsCategoryName, - // userWallet = params.userWallet, - // cryptoCurrency = params.cryptoCurrencyStatus.currency, - // blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), - // isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content, - // predefinedAddressValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.address, - // predefinedMemoValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.tag, - // ), - // onResult = model::onDestinationResult, - // onClick = model::showEditDestination, - // ) + private val destinationBlockComponent = + SendDestinationBlockComponent( + appComponentContext = child("sendConfirmDestinationBlock"), + params = SendDestinationComponentParams.DestinationBlockParams( + state = model.uiState.value.destinationUM, + analyticsCategoryName = params.analyticsCategoryName, + userWallet = params.userWallet, + cryptoCurrency = params.cryptoCurrencyStatus.currency, + blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), + isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content, + predefinedAddressValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.address, + predefinedMemoValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.tag, + ), + onResult = model::onDestinationResult, + onClick = model::showEditDestination, + ) private val amountBlockComponent = SendAmountBlockComponent( appComponentContext = child("sendConfirmAmountBlock"), @@ -59,9 +61,8 @@ internal class SendConfirmComponent( cryptoCurrencyStatus = params.cryptoCurrencyStatus, appCurrency = params.appCurrency, blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), - blockEditDisabledFlow = TODO(), - // isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content, - // predefinedAmountValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.amount, + isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content, + predefinedAmountValue = (params.predefinedValues as? Params.PredefinedValues.Content)?.amount, ), onResult = model::onAmountResult, onClick = model::showEditAmount, @@ -109,8 +110,7 @@ internal class SendConfirmComponent( } fun updateState(state: SendUM) { - // todo - // destinationBlockComponent.updateState(state.destinationUM) + destinationBlockComponent.updateState(state.destinationUM) amountBlockComponent.updateState(state.amountUM) feeBlockComponent.updateState(state.feeUM) model.updateState(state) @@ -123,7 +123,7 @@ internal class SendConfirmComponent( SendConfirmContent( sendUM = state, - // destinationBlockComponent = destinationBlockComponent, + destinationBlockComponent = destinationBlockComponent, amountBlockComponent = amountBlockComponent, feeBlockComponent = feeBlockComponent, notificationsComponent = notificationsComponent, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt index 9b602a5c66..1339378697 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt @@ -36,6 +36,7 @@ import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.features.send.v2.subcomponents.amount.SendAmountBlockComponent +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.notifications import com.tangem.features.send.v2.subcomponents.notifications.NotificationsComponent @@ -50,7 +51,7 @@ private const val TAP_HELP_ANIMATION_DELAY = 500L @Composable internal fun SendConfirmContent( sendUM: SendUM, - // destinationBlockComponent: SendDestinationBlockComponent, + destinationBlockComponent: SendDestinationBlockComponent, amountBlockComponent: SendAmountBlockComponent, feeBlockComponent: SendFeeBlockComponent, notificationsComponent: NotificationsComponent, @@ -64,7 +65,7 @@ internal fun SendConfirmContent( ) { blocks( uiState = sendUM, - // destinationBlockComponent = destinationBlockComponent, + destinationBlockComponent = destinationBlockComponent, amountBlockComponent = amountBlockComponent, feeBlockComponent = feeBlockComponent, ) @@ -121,7 +122,7 @@ private fun SendingText(confirmUM: ConfirmUM.Content?, modifier: Modifier = Modi private fun LazyListScope.blocks( uiState: SendUM, - // destinationBlockComponent: SendDestinationBlockComponent, + destinationBlockComponent: SendDestinationBlockComponent, amountBlockComponent: SendAmountBlockComponent, feeBlockComponent: SendFeeBlockComponent, ) { @@ -144,8 +145,7 @@ private fun LazyListScope.blocks( modifier = Modifier.padding(vertical = 12.dp), ) } - // todo - // destinationBlockComponent.Content(modifier = Modifier) + destinationBlockComponent.Content(modifier = Modifier) amountBlockComponent.Content(modifier = Modifier) feeBlockComponent.Content(modifier = Modifier) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationAlertFactory.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationAlertFactory.kt new file mode 100644 index 0000000000..ed46533724 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationAlertFactory.kt @@ -0,0 +1,28 @@ +package com.tangem.features.send.v2.subcomponents.destination + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.features.send.v2.impl.R +import javax.inject.Inject + +@ModelScoped +internal class SendDestinationAlertFactory @Inject constructor( + val messageSender: UiMessageSender, +) { + + fun getGenericErrorState(onFailedTxEmailClick: () -> Unit) { + messageSender.send( + DialogMessage( + title = resourceReference(id = R.string.send_alert_transaction_failed_title), + message = resourceReference(id = R.string.common_unknown_error), + firstAction = EventMessageAction( + title = resourceReference(R.string.common_support), + onClick = onFailedTxEmailClick, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationBlockComponent.kt new file mode 100644 index 0000000000..ff98000eed --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationBlockComponent.kt @@ -0,0 +1,45 @@ +package com.tangem.features.send.v2.subcomponents.destination + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel +import com.tangem.features.send.v2.subcomponents.destination.ui.DestinationBlock +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +internal class SendDestinationBlockComponent( + appComponentContext: AppComponentContext, + private val params: SendDestinationComponentParams.DestinationBlockParams, + val onClick: () -> Unit, + val onResult: (DestinationUM) -> Unit, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: SendDestinationModel = getOrCreateModel(params = params) + + init { + model.uiState.onEach { + onResult(it) + }.launchIn(componentScope) + } + + fun updateState(destinationUM: DestinationUM) = model.updateState(destinationUM) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + val isClickEnabled by params.blockClickEnableFlow.collectAsStateWithLifecycle() + + DestinationBlock( + destinationUM = state, + isClickDisabled = !isClickEnabled, + isEditingDisabled = params.isPredefinedValues, + onClick = onClick, + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt index e7a6b44743..02ec170b2d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt @@ -1,44 +1,34 @@ package com.tangem.features.send.v2.subcomponents.destination import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.common.SendNavigationModelCallback import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel import com.tangem.features.send.v2.subcomponents.destination.ui.SendDestinationContent import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM -import kotlinx.coroutines.flow.Flow internal class SendDestinationComponent( appComponentContext: AppComponentContext, - private val params: Params, + private val params: SendDestinationComponentParams.DestinationParams, ) : ComposableContentComponent, AppComponentContext by appComponentContext { - private val model: SendDestinationModel = getOrCreateModel(params = params, router = router) + private val model: SendDestinationModel = getOrCreateModel(params = params) fun updateState(state: DestinationUM) = model.updateState(state) @Composable override fun Content(modifier: Modifier) { - val state = model.uiState.collectAsStateWithLifecycle() + val state by model.uiState.collectAsStateWithLifecycle() + val isBalanceHidden by params.isBalanceHidingFlow.collectAsStateWithLifecycle() - SendDestinationContent(state = state.value, model, isBalanceHidden = false) + SendDestinationContent(state = state, clickIntents = model, isBalanceHidden = isBalanceHidden) } - data class Params( - val state: DestinationUM, - val analyticsCategoryName: String, - val userWallet: UserWallet, - val cryptoCurrencyStatus: CryptoCurrencyStatus, - val callback: ModelCallback, - val currentRoute: Flow, - val isEditMode: Boolean, - ) interface ModelCallback : SendNavigationModelCallback { fun onDestinationResult(destinationUM: DestinationUM) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponentParams.kt new file mode 100644 index 0000000000..4dc45b2f96 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponentParams.kt @@ -0,0 +1,39 @@ +package com.tangem.features.send.v2.subcomponents.destination + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent.ModelCallback +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +internal sealed class SendDestinationComponentParams { + + abstract val state: DestinationUM + abstract val analyticsCategoryName: String + abstract val userWallet: UserWallet + abstract val cryptoCurrency: CryptoCurrency + + data class DestinationParams( + override val state: DestinationUM, + override val analyticsCategoryName: String, + override val userWallet: UserWallet, + override val cryptoCurrency: CryptoCurrency, + val isBalanceHidingFlow: StateFlow, + val currentRoute: Flow, + val callback: ModelCallback, + val isEditMode: Boolean, + ) : SendDestinationComponentParams() + + data class DestinationBlockParams( + override val state: DestinationUM, + override val analyticsCategoryName: String, + override val userWallet: UserWallet, + override val cryptoCurrency: CryptoCurrency, + val blockClickEnableFlow: StateFlow, + val predefinedAddressValue: String?, + val predefinedMemoValue: String?, + val isPredefinedValues: Boolean, + ) : SendDestinationComponentParams() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 199c1d86ce..b4d297cca8 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -1,18 +1,32 @@ package com.tangem.features.send.v2.subcomponents.destination.model import androidx.compose.runtime.Stable +import arrow.core.Either import arrow.core.getOrElse import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler 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.navigation.Router +import com.tangem.features.send.v2.send.ui.state.ButtonsUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.SaveBlockchainErrorUseCase +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.BlockchainErrorInfo +import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.tokens.GetCryptoCurrencyUseCase +import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.GetNetworkAddressesUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.usecase.IsUtxoConsolidationAvailableUseCase import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase @@ -22,7 +36,10 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.send.v2.common.NavigationUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.SendRoute -import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent +import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents +import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationAlertFactory +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource import com.tangem.features.send.v2.subcomponents.destination.analytics.SendDestinationAnalyticEvents import com.tangem.features.send.v2.subcomponents.destination.model.transformers.* @@ -31,6 +48,7 @@ import com.tangem.features.send.v2.subcomponents.destination.ui.state.Destinatio import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.coroutines.waitForDelay import com.tangem.utils.transformer.update import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -38,10 +56,11 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +import kotlin.properties.Delegates @Stable @ModelScoped -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class SendDestinationModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, @@ -55,43 +74,57 @@ internal class SendDestinationModel @Inject constructor( private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val isUtxoConsolidationAvailableUseCase: IsUtxoConsolidationAvailableUseCase, private val listenToQrScanningUseCase: ListenToQrScanningUseCase, + private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val getCardInfoUseCase: GetCardInfoUseCase, private val parseQrCodeUseCase: ParseQrCodeUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + private val sendDestinationAlertFactory: SendDestinationAlertFactory, ) : Model(), SendDestinationClickIntents { - private val params: SendDestinationComponent.Params = paramsContainer.require() + private val params: SendDestinationComponentParams = paramsContainer.require() private val _uiState = MutableStateFlow(params.state) val uiState = _uiState.asStateFlow() private val analyticsCategoryName = params.analyticsCategoryName private val userWallet = params.userWallet - private val cryptoCurrencyStatus = params.cryptoCurrencyStatus - private val cryptoCurrency = cryptoCurrencyStatus.currency + private val cryptoCurrency = params.cryptoCurrency + private var cryptoCurrencyStatus by Delegates.notNull() private var validationJobHolder = JobHolder() init { configDestinationNavigation() initialState() - getWalletsAndRecent() + subscribeOnCurrencyStatusUpdates() subscribeOnQRScannerResult() } private fun initialState() { - if (uiState.value is DestinationUM.Empty) { + if ((uiState.value as? DestinationUM.Content)?.isInitialized == false) { _uiState.update( SendDestinationInitialStateTransformer( - cryptoCurrencyStatus = cryptoCurrencyStatus, - onAddressChange = ::onRecipientAddressValueChange, - onMemoChange = ::onRecipientMemoValueChange, + cryptoCurrency = cryptoCurrency, + isInitialized = true, ), ) + val params = params as? SendDestinationComponentParams.DestinationBlockParams + if (params?.predefinedAddressValue != null && params.predefinedMemoValue != null) { + _uiState.update( + SendDestinationPredefinedStateTransformer( + address = params.predefinedAddressValue, + memo = params.predefinedMemoValue, + ), + ) + } } } - fun updateState(state: DestinationUM) { - if (state !is DestinationUM.Empty) { - _uiState.value = state + fun updateState(destinationUM: DestinationUM) { + if (destinationUM is DestinationUM.Content && destinationUM.isInitialized) { + _uiState.value = destinationUM } } @@ -124,6 +157,69 @@ internal class SendDestinationModel @Inject constructor( ) } + private fun subscribeOnCurrencyStatusUpdates() { + val isSingleWalletWithToken = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() + val isMultiCurrency = userWallet.isMultiCurrency + getCurrenciesStatusUpdates( + isSingleWalletWithToken = isSingleWalletWithToken, + isMultiCurrency = isMultiCurrency, + ) + } + + private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) { + getCurrencyStatus( + isSingleWalletWithToken = isSingleWalletWithToken, + isMultiCurrency = isMultiCurrency, + ).onEach { maybeCryptoCurrency -> + maybeCryptoCurrency.fold( + ifRight = { cryptoStatus -> + cryptoCurrencyStatus = cryptoStatus + getWalletsAndRecent() + }, + ifLeft = { + sendDestinationAlertFactory.getGenericErrorState { + onFailedTxEmailClick(it.toString()) + } + }, + ) + }.launchIn(modelScope) + } + + private fun getCurrencyStatus( + isSingleWalletWithToken: Boolean, + isMultiCurrency: Boolean, + ): Flow> { + return if (isMultiCurrency) { + getCurrencyStatusUpdatesUseCase( + userWalletId = userWallet.walletId, + currencyId = cryptoCurrency.id, + isSingleWalletWithTokens = isSingleWalletWithToken, + ) + } else { + getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWallet.walletId) + } + } + + private fun onFailedTxEmailClick(errorMessage: String? = null) { + saveBlockchainErrorUseCase( + error = BlockchainErrorInfo( + errorMessage = errorMessage.orEmpty(), + blockchainId = cryptoCurrency.network.id.value, + derivationPath = cryptoCurrency.network.derivationPath.value, + destinationAddress = "", + tokenSymbol = "", + amount = "", + fee = "", + ), + ) + + val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrNull() ?: return + + modelScope.launch { + sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) + } + } + private fun subscribeOnQRScannerResult() { listenToQrScanningUseCase(SourceType.SEND) .getOrElse { emptyFlow() } @@ -146,13 +242,15 @@ internal class SendDestinationModel @Inject constructor( private fun getWalletsAndRecent() { combine( flow = getWalletsUseCase().conflate().map { - it.toAvailableWallets() + waitForDelay(RECENT_LOAD_DELAY) { it.toAvailableWallets() } }, flow2 = getFixedTxHistoryItemsUseCase( userWalletId = userWallet.walletId, currency = cryptoCurrency, pageSize = RECENT_TX_SIZE, - ).getOrElse { flowOf(emptyList()) }.conflate(), + ).getOrElse { flowOf(emptyList()) }.map { + waitForDelay(RECENT_LOAD_DELAY) { it } + }.conflate(), ) { destinationWalletList, txHistoryList -> val isUtxoConsolidationAvailable = isUtxoConsolidationAvailableUseCase.invokeSync( userWalletId = userWallet.walletId, @@ -240,12 +338,13 @@ internal class SendDestinationModel @Inject constructor( } private fun saveResult() { + val params = params as? SendDestinationComponentParams.DestinationParams ?: return params.callback.onDestinationResult(uiState.value) } private fun onNextClick() { saveResult() - if (params.isEditMode) { + if ((params as? SendDestinationComponentParams.DestinationParams)?.isEditMode == true) { router.pop() } else { router.push(SendRoute.Amount(isEditMode = false)) @@ -253,6 +352,7 @@ internal class SendDestinationModel @Inject constructor( } private fun configDestinationNavigation() { + val params = params as? SendDestinationComponentParams.DestinationParams ?: return combine( flow = uiState, flow2 = params.currentRoute, @@ -301,5 +401,6 @@ internal class SendDestinationModel @Inject constructor( private companion object { const val RECENT_TX_SIZE = 100 + const val RECENT_LOAD_DELAY = 500L } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt index fdd0e7f59e..8a1be0fbc2 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt @@ -4,30 +4,28 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.features.send.v2.impl.R -import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.utils.transformer.Transformer internal class SendDestinationInitialStateTransformer( - val cryptoCurrencyStatus: CryptoCurrencyStatus, - val onAddressChange: (String, EnterAddressSource) -> Unit, - val onMemoChange: (String, Boolean) -> Unit, + val cryptoCurrency: CryptoCurrency, + val isInitialized: Boolean = false, ) : Transformer { override fun transform(prevState: DestinationUM): DestinationUM { - val memoType = when (cryptoCurrencyStatus.currency.network.transactionExtrasType) { + val memoType = when (cryptoCurrency.network.transactionExtrasType) { Network.TransactionExtrasType.NONE -> null Network.TransactionExtrasType.MEMO -> R.string.send_extras_hint_memo Network.TransactionExtrasType.DESTINATION_TAG -> R.string.send_destination_tag_field } return DestinationUM.Content( isPrimaryButtonEnabled = false, + isInitialized = isInitialized, addressTextField = DestinationTextFieldUM.RecipientAddress( value = "", - onValueChange = { onAddressChange(it, EnterAddressSource.InputField) }, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Next, keyboardType = KeyboardType.Text, @@ -40,7 +38,6 @@ internal class SendDestinationInitialStateTransformer( memoTextField = memoType?.let { DestinationTextFieldUM.RecipientMemo( value = "", - onValueChange = { onMemoChange(it, false) }, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Done, keyboardType = KeyboardType.Text, @@ -55,7 +52,7 @@ internal class SendDestinationInitialStateTransformer( }, wallets = loadingListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT), recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT), - networkName = cryptoCurrencyStatus.currency.network.name, + networkName = cryptoCurrency.network.name, isValidating = false, ) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt new file mode 100644 index 0000000000..649422d27c --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt @@ -0,0 +1,93 @@ +package com.tangem.features.send.v2.subcomponents.destination.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import com.tangem.core.ui.components.icons.identicon.IdentIcon +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM + +@Composable +internal fun DestinationBlock( + destinationUM: DestinationUM, + isClickDisabled: Boolean, + isEditingDisabled: Boolean, + onClick: () -> Unit, +) { + val destinationUM = destinationUM as? DestinationUM.Content ?: return + + val backgroundColor = if (isEditingDisabled) { + TangemTheme.colors.button.disabled + } else { + TangemTheme.colors.background.action + } + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(backgroundColor) + .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) + .padding(TangemTheme.dimens.spacing12), + ) { + AddressBlock(destinationUM.addressTextField) + MemoBlock(destinationUM.memoTextField) + } +} + +@Composable +private fun AddressBlock(address: DestinationTextFieldUM.RecipientAddress) { + Text( + text = address.label.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.secondary, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) { + IdentIcon( + address = address.value, + modifier = Modifier + .size(TangemTheme.dimens.size36) + .clip(RoundedCornerShape(TangemTheme.dimens.radius18)) + .background(TangemTheme.colors.background.tertiary), + ) + Text( + text = address.value, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } +} + +@Composable +private fun MemoBlock(memo: DestinationTextFieldUM.RecipientMemo?) { + if (memo != null && memo.value.isNotBlank()) { + HorizontalDivider( + color = TangemTheme.colors.icon.inactive, + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), + ) + Text( + text = memo.label.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.secondary, + ) + Text( + text = memo.value, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) + } +} \ No newline at end of file From ed9e1884594dcf320028c0c95dc56928a04b525c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Apr 2025 11:55:09 +0500 Subject: [PATCH 093/138] Updated on 2026-08-14 --- .../AmountIgnoreReduceTransformer.kt | 11 + .../converters/AmountReduceByTransformer.kt | 1 + .../field/AmountFieldChangeTransformer.kt | 10 +- .../AmountFieldSetMaxAmountTransformer.kt | 5 +- features/send-v2/impl/build.gradle.kts | 4 + .../features/send/v2/common/NavigationUM.kt | 2 + .../send/v2/send/DefaultSendComponent.kt | 89 ++++++- .../v2/send/confirm/model/SendConfirmModel.kt | 2 +- .../features/send/v2/send/model/SendModel.kt | 217 +++++++++++++++++- .../features/send/v2/send/ui/SendContent.kt | 55 +++++ .../amount/SendAmountBlockComponent.kt | 14 +- .../amount/SendAmountComponent.kt | 3 +- .../amount/SendAmountComponentParams.kt | 4 +- .../amount/SendAmountReduceTrigger.kt | 7 + .../amount/model/SendAmountModel.kt | 18 +- .../destination/SendDestinationComponent.kt | 1 - .../destination/model/SendDestinationModel.kt | 2 +- ...ndDestinationPredefinedStateTransformer.kt | 19 ++ .../ui/state/DestinationTextFieldUM.kt | 5 - .../destination/ui/state/DestinationUM.kt | 2 +- .../fee/model/SendFeeAlertFactory.kt | 12 +- .../subcomponents/fee/model/SendFeeModel.kt | 21 +- 22 files changed, 451 insertions(+), 53 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountIgnoreReduceTransformer.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/SendContent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationPredefinedStateTransformer.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountIgnoreReduceTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountIgnoreReduceTransformer.kt new file mode 100644 index 0000000000..2255b4af03 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountIgnoreReduceTransformer.kt @@ -0,0 +1,11 @@ +package com.tangem.common.ui.amountScreen.converters + +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.utils.transformer.Transformer + +object AmountIgnoreReduceTransformer : Transformer { + override fun transform(prevState: AmountState): AmountState { + val state = prevState as? AmountState.Data ?: return prevState + return state.copy(isIgnoreReduce = true) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt index 4fbf0d991a..e52898094a 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt @@ -62,6 +62,7 @@ class AmountReduceByTransformer( val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided return prevState.copy( isPrimaryButtonEnabled = !isZero && !isCheckFailed, + reduceAmountBy = value.reduceAmountBy, amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt index 4094aba847..cbfc6907e8 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt @@ -31,7 +31,6 @@ class AmountFieldChangeTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val maxEnterAmount: EnterAmountBoundary, private val minimumTransactionAmount: EnterAmountBoundary?, - private val reduceAmountBy: BigDecimal = BigDecimal.ZERO, private val value: String, ) : Transformer { @@ -68,7 +67,7 @@ class AmountFieldChangeTransformer( val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided return prevState.copy( isPrimaryButtonEnabled = !isZero && !isCheckFailed, - reduceAmountBy = reduceAmountBy, + reduceAmountBy = BigDecimal.ZERO, amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, @@ -76,10 +75,9 @@ class AmountFieldChangeTransformer( error = when { isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance) isLessThanMinimumIfProvided -> { - val minimumAmount = - minimumTransactionAmount.amount.format { - crypto(cryptoCurrencyStatus.currency) - } + val minimumAmount = minimumTransactionAmount?.amount.format { + crypto(cryptoCurrencyStatus.currency) + } resourceReference( R.string.transfer_notification_invalid_minimum_transaction_amount_text, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt index 62280c0ebe..72dcac8ac9 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt @@ -27,7 +27,6 @@ class AmountFieldSetMaxAmountTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val maxAmount: EnterAmountBoundary, private val minAmount: EnterAmountBoundary?, - private val reduceAmountBy: BigDecimal = BigDecimal.ZERO, ) : Transformer { override fun transform(prevState: AmountState): AmountState { @@ -47,7 +46,7 @@ class AmountFieldSetMaxAmountTransformer( val isLessThanMinimumIfProvided = minAmount?.amount?.let { decimalCryptoValue < it } == true return prevState.copy( isPrimaryButtonEnabled = !isLessThanMinimumIfProvided, - reduceAmountBy = reduceAmountBy, + reduceAmountBy = BigDecimal.ZERO, amountTextField = amountTextField.copy( isValuePasted = true, value = cryptoValue, @@ -55,7 +54,7 @@ class AmountFieldSetMaxAmountTransformer( isError = isLessThanMinimumIfProvided, error = when { isLessThanMinimumIfProvided -> { - val minimumAmount = minAmount.amount.format { crypto(cryptoCurrencyStatus.currency) } + val minimumAmount = minAmount?.amount.format { crypto(cryptoCurrencyStatus.currency) } resourceReference( R.string.transfer_notification_invalid_minimum_transaction_amount_text, wrappedList(minimumAmount, minimumAmount), diff --git a/features/send-v2/impl/build.gradle.kts b/features/send-v2/impl/build.gradle.kts index eff04c059b..6858492736 100644 --- a/features/send-v2/impl/build.gradle.kts +++ b/features/send-v2/impl/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.configToggles) implementation(projects.core.navigation) + api(projects.core.pagination) /** Tangem SDK */ implementation(tangemDeps.blockchain) @@ -51,6 +52,8 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.feedback) implementation(projects.domain.txhistory) + implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.balanceHiding) /** Compose libraries */ implementation(deps.compose.foundation) @@ -64,6 +67,7 @@ dependencies { /** Other dependencies */ implementation(deps.kotlin.immutable.collections) implementation(deps.timber) + implementation(deps.androidx.paging.runtime) /** DI */ implementation(deps.hilt.android) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/NavigationUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/NavigationUM.kt index 149d4a5a5e..46b298b83c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/NavigationUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/NavigationUM.kt @@ -1,9 +1,11 @@ package com.tangem.features.send.v2.common import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference import com.tangem.features.send.v2.send.ui.state.ButtonsUM +@Immutable internal sealed class NavigationUM { data class Content( val title: TextReference, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index d26a2ec9c3..9d9d7a4778 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -2,11 +2,17 @@ package com.tangem.features.send.v2.send import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.value.ObserveLifecycleMode +import com.arkivanov.decompose.value.subscribe import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel @@ -14,10 +20,14 @@ import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents +import com.tangem.features.send.v2.send.confirm.SendConfirmComponent +import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM import com.tangem.features.send.v2.send.model.SendModel +import com.tangem.features.send.v2.send.ui.SendContent import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams @@ -26,10 +36,12 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.launch internal class DefaultSendComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: SendComponent.Params, + private val analyticsEventHandler: AnalyticsEventHandler, ) : SendComponent, AppComponentContext by appComponentContext { private val stackNavigation = StackNavigation() @@ -65,9 +77,44 @@ internal class DefaultSendComponent @AssistedInject constructor( }, ) + init { + childStack.subscribe( + lifecycle = lifecycle, + mode = ObserveLifecycleMode.CREATE_DESTROY, + ) { stack -> + componentScope.launch { + when (val activeComponent = stack.active.instance) { + is SendConfirmComponent -> if (currentRoute.value.isEditMode) { + analyticsEventHandler.send(SendAnalyticEvents.ConfirmationScreenOpened) + activeComponent.updateState(model.uiState.value) + } + is SendAmountComponent -> { + analyticsEventHandler.send(SendAnalyticEvents.AmountScreenOpened) + activeComponent.updateState(model.uiState.value.amountUM) + } + is SendDestinationComponent -> { + analyticsEventHandler.send(SendAnalyticEvents.AddressScreenOpened) + activeComponent.updateState(model.uiState.value.destinationUM) + } + is SendFeeComponent -> { + analyticsEventHandler.send(SendAnalyticEvents.FeeScreenOpened) + } + } + currentRoute.emit(stack.active.configuration) + } + } + } + @Composable override fun Content(modifier: Modifier) { + val stackState by childStack.subscribeAsState() + val state by model.uiState.collectAsStateWithLifecycle() + BackHandler(onBack = ::onChildBack) + SendContent( + state = state, + stackState = stackState, + ) } private fun createChild(route: SendRoute, factoryContext: AppComponentContext) = when (route) { @@ -75,7 +122,7 @@ internal class DefaultSendComponent @AssistedInject constructor( is SendRoute.Destination -> getDestinationComponent(factoryContext, route) is SendRoute.Amount -> getAmountComponent(factoryContext, route) is SendRoute.Fee -> getFeeComponent(factoryContext) - SendRoute.Confirm -> getConfirmComponent() + SendRoute.Confirm -> getConfirmComponent(factoryContext) } private fun getDestinationComponent(factoryContext: AppComponentContext, route: SendRoute) = @@ -84,6 +131,7 @@ internal class DefaultSendComponent @AssistedInject constructor( params = SendDestinationComponentParams.DestinationParams( state = model.uiState.value.destinationUM, currentRoute = currentRoute.filterIsInstance(), + isBalanceHidingFlow = model.isBalanceHiddenFlow, analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, userWallet = model.userWallet, cryptoCurrency = params.currency, @@ -97,6 +145,7 @@ internal class DefaultSendComponent @AssistedInject constructor( params = SendAmountComponentParams.AmountParams( state = model.uiState.value.amountUM, currentRoute = currentRoute.filterIsInstance(), + isBalanceHidingFlow = model.isBalanceHiddenFlow, analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, userWallet = model.userWallet, appCurrency = model.appCurrency, @@ -132,12 +181,46 @@ internal class DefaultSendComponent @AssistedInject constructor( } } - private fun getConfirmComponent() = getStubComponent() // todo + private fun getConfirmComponent(factoryContext: AppComponentContext): SendConfirmComponent { + val predefinedAmount = params.amount + val predefinedTxId = params.transactionId + val predefinedAddress = params.destinationAddress + val predefinedValues = + if (predefinedAmount != null && predefinedTxId != null && predefinedAddress != null) { + SendConfirmComponent.Params.PredefinedValues.Content( + amount = predefinedAmount, + address = predefinedAddress, + tag = params.tag, + transactionId = predefinedTxId, + ) + } else { + SendConfirmComponent.Params.PredefinedValues.Empty + } + return SendConfirmComponent( + appComponentContext = factoryContext, + params = SendConfirmComponent.Params( + state = model.uiState.value, + userWallet = model.userWallet, + currentRoute = currentRoute.filterIsInstance(), + isBalanceHidingFlow = model.isBalanceHiddenFlow, + analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus, + appCurrency = model.appCurrency, + callback = model, + predefinedValues = predefinedValues, + ), + ) + } private fun getStubComponent() = ComposableContentComponent { } private fun onChildBack() { - if (childStack.value.active.configuration == SendRoute.Empty || childStack.value.backStack.isEmpty()) { + val isEmptyRoute = childStack.value.active.configuration == SendRoute.Empty + val isEmptyStack = childStack.value.backStack.isEmpty() + val isSuccess = model.uiState.value.confirmUM is ConfirmUM.Success + + if (isEmptyRoute || isEmptyStack || isSuccess) { router.pop() } else { stackNavigation.pop() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 6f43c416f5..6d4753a5df 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -489,7 +489,7 @@ internal class SendConfirmModel @Inject constructor( flow = uiState, flow2 = params.currentRoute, transform = { state, route -> state to route }, - ).onEach { (state, _) -> + ).distinctUntilChanged().onEach { (state, _) -> val amountUM = state.amountUM as? AmountState.Data val confirmUM = state.confirmUM params.callback.onResult( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 93df96b047..a1d123dc42 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -1,49 +1,108 @@ package com.tangem.features.send.v2.send.model import androidx.compose.runtime.Stable +import arrow.core.Either +import arrow.core.getOrElse import com.tangem.common.ui.amountScreen.models.AmountState 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.navigation.Router +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.SaveBlockchainErrorUseCase +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.BlockchainErrorInfo +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase +import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.send.v2.api.SendComponent +import com.tangem.features.send.v2.common.NavigationUM +import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.send.confirm.model.SendConfirmAlertFactory import com.tangem.features.send.v2.send.confirm.SendConfirmComponent import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent +import com.tangem.features.send.v2.subcomponents.destination.model.transformers.SendDestinationInitialStateTransformer import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.properties.Delegates +internal interface SendComponentCallback : + SendAmountComponent.ModelCallback, + SendFeeComponent.ModelCallback, + SendDestinationComponent.ModelCallback, + SendConfirmComponent.ModelCallback + @Stable @ModelScoped @Suppress("LongParameterList") internal class SendModel @Inject constructor( + paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, -) : Model(), - SendDestinationComponent.ModelCallback, - SendAmountComponent.ModelCallback, - SendFeeComponent.ModelCallback, - SendConfirmComponent.ModelCallback { + private val router: Router, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val listenToQrScanningUseCase: ListenToQrScanningUseCase, + private val parseQrCodeUseCase: ParseQrCodeUseCase, + private val sendConfirmAlertFactory: SendConfirmAlertFactory, + private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, + private val getCardInfoUseCase: GetCardInfoUseCase, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, +) : Model(), SendComponentCallback { + + private val params: SendComponent.Params = paramsContainer.require() + private val userWalletId = params.userWalletId + private val cryptoCurrency = params.currency private val _uiState = MutableStateFlow(initialState()) val uiState = _uiState.asStateFlow() + private val _isBalanceHiddenFlow = MutableStateFlow(false) + val isBalanceHiddenFlow = _isBalanceHiddenFlow.asStateFlow() + var userWallet: UserWallet by Delegates.notNull() var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() var appCurrency: AppCurrency = AppCurrency.Default var predefinedAmountValue: String? = null - override fun onResult(sendUM: SendUM) { - _uiState.value = sendUM + private var balanceHidingJobHolder = JobHolder() + + init { + subscribeOnBalanceHidden() + subscribeOnQRScannerResult() + subscribeOnCurrencyStatusUpdates() + initAppCurrency() + } + + override fun onNavigationResult(navigationUM: NavigationUM) { + _uiState.update { it.copy(navigationUM = navigationUM) } } override fun onDestinationResult(destinationUM: DestinationUM) { @@ -58,10 +117,146 @@ internal class SendModel @Inject constructor( _uiState.update { it.copy(feeUM = feeUM) } } + override fun onResult(sendUM: SendUM) { + _uiState.update { sendUM } + } + + private fun initAppCurrency() { + modelScope.launch { + appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } + } + } + + private fun subscribeOnCurrencyStatusUpdates() { + modelScope.launch { + getUserWalletUseCase(params.userWalletId).fold( + ifRight = { wallet -> + userWallet = wallet + + val isSingleWalletWithToken = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() + val isMultiCurrency = wallet.isMultiCurrency + getCurrenciesStatusUpdates( + isSingleWalletWithToken = isSingleWalletWithToken, + isMultiCurrency = isMultiCurrency, + ) + }, + ifLeft = { + sendConfirmAlertFactory.getGenericErrorState(::onFailedTxEmailClick) + return@launch + }, + ) + } + } + + private fun subscribeOnBalanceHidden() { + getBalanceHidingSettingsUseCase() + .conflate() + .distinctUntilChanged() + .onEach { + _isBalanceHiddenFlow.value = it.isBalanceHidden + } + .launchIn(modelScope) + .saveIn(balanceHidingJobHolder) + } + + private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) { + getCurrencyStatus( + isSingleWalletWithToken = isSingleWalletWithToken, + isMultiCurrency = isMultiCurrency, + ).onEach { maybeCryptoCurrency -> + maybeCryptoCurrency.fold( + ifRight = { cryptoCurrencyStatus -> + onDataLoaded( + currencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = getFeeCurrencyStatus(cryptoCurrencyStatus, isMultiCurrency), + ) + }, + ifLeft = { + sendConfirmAlertFactory.getGenericErrorState { + onFailedTxEmailClick(it.toString()) + } + }, + ) + }.launchIn(modelScope) + } + + private fun getCurrencyStatus( + isSingleWalletWithToken: Boolean, + isMultiCurrency: Boolean, + ): Flow> { + return if (isMultiCurrency) { + getCurrencyStatusUpdatesUseCase( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + isSingleWalletWithTokens = isSingleWalletWithToken, + ) + } else { + getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId) + } + } + + private suspend fun getFeeCurrencyStatus( + cryptoCurrencyStatus: CryptoCurrencyStatus, + isMultiCurrency: Boolean, + ): CryptoCurrencyStatus { + return if (isMultiCurrency) { + getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ).getOrNull() ?: cryptoCurrencyStatus + } else { + cryptoCurrencyStatus + } + } + + private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, feeCurrencyStatus: CryptoCurrencyStatus) { + cryptoCurrencyStatus = currencyStatus + feeCryptoCurrencyStatus = feeCurrencyStatus + + if (params.amount != null) { + router.replaceAll(SendRoute.Confirm) + } + } + + private fun subscribeOnQRScannerResult() { + listenToQrScanningUseCase(SourceType.SEND) + .getOrElse { emptyFlow() } + .onEach(::onQrCodeScanned) + .launchIn(modelScope) + } + + private fun onQrCodeScanned(address: String) { + val parsedQrCode = parseQrCodeUseCase(address, cryptoCurrency).getOrNull() + predefinedAmountValue = parsedQrCode?.amount?.parseBigDecimal(cryptoCurrency.decimals) + } + + private fun onFailedTxEmailClick(errorMessage: String? = null) { + saveBlockchainErrorUseCase( + error = BlockchainErrorInfo( + errorMessage = errorMessage.orEmpty(), + blockchainId = cryptoCurrency.network.id.value, + derivationPath = cryptoCurrency.network.derivationPath.value, + destinationAddress = "", + tokenSymbol = "", + amount = "", + fee = "", + ), + ) + + val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrNull() ?: return + + modelScope.launch { + sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) + } + } + private fun initialState(): SendUM = SendUM( amountUM = AmountState.Empty(), - destinationUM = DestinationUM.Empty(), + destinationUM = SendDestinationInitialStateTransformer( + cryptoCurrency = cryptoCurrency, + ).transform(DestinationUM.Empty()), feeUM = FeeUM.Empty(), confirmUM = ConfirmUM.Empty, + navigationUM = NavigationUM.Empty, ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/SendContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/SendContent.kt new file mode 100644 index 0000000000..8451e8d50d --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/SendContent.kt @@ -0,0 +1,55 @@ +package com.tangem.features.send.v2.send.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.common.NavigationUM +import com.tangem.features.send.v2.send.ui.state.SendUM + +@Composable +internal fun SendContent(state: SendUM, stackState: ChildStack) { + Column( + modifier = Modifier.Companion + .background(color = TangemTheme.colors.background.tertiary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + horizontalAlignment = Alignment.Companion.CenterHorizontally, + ) { + SendAppBar(navigationUM = state.navigationUM) + Children( + stack = stackState, + animation = stackAnimation(slide()), + modifier = Modifier.weight(1f), + ) { + it.instance.Content(Modifier.weight(1f)) + } + SendNavigationButtons(navigationUM = state.navigationUM) + } +} + +@Composable +private fun SendAppBar(navigationUM: NavigationUM) { + val navigationUM = navigationUM as? NavigationUM.Content ?: return + AppBarWithBackButtonAndIcon( + text = navigationUM.title.resolveReference(), + subtitle = navigationUM.subtitle?.resolveReference(), + onBackClick = navigationUM.backIconClick, + onIconClick = navigationUM.additionalIconClick, + backIconRes = navigationUM.backIconRes, + iconRes = navigationUM.additionalIconRes, + backgroundColor = TangemTheme.colors.background.tertiary, + modifier = Modifier.height(TangemTheme.dimens.size56), + ) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt index 01594d7266..b616b506f7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.v2.subcomponents.amount import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.common.ui.amountScreen.models.AmountState @@ -20,7 +21,7 @@ internal class SendAmountBlockComponent( val onClick: () -> Unit, ) : ComposableContentComponent, AppComponentContext by appComponentContext { - private val model: SendAmountModel = getOrCreateModel(params = params, router = router) + private val model: SendAmountModel = getOrCreateModel(params = params) init { model.uiState.onEach { @@ -32,14 +33,13 @@ internal class SendAmountBlockComponent( @Composable override fun Content(modifier: Modifier) { - val state = model.uiState.collectAsStateWithLifecycle() - val isClickEnabled = params.blockClickEnableFlow.collectAsStateWithLifecycle() - val isEditingDisabled = params.blockEditDisabledFlow.collectAsStateWithLifecycle() + val state by model.uiState.collectAsStateWithLifecycle() + val isClickEnabled by params.blockClickEnableFlow.collectAsStateWithLifecycle() AmountBlock( - amountState = state.value, - isClickDisabled = !isClickEnabled.value, - isEditingDisabled = isEditingDisabled.value, + amountState = state, + isClickDisabled = !isClickEnabled, + isEditingDisabled = params.isPredefinedValues, onClick = onClick, ) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt index 6e35e058f5..f79c417846 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt @@ -27,10 +27,11 @@ internal class SendAmountComponent( @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() + val isBalanceHidden by params.isBalanceHidingFlow.collectAsStateWithLifecycle() AmountScreenContent( amountState = state, - isBalanceHidden = false, + isBalanceHidden = isBalanceHidden, clickIntents = model, modifier = Modifier.background(TangemTheme.colors.background.tertiary), ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt index e3bafeb146..7185402604 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt @@ -27,6 +27,7 @@ internal sealed class SendAmountComponentParams { val callback: ModelCallback, val currentRoute: Flow, val predefinedAmountValue: String?, + val isBalanceHidingFlow: StateFlow, ) : SendAmountComponentParams() data class AmountBlockParams( @@ -35,7 +36,8 @@ internal sealed class SendAmountComponentParams { override val userWallet: UserWallet, override val appCurrency: AppCurrency, override val cryptoCurrencyStatus: CryptoCurrencyStatus, - val blockEditDisabledFlow: StateFlow, val blockClickEnableFlow: StateFlow, + val predefinedAmountValue: String?, + val isPredefinedValues: Boolean, ) : SendAmountComponentParams() } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt index 4c4f1acb33..a168fe3532 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt @@ -13,6 +13,7 @@ import javax.inject.Singleton interface SendAmountReduceTrigger { suspend fun triggerReduceBy(reduceBy: ReduceByData) suspend fun triggerReduceTo(reduceTo: BigDecimal) + suspend fun triggerIgnoreReduce() } /** @@ -21,6 +22,7 @@ interface SendAmountReduceTrigger { interface SendAmountReduceListener { val reduceToTriggerFlow: Flow val reduceByTriggerFlow: Flow + val ignoreReduceTriggerFlow: Flow } @Singleton @@ -29,6 +31,7 @@ internal class DefaultSendAmountReduceTrigger @Inject constructor() : SendAmountReduceListener { override val reduceToTriggerFlow = MutableSharedFlow() override val reduceByTriggerFlow = MutableSharedFlow() + override val ignoreReduceTriggerFlow = MutableSharedFlow() override suspend fun triggerReduceBy(reduceBy: ReduceByData) { reduceByTriggerFlow.emit(reduceBy) @@ -37,4 +40,8 @@ internal class DefaultSendAmountReduceTrigger @Inject constructor() : override suspend fun triggerReduceTo(reduceTo: BigDecimal) { reduceToTriggerFlow.emit(reduceTo) } + + override suspend fun triggerIgnoreReduce() { + ignoreReduceTriggerFlow.emit(Unit) + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index e0588bbc0e..d0817e99ca 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -14,11 +14,14 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase import com.tangem.features.send.v2.common.NavigationUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.send.ui.state.ButtonsUM import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents @@ -60,6 +63,7 @@ internal class SendAmountModel @Inject constructor( initMinBoundary() subscribeOnAmountReduceByTriggerUpdates() subscribeOnAmountReduceToTriggerUpdates() + subscribeOnAmountIgnoreReduceTriggerUpdates() } private fun initMinBoundary() { @@ -79,7 +83,6 @@ internal class SendAmountModel @Inject constructor( } private fun initialState() { - val predefinedAmountValue = (params as? SendAmountComponentParams.AmountParams)?.predefinedAmountValue if (uiState.value is AmountState.Empty) { _uiState.update { AmountStateConverterV2( @@ -95,8 +98,9 @@ internal class SendAmountModel @Inject constructor( ), ) } - if (predefinedAmountValue != null) { - onAmountValueChange(predefinedAmountValue) + val params = params as? SendAmountComponentParams.AmountBlockParams + if (params?.predefinedAmountValue != null) { + onAmountValueChange(params.predefinedAmountValue) } } } @@ -191,6 +195,12 @@ internal class SendAmountModel @Inject constructor( .launchIn(modelScope) } + private fun subscribeOnAmountIgnoreReduceTriggerUpdates() { + sendAmountReduceListener.ignoreReduceTriggerFlow + .onEach { _uiState.update(AmountIgnoreReduceTransformer::transform) } + .launchIn(modelScope) + } + private fun saveResult() { val params = params as? SendAmountComponentParams.AmountParams ?: return params.callback.onAmountResult(uiState.value) @@ -202,7 +212,7 @@ internal class SendAmountModel @Inject constructor( flow = uiState, flow2 = params.currentRoute, transform = { state, route -> state to route }, - ).onEach { (state, route) -> + ).distinctUntilChanged().onEach { (state, route) -> params.callback.onNavigationResult( NavigationUM.Content( title = resourceReference(R.string.send_amount_label), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt index 02ec170b2d..f1b52451d8 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt @@ -29,7 +29,6 @@ internal class SendDestinationComponent( SendDestinationContent(state = state, clickIntents = model, isBalanceHidden = isBalanceHidden) } - interface ModelCallback : SendNavigationModelCallback { fun onDestinationResult(destinationUM: DestinationUM) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index b4d297cca8..5622ae7e32 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -357,7 +357,7 @@ internal class SendDestinationModel @Inject constructor( flow = uiState, flow2 = params.currentRoute, transform = { state, route -> state to route }, - ).onEach { (state, route) -> + ).distinctUntilChanged().onEach { (state, route) -> params.callback.onNavigationResult( NavigationUM.Content( title = resourceReference(R.string.send_recipient_label), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationPredefinedStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationPredefinedStateTransformer.kt new file mode 100644 index 0000000000..dc7532f5cf --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationPredefinedStateTransformer.kt @@ -0,0 +1,19 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.transformers + +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.utils.transformer.Transformer + +internal class SendDestinationPredefinedStateTransformer( + private val address: String, + private val memo: String?, +) : Transformer { + + override fun transform(prevState: DestinationUM): DestinationUM { + val state = prevState as? DestinationUM.Content ?: return prevState + + return state.copy( + addressTextField = state.addressTextField.copy(value = address, isValuePasted = false), + memoTextField = memo?.let { state.memoTextField?.copy(value = it, isValuePasted = false) }, + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt index b2c3ac8b6e..ccc6266ae2 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt @@ -10,15 +10,11 @@ internal sealed class DestinationTextFieldUM { /** Current value */ abstract val value: String - /** Lambda be invoked when value is been changed */ - abstract val onValueChange: (String) -> Unit - /** Keyboard options */ abstract val keyboardOptions: KeyboardOptions data class RecipientAddress( override val value: String, - override val onValueChange: (String) -> Unit, override val keyboardOptions: KeyboardOptions, val placeholder: TextReference, val label: TextReference, @@ -29,7 +25,6 @@ internal sealed class DestinationTextFieldUM { data class RecipientMemo( override val value: String, - override val onValueChange: (String) -> Unit, override val keyboardOptions: KeyboardOptions, val placeholder: TextReference, val label: TextReference, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt index 7be35bbc1b..06935a8368 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt @@ -16,7 +16,7 @@ internal sealed class DestinationUM { val wallets: ImmutableList, val networkName: String, val isValidating: Boolean = false, - val isEditingDisabled: Boolean = false, + val isInitialized: Boolean = false, ) : DestinationUM() data class Empty( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeAlertFactory.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeAlertFactory.kt index 6526312b27..107c608bb2 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeAlertFactory.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeAlertFactory.kt @@ -77,7 +77,7 @@ internal class SendFeeAlertFactory @Inject constructor( return isFeeTooHigh } - fun getFeeUpdatedAlert(newFee: TransactionFee, feeUM: FeeUM, onFeeNotIncreased: () -> Unit) { + fun getFeeUpdatedAlert(newFee: TransactionFee, feeUM: FeeUM, proceedAction: () -> Unit, stopAction: () -> Unit) { if (feeUM !is FeeUM.Content) return val feeSelectorUM = feeUM.feeSelectorUM as? FeeSelectorUM.Content ?: return val newFee = when (newFee) { @@ -100,12 +100,16 @@ internal class SendFeeAlertFactory @Inject constructor( DialogMessage( message = resourceReference(id = R.string.send_notification_high_fee_title), dismissOnFirstAction = true, - firstActionBuilder = { okAction() }, - secondActionBuilder = { cancelAction() }, + firstActionBuilder = { + okAction { proceedAction(); onDismissRequest() } + }, + secondActionBuilder = { + cancelAction { stopAction(); onDismissRequest() } + }, ), ) } else { - onFeeNotIncreased() + proceedAction() } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt index 6d1d982599..861798a122 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt @@ -8,12 +8,12 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.navigation.url.UrlOpener -import com.tangem.features.send.v2.send.ui.state.ButtonsUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.features.send.v2.common.NavigationUM import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.send.ui.state.ButtonsUM import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger @@ -208,7 +208,20 @@ internal class SendFeeModel @Inject constructor( modelScope.launch { callFeeUseCase().fold( ifRight = { - feeCheckReloadTrigger.callbackCheckResult(true) + sendFeeAlertFactory.getFeeUpdatedAlert( + newFee = it, + feeUM = uiState.value, + proceedAction = { + modelScope.launch { + feeCheckReloadTrigger.callbackCheckResult(true) + } + }, + stopAction = { + modelScope.launch { + feeCheckReloadTrigger.callbackCheckResult(false) + } + }, + ) _uiState.update( SendFeeLoadedTransformer( fees = it, @@ -223,7 +236,7 @@ internal class SendFeeModel @Inject constructor( ifLeft = { feeError -> feeCheckReloadTrigger.callbackCheckResult(false) _uiState.update(SendFeeFailedTransformer(feeError)) - sendFeeAlertFactory.getFeeUnreachableErrorState(::checkLoadFee) + sendFeeAlertFactory.getFeeUnreachableErrorState(::loadFee) updateFeeNotifications() }, ) @@ -265,7 +278,7 @@ internal class SendFeeModel @Inject constructor( flow = uiState, flow2 = params.currentRoute, transform = { state, route -> state to route }, - ).onEach { (state, _) -> + ).distinctUntilChanged().onEach { (state, _) -> params.callback.onNavigationResult( NavigationUM.Content( title = resourceReference(R.string.common_fee_selector_title), From 28d15c9043f70afc1bc71e0052a5ec8206831952 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Apr 2025 22:41:08 +0300 Subject: [PATCH 094/138] Updated on 2026-08-14 --- .../java/com/tangem/tap/di/ActivityModule.kt | 3 - .../exchangeServices/DefaultRampManager.kt | 112 ++++++++++++------ .../domain/exchange/ExchangeableState.kt | 9 -- .../exchange/ExpressAvailabilityState.kt | 10 ++ .../domain/exchange/RampStateManager.kt | 7 +- .../tokens/GetCryptoCurrencyActionsUseCase.kt | 85 ++++++------- .../topup/model/OnboardingNoteTopUpModel.kt | 21 ++-- .../selecttoken/model/OnrampOperationModel.kt | 5 +- .../tokenlist/model/OnrampTokenListModel.kt | 17 +-- 9 files changed, 142 insertions(+), 127 deletions(-) delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/exchange/ExchangeableState.kt create mode 100644 domain/legacy/src/main/java/com/tangem/domain/exchange/ExpressAvailabilityState.kt diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index 85d5a6630b..368216c920 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -2,7 +2,6 @@ package com.tangem.tap.di import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader -import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.exchange.RampStateManager @@ -55,7 +54,6 @@ internal object ActivityModule { excludedBlockchains: ExcludedBlockchains, dispatchers: CoroutineDispatcherProvider, onrampFeatureToggles: OnrampFeatureToggles, - expressAssetsStore: ExpressAssetsStore, ): RampStateManager { return DefaultRampManager( exchangeService = appStateHolder.exchangeService, @@ -67,7 +65,6 @@ internal object ActivityModule { excludedBlockchains = excludedBlockchains, dispatchers = dispatchers, onrampFeatureToggles = onrampFeatureToggles, - expressAssetsStore = expressAssetsStore, ) } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index d53b637ce1..2671d88af8 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -9,9 +9,8 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE import com.tangem.datasource.api.express.models.response.Asset import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader -import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.domain.core.lce.Lce -import com.tangem.domain.exchange.ExchangeableState +import com.tangem.domain.exchange.ExpressAvailabilityState import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase @@ -27,7 +26,6 @@ import com.tangem.utils.coroutines.runCatching import com.tangem.utils.isNullOrZero import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.withContext @Suppress("LongParameterList") internal class DefaultRampManager( @@ -39,7 +37,6 @@ internal class DefaultRampManager( private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val dispatchers: CoroutineDispatcherProvider, private val onrampFeatureToggles: OnrampFeatureToggles, - private val expressAssetsStore: ExpressAssetsStore, excludedBlockchains: ExcludedBlockchains, ) : RampStateManager { @@ -49,19 +46,12 @@ internal class DefaultRampManager( scanResponse: ScanResponse, userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): Boolean { - return runCatching { - when { - onrampFeatureToggles.isFeatureEnabled -> getOnrampAvailable(userWalletId, cryptoCurrency) - exchangeService != null -> exchangeService.availableForBuy( - scanResponse = scanResponse, - currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), - ) - else -> false - } - } - .getOrNull() - ?: false + ): ScenarioUnavailabilityReason { + val availabilityState = runCatching { + getOnrampAvailableState(scanResponse, userWalletId, cryptoCurrency) + }.getOrNull() ?: ExpressAvailabilityState.Error + + return availabilityState.toReason(cryptoCurrency.name) } override suspend fun availableForSell( @@ -103,8 +93,11 @@ internal class DefaultRampManager( override suspend fun availableForSwap( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): ExchangeableState { - return runCatching { getExchangeableState(userWalletId, cryptoCurrency) }.getOrNull() ?: ExchangeableState.Error + ): ScenarioUnavailabilityReason { + val availabilityState = runCatching { + getExchangeableState(userWalletId, cryptoCurrency) + }.getOrNull() ?: ExpressAvailabilityState.Error + return availabilityState.toReason(cryptoCurrency.name) } override fun getBuyInitializationStatus(): Flow { @@ -134,33 +127,76 @@ internal class DefaultRampManager( private suspend fun getExchangeableState( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): ExchangeableState { - return withContext(dispatchers.io) { - val asset = expressServiceLoader.getInitializationStatus(userWalletId).firstOrNull() - ?: return@withContext ExchangeableState.Loading - when (asset) { - is Lce.Error -> ExchangeableState.Error - is Lce.Loading -> ExchangeableState.Loading - is Lce.Content -> { - val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(it) } - foundAsset?.exchangeAvailable?.toExchangeableState() ?: ExchangeableState.AssetNotFound - } + ): ExpressAvailabilityState { + val asset = expressServiceLoader.getInitializationStatus(userWalletId).firstOrNull() + ?: return ExpressAvailabilityState.Loading + return when (asset) { + is Lce.Error -> ExpressAvailabilityState.Error + is Lce.Loading -> ExpressAvailabilityState.Loading + is Lce.Content -> { + val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(it) } + foundAsset?.exchangeAvailable?.toSwapAvailabilityState() + ?: ExpressAvailabilityState.AssetNotFound } } } - private fun Boolean.toExchangeableState(): ExchangeableState { - return if (this) { - ExchangeableState.Exchangeable - } else { - ExchangeableState.NotExchangeable + private suspend fun getOnrampAvailableState( + scanResponse: ScanResponse, + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): ExpressAvailabilityState { + return when { + onrampFeatureToggles.isFeatureEnabled -> { + val asset = expressServiceLoader.getInitializationStatus(userWalletId).firstOrNull() + ?: return ExpressAvailabilityState.Loading + return when (asset) { + is Lce.Error -> ExpressAvailabilityState.Error + is Lce.Loading -> ExpressAvailabilityState.Loading + is Lce.Content -> { + val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(it) } + foundAsset?.onrampAvailable?.toOnrampAvailabilityState() + ?: ExpressAvailabilityState.AssetNotFound + } + } + } + exchangeService != null -> { + exchangeService.availableForBuy( + scanResponse = scanResponse, + currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), + ).toOnrampAvailabilityState() + } + else -> ExpressAvailabilityState.Error } } - private suspend fun getOnrampAvailable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { - val asset = expressAssetsStore.getSyncOrNull(userWalletId)?.find { cryptoCurrency.findAssetPredicate(it) } + private fun ExpressAvailabilityState.toReason(currencyName: String): ScenarioUnavailabilityReason { + return when (this) { + ExpressAvailabilityState.Available -> { + ScenarioUnavailabilityReason.None + } + ExpressAvailabilityState.AssetNotFound -> ScenarioUnavailabilityReason.AssetNotFound(currencyName) + ExpressAvailabilityState.Error -> ScenarioUnavailabilityReason.ExpressUnreachable(currencyName) + ExpressAvailabilityState.Loading -> ScenarioUnavailabilityReason.ExpressLoading(currencyName) + ExpressAvailabilityState.NotOnrampable -> ScenarioUnavailabilityReason.BuyUnavailable(currencyName) + ExpressAvailabilityState.NotExchangeable -> ScenarioUnavailabilityReason.NotExchangeable(currencyName) + } + } - return asset?.onrampAvailable ?: false + private fun Boolean.toSwapAvailabilityState(): ExpressAvailabilityState { + return if (this) { + ExpressAvailabilityState.Available + } else { + ExpressAvailabilityState.NotExchangeable + } + } + + private fun Boolean.toOnrampAvailabilityState(): ExpressAvailabilityState { + return if (this) { + ExpressAvailabilityState.Available + } else { + ExpressAvailabilityState.NotOnrampable + } } private fun CryptoCurrency.findAssetPredicate(asset: Asset): Boolean { diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/ExchangeableState.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/ExchangeableState.kt deleted file mode 100644 index 9a4f140cf1..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/exchange/ExchangeableState.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.domain.exchange - -sealed interface ExchangeableState { - data object Exchangeable : ExchangeableState - data object NotExchangeable : ExchangeableState - data object Loading : ExchangeableState - data object Error : ExchangeableState - data object AssetNotFound : ExchangeableState -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/ExpressAvailabilityState.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/ExpressAvailabilityState.kt new file mode 100644 index 0000000000..a0e960f17e --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/ExpressAvailabilityState.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.exchange + +sealed interface ExpressAvailabilityState { + data object Available : ExpressAvailabilityState + data object NotExchangeable : ExpressAvailabilityState + data object NotOnrampable : ExpressAvailabilityState + data object Loading : ExpressAvailabilityState + data object Error : ExpressAvailabilityState + data object AssetNotFound : ExpressAvailabilityState +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt index 180a8b16b5..e8a8f3a363 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -18,7 +18,7 @@ interface RampStateManager { scanResponse: ScanResponse, userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): Boolean + ): ScenarioUnavailabilityReason /** * Check if [CryptoCurrency] is available for sell @@ -31,7 +31,10 @@ interface RampStateManager { status: CryptoCurrencyStatus, ): Either - suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): ExchangeableState + suspend fun availableForSwap( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): ScenarioUnavailabilityReason suspend fun fetchBuyServiceData() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 7e2b95240b..315c353ae8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -1,7 +1,6 @@ package com.tangem.domain.tokens import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.exchange.ExchangeableState import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.StatusSource import com.tangem.domain.promo.PromoRepository @@ -168,12 +167,11 @@ class GetCryptoCurrencyActionsUseCase( } // buy - if (rampManager.availableForBuy(userWallet.scanResponse, userWallet.walletId, cryptoCurrency)) { - activeList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)) + val onrampActionState = getOnrampUnavailabilityReason(userWallet, cryptoCurrencyStatus) + if (onrampActionState.unavailabilityReason == ScenarioUnavailabilityReason.None) { + activeList.add(onrampActionState) } else { - disabledList.add( - TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.BuyUnavailable(cryptoCurrency.symbol)), - ) + disabledList.add(onrampActionState) } // region sell @@ -229,38 +227,37 @@ class GetCryptoCurrencyActionsUseCase( cryptoCurrencyStatus: CryptoCurrencyStatus, requirements: AssetRequirementsCondition?, ): List { - val actionsList = mutableListOf() + val activeList = mutableListOf() + val disabledList = mutableListOf() if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - actionsList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None)) + activeList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None)) } - if (rampManager.availableForBuy(userWallet.scanResponse, userWallet.walletId, cryptoCurrencyStatus.currency)) { - actionsList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)) + + // buy (is not depend on cache) + val onrampActionState = getOnrampUnavailabilityReason(userWallet, cryptoCurrencyStatus) + if (onrampActionState.unavailabilityReason == ScenarioUnavailabilityReason.None) { + activeList.add(onrampActionState) } else { - actionsList.add( - TokenActionsState.ActionState.Buy( - ScenarioUnavailabilityReason.BuyUnavailable( - cryptoCurrencyName = cryptoCurrencyStatus.currency.name, - ), - ), - ) + disabledList.add(onrampActionState) } - actionsList.add(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.Unreachable)) - actionsList.add( + + disabledList.add(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.Unreachable)) + disabledList.add( TokenActionsState.ActionState.Swap( unavailabilityReason = ScenarioUnavailabilityReason.Unreachable, showBadge = false, ), ) - actionsList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.Unreachable)) + disabledList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.Unreachable)) if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { val scenario = getReceiveScenario(requirements) - actionsList.add(TokenActionsState.ActionState.Receive(scenario)) + activeList.add(TokenActionsState.ActionState.Receive(scenario)) } - actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable, null)) - actionsList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) + disabledList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable, null)) + activeList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) - return actionsList + return activeList + disabledList } @Suppress("LongMethod") @@ -313,17 +310,12 @@ class GetCryptoCurrencyActionsUseCase( disabledList.add(swapAction) } - // buy - if (rampManager.availableForBuy(userWallet.scanResponse, userWallet.walletId, cryptoCurrency)) { - activeList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)) + // buy (is not depend on cache) + val onrampActionState = getOnrampUnavailabilityReason(userWallet, cryptoCurrencyStatus) + if (onrampActionState.unavailabilityReason == ScenarioUnavailabilityReason.None) { + activeList.add(onrampActionState) } else { - disabledList.add( - TokenActionsState.ActionState.Buy( - ScenarioUnavailabilityReason.BuyUnavailable( - cryptoCurrencyName = cryptoCurrencyStatus.currency.name, - ), - ), - ) + disabledList.add(onrampActionState) } // staking @@ -415,21 +407,7 @@ class GetCryptoCurrencyActionsUseCase( showBadge = false, ) } - val exchangeableState = withTimeoutOrNull(REQUEST_EXCHANGE_DATA_TIMEOUT) { - rampManager.availableForSwap(userWallet.walletId, cryptoCurrency) - } ?: ExchangeableState.Loading - val currencyName = cryptoCurrency.name - - val reason = when (exchangeableState) { - ExchangeableState.Exchangeable -> { - ScenarioUnavailabilityReason.None - } - ExchangeableState.AssetNotFound -> ScenarioUnavailabilityReason.AssetNotFound(currencyName) - ExchangeableState.Error -> ScenarioUnavailabilityReason.ExpressUnreachable(currencyName) - ExchangeableState.Loading -> ScenarioUnavailabilityReason.ExpressLoading(currencyName) - ExchangeableState.NotExchangeable -> ScenarioUnavailabilityReason.NotExchangeable(currencyName) - } - + val reason = rampManager.availableForSwap(userWallet.walletId, cryptoCurrency) val isShowBadge = reason == ScenarioUnavailabilityReason.None && shouldShowSwapStories TokenActionsState.ActionState.Swap( unavailabilityReason = reason, @@ -443,6 +421,15 @@ class GetCryptoCurrencyActionsUseCase( } } + private suspend fun getOnrampUnavailabilityReason( + userWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): TokenActionsState.ActionState { + val cryptoCurrency = cryptoCurrencyStatus.currency + val reason = rampManager.availableForBuy(userWallet.scanResponse, userWallet.walletId, cryptoCurrency) + return TokenActionsState.ActionState.Buy(unavailabilityReason = reason) + } + private fun isAddressAvailable(networkAddress: NetworkAddress?): Boolean { return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty() } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt index f51e0f6df5..15c4a85353 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt @@ -20,6 +20,7 @@ import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet @@ -171,22 +172,18 @@ internal class OnboardingNoteTopUpModel @Inject constructor( _uiState.update { it.copy( amountToCreateAccount = amountToCreateAccount - ?.format( - { - crypto( - symbol = status.currency.symbol, - decimals = status.currency.decimals, - ) - }, - ), - balance = amount?.format( - { + ?.format { crypto( symbol = status.currency.symbol, decimals = status.currency.decimals, ) }, - ).orEmpty(), + balance = amount?.format { + crypto( + symbol = status.currency.symbol, + decimals = status.currency.decimals, + ) + }.orEmpty(), isTopUpDataLoading = status.value.networkAddress == null, ) } @@ -209,7 +206,7 @@ internal class OnboardingNoteTopUpModel @Inject constructor( ) _uiState.update { it.copy( - availableForBuy = availableForBuy, + availableForBuy = availableForBuy == ScenarioUnavailabilityReason.None, availableForBuyLoading = false, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt index 770e77d257..e2f4d63942 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt @@ -20,6 +20,7 @@ import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.features.onramp.impl.R @@ -79,13 +80,13 @@ internal class OnrampOperationModel @Inject constructor( fun onHotTokenClick(status: CryptoCurrencyStatus) { modelScope.launch { - val isAvailable = rampStateManager.availableForBuy( + val unavailabilityReason = rampStateManager.availableForBuy( scanResponse = selectedUserWallet.scanResponse, userWalletId = params.userWalletId, cryptoCurrency = status.currency, ) - if (isAvailable) { + if (unavailabilityReason == ScenarioUnavailabilityReason.None) { analyticsEventHandler.send( event = MainScreenAnalyticsEvent.HotTokenClicked(currencySymbol = status.currency.symbol), ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index 666236c87f..ee6457f6af 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -11,13 +11,13 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.getOrElse -import com.tangem.domain.exchange.ExchangeableState import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.usecase.GetWalletsUseCase @@ -230,7 +230,7 @@ internal class OnrampTokenListModel @Inject constructor( scanResponse = scanResponse, userWalletId = params.userWalletId, cryptoCurrency = status.currency, - ) + ).isAvailable() } OnrampOperation.SELL -> { rampStateManager.availableForSell( @@ -242,21 +242,14 @@ internal class OnrampTokenListModel @Inject constructor( val isAvailable = rampStateManager.availableForSwap( userWalletId = params.userWalletId, cryptoCurrency = status.currency, - ).isSwapAvailable() && !status.currency.isCustom + ).isAvailable() && !status.currency.isCustom isAvailable && status.value !is CryptoCurrencyStatus.NoQuote } } } - private fun ExchangeableState.isSwapAvailable(): Boolean { - return when (this) { - ExchangeableState.AssetNotFound, - ExchangeableState.Error, - ExchangeableState.Loading, - ExchangeableState.NotExchangeable, - -> false - ExchangeableState.Exchangeable -> true - } + private fun ScenarioUnavailabilityReason.isAvailable(): Boolean { + return this == ScenarioUnavailabilityReason.None } } \ No newline at end of file From a4eb7fce060295e898e9b5164eb5469919694d9a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Apr 2025 17:54:38 +0500 Subject: [PATCH 095/138] Updated on 2026-08-14 --- .../tangem/tap/features/main/MainViewModel.kt | 2 +- .../datasource/di/StakingStoreModule.kt | 20 +++++++++++-- .../local/token/DefaultStakingYieldsStore.kt | 21 +++++-------- .../data/staking/DefaultStakingRepository.kt | 30 ++++++++----------- .../staking/FetchStakingTokensUseCase.kt | 4 +-- .../staking/repositories/StakingRepository.kt | 2 +- 6 files changed, 41 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 859d82a082..e6a4d37f48 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -124,7 +124,7 @@ internal class MainViewModel @Inject constructor( private fun fetchStakingTokens() { viewModelScope.launch(dispatchers.main) { - fetchStakingTokensUseCase(true) + fetchStakingTokensUseCase() .onLeft { Timber.e(it.toString(), "Unable to fetch the staking tokens list") } .onRight { Timber.d("Staking token list was fetched successfully") } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt index 655f4a41b3..3d632eaf97 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt @@ -5,10 +5,12 @@ import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.datasource.local.datastore.RuntimeDataStore import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.token.* import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.datasource.utils.listTypes import com.tangem.datasource.utils.mapWithStringKeyTypes import com.tangem.datasource.utils.setTypes import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -27,8 +29,22 @@ internal object StakingStoreModule { @Provides @Singleton - fun provideStakingTokensStore(): StakingYieldsStore { - return DefaultStakingYieldsStore(dataStore = RuntimeDataStore()) + fun provideStakingTokensStore( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + dispatchers: CoroutineDispatcherProvider, + ): StakingYieldsStore { + return DefaultStakingYieldsStore( + dataStore = DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = listTypes(), + defaultValue = emptyList(), + ), + produceFile = { context.dataStoreFile(fileName = "yields_cache") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ), + ) } @Provides diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt index eb27aad46b..874e884f87 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt @@ -1,32 +1,25 @@ package com.tangem.datasource.local.token +import androidx.datastore.core.DataStore import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO -import com.tangem.datasource.local.datastore.core.StringKeyDataStore import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.flow.firstOrNull internal class DefaultStakingYieldsStore( - private val dataStore: StringKeyDataStore>, + private val dataStore: DataStore>, ) : StakingYieldsStore { - private val mutex = Mutex() - override fun get(): Flow> { - return dataStore.get(KEY) + return dataStore.data } override suspend fun getSync(): List { - return dataStore.getSyncOrNull(KEY) ?: emptyList() + return dataStore.data.firstOrNull().orEmpty() } override suspend fun store(items: List) { - mutex.withLock { - dataStore.store(KEY, items) + dataStore.updateData { _ -> + items } } - - companion object { - private const val KEY = "DefaultStakingYieldsStore" - } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 9275635df5..198ecb2e5a 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -108,25 +108,19 @@ internal class DefaultStakingRepository( return integrationIdMap.getOrDefault(getIntegrationKey(cryptoCurrencyId), null) } - override suspend fun fetchEnabledYields(refresh: Boolean) { + override suspend fun fetchEnabledYields() { withContext(dispatchers.io) { - cacheRegistry.invokeOnExpire( - key = YIELDS_STORE_KEY, - skipCache = refresh, - block = { - when (val stakingTokensWithYields = stakeKitApi.getEnabledYields(preferredValidatorsOnly = false)) { - is ApiResponse.Success -> stakingYieldsStore.store( - stakingTokensWithYields.data.data.filter { - it.isAvailable ?: false - }, - ) - else -> { - stakingYieldsStore.store(emptyList()) - throw (stakingTokensWithYields as ApiResponse.Error).cause - } - } - }, - ) + when (val stakingTokensWithYields = stakeKitApi.getEnabledYields(preferredValidatorsOnly = false)) { + is ApiResponse.Success -> stakingYieldsStore.store( + stakingTokensWithYields.data.data.filter { + it.isAvailable ?: false + }, + ) + else -> { + stakingYieldsStore.store(emptyList()) + throw (stakingTokensWithYields as ApiResponse.Error).cause + } + } } } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt index c591ae2bc2..168a65fc0f 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt @@ -14,10 +14,10 @@ class FetchStakingTokensUseCase( private val stakingRepository: StakingRepository, private val stakingErrorResolver: StakingErrorResolver, ) { - suspend operator fun invoke(isRefresh: Boolean = false): Either { + suspend operator fun invoke(): Either { return either { catch( - block = { stakingRepository.fetchEnabledYields(isRefresh) }, + block = { stakingRepository.fetchEnabledYields() }, catch = { stakingErrorResolver.resolve(it) }, ) } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index b583c5f491..d97ac0f2b9 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -27,7 +27,7 @@ interface StakingRepository { fun getSupportedIntegrationId(cryptoCurrencyId: CryptoCurrency.ID): String? - suspend fun fetchEnabledYields(refresh: Boolean) + suspend fun fetchEnabledYields() suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo From f553b7572c7caa1a67332cc2c9b41f0d59c0bcf4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Apr 2025 18:02:38 +0500 Subject: [PATCH 096/138] Updated on 2026-08-14 --- .../tangem/domain/tokens/repository/MockStakingRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index 2b63da6411..04be38d010 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -28,7 +28,7 @@ class MockStakingRepository : StakingRepository { override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = "" - override suspend fun fetchEnabledYields(refresh: Boolean) { + override suspend fun fetchEnabledYields() { /* no-op */ } From 38c7183080890d4c9daff85fafee1c012630d4d1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Apr 2025 03:34:02 +0500 Subject: [PATCH 097/138] Updated on 2026-08-14 --- .../nft/collections/ui/NFTCollectionAsset.kt | 27 +++++- .../features/nft/details/ui/NFTDetailsLogo.kt | 93 +++++++++++++++++++ 2 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsLogo.kt diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAsset.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAsset.kt index f1925ed862..dfb100463f 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAsset.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAsset.kt @@ -1,16 +1,20 @@ package com.tangem.features.nft.collections.ui import android.content.res.Configuration +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -24,6 +28,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.nft.collections.entity.NFTCollectionAssetUM import com.tangem.features.nft.collections.entity.NFTSalePriceUM +import com.tangem.features.nft.impl.R @Composable internal fun NFTCollectionAsset(state: NFTCollectionAssetUM, modifier: Modifier = Modifier) { @@ -42,11 +47,7 @@ internal fun NFTCollectionAsset(state: NFTCollectionAssetUM, modifier: Modifier RectangleShimmer(radius = TangemTheme.dimens.radius16) }, error = { - Box( - modifier = Modifier - .clip(shape = TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.field.primary), - ) + Placeholder() }, contentScale = ContentScale.Crop, contentDescription = null, @@ -66,6 +67,22 @@ internal fun NFTCollectionAsset(state: NFTCollectionAssetUM, modifier: Modifier } } +@Composable +private fun Placeholder(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.field.focused), + contentAlignment = Alignment.Center, + ) { + Image( + modifier = Modifier.size(TangemTheme.dimens.size52), + painter = painterResource(R.drawable.ic_nft_placeholder_76), + contentDescription = null, + ) + } +} + @Preview(widthDp = 180, showBackground = true) @Preview(widthDp = 180, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsLogo.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsLogo.kt new file mode 100644 index 0000000000..6c49a7f86e --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsLogo.kt @@ -0,0 +1,93 @@ +package com.tangem.features.nft.details.ui + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.details.entity.NFTAssetUM +import com.tangem.features.nft.impl.R + +@Composable +internal fun NFTDetailsLogo(state: NFTAssetUM.Media?, modifier: Modifier = Modifier) { + if (state == null) { + Placeholder( + modifier = modifier + .fillMaxWidth(), + ) + } else { + SubcomposeAsyncImage( + modifier = modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium), + model = ImageRequest.Builder(LocalContext.current) + .data(state.url) + .crossfade(true) + .build(), + loading = { + RectangleShimmer(radius = TangemTheme.dimens.radius16) + }, + error = { + Placeholder() + }, + contentScale = ContentScale.Crop, + contentDescription = null, + ) + } +} + +@Composable +private fun Placeholder(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.field.focused), + contentAlignment = Alignment.Center, + ) { + Image( + modifier = Modifier.size(TangemTheme.dimens.size120), + painter = painterResource(R.drawable.ic_nft_placeholder_76), + contentDescription = null, + ) + } +} + +@Preview(widthDp = 360) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTDetailsInfoBlock(@PreviewParameter(NFTAssetMediaProvider::class) state: NFTAssetUM.Media?) { + TangemThemePreview { + NFTDetailsLogo( + state = state, + modifier = Modifier + .aspectRatio(1f), + ) + } +} + +private class NFTAssetMediaProvider : CollectionPreviewParameterProvider( + collection = listOf( + null, + NFTAssetUM.Media( + mimetype = "image/jpeg", + url = "img1", + ), + ), +) \ No newline at end of file From faf7d0219ae0f9c9607c5cf652ec58e4fae225e2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Apr 2025 17:20:28 +0500 Subject: [PATCH 098/138] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 11 + .../nft/details/ui/NFTDetailsBlocksGroup.kt | 246 ++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsBlocksGroup.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 0eba9dda01..a23e1b705f 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -179,6 +179,7 @@ Search Search tokens sec + See all Seed phrase Select action Sell @@ -519,6 +520,16 @@ NFT collections Unable to load the data Choose network + Last sale price + Rarity label + Rarity rank + Traits + Base information + Token Standard + Contract Address + Token ID + Token Address + Chain Set up a single access code to protect all your devices. Protect Set an individual access code for each card or ring later. diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsBlocksGroup.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsBlocksGroup.kt new file mode 100644 index 0000000000..8df906ff96 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsBlocksGroup.kt @@ -0,0 +1,246 @@ +package com.tangem.features.nft.details.ui + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.details.entity.NFTAssetUM +import com.tangem.features.nft.impl.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun NFTDetailsBlocksGroup( + title: TextReference, + action: (@Composable BoxScope.() -> Unit)?, + items: ImmutableList, + modifier: Modifier = Modifier, +) { + val itemsCount = items.size + if (items.size > 0) { + val rowCount = (itemsCount + 1) / 2 + val paddingValues = PaddingValues( + horizontal = TangemTheme.dimens.spacing6, + vertical = TangemTheme.dimens.spacing8, + ) + InformationBlock( + modifier = modifier, + title = { + NFTDetailsGroupTitle( + text = title, + ) + }, + action = action, + contentHorizontalPadding = TangemTheme.dimens.spacing6, + ) { + Column( + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing12), + ) { + repeat(rowCount) { rowIndex -> + val item1 = items[rowIndex * 2] + val item2 = items.getOrNull(rowIndex * 2 + 1) + key(rowIndex) { + Row( + modifier = Modifier.fillMaxWidth(), + ) { + NFTDetailsGroupBlock( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + title = item1.title, + value = stringReference(item1.value), + titleEllipsis = item1.titleTextEllipsis, + valueEllipsis = item1.valueTextEllipsis, + ) + if (item2 == null) { + Box( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + ) + } else { + NFTDetailsGroupBlock( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + title = item2.title, + value = stringReference(item2.value), + titleEllipsis = item2.titleTextEllipsis, + valueEllipsis = item2.valueTextEllipsis, + ) + } + } + } + } + } + } + } +} + +@Composable +internal fun NFTDetailsGroupTitle(text: TextReference, modifier: Modifier = Modifier) { + Text( + modifier = modifier, + text = text.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) +} + +@Composable +internal fun NFTDetailsGroupBlock( + title: TextReference, + value: TextReference, + modifier: Modifier = Modifier, + titleEllipsis: TextEllipsis = TextEllipsis.End, + valueEllipsis: TextEllipsis = TextEllipsis.End, +) { + Column( + modifier = modifier, + ) { + EllipsisText( + text = title.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ellipsis = titleEllipsis, + ) + EllipsisText( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4), + text = value.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ellipsis = valueEllipsis, + ) + } +} + +@Composable +internal fun NFTBlocksGroupAction( + text: TextReference, + startIcon: @Composable RowScope.() -> Unit, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val interactionSource = remember { MutableInteractionSource() } + + Row( + modifier = modifier + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = onClick, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2), + ) { + startIcon() + Text( + text = text.resolveReference(), + color = TangemTheme.colors.text.accent, + style = TangemTheme.typography.subtitle2, + ) + } +} + +@Composable +internal fun NFTBlocksGroupActionIcon(@DrawableRes iconRes: Int, modifier: Modifier = Modifier) { + Icon( + modifier = modifier.size(size = TangemTheme.dimens.size16), + painter = painterResource(id = iconRes), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) +} + +@Preview(widthDp = 360) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTDetailsInfoBlock( + @PreviewParameter(NFTAssetBlocksProvider::class) state: ImmutableList, +) { + TangemThemePreview { + Column { + NFTDetailsBlocksGroup( + title = stringReference("Title"), + action = null, + items = state, + ) + NFTDetailsBlocksGroup( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + title = stringReference("Title"), + action = { + NFTBlocksGroupAction( + text = resourceReference(R.string.common_see_all), + startIcon = { }, + onClick = { }, + ) + }, + items = state, + ) + NFTDetailsBlocksGroup( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + title = stringReference("Title"), + action = { + NFTBlocksGroupAction( + text = resourceReference(R.string.common_explore), + startIcon = { + NFTBlocksGroupActionIcon(iconRes = R.drawable.ic_compass_24) + }, + onClick = { }, + ) + }, + items = state, + ) + } + } +} + +private class NFTAssetBlocksProvider : CollectionPreviewParameterProvider>( + collection = listOf( + persistentListOf( + NFTAssetUM.BlockItem( + title = stringReference("Tier"), + value = "Infinite", + ), + NFTAssetUM.BlockItem( + title = stringReference("Phygital Toy"), + value = "None", + ), + NFTAssetUM.BlockItem( + title = stringReference("Class"), + value = "CYBER", + ), + NFTAssetUM.BlockItem( + title = stringReference("Accessory"), + value = "No accessory", + ), + NFTAssetUM.BlockItem( + title = stringReference("Sneakers"), + value = "Boots", + ), + ), + ), +) \ No newline at end of file From 612f30dda1e8d5c7c53291e1e2e0b62d9f3fd7d8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Apr 2025 17:08:00 +0300 Subject: [PATCH 099/138] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 34b5b0126e..a85571bfc2 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 @@ -49,11 +49,11 @@ }, { "name": "TWIN_REFACTORING_ENABLED", - "version": "undefined" + "version": "5.23.0" }, { "name": "SEND_V2_ENABLED", - "version": "undefined" + "version": "5.23.0" }, { "name": "WALLET_CONNECT_REDESIGN_ENABLED", From 7bb88d55c9ae0347c7392e482fd94175c4abe89f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Apr 2025 18:00:59 +0300 Subject: [PATCH 100/138] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a85571bfc2..c0d2c19ebd 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 @@ -13,7 +13,7 @@ }, { "name": "NAVIGATION_REFACTORING", - "version": "undefined" + "version": "5.23.0" }, { "name": "VISA_ONBOARDING_ENABLED", From e895d15e8a4fd1b7267d48d13db92a61ad061d8c Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Apr 2025 08:42:19 +0300 Subject: [PATCH 101/138] Updated on 2026-08-14 --- .../src/main/res/raw/anim_visa_loading.json | 1 + .../impl/DefaultOnboardingVisaComponent.kt | 8 ++- .../OnboardingVisaInProgressComponent.kt | 10 ++- .../inprogress/ui/OnboardingVisaInProgress.kt | 68 ++++++++++++++++--- .../v2/visa/impl/model/OnboardingVisaModel.kt | 5 +- 5 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 core/ui/src/main/res/raw/anim_visa_loading.json diff --git a/core/ui/src/main/res/raw/anim_visa_loading.json b/core/ui/src/main/res/raw/anim_visa_loading.json new file mode 100644 index 0000000000..1e2b97912b --- /dev/null +++ b/core/ui/src/main/res/raw/anim_visa_loading.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":130,"w":512,"h":512,"nm":"Main","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"02","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":28,"s":[-5]},{"i":{"x":[0.833],"y":[6]},"o":{"x":[0.333],"y":[0]},"t":36,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.111]},"t":49,"s":[0]},{"i":{"x":[0.833],"y":[-4.233]},"o":{"x":[0.333],"y":[0]},"t":75,"s":[-90]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.109]},"t":90,"s":[-90]},{"t":118,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[375,137.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":28,"s":[137,374.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":36,"s":[137,374.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.111},"t":49,"s":[137,374.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":75,"s":[205,300,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.109},"t":90,"s":[205,300,0],"to":[0,0,0],"ti":[0,0,0]},{"t":118,"s":[137,374.875,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-50,-197.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":49,"s":[170,170]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":75,"s":[80,40]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":90,"s":[80,40]},{"t":118,"s":[170,170]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[12]},{"i":{"x":[0.833],"y":[-5.802]},"o":{"x":[0.167],"y":[0]},"t":75,"s":[3]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.109]},"t":90,"s":[3]},{"t":118,"s":[120]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":75,"s":[0.568627476692,0.568627476692,0.568627476692,1]},{"t":90,"s":[0.568627476692,0.568627476692,0.568627476692,1]}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"t":75,"s":[0.568627476692,0.568627476692,0.568627476692,1]}],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":36,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":75,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[100]},{"t":118,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-50,-189.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":130,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"03","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":28,"s":[-5]},{"i":{"x":[0.833],"y":[6]},"o":{"x":[0.333],"y":[0]},"t":36,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.111]},"t":49,"s":[0]},{"i":{"x":[0.833],"y":[-4.233]},"o":{"x":[0.333],"y":[0]},"t":75,"s":[-90]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.109]},"t":90,"s":[-90]},{"t":118,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[137,374.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":28,"s":[375,137.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":36,"s":[375,137.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.111},"t":49,"s":[375,137.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":75,"s":[297,300,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.109},"t":90,"s":[297,300,0],"to":[0,0,0],"ti":[0,0,0]},{"t":118,"s":[375,137.5,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-203,39.75,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":49,"s":[170,170]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":75,"s":[80,40]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":90,"s":[80,40]},{"t":118,"s":[170,170]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[120]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":75,"s":[3]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[3]},{"t":118,"s":[12]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0.56862745098,0.56862745098,0.56862745098,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[0.56862745098,0.56862745098,0.56862745098,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[0.56862745098,0.56862745098,0.56862745098,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":75,"s":[0.56862745098,0.56862745098,0.56862745098,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[0.56862745098,0.56862745098,0.56862745098,1]},{"t":118,"s":[0.811764717102,0.811764717102,0.811764717102,1]}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0.56862745098,0.56862745098,0.56862745098,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[0.56862745098,0.56862745098,0.56862745098,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[0.56862745098,0.56862745098,0.56862745098,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":75,"s":[0.56862745098,0.56862745098,0.56862745098,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[0.56862745098,0.56862745098,0.56862745098,1]},{"t":118,"s":[0.56862745098,0.56862745098,0.56862745098,1]}],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":75,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[100]},{"t":118,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-203,39.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":130,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"04","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":28,"s":[-5]},{"i":{"x":[0.833],"y":[6]},"o":{"x":[0.333],"y":[0]},"t":36,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.111]},"t":49,"s":[0]},{"i":{"x":[0.833],"y":[-4.233]},"o":{"x":[0.333],"y":[0]},"t":75,"s":[-90]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.109]},"t":90,"s":[-90]},{"t":118,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[375,374.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":28,"s":[137,137.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":36,"s":[137,137.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.111},"t":49,"s":[137,137.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":75,"s":[256,200,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.109},"t":90,"s":[256,200,0],"to":[0,0,0],"ti":[0,0,0]},{"t":118,"s":[375,374.875,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-119,-120.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":49,"s":[170,170]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":75,"s":[40,128]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":90,"s":[40,128]},{"t":118,"s":[170,170]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[12]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":75,"s":[3]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[3]},{"t":118,"s":[12]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":75,"s":[0.568627476692,0.568627476692,0.568627476692,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[0.568627476692,0.568627476692,0.568627476692,1]},{"t":118,"s":[0.811764717102,0.811764717102,0.811764717102,1]}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":75,"s":[0.56862745098,0.56862745098,0.56862745098,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[0.56862745098,0.56862745098,0.56862745098,1]},{"t":118,"s":[0.811764705882,0.811764705882,0.811764705882,1]}],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[100]},{"t":118,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-119,-120.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"fl","c":{"a":0,"k":[0.811764705882,0.811764705882,0.811764705882,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":36,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":75,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[100]},{"t":118,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":130,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"01","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":28,"s":[-5]},{"i":{"x":[0.833],"y":[6]},"o":{"x":[0.333],"y":[0]},"t":36,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.111]},"t":49,"s":[0]},{"i":{"x":[0.833],"y":[6.233]},"o":{"x":[0.333],"y":[0]},"t":75,"s":[-90]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.109]},"t":90,"s":[-90]},{"t":118,"s":[-180]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":0,"s":[137,137.5,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":0.667},"o":{"x":0.333,"y":0.333},"t":28,"s":[375,374.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":36,"s":[375,374.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.111},"t":49,"s":[375,374.875,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0.333},"t":75,"s":[256,256,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.109},"t":90,"s":[256,256,0],"to":[0,0,0],"ti":[0,0,0]},{"t":118,"s":[137,137.5,0]}],"ix":2,"l":2},"a":{"a":0,"k":[-119,-120.25,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":49,"s":[170,170]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":75,"s":[270,440]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":90,"s":[270,440]},{"t":118,"s":[170,170]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[12]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":75,"s":[48]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[48]},{"t":118,"s":[12]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"t":49,"s":[0.811764705882,0.811764705882,0.811764705882,1]}],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":20,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":75,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[0.811764705882,0.811764705882,0.811764705882,1]},{"t":118,"s":[0.811764705882,0.811764705882,0.811764705882,1]}],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":49,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":75,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":90,"s":[100]},{"t":118,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-119,-120.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":130,"st":0,"ct":1,"bm":0}],"markers":[{"tm":28,"cm":"1","dr":0},{"tm":36,"cm":"2","dr":0},{"tm":49,"cm":"3","dr":0},{"tm":75,"cm":"4","dr":0},{"tm":90,"cm":"5","dr":0},{"tm":118,"cm":"6","dr":0}],"props":{}} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt index 108d8134bc..b3cc93357a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt @@ -103,7 +103,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( config = OnboardingVisaWelcomeComponent.Config.Welcome, params = OnboardingVisaWelcomeComponent.Params( childParams = childParams, - onDone = { model.stackNavigation.push(OnboardingVisaRoute.AccessCode) }, + onDone = { model.stackNavigation.pushNew(OnboardingVisaRoute.AccessCode) }, ), ) is OnboardingVisaRoute.WelcomeBack -> OnboardingVisaWelcomeComponent( @@ -146,6 +146,12 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( appComponentContext = factoryContext, config = OnboardingVisaInProgressComponent.Config( scanResponse = model.currentScanResponse.value, + type = when (route.from) { + OnboardingVisaRoute.InProgress.From.Approve -> + OnboardingVisaInProgressComponent.Config.Type.AfterApprove + OnboardingVisaRoute.InProgress.From.PinCode -> + OnboardingVisaInProgressComponent.Config.Type.AfterPinCode + }, ), params = OnboardingVisaInProgressComponent.Params( childParams = childParams, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/OnboardingVisaInProgressComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/OnboardingVisaInProgressComponent.kt index 6aaf63c0bc..2729fc42db 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/OnboardingVisaInProgressComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/OnboardingVisaInProgressComponent.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.onEach internal class OnboardingVisaInProgressComponent( appComponentContext: AppComponentContext, - config: Config, + private val config: Config, private val params: Params, ) : ComposableContentComponent, AppComponentContext by appComponentContext { @@ -35,12 +35,18 @@ internal class OnboardingVisaInProgressComponent( OnboardingVisaInProgress( modifier = modifier, + type = config.type, ) } data class Config( val scanResponse: ScanResponse, - ) + val type: Type, + ) { + enum class Type { + AfterApprove, AfterPinCode + } + } data class Params( val childParams: DefaultOnboardingVisaComponent.ChildParams, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/ui/OnboardingVisaInProgress.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/ui/OnboardingVisaInProgress.kt index d30e9a0b79..6a65f28e38 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/ui/OnboardingVisaInProgress.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/ui/OnboardingVisaInProgress.kt @@ -1,29 +1,77 @@ package com.tangem.features.onboarding.v2.visa.impl.child.inprogress.ui +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent @Composable -internal fun OnboardingVisaInProgress(modifier: Modifier = Modifier) { - Column( - modifier +internal fun OnboardingVisaInProgress( + type: OnboardingVisaInProgressComponent.Config.Type, + modifier: Modifier = Modifier, +) { + val composition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.anim_visa_loading)) + val progress by animateLottieCompositionAsState( + composition, + iterations = LottieConstants.IterateForever, + ) + + Box( + modifier = modifier .fillMaxSize() .navigationBarsPadding(), + contentAlignment = Alignment.Center, ) { Column( - Modifier - .weight(1f) - .padding(top = 12.dp) - .padding(horizontal = 16.dp), + Modifier.padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { - // TODO + LottieAnimation( + modifier = Modifier.size(64.dp), + composition = composition, + progress = { progress }, + ) + SpacerH(26.dp) + Text( + text = stringResourceSafe(R.string.visa_onboarding_in_progress_title), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + SpacerH12() + Text( + text = when (type) { + OnboardingVisaInProgressComponent.Config.Type.AfterApprove -> + stringResourceSafe(R.string.visa_onboarding_in_progress_description) + OnboardingVisaInProgressComponent.Config.Type.AfterPinCode -> + stringResourceSafe(R.string.visa_onboarding_in_progress_issuer_description) + }, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) } } } @@ -32,6 +80,8 @@ internal fun OnboardingVisaInProgress(modifier: Modifier = Modifier) { @Composable private fun Preview() { TangemThemePreview { - OnboardingVisaInProgress() + OnboardingVisaInProgress( + type = OnboardingVisaInProgressComponent.Config.Type.AfterPinCode, + ) } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt index 5e98a81f2b..f93540c8ee 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/model/OnboardingVisaModel.kt @@ -4,7 +4,6 @@ import androidx.compose.runtime.Stable import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.pop -import com.arkivanov.decompose.router.stack.push import com.arkivanov.decompose.router.stack.pushNew import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -69,7 +68,7 @@ internal class OnboardingVisaModel @Inject constructor( customerWalletAddress = result.customerWalletTargetAddress, request = result.customerWalletDataToSignRequest, ) - stackNavigation.push( + stackNavigation.pushNew( if (foundWalletCardId != null) { OnboardingVisaRoute.TangemWalletApproveOption( foundWalletCardId = foundWalletCardId, @@ -100,7 +99,7 @@ internal class OnboardingVisaModel @Inject constructor( route: OnboardingVisaRoute.ChooseWallet, event: OnboardingVisaChooseWalletComponent.Params.Event, ) { - stackNavigation.push( + stackNavigation.pushNew( when (event) { OnboardingVisaChooseWalletComponent.Params.Event.TangemWallet -> OnboardingVisaRoute.TangemWalletApproveOption( From 790086eeddacafb884f161d2537bc185ca459dc0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Apr 2025 06:28:13 +0000 Subject: [PATCH 102/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 32be17375b..254de504ec 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "release-app_5.22-997" +tangemBlockchainSdk = "develop-1000" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.22-440" +tangemCardSdk = "develop-445" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From d53f4d0346d407aa9454d8ab9f16496473ebddca Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Apr 2025 11:33:22 +0500 Subject: [PATCH 103/138] Updated on 2026-08-14 --- .../block/information/InformationBlock.kt | 6 +- .../features/nft/details/entity/NFTAssetUM.kt | 55 ++-- .../nft/details/model/NFTDetailsModel.kt | 6 +- .../nft/details/ui/NFTDetailsInfoGroup.kt | 281 ++++++++++++++++++ .../features/nft/details/ui/NFTDetailsLogo.kt | 58 ++-- 5 files changed, 354 insertions(+), 52 deletions(-) create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt index 3c5b7a606d..e5c28bf0a6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt @@ -30,7 +30,7 @@ class InformationBlockContentScope(val scope: BoxScope) : BoxScope by scope @Composable fun InformationBlock( - title: @Composable BoxScope.() -> Unit, + title: (@Composable BoxScope.() -> Unit)?, modifier: Modifier = Modifier, contentHorizontalPadding: Dp = TangemTheme.dimens.spacing12, shape: Shape = TangemTheme.shapes.roundedCornersXMedium, @@ -43,7 +43,9 @@ fun InformationBlock( .background(color = TangemTheme.colors.background.action), horizontalAlignment = Alignment.Start, ) { - NetworkTitle(title = title, action = action) + if (title != null) { + NetworkTitle(title = title, action = action) + } if (content != null) { Box( diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt index e5bea7ed65..2b85b58fe8 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/NFTAssetUM.kt @@ -7,29 +7,31 @@ import kotlinx.collections.immutable.ImmutableList data class NFTAssetUM( val name: String, - val salePrice: SalePrice, - val description: String?, - val rarity: Rarity?, - val media: Media?, + val media: Media, + val topInfo: TopInfo, val traits: ImmutableList, val baseInfoItems: ImmutableList, ) { - data class Media( - val mimetype: String?, - val url: String, - ) - data class Rarity( - val rank: String, - val label: String, - ) + @Immutable + sealed class TopInfo { + data object Empty : TopInfo() + data class Content( + val title: TextReference?, + val salePrice: SalePrice, + val description: String?, + val rarity: Rarity, + ) : TopInfo() + } - data class BlockItem( - val title: TextReference, - val titleTextEllipsis: TextEllipsis = TextEllipsis.End, - val value: String, - val valueTextEllipsis: TextEllipsis = TextEllipsis.End, - ) + @Immutable + sealed class Media { + data object Empty : Media() + data class Content( + val mimetype: String?, + val url: String, + ) : Media() + } @Immutable sealed class SalePrice { @@ -37,4 +39,21 @@ data class NFTAssetUM( data object Empty : SalePrice() data class Content(val value: String, val fiatValue: String) : SalePrice() } + + @Immutable + sealed class Rarity { + data object Empty : Rarity() + data class Content( + val rank: String, + val label: String, + val showDivider: Boolean, + ) : Rarity() + } + + data class BlockItem( + val title: TextReference, + val titleTextEllipsis: TextEllipsis = TextEllipsis.End, + val value: String, + val valueTextEllipsis: TextEllipsis = TextEllipsis.End, + ) } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt index 19448754bf..e421efcde8 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt @@ -28,10 +28,8 @@ internal class NFTDetailsModel @Inject constructor( value = NFTDetailsUM( nftAsset = NFTAssetUM( name = "", - salePrice = NFTAssetUM.SalePrice.Empty, - description = null, - rarity = null, - media = null, + media = NFTAssetUM.Media.Empty, + topInfo = NFTAssetUM.TopInfo.Empty, traits = persistentListOf(), baseInfoItems = persistentListOf(), ), diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt new file mode 100644 index 0000000000..974a3329d9 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt @@ -0,0 +1,281 @@ +package com.tangem.features.nft.details.ui + +import android.content.res.Configuration +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.block.information.InformationBlock +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.details.entity.NFTAssetUM +import com.tangem.features.nft.impl.R + +@Composable +internal fun NFTDetailsInfoGroup( + state: NFTAssetUM.TopInfo, + onReadMoreClick: () -> Unit, + modifier: Modifier = Modifier, +) { + when (state) { + is NFTAssetUM.TopInfo.Empty -> Unit + is NFTAssetUM.TopInfo.Content -> { + InformationBlock( + modifier = modifier, + title = if (state.title != null) { + { NFTDetailsGroupTitle(state.title) } + } else { + null + }, + contentHorizontalPadding = 0.dp, + ) { + Column( + modifier = Modifier, + ) { + SalePrice(state.salePrice) + Description(state.description, onReadMoreClick) + Rarity(state.rarity) + } + } + } + } +} + +@Composable +private fun SalePrice(state: NFTAssetUM.SalePrice, modifier: Modifier = Modifier) { + AnimatedVisibility( + modifier = Modifier.fillMaxWidth().animateContentSize(), + visible = state !is NFTAssetUM.SalePrice.Empty, + enter = fadeIn(), + exit = fadeOut(), + ) { + when (state) { + is NFTAssetUM.SalePrice.Empty -> Unit + is NFTAssetUM.SalePrice.Loading -> { + Column( + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + ) { + TextShimmer( + modifier = Modifier + .width(TangemTheme.dimens.size158), + style = TangemTheme.typography.head, + textSizeHeight = true, + ) + TextShimmer( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4) + .width(TangemTheme.dimens.size90), + style = TangemTheme.typography.caption2, + textSizeHeight = true, + ) + } + } + is NFTAssetUM.SalePrice.Content -> { + Column( + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + ), + verticalArrangement = Arrangement.SpaceAround, + ) { + Text( + text = state.value, + style = TangemTheme.typography.head, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4), + text = state.fiatValue, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } + } + } +} + +@Composable +private fun Description(description: String?, onReadMoreClick: () -> Unit, modifier: Modifier = Modifier) { + if (!description.isNullOrEmpty()) { + // TODO configure right way to display 3 rows and "Read more" label + EllipsisText( + modifier = modifier + .clickable { onReadMoreClick() } + .padding( + horizontal = TangemTheme.dimens.spacing12, + vertical = TangemTheme.dimens.spacing8, + ), + text = description, + style = TangemTheme.typography.body2.copy( + color = TangemTheme.colors.text.tertiary, + ), + ) + } +} + +@Composable +private fun Rarity(state: NFTAssetUM.Rarity, modifier: Modifier = Modifier) { + when (state) { + is NFTAssetUM.Rarity.Empty -> Unit + is NFTAssetUM.Rarity.Content -> { + if (state.showDivider) { + Divider( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), + ) + } + Row( + modifier = modifier + .fillMaxWidth() + .padding( + horizontal = TangemTheme.dimens.spacing6, + vertical = TangemTheme.dimens.spacing8, + ), + ) { + NFTDetailsGroupBlock( + modifier = Modifier + .weight(1f) + .padding( + horizontal = TangemTheme.dimens.spacing6, + vertical = TangemTheme.dimens.spacing4, + ), + title = resourceReference(R.string.nft_details_rarity_label), + value = stringReference(state.label), + ) + NFTDetailsGroupBlock( + modifier = Modifier + .weight(1f) + .padding( + horizontal = TangemTheme.dimens.spacing6, + vertical = TangemTheme.dimens.spacing4, + ), + title = resourceReference(R.string.nft_details_rarity_rank), + value = stringReference(state.rank), + ) + } + } + } +} + +@Composable +private fun Divider(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size8), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size1) + .background(TangemTheme.colors.stroke.primary), + ) + } +} + +@Preview(widthDp = 360) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTDetailsInfoBlock(@PreviewParameter(NFTAssetInfoProvider::class) state: NFTAssetUM.TopInfo) { + TangemThemePreview { + NFTDetailsInfoGroup( + state = state, + onReadMoreClick = { }, + ) + } +} + +@Suppress("LongMethod", "MagicNumber") +private class NFTAssetInfoProvider : CollectionPreviewParameterProvider( + collection = listOf( + NFTAssetUM.TopInfo.Content( + title = resourceReference(R.string.nft_details_last_sale_price), + salePrice = NFTAssetUM.SalePrice.Content(value = "0,012 ETH", fiatValue = "32.34$"), + description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3), + rarity = NFTAssetUM.Rarity.Content( + rank = "Top 1% rarity", + label = "115.28", + showDivider = true, + ), + ), + NFTAssetUM.TopInfo.Content( + title = resourceReference(R.string.nft_details_last_sale_price), + salePrice = NFTAssetUM.SalePrice.Loading, + description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3), + rarity = NFTAssetUM.Rarity.Content( + rank = "Top 1% rarity", + label = "115.28", + showDivider = true, + ), + ), + NFTAssetUM.TopInfo.Content( + title = resourceReference(R.string.nft_details_last_sale_price), + salePrice = NFTAssetUM.SalePrice.Content(value = "0,012 ETH", fiatValue = "32.34$"), + description = null, + rarity = NFTAssetUM.Rarity.Content( + rank = "Top 1% rarity", + label = "115.28", + showDivider = true, + ), + ), + NFTAssetUM.TopInfo.Content( + title = resourceReference(R.string.nft_details_last_sale_price), + salePrice = NFTAssetUM.SalePrice.Content(value = "0,012 ETH", fiatValue = "32.34$"), + description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3), + rarity = NFTAssetUM.Rarity.Empty, + ), + NFTAssetUM.TopInfo.Content( + title = resourceReference(R.string.nft_details_last_sale_price), + salePrice = NFTAssetUM.SalePrice.Content(value = "0,012 ETH", fiatValue = "32.34$"), + description = null, + rarity = NFTAssetUM.Rarity.Empty, + ), + NFTAssetUM.TopInfo.Content( + title = null, + salePrice = NFTAssetUM.SalePrice.Empty, + description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3), + rarity = NFTAssetUM.Rarity.Content( + rank = "Top 1% rarity", + label = "115.28", + showDivider = true, + ), + ), + NFTAssetUM.TopInfo.Content( + title = null, + salePrice = NFTAssetUM.SalePrice.Empty, + description = null, + rarity = NFTAssetUM.Rarity.Content( + rank = "Top 1% rarity", + label = "115.28", + showDivider = false, + ), + ), + NFTAssetUM.TopInfo.Content( + title = null, + salePrice = NFTAssetUM.SalePrice.Empty, + description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3), + rarity = NFTAssetUM.Rarity.Empty, + ), + ), +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsLogo.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsLogo.kt index 6c49a7f86e..3111614efd 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsLogo.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsLogo.kt @@ -26,30 +26,32 @@ import com.tangem.features.nft.details.entity.NFTAssetUM import com.tangem.features.nft.impl.R @Composable -internal fun NFTDetailsLogo(state: NFTAssetUM.Media?, modifier: Modifier = Modifier) { - if (state == null) { - Placeholder( - modifier = modifier - .fillMaxWidth(), - ) - } else { - SubcomposeAsyncImage( - modifier = modifier - .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium), - model = ImageRequest.Builder(LocalContext.current) - .data(state.url) - .crossfade(true) - .build(), - loading = { - RectangleShimmer(radius = TangemTheme.dimens.radius16) - }, - error = { - Placeholder() - }, - contentScale = ContentScale.Crop, - contentDescription = null, - ) +internal fun NFTDetailsLogo(state: NFTAssetUM.Media, modifier: Modifier = Modifier) { + when (state) { + is NFTAssetUM.Media.Empty -> { + Placeholder( + modifier = modifier.fillMaxWidth(), + ) + } + is NFTAssetUM.Media.Content -> { + SubcomposeAsyncImage( + modifier = modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium), + model = ImageRequest.Builder(LocalContext.current) + .data(state.url) + .crossfade(true) + .build(), + loading = { + RectangleShimmer(radius = TangemTheme.dimens.radius16) + }, + error = { + Placeholder() + }, + contentScale = ContentScale.Crop, + contentDescription = null, + ) + } } } @@ -72,7 +74,7 @@ private fun Placeholder(modifier: Modifier = Modifier) { @Preview(widthDp = 360) @Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_NFTDetailsInfoBlock(@PreviewParameter(NFTAssetMediaProvider::class) state: NFTAssetUM.Media?) { +private fun Preview_NFTDetailsInfoBlock(@PreviewParameter(NFTAssetMediaProvider::class) state: NFTAssetUM.Media) { TangemThemePreview { NFTDetailsLogo( state = state, @@ -82,10 +84,10 @@ private fun Preview_NFTDetailsInfoBlock(@PreviewParameter(NFTAssetMediaProvider: } } -private class NFTAssetMediaProvider : CollectionPreviewParameterProvider( +private class NFTAssetMediaProvider : CollectionPreviewParameterProvider( collection = listOf( - null, - NFTAssetUM.Media( + NFTAssetUM.Media.Empty, + NFTAssetUM.Media.Content( mimetype = "image/jpeg", url = "img1", ), From d09f2d3bc5229500bb5c353ff608180b9d1c533b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Apr 2025 13:21:10 +0500 Subject: [PATCH 104/138] Updated on 2026-08-14 --- .../features/nft/details/ui/NFTDetails.kt | 47 ++++- .../nft/details/ui/NFTDetailsAsset.kt | 178 ++++++++++++++++++ .../nft/details/ui/NFTDetailsInfoGroup.kt | 27 +-- 3 files changed, 230 insertions(+), 22 deletions(-) create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt index b40e1ab39c..31af37f744 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt @@ -1,12 +1,57 @@ package com.tangem.features.nft.details.ui import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme import com.tangem.features.nft.details.entity.NFTDetailsUM +import com.tangem.features.nft.impl.R -@Suppress("UnusedPrivateMember") @Composable internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) { BackHandler(onBack = state.onBackClick) + + Scaffold( + modifier = modifier, + containerColor = TangemTheme.colors.background.secondary, + topBar = { + TangemTopAppBar( + modifier = Modifier.statusBarsPadding(), + startButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_back_24, + onIconClicked = state.onBackClick, + ), + title = state.nftAsset.name, + ) + }, + content = { innerPadding -> + NFTDetailsAsset( + state = state.nftAsset, + onReadMoreClick = state.onReadMoreClick, + onSeeAllClick = state.onSeeAllClick, + onExploreClick = state.onExploreClick, + modifier = Modifier + .padding(innerPadding), + ) + }, + floatingActionButtonPosition = FabPosition.Center, + floatingActionButton = { + PrimaryButton( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + text = stringResourceSafe(id = R.string.common_send), + onClick = state.onSendClick, + ) + }, + ) } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt new file mode 100644 index 0000000000..b3806888f4 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsAsset.kt @@ -0,0 +1,178 @@ +package com.tangem.features.nft.details.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.components.bottomFade +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.details.entity.NFTAssetUM +import com.tangem.features.nft.impl.R +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun NFTDetailsAsset( + state: NFTAssetUM, + onReadMoreClick: () -> Unit, + onSeeAllClick: () -> Unit, + onExploreClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val scrollState = rememberScrollState() + + val bottomBarHeight = with(LocalDensity.current) { + WindowInsets.systemBars.getBottom(density = this).toDp() + } + + val bottomPadding = TangemTheme.dimens.spacing76 + bottomBarHeight + + Box( + modifier = Modifier + .fillMaxSize() + .bottomFade(), + ) { + Column( + modifier = modifier + .verticalScroll(scrollState) + .padding( + start = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = bottomPadding, + ) + .fillMaxSize(), + ) { + NFTDetailsLogo( + state = state.media, + modifier = Modifier + .aspectRatio(1f), + ) + NFTDetailsInfoGroup( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing12), + state = state.topInfo, + onReadMoreClick = onReadMoreClick, + ) + NFTDetailsBlocksGroup( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing12), + items = state.traits, + title = resourceReference(R.string.nft_details_traits), + action = { + NFTBlocksGroupAction( + text = resourceReference(R.string.common_see_all), + startIcon = { }, + onClick = onSeeAllClick, + ) + }, + ) + NFTDetailsBlocksGroup( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing12), + items = state.baseInfoItems, + title = resourceReference(R.string.nft_details_base_information), + action = { + NFTBlocksGroupAction( + text = resourceReference(R.string.common_explore), + startIcon = { + NFTBlocksGroupActionIcon(iconRes = R.drawable.ic_compass_24) + }, + onClick = onExploreClick, + ) + }, + ) + } + } +} + +@Preview(widthDp = 360, heightDp = 1000, showBackground = true) +@Preview(widthDp = 360, heightDp = 1000, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTDetailsAssetAsset(@PreviewParameter(NFTAssetProvider::class) state: NFTAssetUM) { + TangemThemePreview { + NFTDetailsAsset( + state = state, + onReadMoreClick = { }, + onSeeAllClick = { }, + onExploreClick = { }, + ) + } +} + +@Suppress("MagicNumber") +private class NFTAssetProvider : CollectionPreviewParameterProvider( + collection = listOf( + NFTAssetUM( + name = "Nethers #9744", + media = NFTAssetUM.Media.Content( + url = "img1", + mimetype = "image/jpeg", + ), + topInfo = NFTAssetUM.TopInfo.Content( + title = resourceReference(R.string.nft_details_last_sale_price), + salePrice = NFTAssetUM.SalePrice.Content(value = "0,012 ETH", fiatValue = "32.34$"), + description = "Base edition by Piux. An illustration of Crypto Robot #7804".repeat(3), + rarity = NFTAssetUM.Rarity.Content( + rank = "Top 1% rarity", + label = "115.28", + showDivider = true, + ), + ), + traits = persistentListOf( + NFTAssetUM.BlockItem( + title = stringReference("Tier"), + value = "Infinite", + ), + NFTAssetUM.BlockItem( + title = stringReference("Phygital Toy"), + value = "None", + ), + NFTAssetUM.BlockItem( + title = stringReference("Class"), + value = "CYBER", + ), + NFTAssetUM.BlockItem( + title = stringReference("Accessory"), + value = "No accessory", + ), + NFTAssetUM.BlockItem( + title = stringReference("Sneakers"), + value = "Boots", + ), + NFTAssetUM.BlockItem( + title = stringReference("Artist"), + value = "DJ Dragoon", + ), + ), + baseInfoItems = persistentListOf( + NFTAssetUM.BlockItem( + title = resourceReference(R.string.nft_details_token_standard), + value = "ERC-721", + ), + NFTAssetUM.BlockItem( + title = resourceReference(R.string.nft_details_contract_address), + value = "0x6811f2fgd892ac83f719", + valueTextEllipsis = TextEllipsis.Middle, + ), + NFTAssetUM.BlockItem( + title = resourceReference(R.string.nft_details_token_id), + value = "100200273", + ), + NFTAssetUM.BlockItem( + title = resourceReference(R.string.nft_details_chain), + value = "Ethereum", + ), + ), + ), + ), +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt index 974a3329d9..aa34278977 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsInfoGroup.kt @@ -2,12 +2,11 @@ package com.tangem.features.nft.details.ui import android.content.res.Configuration import androidx.compose.animation.* -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -140,8 +139,11 @@ private fun Rarity(state: NFTAssetUM.Rarity, modifier: Modifier = Modifier) { is NFTAssetUM.Rarity.Empty -> Unit is NFTAssetUM.Rarity.Content -> { if (state.showDivider) { - Divider( - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), + HorizontalDivider( + modifier = Modifier.padding( + horizontal = TangemTheme.dimens.spacing12, + ), + color = TangemTheme.colors.stroke.primary, ) } Row( @@ -177,23 +179,6 @@ private fun Rarity(state: NFTAssetUM.Rarity, modifier: Modifier = Modifier) { } } -@Composable -private fun Divider(modifier: Modifier = Modifier) { - Box( - modifier = modifier - .fillMaxWidth() - .height(TangemTheme.dimens.size8), - contentAlignment = Alignment.Center, - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(TangemTheme.dimens.size1) - .background(TangemTheme.colors.stroke.primary), - ) - } -} - @Preview(widthDp = 360) @Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable From bedb9de62f8e31e65ec80eae7574497774cafcc7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Apr 2025 12:08:59 +0300 Subject: [PATCH 105/138] Updated on 2026-08-14 --- .../data/visa/utils/VisaTxDetailsFactory.kt | 2 - .../tangem/domain/visa/model/VisaTxDetails.kt | 2 - .../model/VisaTxDetailsBottomSheetConfig.kt | 2 - .../VisaTxDetailsBottomSheetConverter.kt | 2 - .../visa/BalancesAndLimitsBottomSheet.kt | 27 ++++----- .../visa/VisaTxDetailsBottomSheet.kt | 58 ++++++++----------- 6 files changed, 38 insertions(+), 55 deletions(-) diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt index c69adf5664..8f9b41148f 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt @@ -13,7 +13,6 @@ internal class VisaTxDetailsFactory { type = transaction.transactionType, status = transaction.transactionStatus, blockchainAmount = transaction.blockchainAmount, - blockchainFee = transaction.blockchainFee, transactionAmount = transaction.transactionAmount, transactionCurrencyCode = transaction.transactionCurrencyCode, merchantName = transaction.merchantName, @@ -33,7 +32,6 @@ internal class VisaTxDetailsFactory { billingAmount = request.billingAmount, billingCurrencyCode = request.billingCurrencyCode, blockchainAmount = request.blockchainAmount, - blockchainFee = request.blockchainFee, errorCode = request.errorCode, requestDate = request.requestDt, requestStatus = request.requestStatus, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaTxDetails.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaTxDetails.kt index 1e833b7c16..9664728814 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaTxDetails.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaTxDetails.kt @@ -9,7 +9,6 @@ data class VisaTxDetails( val type: String, val status: String, val blockchainAmount: BigDecimal, - val blockchainFee: BigDecimal, val transactionAmount: BigDecimal, val transactionCurrencyCode: Int, val merchantName: String?, @@ -25,7 +24,6 @@ data class VisaTxDetails( val billingAmount: BigDecimal, val billingCurrencyCode: Int, val blockchainAmount: BigDecimal, - val blockchainFee: BigDecimal, val errorCode: Int, val requestDate: DateTime, val requestStatus: String, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/VisaTxDetailsBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/VisaTxDetailsBottomSheetConfig.kt index 6389eb3a0f..bf64d76838 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/VisaTxDetailsBottomSheetConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/VisaTxDetailsBottomSheetConfig.kt @@ -13,7 +13,6 @@ internal data class VisaTxDetailsBottomSheetConfig( val type: String, val status: String, val blockchainAmount: String, - val blockchainFee: String, val transactionAmount: String, val transactionCurrencyCode: String, val merchantName: String, @@ -27,7 +26,6 @@ internal data class VisaTxDetailsBottomSheetConfig( val type: String, val status: String, val blockchainAmount: String, - val blockchainFee: String, val transactionAmount: String, val currencyCode: String, val errorCode: Int, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt index dee6043f7c..ba80bbf6a3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt @@ -33,7 +33,6 @@ internal class VisaTxDetailsBottomSheetConverter( type = details.type.capitalize(), status = details.status.capitalize(), blockchainAmount = formatNetworkAmount(details.blockchainAmount), - blockchainFee = formatNetworkAmount(details.blockchainFee), transactionAmount = formatFiatAmount(details.transactionAmount, details.fiatCurrency), transactionCurrencyCode = details.transactionCurrencyCode.toString(), merchantName = details.merchantName?.capitalize() ?: UNKNOWN, @@ -52,7 +51,6 @@ internal class VisaTxDetailsBottomSheetConverter( type = request.requestType.capitalize(), status = request.requestStatus.capitalize(), blockchainAmount = formatNetworkAmount(request.blockchainAmount), - blockchainFee = formatNetworkAmount(request.blockchainFee), transactionAmount = formatFiatAmount(request.transactionAmount, request.fiatCurrency), currencyCode = request.billingCurrencyCode.toString(), errorCode = request.errorCode, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt index 426ce24da0..24b21a963a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt @@ -16,7 +16,8 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.impl.R @@ -38,7 +39,7 @@ private fun BalancesAndLimitsContent(config: BalancesAndLimitsBottomSheetConfig, modifier = modifier, title = { Text( - text = "Balances & Limits", + text = stringResourceSafe(R.string.visa_main_balances_and_limits), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, ) @@ -56,26 +57,26 @@ private fun BalancesAndLimitsContent(config: BalancesAndLimitsBottomSheetConfig, private fun BalancesBlock(balances: BalancesAndLimitsBottomSheetConfig.Balance, modifier: Modifier = Modifier) { BlockContent( modifier = modifier, - title = stringReference("Balance"), + title = resourceReference(R.string.common_balance), content = { BlockItem( - title = stringReference("Total"), + title = resourceReference(R.string.visa_balance_limits_details_total), value = balances.totalBalance, ) BlockItem( - title = stringReference("AML Verified"), + title = resourceReference(R.string.visa_balance_limits_details_aml_verified), value = balances.amlVerified, ) BlockItem( - title = stringReference("Available"), + title = resourceReference(R.string.visa_balance_limits_details_available), value = balances.availableBalance, ) BlockItem( - title = stringReference("Blocked"), + title = resourceReference(R.string.visa_balance_limits_details_blocked), value = balances.blockedBalance, ) BlockItem( - title = stringReference("Debit"), + title = resourceReference(R.string.visa_balance_limits_details_debt), value = balances.debit, ) }, @@ -89,24 +90,24 @@ private fun BalancesBlock(balances: BalancesAndLimitsBottomSheetConfig.Balance, private fun LimitsBlock(limits: BalancesAndLimitsBottomSheetConfig.Limit, modifier: Modifier = Modifier) { BlockContent( modifier = modifier, - title = stringReference("Limits"), + title = resourceReference(R.string.visa_balance_limits_details_limits), content = { BlockItem( - title = stringReference("Total"), + title = resourceReference(R.string.visa_balance_limits_details_total), value = limits.total, ) BlockItem( - title = stringReference("Other (no-otp)"), + title = resourceReference(R.string.visa_balance_limits_details_no_otp_limit), value = limits.other, ) BlockItem( - title = stringReference("Single transaction"), + title = resourceReference(R.string.visa_balance_limits_details_single_transaction), value = limits.singleTransaction, ) }, description = { Text( - text = "Available till ${limits.availableBy}", + text = stringResourceSafe(R.string.visa_main_available_till_date, limits.availableBy), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt index 228fef3f7d..2f1df0fb3e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaTxDetailsBottomSheet.kt @@ -18,7 +18,8 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.components.SpacerW12 import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.impl.R @@ -42,7 +43,7 @@ private fun VisaTxDetailsBottomSheetContent(config: VisaTxDetailsBottomSheetConf blocksCount = config.requests.size.inc(), title = { Text( - text = "Transaction Details", + text = stringResourceSafe(R.string.visa_transaction_details_header), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, ) @@ -61,46 +62,42 @@ private fun VisaTxDetailsBottomSheetContent(config: VisaTxDetailsBottomSheetConf private fun TransactionBlock(transaction: VisaTxDetailsBottomSheetConfig.Transaction, modifier: Modifier = Modifier) { BlockContent( modifier = modifier, - title = stringReference(value = "Transaction"), + title = resourceReference(R.string.visa_transaction_details_title), content = { BlockItem( - title = stringReference(value = "Type"), + title = resourceReference(R.string.visa_transaction_details_type), value = transaction.type, ) BlockItem( - title = stringReference(value = "Status"), + title = resourceReference(R.string.visa_transaction_details_status), value = transaction.status, ) BlockItem( - title = stringReference(value = "Blockchain Amount"), + title = resourceReference(R.string.visa_transaction_details_blockchain_amount), value = transaction.blockchainAmount, ) BlockItem( - title = stringReference(value = "Blockchain Fee"), - value = transaction.blockchainFee, - ) - BlockItem( - title = stringReference(value = "Transaction Amount"), + title = resourceReference(R.string.visa_transaction_details_transaction_amount), value = transaction.transactionAmount, ) BlockItem( - title = stringReference(value = "Currency Code"), + title = resourceReference(R.string.visa_transaction_details_currency_code), value = transaction.transactionCurrencyCode, ) BlockItem( - title = stringReference(value = "Merchant Name"), + title = resourceReference(R.string.visa_transaction_details_merchant_name), value = transaction.merchantName, ) BlockItem( - title = stringReference(value = "Merchant City"), + title = resourceReference(R.string.visa_transaction_details_merchant_city), value = transaction.merchantCity, ) BlockItem( - title = stringReference(value = "Merchant Country Code"), + title = resourceReference(R.string.visa_transaction_details_merchant_country_code), value = transaction.merchantCountryCode, ) BlockItem( - title = stringReference(value = "Merchant Category Code"), + title = resourceReference(R.string.visa_transaction_details_merchant_category_code), value = transaction.merchantCategoryCode, ) }, @@ -112,7 +109,7 @@ private fun TransactionBlock(transaction: VisaTxDetailsBottomSheetConfig.Transac private fun BlockchainRequestBlock(request: VisaTxDetailsBottomSheetConfig.Request, modifier: Modifier = Modifier) { BlockContent( modifier = modifier, - title = stringReference(value = "Blockchain request"), + title = resourceReference(R.string.visa_transaction_details_transaction_request), description = { if (request.onExploreClick != null) { Row( @@ -127,7 +124,7 @@ private fun BlockchainRequestBlock(request: VisaTxDetailsBottomSheetConfig.Reque tint = TangemTheme.colors.icon.informative, ) Text( - text = "Explore", + text = stringResourceSafe(R.string.common_explore), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.caption1, ) @@ -137,43 +134,39 @@ private fun BlockchainRequestBlock(request: VisaTxDetailsBottomSheetConfig.Reque }, content = { BlockItem( - title = stringReference(value = "Type"), + title = resourceReference(R.string.visa_transaction_details_type), value = request.type, ) BlockItem( - title = stringReference(value = "Status"), + title = resourceReference(R.string.visa_transaction_details_status), value = request.status, ) BlockItem( - title = stringReference(value = "Blockchain Amount"), + title = resourceReference(R.string.visa_transaction_details_blockchain_amount), value = request.blockchainAmount, ) BlockItem( - title = stringReference(value = "Blockchain Fee"), - value = request.blockchainFee, - ) - BlockItem( - title = stringReference(value = "Transaction Amount"), + title = resourceReference(R.string.visa_transaction_details_transaction_amount), value = request.transactionAmount, ) BlockItem( - title = stringReference(value = "Currency Code"), + title = resourceReference(R.string.visa_transaction_details_currency_code), value = request.currencyCode, ) BlockItem( - title = stringReference(value = "Error Code"), + title = resourceReference(R.string.visa_transaction_details_error_code), value = request.errorCode.toString(), ) BlockItem( - title = stringReference(value = "Date"), + title = resourceReference(R.string.visa_transaction_details_date), value = request.date, ) BlockItem( - title = stringReference(value = "Tx Hash"), + title = resourceReference(R.string.visa_transaction_details_transaction_hash), value = request.txHash, ) BlockItem( - title = stringReference(value = "Tx Status"), + title = resourceReference(R.string.visa_transaction_details_transaction_status), value = request.txStatus, ) }, @@ -235,7 +228,6 @@ private class VisaTxDetailsBottomSheetParameterProvider : type = "payment", status = "authorized", blockchainAmount = "1.0614 USDT", - blockchainFee = "0.12", transactionAmount = "0.99 €", transactionCurrencyCode = "978", merchantName = "SQ *FORMATIVE", @@ -249,7 +241,6 @@ private class VisaTxDetailsBottomSheetParameterProvider : type = "authorize_payment", status = "accepted", blockchainAmount = "1.0593 USDT", - blockchainFee = "0.10", transactionAmount = "0.99 €", currencyCode = "978", errorCode = 0, @@ -263,7 +254,6 @@ private class VisaTxDetailsBottomSheetParameterProvider : type = "settlement", status = "accepted", blockchainAmount = "1.0614 USDT", - blockchainFee = "0.12", transactionAmount = "0.99 €", currencyCode = "978", errorCode = 0, From 06ace407206b8982a3b4ff693910f485933adaac Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Apr 2025 18:50:11 +0300 Subject: [PATCH 106/138] Updated on 2026-08-14 --- .../feature/swap/domain/models/ui/SwapState.kt | 4 ++++ .../tangem/feature/swap/domain/SwapInteractorImpl.kt | 12 +++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 98c167b26c..a4336b8eae 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -137,6 +137,10 @@ data class TxFee( val gasBudget: Long, val gasPrice: Long, ) : Params() + + data class Hedera( + val additionalHBARFee: BigDecimal, + ) : Params() } } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 08e7d43af8..29e7ee6c0d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -849,6 +849,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( gasBudget = feeParams.gasBudget, ) } + Hedera, + HederaTestnet, + -> { + val feeParams = requireNotNull(fee.params as? TxFee.Params.Hedera) + Fee.Hedera(amount = feeAmount, additionalHBARFee = feeParams.additionalHBARFee) + } // endregion // region Blockchains with common fees or EVM-like fees Unknown, @@ -929,8 +935,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( ShibariumTestnet, Algorand, AlgorandTestnet, - Hedera, - HederaTestnet, Aurora, AuroraTestnet, Areon, @@ -1900,6 +1904,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( gasPrice = fee.gasPrice, gasBudget = fee.gasBudget, ) + is Fee.Hedera -> TxFee.Params.Hedera( + additionalHBARFee = fee.additionalHBARFee, + ) is Fee.Aptos, is Fee.Bitcoin, is Fee.CardanoToken, @@ -1910,7 +1917,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( is Fee.Tron, is Fee.VeChain, is Fee.Alephium, - is Fee.Hedera, -> null } From f760fc3a120c7cc5549362b0e2ca698f90e71378 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Apr 2025 14:27:22 +0500 Subject: [PATCH 107/138] Updated on 2026-08-14 --- .../portfolio/impl/model/TokenActionsHandler.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index 61cae107f4..02e1ba82e2 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -1,13 +1,14 @@ package com.tangem.features.markets.portfolio.impl.model import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig +import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.share.ShareManager import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig -import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.SnackbarMessage @@ -41,6 +42,7 @@ internal class TokenActionsHandler @AssistedInject constructor( private val isDemoCardUseCase: IsDemoCardUseCase, private val messageSender: UiMessageSender, private val onrampFeatureToggles: OnrampFeatureToggles, + private val shareManager: ShareManager, ) { private val disabledActionsInDemoMode = buildSet { @@ -109,8 +111,8 @@ internal class TokenActionsHandler @AssistedInject constructor( network = currency.network, networkAddress = networkAddress, showMemoDisclaimer = currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, - onCopyClick = {}, - onShareClick = {}, + onCopyClick = { clipboardManager.setText(networkAddress.defaultAddress.value, isSensitive = true) }, + onShareClick = { shareManager.shareText(networkAddress.defaultAddress.value) }, ), ) } From d5fdc0d921d2238b73c8a21184ef50ec043e7008 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Apr 2025 18:26:18 +0300 Subject: [PATCH 108/138] Updated on 2026-08-14 --- .../InvalidatePendingTransactionsUseCase.kt | 18 +++++++++++++----- .../impl/presentation/model/StakingModel.kt | 1 + 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt index 45892df5b3..2d48d88f25 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Either import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.Token import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.repositories.StakingErrorResolver @@ -18,11 +19,13 @@ class InvalidatePendingTransactionsUseCase( operator fun invoke( balanceItems: List, processingActions: List, + token: Token, ): Either> { return Either.catch { val balancesToDisplay = mergeBalancesAndProcessingActions( realBalances = balanceItems, processingActions = processingActions, + token = token, ) balancesToDisplay }.mapLeft { @@ -33,16 +36,17 @@ class InvalidatePendingTransactionsUseCase( private fun mergeBalancesAndProcessingActions( realBalances: List, processingActions: List, + token: Token, ): List { val balances = realBalances.toMutableList() processingActions.forEach { action -> when (action.type) { StakingActionType.STAKE, StakingActionType.VOTE -> { - addStubStakedPendingTransaction(balances, action) + addStubStakedPendingTransaction(balances, action, token) } StakingActionType.VOTE_LOCKED -> { - addStubStakedPendingTransaction(balances, action) + addStubStakedPendingTransaction(balances, action, token) removeLockedBalance(balances, action) } StakingActionType.WITHDRAW -> { @@ -77,15 +81,19 @@ class InvalidatePendingTransactionsUseCase( } } - private fun addStubStakedPendingTransaction(balances: MutableList, action: StakingAction) { + private fun addStubStakedPendingTransaction( + balances: MutableList, + action: StakingAction, + token: Token, + ) { balances.add( BalanceItem( groupId = UUID.randomUUID().toString(), - token = balances[0].token, + token = token, type = BalanceType.STAKED, amount = action.amount, rawCurrencyId = null, - validatorAddress = action.validatorAddress ?: action.validatorAddresses?.get(0) ?: "", + validatorAddress = action.validatorAddress ?: action.validatorAddresses?.getOrNull(0) ?: "", date = null, pendingActions = emptyList(), pendingActionsConstraints = emptyList(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 9a9798fba7..63648d1762 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -158,6 +158,7 @@ internal class StakingModel @Inject constructor( return invalidatePendingTransactionsUseCase( balanceItems = yieldBalance?.balance?.items ?: emptyList(), processingActions = processingActions, + token = yield.token, ).getOrElse { emptyList() } } From b73b6c3fe70998cd257bda38f41d51f2f215942c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Apr 2025 16:04:08 +0700 Subject: [PATCH 109/138] Updated on 2026-08-14 --- .../initialize/DefaultWcInitializeUseCase.kt | 1 + .../sessions/DefaultWcSessionsManager.kt | 93 +++++++++++++++---- .../data/walletconnect/utils/WcSdkObserver.kt | 2 + .../repository/WcSessionsManager.kt | 3 +- .../usecase/WcSessionsUseCase.kt | 17 ++++ .../usecase/disconnect/WcDisconnectUseCase.kt | 30 ++++++ 6 files changed, 128 insertions(+), 18 deletions(-) create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSessionsUseCase.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt index 5889dbf628..6330859a84 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt @@ -55,6 +55,7 @@ internal class DefaultWcInitializeUseCase( onSuccess = { val walletDelegate = defineWalletDelegate() WalletKit.setWalletDelegate(walletDelegate) + wcSdkObservers.forEach { it.onWcSdkInit() } }, onError = { error -> Timber.e("Error while initializing Web3Wallet: $error") diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index 2807f9d74c..90ccec710d 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -1,5 +1,8 @@ package com.tangem.data.walletconnect.sessions +import arrow.core.Either +import arrow.core.left +import arrow.core.right import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.data.walletconnect.utils.WcSdkObserver @@ -13,10 +16,14 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout import timber.log.Timber +import kotlin.coroutines.resume +import kotlin.time.Duration.Companion.seconds internal class DefaultWcSessionsManager constructor( private val store: WalletConnectStore, @@ -26,19 +33,50 @@ internal class DefaultWcSessionsManager constructor( private val scope: CoroutineScope, ) : WcSessionsManager, WcSdkObserver { + private val onSessionDelete = Channel(capacity = Channel.BUFFERED) + private val oneTimeMigration = MutableStateFlow(true) + override val sessions: Flow>> get() = store.sessions - .onEach(::migrateLegacyStore) - .map(::associateWithSdk) - .distinctUntilChanged() + .transform { inStore -> + if (oneTimeMigration.value) { + oneTimeMigration.value = false + val someMigrated = migrateLegacyStore(inStore) + if (someMigrated) return@transform // ignore emit, wait next one + } + val inSdk: List = WalletKit.getListOfActiveSessions() + val associatedSessions: List = associateWithSdk(inSdk, inStore) + val someRemove = removeUnknownSessions(inStore, associatedSessions) + if (someRemove) return@transform // ignore emit, wait next one + emit(associatedSessions.groupBy { it.userWalletId }) + } .flowOn(dispatchers.io) + override fun onWcSdkInit() { + oneTimeMigration.value = true + listenOnSessionDelete() + } + override suspend fun saveSession(userWalletId: UserWalletId, session: WcSession) { store.saveSession(WcSessionDTO(session.sdkModel.topic, session.userWalletId)) } - override suspend fun removeSession(userWalletId: UserWalletId, session: WcSession) { - store.removeSession(WcSessionDTO(session.sdkModel.topic, session.userWalletId)) + override suspend fun removeSession(userWalletId: UserWalletId, session: WcSession): Either { + val topic = session.sdkModel.topic + val sdkCall = sdkDisconnectSession(topic) + sdkCall.onLeft { return it.left() } + suspend fun waitSdkCallback() = onSessionDelete.receiveAsFlow().first { + val isSomeError = it is Wallet.Model.SessionDelete.Error + val isDeleted = it is Wallet.Model.SessionDelete.Success && it.topic == topic + isSomeError || isDeleted + } + + val waitSdkCallback = runCatching { withTimeout(10.seconds) { waitSdkCallback() } } + val sdkCallback = waitSdkCallback.getOrElse { return it.left() } + return when (sdkCallback) { + is Wallet.Model.SessionDelete.Error -> sdkCallback.error.left() + is Wallet.Model.SessionDelete.Success -> Unit.right() + } } override suspend fun findSessionByTopic(topic: String): WcSession? = withContext(dispatchers.io) { @@ -48,45 +86,66 @@ internal class DefaultWcSessionsManager constructor( } override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { - if (sessionDelete !is Wallet.Model.SessionDelete.Success) return Timber.i("onSessionDelete: $sessionDelete") - scope.launch { - val storedSessions = store.findSessionByTopic(sessionDelete.topic) ?: return@launch - store.removeSession(storedSessions) - } + onSessionDelete.trySend(sessionDelete) } - private suspend fun migrateLegacyStore(inNewStoreSessions: Set) { + private suspend fun migrateLegacyStore(inNewStoreSessions: Set): Boolean { val walletIds = getWallets.invokeSync().mapTo(mutableSetOf()) { it.walletId } val inLegacyStoreSessions = walletIds .map { walletId -> flow { emit(legacyStore.loadSessions(walletId.stringValue).map { WcSessionDTO(it.topic, walletId) }) } } .merge() - .first() + .reduce { accumulator, value -> accumulator.plus(value) } val mustSaveInNewStore = inLegacyStoreSessions.subtract(inNewStoreSessions) if (mustSaveInNewStore.isNotEmpty()) store.saveSessions(mustSaveInNewStore) + return mustSaveInNewStore.isNotEmpty() } - private suspend fun associateWithSdk(storeSessions: Set): Map> { - val sdkSessions = WalletKit.getListOfActiveSessions() + private fun associateWithSdk( + sdkSessions: List, + storeSessions: Set, + ): List { val wcSessions = sdkSessions.mapNotNull { sdkSession -> val storedSessions = storeSessions.find { it.topic == sdkSession.topic } ?: return@mapNotNull null WcSession(userWalletId = storedSessions.walletId, sdkModel = WcSdkSessionConverter.convert(sdkSession)) } + return wcSessions + } + private suspend fun removeUnknownSessions(storeSessions: Set, wcSessions: List): Boolean { val unknownStoredSessions = storeSessions .filterNot { dto -> wcSessions.any { it.sdkModel.topic == dto.topic } } + val haveSomeUnknown = unknownStoredSessions.isNotEmpty() - if (unknownStoredSessions.isNotEmpty()) { + if (haveSomeUnknown) { unknownStoredSessions.forEach { unknown -> legacyStore.removeSession(unknown.walletId.stringValue, unknown.topic) } store.removeSessions(unknownStoredSessions.toSet()) } + return haveSomeUnknown + } - return wcSessions.groupBy { it.userWalletId } + private suspend fun sdkDisconnectSession(topic: String): Either { + return suspendCancellableCoroutine { continuation -> + WalletKit.disconnectSession( + params = Wallet.Params.SessionDisconnect(topic), + onSuccess = { continuation.resume(Unit.right()) }, + onError = { continuation.resume(it.throwable.left()) }, + ) + } + } + + private fun listenOnSessionDelete() { + onSessionDelete.receiveAsFlow() + .filterIsInstance() + .mapNotNull { sessionDelete -> store.findSessionByTopic(sessionDelete.topic) } + .onEach { sessionDto -> store.removeSession(sessionDto) } + .flowOn(dispatchers.io) + .launchIn(scope) } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt index 5c15ea1b80..4c70570c4d 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt @@ -8,6 +8,8 @@ internal interface WcSdkObserver : WalletKit.WalletDelegate { override val onSessionAuthenticate: ((Wallet.Model.SessionAuthenticate, Wallet.Model.VerifyContext) -> Unit)? get() = super.onSessionAuthenticate + fun onWcSdkInit() {} + override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) {} override fun onError(error: Wallet.Model.Error) {} diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt index 7deafa02b2..6c2a996c91 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt @@ -1,5 +1,6 @@ package com.tangem.domain.walletconnect.repository +import arrow.core.Either import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -7,6 +8,6 @@ import kotlinx.coroutines.flow.Flow interface WcSessionsManager { val sessions: Flow>> suspend fun saveSession(userWalletId: UserWalletId, session: WcSession) - suspend fun removeSession(userWalletId: UserWalletId, session: WcSession) + suspend fun removeSession(userWalletId: UserWalletId, session: WcSession): Either suspend fun findSessionByTopic(topic: String): WcSession? } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSessionsUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSessionsUseCase.kt new file mode 100644 index 0000000000..0adbf1dff6 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSessionsUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.walletconnect.usecase + +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.repository.WcSessionsManager +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first + +class WcSessionsUseCase(private val sessionsManager: WcSessionsManager) { + operator fun invoke(): Flow>> { + return sessionsManager.sessions + } + + suspend fun invokeSync(): Map> { + return sessionsManager.sessions.first() + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt new file mode 100644 index 0000000000..a34bd8e9f6 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/disconnect/WcDisconnectUseCase.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.walletconnect.usecase.disconnect + +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.repository.WcSessionsManager +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.toList + +class WcDisconnectUseCase( + private val sessionsManager: WcSessionsManager, +) { + + suspend fun disconnectAll() { + sessionsManager.sessions.first() + .flatMap { it.value } + .map { session -> flow { emit(disconnect(session)) } } + .merge() + .toList() + } + + suspend fun disconnect(topic: String) { + val session = sessionsManager.findSessionByTopic(topic) ?: return + disconnect(session) + } + + suspend fun disconnect(session: WcSession) { + sessionsManager.removeSession(session.userWalletId, session) + } +} \ No newline at end of file From d5299f8094fcb11d43e9c7647d3b7eed0a414f53 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Apr 2025 18:54:22 +0400 Subject: [PATCH 110/138] Updated on 2026-08-14 --- .../java/com/tangem/tap/TangemApplication.kt | 5 ++++ .../tangem/tap/data/DefaultCardSdkProvider.kt | 28 ++++++++++++++++++- .../sdk/mocks/content/Wallet2MockContent.kt | 5 ++++ .../sdk/mocks/content/WalletMockContent.kt | 5 ++++ .../datasource/api/common/config/ApiConfig.kt | 2 ++ .../api/common/config/TangemCardSdk.kt | 26 +++++++++++++++++ .../config/managers/ApiConfigsManager.kt | 2 +- .../config/managers/DevApiConfigsManager.kt | 5 ++-- .../tangem/datasource/di/ApiConfigsModule.kt | 7 +++-- .../managers/ProdApiConfigsManagerTest.kt | 11 ++++++++ .../card/DefaultCardSdkConfigRepository.kt | 6 +++- .../repository/CardSdkConfigRepository.kt | 3 ++ features/tester/impl/build.gradle.kts | 1 + .../EnvironmentsTogglesViewModel.kt | 21 +++++++++++++- gradle/tangem_dependencies.toml | 2 +- 15 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemCardSdk.kt diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 256468df9e..4cc52e4409 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -54,6 +54,7 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.operations.attestation.OnlineCardVerifier +import com.tangem.operations.attestation.api.TangemApiServiceLogging import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler @@ -276,6 +277,10 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat createNetworkLoggingInterceptor(), ChuckerInterceptor(this), ) + TangemApiServiceLogging.addInterceptors( + createNetworkLoggingInterceptor(), + ChuckerInterceptor(this), + ) } derivationsFinder = DerivationsFinder( diff --git a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt index 4c9bbe446d..4fcd790936 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt @@ -15,6 +15,12 @@ import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.crypto.bip39.Wordlist import com.tangem.data.card.sdk.CardSdkOwner import com.tangem.data.card.sdk.CardSdkProvider +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.datasource.api.common.config.managers.ApiConfigsManager +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.sdk.DefaultSessionViewDelegate import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles import com.tangem.sdk.extensions.* @@ -23,6 +29,11 @@ import com.tangem.sdk.nfc.NfcManager import com.tangem.sdk.storage.create import com.tangem.tap.foregroundActivityObserver import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking import javax.inject.Inject import javax.inject.Singleton @@ -37,6 +48,8 @@ internal class DefaultCardSdkProvider @Inject constructor( private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val dispatchers: CoroutineDispatcherProvider, private val cardSdkFeatureToggles: CardSdkFeatureToggles, + private val apiConfigsManager: ApiConfigsManager, + appPreferencesStore: AppPreferencesStore, ) : CardSdkProvider, CardSdkOwner { private val observer = Observer() @@ -46,6 +59,17 @@ internal class DefaultCardSdkProvider @Inject constructor( override val sdk: TangemSdk get() = holder?.sdk ?: tryToRegisterWithForegroundActivity() + init { + appPreferencesStore.getObjectMap(PreferencesKeys.apiConfigsEnvironmentKey) + .map { it[ApiConfig.ID.TangemCardSdk.name] == ApiEnvironment.PROD } + .onEach { isProd -> + holder?.let { + it.sdk.config.isTangemAttestationProdEnv = isProd + } + } + .launchIn(CoroutineScope(SupervisorJob() + dispatchers.main)) + } + override fun register(activity: FragmentActivity) = runBlocking(dispatchers.mainImmediate) { if (activity.isDestroyed || activity.isFinishing || activity.isChangingConfigurations) { val message = "Tangem SDK owner registration skipped: activity is destroyed or finishing" @@ -133,6 +157,9 @@ internal class DefaultCardSdkProvider @Inject constructor( wordlist = Wordlist.getWordlist(activity), config = config.apply { isNewOnlineAttestationEnabled = cardSdkFeatureToggles.isNewAttestationEnabled + + val apiConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.TangemCardSdk) + isTangemAttestationProdEnv = apiConfig.environment == ApiEnvironment.PROD }, ) @@ -186,7 +213,6 @@ internal class DefaultCardSdkProvider @Inject constructor( val config = Config( linkedTerminal = true, - allowUntrustedCards = true, filter = CardFilter( allowedCardTypes = FirmwareVersion.FirmwareType.entries.toList(), maxFirmwareVersion = FirmwareVersion(major = 6, minor = 33), diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt index 93a6fcbeaa..9b8fcc752f 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt @@ -32,6 +32,11 @@ object Wallet2MockContent : MockContent { name = "TANGEM 2.0", publicKey = byteArrayOf(2, -120, 89, -52, -60, 36, -73, -17, 103, 107, -110, -36, 3, 110, -122, 72, 43, -38, 8, 30, -50, 25, -23, -17, 38, 94, 5, -112, -20, 9, 54, -24, -32), ), + manufacturer = Card.Manufacturer( + name = "TANGEM SDK", + manufactureDate = Date(1743759687), + signature = byteArrayOf(), + ), walletCurves = listOf( EllipticCurve.Secp256k1, EllipticCurve.Ed25519, diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt index 22263344cf..8b3f45673e 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt @@ -32,6 +32,11 @@ object WalletMockContent : MockContent { name = "TANGEM AG", publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2), ), + manufacturer = Card.Manufacturer( + name = "TANGEM", + manufactureDate = Date(1743759687), + signature = byteArrayOf(), + ), walletCurves = listOf(EllipticCurve.Secp256k1, EllipticCurve.Ed25519, EllipticCurve.Bls12381G2Aug), firmwareVersion = FirmwareVersion( major = 4, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt index 952c80a5dc..66924d6e19 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt @@ -26,6 +26,7 @@ sealed class ApiConfig { StakeKit, TangemVisaAuth, TangemVisa, + TangemCardSdk, } private fun initializeId(): ID { @@ -35,6 +36,7 @@ sealed class ApiConfig { is StakeKit -> ID.StakeKit is TangemVisaAuth -> ID.TangemVisaAuth is TangemVisa -> ID.TangemVisa + is TangemCardSdk -> ID.TangemCardSdk } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemCardSdk.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemCardSdk.kt new file mode 100644 index 0000000000..49edb4a7d8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemCardSdk.kt @@ -0,0 +1,26 @@ +package com.tangem.datasource.api.common.config + +/** + * Tangem API [ApiConfig] that used in CardSDK + * +[REDACTED_AUTHOR] + */ +internal class TangemCardSdk : ApiConfig() { + + override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD + + override val environmentConfigs: List = listOf( + createDevEnvironment(), + createProdEnvironment(), + ) + + private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.DEV, + baseUrl = "[REDACTED_ENV_URL]", + ) + + private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://api.tangem-tech.com/", + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ApiConfigsManager.kt index 880a57f286..e7c3054219 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ApiConfigsManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/ApiConfigsManager.kt @@ -11,7 +11,7 @@ import com.tangem.datasource.api.common.config.ApiEnvironmentConfig interface ApiConfigsManager { /** Initialize resources */ - suspend fun initialize() {} + fun initialize() {} /** Get environment config by [id] */ fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt index 4f4efdd955..7d60844b81 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt @@ -9,6 +9,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.* /** @@ -28,7 +29,7 @@ internal class DevApiConfigsManager( private val _apiConfigs = MutableStateFlow(value = apiConfigs.associateWith { it.defaultEnvironment }) - override suspend fun initialize() { + override fun initialize() { // We can't use appPreferencesStore.getObjectMap as base flow, // because we should keep possibility to work with configs synchronous. // See [getBaseUrl] @@ -42,7 +43,7 @@ internal class DevApiConfigsManager( } } } - .launchIn(CoroutineScope(dispatchers.main)) + .launchIn(CoroutineScope(SupervisorJob() + dispatchers.main)) } override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig { diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index 20d50bcd28..5035774b7f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -2,9 +2,6 @@ package com.tangem.datasource.di import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.config.* -import com.tangem.datasource.api.common.config.Express -import com.tangem.datasource.api.common.config.StakeKit -import com.tangem.datasource.api.common.config.TangemTech import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider @@ -48,4 +45,8 @@ internal object ApiConfigsModule { @Provides @IntoSet fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemVisa(appVersionProvider) + + @Provides + @IntoSet + fun provideTangemCardSdkConfig(): ApiConfig = TangemCardSdk() } \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index 005c4be7fc..d4158d4965 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -89,6 +89,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { is StakeKit -> createStakeKitModel() is TangemVisaAuth -> createVisaAuthModel() is TangemVisa -> createVisaModel() + is TangemCardSdk -> createTangemCardSdkModel() } } @@ -204,6 +205,16 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { ) } + private fun createTangemCardSdkModel(): Model { + return Model( + id = ApiConfig.ID.TangemCardSdk, + expected = ApiEnvironmentConfig( + environment = ApiEnvironment.PROD, + baseUrl = "https://api.tangem-tech.com/", + ), + ) + } + private fun String.checkHeaderValueOrEmpty(): String { for (i in this.indices) { val c = this[i] diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt index 84aa2bcf9e..61be7f90d6 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt @@ -6,8 +6,8 @@ import com.tangem.common.UserCodeType import com.tangem.common.core.CardIdDisplayFormat import com.tangem.common.core.UserCodeRequestPolicy import com.tangem.data.card.sdk.CardSdkProvider -import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.models.TwinKey +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.models.scan.ProductType /** @@ -70,4 +70,8 @@ internal class DefaultCardSdkConfigRepository( override fun setLinkedTerminal(isLinked: Boolean?) { sdk.config.linkedTerminal = isLinked } + + override fun setTangemApiProdEnvFlag(flag: Boolean) { + sdk.config.isTangemAttestationProdEnv = flag + } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt index ecf06c78d8..c985aac75d 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt @@ -36,4 +36,7 @@ interface CardSdkConfigRepository { /** Set linked terminal by [isLinked] */ fun setLinkedTerminal(isLinked: Boolean?) + + /** Set [flag] that determines whether to use Prod environment for Tangem API */ + fun setTangemApiProdEnvFlag(flag: Boolean) } \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 5f26b9b233..191d380edf 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { /** Domain modules */ implementation(projects.domain.appTheme) implementation(projects.domain.appTheme.models) + implementation(projects.domain.card) implementation(projects.domain.feedback) /** Other libraries */ diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt index e2d3b7e6a3..3777d8a26a 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt @@ -6,6 +6,7 @@ import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiEnvironment import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.config.managers.MutableApiConfigsManager +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.feature.tester.impl.BuildConfig import com.tangem.feature.tester.impl.R import com.tangem.feature.tester.presentation.environments.state.EnvironmentTogglesScreenUM @@ -28,6 +29,7 @@ import javax.inject.Inject @HiltViewModel internal class EnvironmentsTogglesViewModel @Inject constructor( apiConfigsManager: ApiConfigsManager, + private val cardSdkConfigRepository: CardSdkConfigRepository, ) : ViewModel() { /** Current ui state */ @@ -92,7 +94,24 @@ internal class EnvironmentsTogglesViewModel @Inject constructor( private fun onToggleValueChange(id: String, name: String) { viewModelScope.launch { - mutableApiConfigsManager.changeEnvironment(id = id, environment = ApiEnvironment.valueOf(name)) + val environment = ApiEnvironment.valueOf(name) + + handleIfTangemCardSdkConfig(id = id, environment = environment) + + mutableApiConfigsManager.changeEnvironment(id = id, environment = environment) + } + } + + /** Special logic for [ApiConfig.ID.TangemCardSdk] */ + private fun handleIfTangemCardSdkConfig(id: String, environment: ApiEnvironment) { + if (id == ApiConfig.ID.TangemCardSdk.name) { + cardSdkConfigRepository.setTangemApiProdEnvFlag( + flag = when (environment) { + ApiEnvironment.PROD -> true + ApiEnvironment.DEV -> false + else -> false + }, + ) } } } \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 254de504ec..9b7a21e5a5 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] tangemBlockchainSdk = "develop-1000" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-445" +tangemCardSdk = "develop-449" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 2c487afef298078221fd75ff98a490099aa29965 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Apr 2025 16:20:13 +0500 Subject: [PATCH 111/138] Updated on 2026-08-14 --- .../usecase/ValidateWalletAddressUseCase.kt | 26 ++++ .../send/v2/send/DefaultSendComponent.kt | 2 +- .../v2/send/confirm/SendConfirmComponent.kt | 2 +- .../SendDestinationComponentParams.kt | 8 +- .../destination/model/SendDestinationModel.kt | 140 +++--------------- .../SendRecipientWalletListConverter.kt | 8 +- .../SendDestinationRecentListTransformer.kt | 11 +- 7 files changed, 63 insertions(+), 134 deletions(-) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt index 1452cbea68..c5a93fdc80 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.transaction.usecase import arrow.core.left import arrow.core.right +import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.transaction.WalletAddressServiceRepository @@ -43,4 +44,29 @@ class ValidateWalletAddressUseCase( else -> AddressValidation.Success.Valid.right() } } + + suspend operator fun invoke( + userWalletId: UserWalletId, + network: Network, + address: String, + senderAddresses: List, + ): AddressValidationResult { + val isUtxoConsolidationAvailable = + walletManagersFacade.checkUtxoConsolidationAvailability(userWalletId, network) + val isCurrentAddress = senderAddresses.any { it.address == address } + + val isForbidSelfSend = isCurrentAddress && !isUtxoConsolidationAvailable + + val decodedXAddress = BlockchainUtils.decodeRippleXAddress(address, network.id.value) + val addressToValidate = decodedXAddress?.address ?: address + + val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, addressToValidate) + + return when { + !isValidAddress -> AddressValidation.Error.InvalidAddress.left() + isForbidSelfSend -> AddressValidation.Error.AddressInWallet.left() + decodedXAddress != null -> AddressValidation.Success.ValidXAddress.right() + else -> AddressValidation.Success.Valid.right() + } + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 9d9d7a4778..3ce42734fc 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -133,7 +133,7 @@ internal class DefaultSendComponent @AssistedInject constructor( currentRoute = currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, - userWallet = model.userWallet, + userWalletId = params.userWalletId, cryptoCurrency = params.currency, callback = model, isEditMode = route.isEditMode, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt index 28e8110f7f..3ed30ca608 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt @@ -41,7 +41,7 @@ internal class SendConfirmComponent( params = SendDestinationComponentParams.DestinationBlockParams( state = model.uiState.value.destinationUM, analyticsCategoryName = params.analyticsCategoryName, - userWallet = params.userWallet, + userWalletId = params.userWallet.walletId, cryptoCurrency = params.cryptoCurrencyStatus.currency, blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), isPredefinedValues = params.predefinedValues is Params.PredefinedValues.Content, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponentParams.kt index 4dc45b2f96..f4a69acb24 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponentParams.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponentParams.kt @@ -1,7 +1,7 @@ package com.tangem.features.send.v2.subcomponents.destination import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.send.v2.send.SendRoute import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent.ModelCallback import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM @@ -12,14 +12,14 @@ internal sealed class SendDestinationComponentParams { abstract val state: DestinationUM abstract val analyticsCategoryName: String - abstract val userWallet: UserWallet + abstract val userWalletId: UserWalletId abstract val cryptoCurrency: CryptoCurrency data class DestinationParams( override val state: DestinationUM, override val analyticsCategoryName: String, - override val userWallet: UserWallet, override val cryptoCurrency: CryptoCurrency, + override val userWalletId: UserWalletId, val isBalanceHidingFlow: StateFlow, val currentRoute: Flow, val callback: ModelCallback, @@ -29,7 +29,7 @@ internal sealed class SendDestinationComponentParams { data class DestinationBlockParams( override val state: DestinationUM, override val analyticsCategoryName: String, - override val userWallet: UserWallet, + override val userWalletId: UserWalletId, override val cryptoCurrency: CryptoCurrency, val blockClickEnableFlow: StateFlow, val predefinedAddressValue: String?, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 5622ae7e32..b62798fbb4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -1,7 +1,6 @@ package com.tangem.features.send.v2.subcomponents.destination.model import androidx.compose.runtime.Stable -import arrow.core.Either import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter @@ -10,23 +9,10 @@ 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.navigation.Router -import com.tangem.features.send.v2.send.ui.state.ButtonsUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.feedback.GetCardInfoUseCase -import com.tangem.domain.feedback.SaveBlockchainErrorUseCase -import com.tangem.domain.feedback.SendFeedbackEmailUseCase -import com.tangem.domain.feedback.models.BlockchainErrorInfo -import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.qrscanning.models.SourceType -import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase -import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.tokens.GetCryptoCurrencyUseCase -import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.GetNetworkAddressesUseCase -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase -import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.transaction.usecase.IsUtxoConsolidationAvailableUseCase import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase @@ -38,7 +24,7 @@ import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.SendRoute import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents.SendScreenSource -import com.tangem.features.send.v2.subcomponents.destination.SendDestinationAlertFactory +import com.tangem.features.send.v2.send.ui.state.ButtonsUM import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource import com.tangem.features.send.v2.subcomponents.destination.analytics.SendDestinationAnalyticEvents @@ -56,7 +42,6 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject -import kotlin.properties.Delegates @Stable @ModelScoped @@ -73,15 +58,7 @@ internal class SendDestinationModel @Inject constructor( private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase, private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val isUtxoConsolidationAvailableUseCase: IsUtxoConsolidationAvailableUseCase, - private val listenToQrScanningUseCase: ListenToQrScanningUseCase, - private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, - private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, - private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, - private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, - private val getCardInfoUseCase: GetCardInfoUseCase, - private val parseQrCodeUseCase: ParseQrCodeUseCase, private val analyticsEventHandler: AnalyticsEventHandler, - private val sendDestinationAlertFactory: SendDestinationAlertFactory, ) : Model(), SendDestinationClickIntents { private val params: SendDestinationComponentParams = paramsContainer.require() @@ -89,17 +66,26 @@ internal class SendDestinationModel @Inject constructor( val uiState = _uiState.asStateFlow() private val analyticsCategoryName = params.analyticsCategoryName - private val userWallet = params.userWallet private val cryptoCurrency = params.cryptoCurrency - private var cryptoCurrencyStatus by Delegates.notNull() + private val userWalletId = params.userWalletId + + private val senderAddresses = MutableStateFlow>(emptyList()) private var validationJobHolder = JobHolder() init { + initSenderAddress() configDestinationNavigation() initialState() - subscribeOnCurrencyStatusUpdates() - subscribeOnQRScannerResult() + } + + private fun initSenderAddress() { + modelScope.launch { + senderAddresses.value = getNetworkAddressesUseCase.invokeSync(userWalletId, cryptoCurrency.network) + } + senderAddresses.onEach { + getWalletsAndRecent() + }.launchIn(modelScope) } private fun initialState() { @@ -157,95 +143,13 @@ internal class SendDestinationModel @Inject constructor( ) } - private fun subscribeOnCurrencyStatusUpdates() { - val isSingleWalletWithToken = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() - val isMultiCurrency = userWallet.isMultiCurrency - getCurrenciesStatusUpdates( - isSingleWalletWithToken = isSingleWalletWithToken, - isMultiCurrency = isMultiCurrency, - ) - } - - private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) { - getCurrencyStatus( - isSingleWalletWithToken = isSingleWalletWithToken, - isMultiCurrency = isMultiCurrency, - ).onEach { maybeCryptoCurrency -> - maybeCryptoCurrency.fold( - ifRight = { cryptoStatus -> - cryptoCurrencyStatus = cryptoStatus - getWalletsAndRecent() - }, - ifLeft = { - sendDestinationAlertFactory.getGenericErrorState { - onFailedTxEmailClick(it.toString()) - } - }, - ) - }.launchIn(modelScope) - } - - private fun getCurrencyStatus( - isSingleWalletWithToken: Boolean, - isMultiCurrency: Boolean, - ): Flow> { - return if (isMultiCurrency) { - getCurrencyStatusUpdatesUseCase( - userWalletId = userWallet.walletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = isSingleWalletWithToken, - ) - } else { - getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWallet.walletId) - } - } - - private fun onFailedTxEmailClick(errorMessage: String? = null) { - saveBlockchainErrorUseCase( - error = BlockchainErrorInfo( - errorMessage = errorMessage.orEmpty(), - blockchainId = cryptoCurrency.network.id.value, - derivationPath = cryptoCurrency.network.derivationPath.value, - destinationAddress = "", - tokenSymbol = "", - amount = "", - fee = "", - ), - ) - - val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrNull() ?: return - - modelScope.launch { - sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) - } - } - - private fun subscribeOnQRScannerResult() { - listenToQrScanningUseCase(SourceType.SEND) - .getOrElse { emptyFlow() } - .onEach(::onQrCodeScanned) - .launchIn(modelScope) - } - - private fun onQrCodeScanned(address: String) { - parseQrCodeUseCase(address, cryptoCurrency).fold( - ifRight = { parsedCode -> - onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode) - parsedCode.memo?.let { onRecipientMemoValueChange(it) } - }, - ifLeft = { - onRecipientAddressValueChange(address, EnterAddressSource.QRCode) - }, - ) - } - private fun getWalletsAndRecent() { combine( flow = getWalletsUseCase().conflate().map { waitForDelay(RECENT_LOAD_DELAY) { it.toAvailableWallets() } }, flow2 = getFixedTxHistoryItemsUseCase( - userWalletId = userWallet.walletId, + userWalletId = userWalletId, currency = cryptoCurrency, pageSize = RECENT_TX_SIZE, ).getOrElse { flowOf(emptyList()) }.map { @@ -253,15 +157,17 @@ internal class SendDestinationModel @Inject constructor( }.conflate(), ) { destinationWalletList, txHistoryList -> val isUtxoConsolidationAvailable = isUtxoConsolidationAvailableUseCase.invokeSync( - userWalletId = userWallet.walletId, + userWalletId = userWalletId, network = cryptoCurrency.network, ) + _uiState.update( SendDestinationRecentListTransformer( - cryptoCurrencyStatus = cryptoCurrencyStatus, + cryptoCurrency = cryptoCurrency, + senderAddress = senderAddresses.value.firstOrNull()?.address, isUtxoConsolidationAvailable = isUtxoConsolidationAvailable, destinationWalletList = destinationWalletList, - txHistoryList, + txHistoryList = txHistoryList, ), ) }.launchIn(modelScope) @@ -308,10 +214,10 @@ internal class SendDestinationModel @Inject constructor( _uiState.update(SendDestinationValidationStartedTransformer) val addressValidationResult = validateWalletAddressUseCase( - userWalletId = userWallet.walletId, + userWalletId = userWalletId, network = cryptoCurrency.network, address = address, - currencyAddress = cryptoCurrencyStatus.value.networkAddress?.availableAddresses, + senderAddresses = senderAddresses.value, ) val memoValidationResult = validateWalletMemoUseCase( memo = memo.orEmpty(), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt index 674921ad63..6837943a94 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt @@ -2,7 +2,6 @@ package com.tangem.features.send.v2.subcomponents.destination.model.converters import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM import com.tangem.features.send.v2.subcomponents.destination.model.transformers.WALLET_DEFAULT_COUNT import com.tangem.features.send.v2.subcomponents.destination.model.transformers.WALLET_KEY_TAG @@ -13,7 +12,7 @@ import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList internal class SendRecipientWalletListConverter( - private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val senderAddress: String?, private val isUtxoConsolidationAvailable: Boolean, ) : Converter, PersistentList> { @@ -25,14 +24,11 @@ internal class SendRecipientWalletListConverter( private fun List.filterWallets(): PersistentList { var walletsCounter = 0 - val currentAddress: String = runCatching { - cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value - }.getOrNull().orEmpty() return this.filterNotNull() .filter { val isCoin = it.cryptoCurrency is CryptoCurrency.Coin - val isNotSameAddress = it.address != currentAddress + val isNotSameAddress = it.address != senderAddress val isNotBlankAddress = it.address.isNotBlank() isNotBlankAddress && isCoin && (isNotSameAddress || isUtxoConsolidationAvailable) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationRecentListTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationRecentListTransformer.kt index f1a3f20621..8f670cab2d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationRecentListTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationRecentListTransformer.kt @@ -1,6 +1,6 @@ package com.tangem.features.send.v2.subcomponents.destination.model.transformers -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.features.send.v2.subcomponents.destination.model.converters.SendRecipientHistoryListConverter @@ -9,7 +9,8 @@ import com.tangem.features.send.v2.subcomponents.destination.ui.state.Destinatio import com.tangem.utils.transformer.Transformer internal class SendDestinationRecentListTransformer( - private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val senderAddress: String?, + private val cryptoCurrency: CryptoCurrency, private val isUtxoConsolidationAvailable: Boolean, private val destinationWalletList: List, private val txHistoryList: List, @@ -19,11 +20,11 @@ internal class SendDestinationRecentListTransformer( return state.copy( wallets = SendRecipientWalletListConverter( - cryptoCurrencyStatus, - isUtxoConsolidationAvailable, + senderAddress = senderAddress, + isUtxoConsolidationAvailable = isUtxoConsolidationAvailable, ).convert(destinationWalletList), recent = SendRecipientHistoryListConverter( - cryptoCurrency = cryptoCurrencyStatus.currency, + cryptoCurrency = cryptoCurrency, ).convert(txHistoryList), ) } From 469c0a43171acbccb50783b45efd7b4ab59da6e0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Apr 2025 16:52:37 +0300 Subject: [PATCH 112/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 9b7a21e5a5..8361e7d217 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "develop-1000" +tangemBlockchainSdk = "develop-1005" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-449" +tangemCardSdk = "develop-451" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From b979d5d8a8f503a5c35621b37b1b4bb99cce2eff Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Apr 2025 17:02:11 +0300 Subject: [PATCH 113/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 8361e7d217..f33bae32b2 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "develop-1005" +tangemBlockchainSdk = "releases-5.23-1007" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-451" +tangemCardSdk = "releases-5.23-452" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 4a38b9395a2e93bbbd56c4189c450ebafbddf8e9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Apr 2025 13:15:48 +0300 Subject: [PATCH 114/138] Updated on 2026-08-14 --- .../v2/twin/impl/model/OnboardingTwinModel.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index bfbd7e56c8..1d6bb00216 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -16,6 +16,9 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig +import com.tangem.core.analytics.Analytics +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList import com.tangem.core.ui.format.bigdecimal.crypto @@ -33,6 +36,7 @@ import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -77,6 +81,8 @@ internal class OnboardingTwinModel @Inject constructor( private val urlOpener: UrlOpener, private val uiMessageSender: UiMessageSender, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val clipboardManager: ClipboardManager, + private val shareManager: ShareManager, ) : Model() { private val params = paramsContainer.require() @@ -376,8 +382,14 @@ internal class OnboardingTwinModel @Inject constructor( networkAddress = networkAddress, showMemoDisclaimer = currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, - onCopyClick = {}, - onShareClick = {}, + onCopyClick = { + Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) + clipboardManager.setText(text = it, isSensitive = true) + }, + onShareClick = { + Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) + shareManager.shareText(text = it) + }, ), ), ) From 51b79c2941c3b8e037ce2c69a416fa4e20c57d3e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Apr 2025 13:45:01 +0300 Subject: [PATCH 115/138] Updated on 2026-08-14 --- .../v2/twin/impl/model/OnboardingTwinModel.kt | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index 1d6bb00216..13f2c6af15 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -157,7 +157,7 @@ internal class OnboardingTwinModel @Inject constructor( private fun createFirstWallet() { modelScope.launch { - setLoading(false) + setLoading(true) val result = tangemSdkManager.createFirstTwinWallet( cardId = params.scanResponse.card.cardId, @@ -170,7 +170,10 @@ internal class OnboardingTwinModel @Inject constructor( ) when (result) { - is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error) + is CompletionResult.Failure -> { + setLoading(false) + showCardVerificationFailedDialog(result.error) + } is CompletionResult.Success -> { if (params.mode == Mode.CreateWallet) { cardRepository.startCardActivation(result.data.cardId) @@ -228,7 +231,10 @@ internal class OnboardingTwinModel @Inject constructor( ) when (result) { - is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error) + is CompletionResult.Failure -> { + setLoading(false) + showCardVerificationFailedDialog(result.error) + } is CompletionResult.Success -> { if (params.mode == Mode.CreateWallet) { cardRepository.startCardActivation(result.data.cardId) @@ -257,6 +263,8 @@ internal class OnboardingTwinModel @Inject constructor( private fun createThirdWallet(secondCardPublicKey: ByteArray) { modelScope.launch { + setLoading(true) + val result = tangemSdkManager.finalizeTwin( secondCardPublicKey = secondCardPublicKey, issuerKeyPair = getIssuerKeys(), @@ -270,7 +278,10 @@ internal class OnboardingTwinModel @Inject constructor( ) when (result) { - is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error) + is CompletionResult.Failure -> { + setLoading(false) + showCardVerificationFailedDialog(result.error) + } is CompletionResult.Success -> { scanComplete(result.data) } From cf8d176ab6aaaca1cb51ccce11debb43d540a541 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Apr 2025 15:15:37 +0300 Subject: [PATCH 116/138] Updated on 2026-08-14 --- .../topup/model/OnboardingNoteTopUpModel.kt | 6 +- .../v2/twin/impl/model/OnboardingTwinModel.kt | 123 ++++++++++-------- .../v2/twin/impl/ui/OnboardingTwin.kt | 1 + .../v2/twin/impl/ui/state/OnboardingTwinUM.kt | 7 + 4 files changed, 81 insertions(+), 56 deletions(-) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt index 15c4a85353..021509cd04 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt @@ -72,6 +72,7 @@ internal class OnboardingNoteTopUpModel @Inject constructor( observeArtwork() modelScope.launch { createUserWalletIfNull() + cardRepository.finishCardActivation(scanResponse.card.cardId) observeCryptoCurrencyStatus() refreshBalance() } @@ -163,10 +164,7 @@ internal class OnboardingNoteTopUpModel @Inject constructor( val amountToCreateAccount = (status.value as? CryptoCurrencyStatus.NoAccount)?.amountToCreateAccount if (amount?.isPositive() == true || hasCurrentNetworkTransactions) { - modelScope.launch { - cardRepository.finishCardActivation(scanResponse.card.cardId) - params.onDone() - } + params.onDone() } _uiState.update { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index 13f2c6af15..1219a5763f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -27,6 +27,7 @@ import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.getTwinCardNumber +import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.ScanResponse @@ -54,6 +55,7 @@ import com.tangem.sdk.extensions.localizedDescriptionRes import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.firstOrNull @@ -108,10 +110,14 @@ internal class OnboardingTwinModel @Inject constructor( ) } Mode.CreateWallet -> { - OnboardingTwinUM.Welcome( - pairCardNumber = firstCardTwinNumber.pairNumber().number, - onContinueClick = ::navigateToFirstScan, - ) + if (params.scanResponse.twinsIsTwinned()) { + OnboardingTwinUM.TopUpPrepare + } else { + OnboardingTwinUM.Welcome( + pairCardNumber = firstCardTwinNumber.pairNumber().number, + onContinueClick = ::navigateToFirstScan, + ) + } } }, ) @@ -120,11 +126,19 @@ internal class OnboardingTwinModel @Inject constructor( val innerNavigationState = MutableStateFlow(DefaultOnboardingTwinComponent.TwinInnerNavigationState(1)) init { - if (_uiState.value is OnboardingTwinUM.Welcome) { - analyticsEventHandler.send(OnboardingEvent.Twins.ScreenOpened) - modelScope.launch { - saveTwinsOnboardingShownUseCase() + when (_uiState.value) { + is OnboardingTwinUM.Welcome -> { + analyticsEventHandler.send(OnboardingEvent.Twins.ScreenOpened) + modelScope.launch { + saveTwinsOnboardingShownUseCase() + } } + OnboardingTwinUM.TopUpPrepare -> { + modelScope.launch { + setTopUpState(params.scanResponse) + } + } + else -> {} } } @@ -298,55 +312,60 @@ internal class OnboardingTwinModel @Inject constructor( Mode.CreateWallet -> { modelScope.launch { setLoading(true) - - val userWallet = userWalletBuilderFactory.create(scanResponse).build() ?: run { - Timber.e("User wallet not created") - setLoading(false) - return@launch - } - - userWalletsListManager.save(userWallet, canOverride = true) - - fetchCurrencyStatusUseCase.invoke( - userWalletId = userWallet.walletId, - refresh = true, - ).onLeft { - Timber.e("Unable to fetch currency status: $it") - setLoading(false) - } - - val cryptoCurrencyStatus = - getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId).firstOrNull()?.getOrNull() - ?: run { - setLoading(false) - Timber.e("Unable to get currency status") - return@launch - } - - launch { - getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId) - .collect { - it.onRight { status -> - applyCryptoCurrencyStatusToState(status) - } - } - } - - _uiState.value = OnboardingTwinUM.TopUp( - onBuyCryptoClick = { onBuyCryptoClick(cryptoCurrencyStatus) }, - onRefreshClick = { onRefreshBalanceClick(userWallet) }, - onShowAddressClick = { onShowAddressClick(cryptoCurrencyStatus) }, - isLoading = true, - ) - - innerNavigationState.update { - it.copy(stackSize = 4) - } + setTopUpState(scanResponse) }.saveIn(cryptoCurrencyStatusJobHolder) } } } + private suspend fun setTopUpState(scanResponse: ScanResponse) = coroutineScope { + val userWallet = userWalletBuilderFactory.create(scanResponse).build() ?: run { + Timber.e("User wallet not created") + setLoading(false) + return@coroutineScope + } + + userWalletsListManager.save(userWallet, canOverride = true) + + cardRepository.finishCardActivation(params.scanResponse.card.cardId) + + fetchCurrencyStatusUseCase.invoke( + userWalletId = userWallet.walletId, + refresh = true, + ).onLeft { + Timber.e("Unable to fetch currency status: $it") + setLoading(false) + } + + val cryptoCurrencyStatus = + getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId).firstOrNull()?.getOrNull() + ?: run { + setLoading(false) + Timber.e("Unable to get currency status") + return@coroutineScope + } + + launch { + getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId) + .collect { + it.onRight { status -> + applyCryptoCurrencyStatusToState(status) + } + } + } + + _uiState.value = OnboardingTwinUM.TopUp( + onBuyCryptoClick = { onBuyCryptoClick(cryptoCurrencyStatus) }, + onRefreshClick = { onRefreshBalanceClick(userWallet) }, + onShowAddressClick = { onShowAddressClick(cryptoCurrencyStatus) }, + isLoading = true, + ) + + innerNavigationState.update { + it.copy(stackSize = 4) + } + } + private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { val amount = (status.value as? CryptoCurrencyStatus.Loaded)?.amount ?: return if (amount > BigDecimal.ZERO) { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt index 01799c96cc..c67669148b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt @@ -62,6 +62,7 @@ internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifi is OnboardingTwinUM.ScanCard -> ScanCard(st) is OnboardingTwinUM.TopUp -> TopUp(st) is OnboardingTwinUM.Welcome -> Welcome(st) + OnboardingTwinUM.TopUpPrepare -> {} } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt index 89ffc6efc6..7341af8545 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt @@ -11,6 +11,12 @@ internal sealed class OnboardingTwinUM { abstract val isLoading: Boolean abstract val artwork: TwinWalletArtworkUM + data object TopUpPrepare : OnboardingTwinUM() { + override val stepIndex: Int = 0 + override val isLoading: Boolean = false + override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Spread + } + data class Welcome( override val isLoading: Boolean = false, val pairCardNumber: Int = 2, @@ -67,5 +73,6 @@ internal sealed class OnboardingTwinUM { is ResetWarning -> copy() is ScanCard -> copy(isLoading = isLoading) is TopUp -> copy(isLoading = isLoading) + TopUpPrepare -> this } } \ No newline at end of file From 8a7c53479e461c35779902803cf630c8423a2a6f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Apr 2025 17:31:46 +0500 Subject: [PATCH 117/138] Updated on 2026-08-14 --- .../core/ui/extensions/BlockchainIcons.kt | 3 + .../src/main/res/drawable/ic_pepecoin_22.xml | 78 ++++++++++++++ .../src/main/res/drawable/img_pepecoin_22.xml | 100 ++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 core/ui/src/main/res/drawable/ic_pepecoin_22.xml create mode 100644 core/ui/src/main/res/drawable/img_pepecoin_22.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index 44c9a4dd6d..eb7f85d306 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -93,6 +93,7 @@ fun getActiveIconRes(blockchainId: String): Int { "scroll", "scroll/test" -> R.drawable.ic_alert_24 // FIXME: add icon during full integration "zklink", "zklink/test" -> R.drawable.img_zklink_22 "vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22 + "pepecoin", "pepecoin/test" -> R.drawable.img_pepecoin_22 else -> R.drawable.ic_alert_24 } } @@ -184,6 +185,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "scroll", "scroll/test" -> R.drawable.ic_alert_24 // FIXME: add icon during full integration "zklink", "zklink/test" -> R.drawable.img_zklink_22 "vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22 + "pepecoin-network", "pepecoin-network/test" -> R.drawable.img_pepecoin_22 else -> R.drawable.ic_alert_24 } } @@ -278,6 +280,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "scroll", "scroll/test" -> R.drawable.ic_alert_24 // FIXME: add icon during full integration "zklink", "zklink/test" -> R.drawable.ic_zklink_22 "vanar-chain", "vanar-chain/test" -> R.drawable.ic_vanar_22 + "pepecoin", "pepecoin/test" -> R.drawable.ic_pepecoin_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_pepecoin_22.xml b/core/ui/src/main/res/drawable/ic_pepecoin_22.xml new file mode 100644 index 0000000000..d7a3b4222b --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_pepecoin_22.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_pepecoin_22.xml b/core/ui/src/main/res/drawable/img_pepecoin_22.xml new file mode 100644 index 0000000000..6c6907e9d7 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_pepecoin_22.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From e8d25878c2793577462f27a9d94e4cd50a07b770 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 8 Apr 2025 11:02:20 +0500 Subject: [PATCH 118/138] Updated on 2026-08-14 --- .../ui/OnboardingNoteCreateWalletScreen.kt | 15 ++-- .../topup/ui/OnboardingNoteTopUpHeader.kt | 76 ++++++++++--------- .../topup/ui/OnboardingNoteTopUpScreen.kt | 19 +++-- 3 files changed, 60 insertions(+), 50 deletions(-) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/OnboardingNoteCreateWalletScreen.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/OnboardingNoteCreateWalletScreen.kt index eba5be11b9..f12d83941a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/OnboardingNoteCreateWalletScreen.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/OnboardingNoteCreateWalletScreen.kt @@ -12,6 +12,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -34,20 +36,20 @@ internal fun OnboardingNoteCreateWallet(state: OnboardingNoteCreateWalletUM, mod url2 = null, url3 = null, modifier = Modifier - .padding(horizontal = 34.dp) - .padding(top = 56.dp) + .padding(top = 64.dp) + .padding(horizontal = 24.dp) .weight(ALL_STEPS_TOP_CONTAINER_WEIGHT) .fillMaxWidth(), state = WalletArtworksState.Folded, ) Column( - Modifier + modifier = Modifier.weight(1 - ALL_STEPS_TOP_CONTAINER_WEIGHT) .fillMaxWidth() .verticalScroll(rememberScrollState()) - .padding(32.dp) - .weight(1.0f), + .padding(horizontal = 32.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { + SpacerHMax() Text( text = stringResource(R.string.onboarding_create_wallet_header), style = TangemTheme.typography.h2, @@ -55,7 +57,7 @@ internal fun OnboardingNoteCreateWallet(state: OnboardingNoteCreateWalletUM, mod textAlign = TextAlign.Center, modifier = Modifier.padding(top = 16.dp), ) - + SpacerH16() Text( text = stringResource(R.string.onboarding_create_wallet_body), style = TangemTheme.typography.body1, @@ -63,6 +65,7 @@ internal fun OnboardingNoteCreateWallet(state: OnboardingNoteCreateWalletUM, mod textAlign = TextAlign.Center, modifier = Modifier.padding(top = 12.dp), ) + SpacerHMax() } PrimaryButtonIconEnd( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt index 381b360e79..64125adc75 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt @@ -32,54 +32,58 @@ fun OnboardingNoteTopUpHeader( ) { Box( modifier = modifier - .heightIn(min = 228.dp) + .heightIn(min = 180.dp) .widthIn(max = 450.dp), contentAlignment = Alignment.Center, ) { Box( modifier = Modifier - .padding(vertical = 24.dp) + .padding(vertical = 24.dp, horizontal = 16.dp) .fillMaxSize() .background( TangemTheme.colors.button.secondary, shape = TangemTheme.shapes.roundedCornersMedium, ), - ) - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(horizontal = 32.dp), + contentAlignment = Alignment.Center, ) { - WalletCard( - modifier = Modifier.width(120.dp), - url = cardArtworkUrl, - ) - SpacerHMax() - Text( - text = stringResourceSafe(R.string.common_balance_title), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - SpacerH8() - Text( - modifier = if (balance.isEmpty()) { - Modifier - .width(120.dp) - .clip(RoundedCornerShape(size = TangemTheme.dimens.radius3)) - .shimmer(LocalTangemShimmer.current) - } else { - Modifier - }, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - text = balance, - ) - SpacerHMax() - RefreshButton( - isRefreshing = isRefreshing, - onRefreshBalanceClick = onRefreshBalanceClick, - ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(horizontal = 32.dp), + ) { + SpacerHMax() + Text( + text = stringResourceSafe(R.string.common_balance_title), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + SpacerH8() + Text( + modifier = if (balance.isEmpty()) { + Modifier + .width(120.dp) + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius3)) + .shimmer(LocalTangemShimmer.current) + } else { + Modifier + }, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + text = balance, + ) + SpacerHMax() + } } + WalletCard( + modifier = Modifier.width(120.dp).align(Alignment.TopCenter), + url = cardArtworkUrl, + ) + RefreshButton( + modifier = Modifier.align(Alignment.BottomCenter), + isRefreshing = isRefreshing, + onRefreshBalanceClick = onRefreshBalanceClick, + ) } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt index 1ae01521ea..b7bee01049 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt @@ -14,10 +14,13 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.note.impl.ALL_STEPS_TOP_CONTAINER_WEIGHT import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM @Composable @@ -35,20 +38,19 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif onRefreshBalanceClick = state.onRefreshBalanceClick, isRefreshing = state.isRefreshing, modifier = Modifier - .padding(horizontal = 34.dp) - .padding(top = 16.dp) - .height(228.dp) + .padding(top = 64.dp) + .padding(horizontal = 24.dp) + .weight(ALL_STEPS_TOP_CONTAINER_WEIGHT) .fillMaxWidth(), ) Column( - modifier = Modifier + modifier = Modifier.weight(1 - ALL_STEPS_TOP_CONTAINER_WEIGHT) .fillMaxWidth() .verticalScroll(rememberScrollState()) - .padding(horizontal = 32.dp) - .padding(top = 16.dp) - .weight(1.0f), + .padding(horizontal = 32.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { + SpacerHMax() Text( text = stringResourceSafe(R.string.onboarding_topup_title), style = TangemTheme.typography.h2, @@ -65,13 +67,14 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif } else { stringResourceSafe(R.string.onboarding_top_up_body) } + SpacerH16() Text( text = text, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, - modifier = Modifier.padding(top = 12.dp), ) + SpacerHMax() } BottomButtons(state) From b7e95ab3baa72c65010d63271b3743b97248f910 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Apr 2025 20:08:14 +0500 Subject: [PATCH 119/138] Updated on 2026-08-14 --- .../v2/subcomponents/destination/model/SendDestinationModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index b62798fbb4..17a75c6f3b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -82,6 +82,7 @@ internal class SendDestinationModel @Inject constructor( private fun initSenderAddress() { modelScope.launch { senderAddresses.value = getNetworkAddressesUseCase.invokeSync(userWalletId, cryptoCurrency.network) + .filter { cryptoCurrency.id == it.cryptoCurrency.id } } senderAddresses.onEach { getWalletsAndRecent() From 4e775f072aee7c2acc501564ee9ec6fcfcc8d234 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Apr 2025 20:09:16 +0500 Subject: [PATCH 120/138] Updated on 2026-08-14 --- .../destination/model/SendDestinationModel.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 17a75c6f3b..8c1016c434 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -10,6 +10,9 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.tokens.GetCryptoCurrencyUseCase import com.tangem.domain.tokens.GetNetworkAddressesUseCase import com.tangem.domain.tokens.model.CryptoCurrencyAddress @@ -58,6 +61,8 @@ internal class SendDestinationModel @Inject constructor( private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase, private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val isUtxoConsolidationAvailableUseCase: IsUtxoConsolidationAvailableUseCase, + private val listenToQrScanningUseCase: ListenToQrScanningUseCase, + private val parseQrCodeUseCase: ParseQrCodeUseCase, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model(), SendDestinationClickIntents { private val params: SendDestinationComponentParams = paramsContainer.require() @@ -76,6 +81,7 @@ internal class SendDestinationModel @Inject constructor( init { initSenderAddress() configDestinationNavigation() + subscribeOnQRScannerResult() initialState() } @@ -144,6 +150,28 @@ internal class SendDestinationModel @Inject constructor( ) } + private fun subscribeOnQRScannerResult() { + listenToQrScanningUseCase(SourceType.SEND) + .getOrElse { emptyFlow() } + .onEach(::onQrCodeScanned) + .launchIn(modelScope) + } + + private fun onQrCodeScanned(address: String) { + val parsedQrCode = parseQrCodeUseCase(address, cryptoCurrency).getOrNull() ?: return + _uiState.update( + SendDestinationPredefinedStateTransformer( + address = parsedQrCode.address, + memo = parsedQrCode.memo, + ), + ) + validate( + address = parsedQrCode.address, + memo = parsedQrCode.memo, + type = EnterAddressSource.QRCode, + ) + } + private fun getWalletsAndRecent() { combine( flow = getWalletsUseCase().conflate().map { From 46cb02aaec46d86db78946c091bec0b27c45b547 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Apr 2025 20:12:10 +0500 Subject: [PATCH 121/138] Updated on 2026-08-14 --- .../v2/send/confirm/model/SendConfirmModel.kt | 30 ++--- .../SendConfirmInitialStateTransformer.kt | 122 +----------------- ...endConfirmationNotificationsTransformer.kt | 112 ++++++++++++++++ .../v2/send/confirm/ui/SendConfirmContent.kt | 22 ++-- 4 files changed, 137 insertions(+), 149 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 6d4753a5df..5ddadd49f3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -15,7 +15,6 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener -import com.tangem.features.send.v2.send.ui.state.ButtonsUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.feedback.GetCardInfoUseCase @@ -49,6 +48,7 @@ import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSe import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSentStateTransformer import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmationNotificationsTransformer import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM +import com.tangem.features.send.v2.send.ui.state.ButtonsUM import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger @@ -274,20 +274,14 @@ internal class SendConfirmModel @Inject constructor( private fun initialState() { val confirmUM = uiState.value.confirmUM - val amountUM = uiState.value.amountUM - val feeUM = uiState.value.feeUM modelScope.launch { val isShowTapHelp = isSendTapHelpEnabledUseCase().getOrElse { false } - if (confirmUM is ConfirmUM.Empty || feeUM is FeeUM.Empty) { + if (confirmUM is ConfirmUM.Empty) { _uiState.update { it.copy( confirmUM = SendConfirmInitialStateTransformer( - appCurrency = appCurrency, - feeUM = feeUM, - amountUM = amountUM, isShowTapHelp = isShowTapHelp, - isSubtracted = false, ).transform(uiState.value.confirmUM), ) } @@ -472,15 +466,17 @@ internal class SendConfirmModel @Inject constructor( feeError = feeError, ), ) - } - _uiState.update { - it.copy( - confirmUM = SendConfirmationNotificationsTransformer( - feeUM = uiState.value.feeUM, - analyticsEventHandler = analyticsEventHandler, - cryptoCurrency = cryptoCurrencyStatus.currency, - ).transform(uiState.value.confirmUM), - ) + _uiState.update { + it.copy( + confirmUM = SendConfirmationNotificationsTransformer( + feeUM = uiState.value.feeUM, + amountUM = uiState.value.amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrencyStatus.currency, + appCurrency = appCurrency, + ).transform(uiState.value.confirmUM), + ) + } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt index a3da5886b9..c165ade1ff 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmInitialStateTransformer.kt @@ -1,137 +1,19 @@ package com.tangem.features.send.v2.send.confirm.model.transformers -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.transaction.Fee -import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.core.ui.extensions.* -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.fee -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.features.send.v2.impl.R +import com.tangem.core.ui.extensions.TextReference import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM -import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM -import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf internal class SendConfirmInitialStateTransformer( - private val appCurrency: AppCurrency, - private val feeUM: FeeUM, - private val amountUM: AmountState, private val isShowTapHelp: Boolean, - private val isSubtracted: Boolean, ) : Transformer { override fun transform(prevState: ConfirmUM): ConfirmUM { return ConfirmUM.Content( isSending = false, showTapHelp = isShowTapHelp, - sendingFooter = getSendingFooterText(), + sendingFooter = TextReference.EMPTY, notifications = persistentListOf(), ) } - - private fun getSendingFooterText(): TextReference { - val feeUM = feeUM as? FeeUM.Content - val amountUM = amountUM as? AmountState.Data - - if (feeUM == null || amountUM == null) return TextReference.EMPTY - - val fee = (feeUM.feeSelectorUM as? FeeSelectorUM.Content)?.selectedFee - val fiatAmountValue = amountUM.amountTextField.fiatAmount.value - val fiatFeeValue = feeUM.rate?.let { fee?.amount?.value?.multiply(it) } - - val fiatSendingValue = if (isSubtracted) { - fiatAmountValue - } else { - if (feeUM.isFeeConvertibleToFiat) { - fiatFeeValue?.let { fiatAmountValue?.plus(it) } - } else { - fiatAmountValue - } - } - - val fiatSending = fiatSendingValue.format { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - } - val fiatFee = formatFiatFee( - amount = fee?.amount, - isFeeConvertibleToFiat = feeUM.isFeeConvertibleToFiat, - isFeeApproximate = feeUM.isFeeApproximate, - ) - - return if (feeUM.isTronToken && fee is Fee.Tron) { - getTokenFeeSendingText( - fee = fee, - fiatFee = fiatFee, - fiatSending = fiatSending, - ) - } else { - resourceReference( - id = if (feeUM.isFeeConvertibleToFiat) { - R.string.send_summary_transaction_description - } else { - R.string.send_summary_transaction_description_no_fiat_fee - }, - formatArgs = wrappedList(fiatSending, fiatFee), - ) - } - } - - private fun formatFiatFee(amount: Amount?, isFeeConvertibleToFiat: Boolean, isFeeApproximate: Boolean): String { - return if (isFeeConvertibleToFiat) { - amount?.value.format { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - } - } else { - amount?.value.format { - crypto( - decimals = amount?.decimals ?: 0, - symbol = amount?.currencySymbol.orEmpty(), - ).fee( - canBeLower = isFeeApproximate, - ) - } - } - } - - private fun getTokenFeeSendingText(fee: Fee.Tron, fiatFee: String, fiatSending: String): TextReference { - val suffix = when { - fee.remainingEnergy == 0L -> { - resourceReference( - R.string.send_summary_transaction_description_suffix_including, - wrappedList(fiatFee), - ) - } - fee.feeEnergy <= fee.remainingEnergy -> { - resourceReference( - R.string.send_summary_transaction_description_suffix_fee_covered, - wrappedList(fee.feeEnergy), - ) - } - else -> { - resourceReference( - R.string.send_summary_transaction_description_suffix_fee_reduced, - wrappedList(fee.remainingEnergy), - ) - } - } - val prefix = resourceReference( - R.string.send_summary_transaction_description_prefix, - wrappedList(fiatSending), - ) - - return combinedReference(prefix, COMMA_SEPARATOR, suffix) - } - - companion object { - private val COMMA_SEPARATOR = stringReference(", ") - } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt index 0b7ea0f46f..0066a9268d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt @@ -1,10 +1,20 @@ package com.tangem.features.send.v2.send.confirm.model.transformers +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM import com.tangem.features.send.v2.subcomponents.fee.model.checkIfFeeTooHigh @@ -16,13 +26,16 @@ import kotlinx.collections.immutable.toPersistentList internal class SendConfirmationNotificationsTransformer( private val feeUM: FeeUM, + private val amountUM: AmountState, private val analyticsEventHandler: AnalyticsEventHandler, private val cryptoCurrency: CryptoCurrency, + private val appCurrency: AppCurrency, ) : Transformer { override fun transform(prevState: ConfirmUM): ConfirmUM { val state = prevState as? ConfirmUM.Content ?: return prevState val feeUM = feeUM as? FeeUM.Content ?: return prevState return state.copy( + sendingFooter = getSendingFooterText(), notifications = buildList { addTooHighNotification(feeUM.feeSelectorUM) addTooLowNotification(feeUM) @@ -52,4 +65,103 @@ internal class SendConfirmationNotificationsTransformer( add(NotificationUM.Warning.TooHigh(diff)) } } + + private fun getSendingFooterText(): TextReference { + val feeUM = feeUM as? FeeUM.Content + val amountUM = amountUM as? AmountState.Data + val fee = (feeUM?.feeSelectorUM as? FeeSelectorUM.Content)?.selectedFee + + if (fee == null || amountUM == null) return TextReference.EMPTY + + val fiatAmountValue = amountUM.amountTextField.fiatAmount.value + val fiatFeeValue = feeUM.rate?.let { fee.amount.value?.multiply(it) } + + val fiatSendingValue = if (feeUM.isFeeConvertibleToFiat) { + fiatFeeValue?.let { fiatAmountValue?.plus(it) } + } else { + fiatAmountValue + } + + val fiatSending = fiatSendingValue.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + val fiatFee = formatFiatFee( + amount = fee.amount, + isFeeConvertibleToFiat = feeUM.isFeeConvertibleToFiat, + isFeeApproximate = feeUM.isFeeApproximate, + ) + + return if (feeUM.isTronToken && fee is Fee.Tron) { + getTokenFeeSendingText( + fee = fee, + fiatFee = fiatFee, + fiatSending = fiatSending, + ) + } else { + resourceReference( + id = if (feeUM.isFeeConvertibleToFiat) { + R.string.send_summary_transaction_description + } else { + R.string.send_summary_transaction_description_no_fiat_fee + }, + formatArgs = wrappedList(fiatSending, fiatFee), + ) + } + } + + private fun formatFiatFee(amount: Amount?, isFeeConvertibleToFiat: Boolean, isFeeApproximate: Boolean): String { + return if (isFeeConvertibleToFiat) { + amount?.value.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + } else { + amount?.value.format { + crypto( + decimals = amount?.decimals ?: 0, + symbol = amount?.currencySymbol.orEmpty(), + ).fee( + canBeLower = isFeeApproximate, + ) + } + } + } + + private fun getTokenFeeSendingText(fee: Fee.Tron, fiatFee: String, fiatSending: String): TextReference { + val suffix = when { + fee.remainingEnergy == 0L -> { + resourceReference( + R.string.send_summary_transaction_description_suffix_including, + wrappedList(fiatFee), + ) + } + fee.feeEnergy <= fee.remainingEnergy -> { + resourceReference( + R.string.send_summary_transaction_description_suffix_fee_covered, + wrappedList(fee.feeEnergy), + ) + } + else -> { + resourceReference( + R.string.send_summary_transaction_description_suffix_fee_reduced, + wrappedList(fee.remainingEnergy), + ) + } + } + val prefix = resourceReference( + R.string.send_summary_transaction_description_prefix, + wrappedList(fiatSending), + ) + + return combinedReference(prefix, COMMA_SEPARATOR, suffix) + } + + companion object { + private val COMMA_SEPARATOR = stringReference(", ") + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt index 1339378697..f3a151a432 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt @@ -23,12 +23,10 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.Keyboard +import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.components.transactions.TransactionDoneTitle -import com.tangem.core.ui.extensions.resolveAnnotatedReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat @@ -83,21 +81,22 @@ internal fun SendConfirmContent( ) } } - SendingText(confirmUM = confirmUM) + SpacerHMax() + SendingText(footerText = confirmUM?.sendingFooter ?: TextReference.EMPTY) } } @Composable -private fun SendingText(confirmUM: ConfirmUM.Content?, modifier: Modifier = Modifier) { - var isVisibleProxy by remember { mutableStateOf(confirmUM != null) } +private fun SendingText(footerText: TextReference, modifier: Modifier = Modifier) { + var isVisibleProxy by remember { mutableStateOf(footerText != TextReference.EMPTY) } val keyboard by keyboardAsState() // the text should appear when the keyboard is closed - LaunchedEffect(confirmUM != null, keyboard) { - if (confirmUM != null && keyboard is Keyboard.Opened) { + LaunchedEffect(footerText != TextReference.EMPTY, keyboard) { + if (footerText != TextReference.EMPTY && keyboard is Keyboard.Opened) { return@LaunchedEffect } - isVisibleProxy = confirmUM != null + isVisibleProxy = footerText != TextReference.EMPTY } AnimatedVisibility( @@ -107,9 +106,8 @@ private fun SendingText(confirmUM: ConfirmUM.Content?, modifier: Modifier = Modi exit = fadeOut(tween(durationMillis = 300)), label = "Animate show sending state text", ) { - val wrappedConfirmUM = remember(this) { requireNotNull(confirmUM?.sendingFooter) } Text( - text = wrappedConfirmUM.resolveAnnotatedReference(), + text = footerText.resolveAnnotatedReference(), textAlign = TextAlign.Center, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.primary1, From 70f6c120c36175dffe10c6807059a2aa9715e1fc Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Apr 2025 20:13:06 +0500 Subject: [PATCH 122/138] Updated on 2026-08-14 --- .../SendDestinationValidationResultTransformer.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt index 8830a8ece8..120cc16a1c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.v2.subcomponents.destination.model.transformers import arrow.core.Either +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.transaction.error.AddressValidation import com.tangem.domain.transaction.error.AddressValidationResult @@ -8,6 +9,7 @@ import com.tangem.domain.transaction.error.ValidateMemoError import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList internal class SendDestinationValidationResultTransformer( private val addressValidationResult: AddressValidationResult, @@ -39,6 +41,12 @@ internal class SendDestinationValidationResultTransformer( isError = state.memoTextField.value.isNotEmpty() && !isValidMemo, isEnabled = !shouldDisableMemo(), ), + recent = state.recent.map { recent -> + recent.copy(isVisible = !isValidAddress && (recent.isLoading || recent.title != TextReference.EMPTY)) + }.toPersistentList(), + wallets = state.wallets.map { wallet -> + wallet.copy(isVisible = !isValidAddress && (wallet.isLoading || wallet.title != TextReference.EMPTY)) + }.toPersistentList(), ) } From 0c55931ddc73b74f20ba7ac85293fe4d7f72c79b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Apr 2025 20:22:00 +0500 Subject: [PATCH 123/138] Updated on 2026-08-14 --- .../v2/send/confirm/model/SendConfirmModel.kt | 2 +- .../amount/SendAmountReduceTrigger.kt | 1 + .../subcomponents/amount/di/SendAmountModule.kt | 16 ++++++---------- .../amount/model/SendAmountModel.kt | 2 +- .../destination/model/SendDestinationModel.kt | 2 +- .../v2/subcomponents/fee/model/SendFeeModel.kt | 2 +- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 5ddadd49f3..7e089f9d66 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -485,7 +485,7 @@ internal class SendConfirmModel @Inject constructor( flow = uiState, flow2 = params.currentRoute, transform = { state, route -> state to route }, - ).distinctUntilChanged().onEach { (state, _) -> + ).onEach { (state, _) -> val amountUM = state.amountUM as? AmountState.Data val confirmUM = state.confirmUM params.callback.onResult( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt index a168fe3532..901ee1f0df 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt @@ -29,6 +29,7 @@ interface SendAmountReduceListener { internal class DefaultSendAmountReduceTrigger @Inject constructor() : SendAmountReduceTrigger, SendAmountReduceListener { + override val reduceToTriggerFlow = MutableSharedFlow() override val reduceByTriggerFlow = MutableSharedFlow() override val ignoreReduceTriggerFlow = MutableSharedFlow() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/di/SendAmountModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/di/SendAmountModule.kt index 5152941ae4..b486aeaeae 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/di/SendAmountModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/di/SendAmountModule.kt @@ -3,25 +3,21 @@ package com.tangem.features.send.v2.subcomponents.amount.di import com.tangem.features.send.v2.subcomponents.amount.DefaultSendAmountReduceTrigger import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceTrigger +import dagger.Binds import dagger.Module -import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module -internal object SendAmountModule { +internal interface SendAmountModule { - @Provides @Singleton - fun provideSendAmountReduceTrigger(): SendAmountReduceTrigger { - return DefaultSendAmountReduceTrigger() - } + @Binds + fun provideSendAmountReduceTrigger(impl: DefaultSendAmountReduceTrigger): SendAmountReduceTrigger - @Provides @Singleton - fun provideSendAmountReduceListener(): SendAmountReduceListener { - return DefaultSendAmountReduceTrigger() - } + @Binds + fun provideSendAmountReduceListener(impl: DefaultSendAmountReduceTrigger): SendAmountReduceListener } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index d0817e99ca..5f37e5f84f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -212,7 +212,7 @@ internal class SendAmountModel @Inject constructor( flow = uiState, flow2 = params.currentRoute, transform = { state, route -> state to route }, - ).distinctUntilChanged().onEach { (state, route) -> + ).onEach { (state, route) -> params.callback.onNavigationResult( NavigationUM.Content( title = resourceReference(R.string.send_amount_label), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 8c1016c434..2f4e77b25d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -292,7 +292,7 @@ internal class SendDestinationModel @Inject constructor( flow = uiState, flow2 = params.currentRoute, transform = { state, route -> state to route }, - ).distinctUntilChanged().onEach { (state, route) -> + ).onEach { (state, route) -> params.callback.onNavigationResult( NavigationUM.Content( title = resourceReference(R.string.send_recipient_label), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt index 861798a122..0b53128a92 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt @@ -278,7 +278,7 @@ internal class SendFeeModel @Inject constructor( flow = uiState, flow2 = params.currentRoute, transform = { state, route -> state to route }, - ).distinctUntilChanged().onEach { (state, _) -> + ).onEach { (state, _) -> params.callback.onNavigationResult( NavigationUM.Content( title = resourceReference(R.string.common_fee_selector_title), From 7f17dfa2e37d0878c807db5d2dee56147fc2f06a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Apr 2025 20:23:54 +0500 Subject: [PATCH 124/138] Updated on 2026-08-14 --- .../common/ui/amountScreen/ui/AmountBlock.kt | 18 +++++++----------- .../destination/ui/DestinationBlock.kt | 10 ++-------- .../send/v2/subcomponents/fee/ui/FeeBlock.kt | 8 +------- .../presentation/ui/send/RecipientBlock.kt | 8 +------- .../ui/StakingConfirmationContent.kt | 2 +- .../presentation/ui/block/StakingFeeBlock.kt | 14 +++----------- 6 files changed, 15 insertions(+), 45 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt index bcac8bb21b..3fd2bd8ad8 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt @@ -21,10 +21,10 @@ import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.format.bigdecimal.anyDecimals import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter import java.math.BigDecimal @Composable @@ -33,15 +33,11 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis val amount = amountState.amountTextField val cryptoAmount = formatWithSymbol(amount.value, amount.cryptoAmount.currencySymbol) - val fiatAmount = BigDecimalFormatter.formatFiatAmount( - fiatAmount = amount.fiatAmount.value, - fiatCurrencySymbol = amount.fiatAmount.currencySymbol, - fiatCurrencyCode = amountState.appCurrencyCode, - ) - val backgroundColor = if (isEditingDisabled) { - TangemTheme.colors.button.disabled - } else { - TangemTheme.colors.background.action + val fiatAmount = amount.fiatAmount.value.format { + fiat( + fiatCurrencySymbol = amount.fiatAmount.currencySymbol, + fiatCurrencyCode = amountState.appCurrencyCode, + ) } val (firstAmount, secondAmount) = if (amount.isFiatValue) { @@ -54,7 +50,7 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(backgroundColor) + .background(TangemTheme.colors.background.action) .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) .padding(TangemTheme.dimens.spacing16), ) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt index 649422d27c..9fe2722f12 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/DestinationBlock.kt @@ -23,19 +23,13 @@ internal fun DestinationBlock( isEditingDisabled: Boolean, onClick: () -> Unit, ) { - val destinationUM = destinationUM as? DestinationUM.Content ?: return - - val backgroundColor = if (isEditingDisabled) { - TangemTheme.colors.button.disabled - } else { - TangemTheme.colors.background.action - } + if (destinationUM !is DestinationUM.Content) return Column( modifier = Modifier .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(backgroundColor) + .background(TangemTheme.colors.background.action) .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) .padding(TangemTheme.dimens.spacing12), ) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt index b1c82b14af..10284bde92 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt @@ -32,17 +32,11 @@ internal fun FeeBlock(feeUM: FeeUM, isClickEnabled: Boolean, onClick: () -> Unit val feeSelectorUM = feeUM.feeSelectorUM as? FeeSelectorUM.Content val isEditingDisabled = feeSelectorUM?.fees is TransactionFee.Single - val backgroundColor = if (isEditingDisabled) { - TangemTheme.colors.button.disabled - } else { - TangemTheme.colors.background.action - } - Column( modifier = Modifier .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(backgroundColor) + .background(TangemTheme.colors.background.action) .clickable(enabled = isClickEnabled && !isEditingDisabled, onClick = onClick) .padding(12.dp), ) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt index 313451a067..9451e29e99 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt @@ -29,17 +29,11 @@ internal fun RecipientBlock( isEditingDisabled: Boolean, onClick: () -> Unit, ) { - val backgroundColor = if (isEditingDisabled) { - TangemTheme.colors.button.disabled - } else { - TangemTheme.colors.background.action - } - Column( modifier = Modifier .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(backgroundColor) + .background(TangemTheme.colors.background.action) .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) .padding(TangemTheme.dimens.spacing12), ) { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index 49d1aa49dd..42f7777f9f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -71,7 +71,7 @@ internal fun StakingConfirmationContent( isClickable = !isTransactionInProgress, onClick = clickIntents::openValidators, ) - StakingFeeBlock(feeState = state.feeState, isTransactionSent = isTransactionSent) + StakingFeeBlock(feeState = state.feeState) NotificationsBlock(notifications = state.notifications) Spacer(Modifier) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index cfd283c0d1..0b4973970b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -32,17 +32,12 @@ import com.tangem.features.staking.impl.presentation.state.FeeState import java.math.BigDecimal @Composable -internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) { - val backgroundColor = if (isTransactionSent) { - TangemTheme.colors.background.action - } else { - TangemTheme.colors.button.disabled - } +internal fun StakingFeeBlock(feeState: FeeState) { Column( modifier = Modifier .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(backgroundColor) + .background(TangemTheme.colors.background.action) .padding(TangemTheme.dimens.spacing12), ) { Text( @@ -144,10 +139,7 @@ private fun BoxScope.FeeError(feeState: FeeState) { @Composable private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) value: FeeState) { TangemThemePreview { - StakingFeeBlock( - feeState = value, - isTransactionSent = false, - ) + StakingFeeBlock(feeState = value) } } From bea5095daa71fc5f4b1ed79774feb36d7e3c4843 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 8 Apr 2025 11:47:20 +0500 Subject: [PATCH 125/138] Updated on 2026-08-14 --- .../SendDestinationValidationResultTransformer.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt index 120cc16a1c..a67c26d520 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt @@ -30,6 +30,8 @@ internal class SendDestinationValidationResultTransformer( } }.leftOrNull() + val shouldDisableMemo = shouldDisableMemo() + return state.copy( isValidating = false, isPrimaryButtonEnabled = isValidAddress && isValidMemo, @@ -38,8 +40,9 @@ internal class SendDestinationValidationResultTransformer( isError = state.addressTextField.value.isNotEmpty() && !isValidAddress, ), memoTextField = state.memoTextField?.copy( + value = state.memoTextField.value.takeIf { !shouldDisableMemo }.orEmpty(), isError = state.memoTextField.value.isNotEmpty() && !isValidMemo, - isEnabled = !shouldDisableMemo(), + isEnabled = !shouldDisableMemo, ), recent = state.recent.map { recent -> recent.copy(isVisible = !isValidAddress && (recent.isLoading || recent.title != TextReference.EMPTY)) From 4916a6ffe5eec818ac73482a2194ba2c887085f8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 7 Apr 2025 18:25:40 +0400 Subject: [PATCH 126/138] Updated on 2026-08-14 --- .../tokens/AddCryptoCurrenciesUseCase.kt | 95 ++++++++----------- .../impl/DefaultAddCustomTokenComponent.kt | 29 +++++- .../model/OnrampAddToPortfolioModel.kt | 2 +- .../referral/domain/ReferralInteractorImpl.kt | 6 +- 4 files changed, 70 insertions(+), 62 deletions(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt index 2b30de2785..56fc636d4f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt @@ -4,7 +4,6 @@ import arrow.core.Either import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either -import arrow.core.toNonEmptyListOrNull import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency @@ -30,20 +29,6 @@ class AddCryptoCurrenciesUseCase( private val tokensFeatureToggles: TokensFeatureToggles, ) { - /** - * Adds a [currency] to the wallet identified by [userWalletId]. - * - * After successfully adding a currency, it also refreshes the networks for tokens - * that are being added and have corresponding coins in the existing currencies list. - * - * @param userWalletId The ID of the user's wallet. - * @param currency Cryptocurrency to add. - * @return Either an [Throwable] or [Unit] indicating the success of the operation. - */ - suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either { - return invoke(userWalletId, listOf(currency)) - } - /** * Adds a [cryptoCurrency] token with specific [network] and derivation to the wallet identified by [userWalletId]. * @@ -61,34 +46,32 @@ class AddCryptoCurrenciesUseCase( network: Network, ): Either = either { val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency = cryptoCurrency, network = network) - invoke(userWalletId = userWalletId, currencies = listOf(tokenToAdd)) + invoke(userWalletId = userWalletId, currency = tokenToAdd) } /** - * Adds a list of [currencies] to the wallet identified by [userWalletId]. + * Adds a [currency] to the wallet identified by [userWalletId]. * - * After successfully adding currencies, it also refreshes the networks for tokens + * After successfully adding a currency, it also refreshes the networks for tokens * that are being added and have corresponding coins in the existing currencies list. * * @param userWalletId The ID of the user's wallet. - * @param currencies The list of cryptocurrencies to add. + * @param currency Cryptocurrency to add. * @return Either an [Throwable] or [Unit] indicating the success of the operation. */ - suspend operator fun invoke( - userWalletId: UserWalletId, - currencies: List, - ): Either = either { - val existingCurrencies = catch({ currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) }) { - raise(it) - } - val currenciesToAdd = currencies - .filterNot(existingCurrencies::contains) - .toNonEmptyListOrNull() - ?: return@either + suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either = + either { + val existingCurrencies = catch( + block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) }, + catch = ::raise, + ) + val currencyToAdd = currency.takeUnless(existingCurrencies::contains) ?: return@either - addCurrencies(userWalletId, currenciesToAdd) - refreshUpdatedNetworks(userWalletId, currenciesToAdd, existingCurrencies) - } + addCurrencies(userWalletId, currencyToAdd) + refreshUpdatedNetworks(userWalletId, currencyToAdd, existingCurrencies) + refreshUpdatedYieldBalances(userWalletId, currencyToAdd) + refreshUpdatedQuotes(currencyToAdd) + } suspend operator fun invoke( userWalletId: UserWalletId, @@ -110,10 +93,10 @@ class AddCryptoCurrenciesUseCase( return@either foundToken } val tokenToAdd = createTokenCurrency(userWalletId, contractAddress, networkId) - addCurrencies(userWalletId, listOf(tokenToAdd)) - refreshUpdatedNetworks(userWalletId, listOf(tokenToAdd), existingCurrencies) - refreshUpdatedYieldBalances(userWalletId, existingCurrencies) - refreshUpdatedQuotes(existingCurrencies) + addCurrencies(userWalletId, tokenToAdd) + refreshUpdatedNetworks(userWalletId, tokenToAdd, existingCurrencies) + refreshUpdatedYieldBalances(userWalletId, tokenToAdd) + refreshUpdatedQuotes(tokenToAdd) tokenToAdd } @@ -123,23 +106,24 @@ class AddCryptoCurrenciesUseCase( */ private suspend fun Raise.refreshUpdatedNetworks( userWalletId: UserWalletId, - currenciesToAdd: List, + currencyToAdd: CryptoCurrency, existingCurrencies: List, ) { - val networksToUpdate = currenciesToAdd - .asSequence() - .filterIsInstance() - .filter { hasCoinForToken(existingCurrencies, it) } - .mapTo(hashSetOf(), CryptoCurrency.Token::network) + val networksToUpdate = currencyToAdd.takeIf { currency -> + currency is CryptoCurrency.Token && hasCoinForToken(existingCurrencies, currency) + } + ?.network - val networkToUpdate = currenciesToAdd.map { it.network } - .subtract(existingCurrencies.map { it.network }.toSet()) + val networkToUpdate = currencyToAdd.takeIf { + !existingCurrencies.map(CryptoCurrency::network).contains(it.network) + } + ?.network if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { multiNetworkStatusFetcher( MultiNetworkStatusFetcher.Params( userWalletId = userWalletId, - networks = networksToUpdate + networkToUpdate, + networks = setOfNotNull(networksToUpdate, networkToUpdate), ), ) } else { @@ -147,7 +131,7 @@ class AddCryptoCurrenciesUseCase( { networksRepository.getNetworkStatusesSync( userWalletId = userWalletId, - networks = networksToUpdate + networkToUpdate, + networks = setOfNotNull(networksToUpdate, networkToUpdate), refresh = true, ) }, @@ -157,20 +141,17 @@ class AddCryptoCurrenciesUseCase( } } - private suspend fun refreshUpdatedYieldBalances( - userWalletId: UserWalletId, - existingCurrencies: List, - ) { - stakingRepository.fetchMultiYieldBalance( + private suspend fun refreshUpdatedYieldBalances(userWalletId: UserWalletId, addedCurrency: CryptoCurrency) { + stakingRepository.fetchSingleYieldBalance( userWalletId = userWalletId, - cryptoCurrencies = existingCurrencies, + cryptoCurrency = addedCurrency, refresh = true, ) } - private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { + private suspend fun refreshUpdatedQuotes(currencyToAdd: CryptoCurrency) { quotesRepository.fetchQuotes( - currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, + currenciesIds = setOfNotNull(currencyToAdd.id.rawCurrencyId), refresh = true, ) } @@ -194,9 +175,9 @@ class AddCryptoCurrenciesUseCase( ) } - private suspend fun Raise.addCurrencies(userWalletId: UserWalletId, tokens: List) { + private suspend fun Raise.addCurrencies(userWalletId: UserWalletId, currency: CryptoCurrency) { catch( - { currenciesRepository.addCurrencies(userWalletId, tokens) }, + { currenciesRepository.addCurrencies(userWalletId, listOf(currency)) }, ) { raise(it) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt index edceddb46d..822eb5efd4 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultAddCustomTokenComponent.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.DelicateDecomposeApi import com.arkivanov.decompose.extensions.compose.stack.Children import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.extensions.compose.subscribeAsState @@ -13,6 +14,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.tokens.model.Network import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent import com.tangem.features.managetokens.component.AddCustomTokenComponent import com.tangem.features.managetokens.component.CustomTokenFormComponent @@ -171,6 +173,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( showForm(derivationPath = derivationPath) } + @OptIn(DelicateDecomposeApi::class) private fun showDerivationPathSelector(formValues: CustomTokenFormValues) { val currentConfig = contentStack.value.active.configuration @@ -181,6 +184,7 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( navigation.push(config) } + @OptIn(DelicateDecomposeApi::class) private fun showNetworkSelector(formValues: CustomTokenFormValues) { val currentConfig = contentStack.value.active.configuration @@ -194,14 +198,35 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor( private fun showForm(network: SelectedNetwork? = null, derivationPath: SelectedDerivationPath? = null) { val currentConfig = contentStack.value.active.configuration + val selectedNetwork = network ?: currentConfig.selectedNetwork + val selectedDerivationPath = derivationPath ?: currentConfig.selectedDerivationPath + val config = currentConfig.copy( step = AddCustomTokenConfig.Step.FORM, - selectedNetwork = network ?: currentConfig.selectedNetwork, - selectedDerivationPath = derivationPath ?: currentConfig.selectedDerivationPath, + selectedNetwork = selectedNetwork, + selectedDerivationPath = defineSelectedDerivationPath(selectedNetwork, selectedDerivationPath), ) + navigation.replaceAll(config) } + private fun defineSelectedDerivationPath( + selectedNetwork: SelectedNetwork?, + selectedDerivationPath: SelectedDerivationPath?, + ): SelectedDerivationPath? { + // if default derivation path + if (selectedDerivationPath?.value == selectedNetwork?.derivationPath) return null + + // map to custom derivation path + return if (selectedDerivationPath != null && selectedDerivationPath.value.value != null) { + selectedDerivationPath.copy( + value = Network.DerivationPath.Custom(value = requireNotNull(selectedDerivationPath.value.value)), + ) + } else { + null + } + } + private fun dismissAndNotify() { dismiss() params.onCurrencyAdded() diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt index ed8f3c543a..97488cd589 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt @@ -71,7 +71,7 @@ internal class OnrampAddToPortfolioModel @Inject constructor( addCryptoCurrenciesUseCase( userWalletId = params.userWalletId, - currencies = listOfNotNull(params.cryptoCurrency), + currency = params.cryptoCurrency, ) .onRight { params.onSuccessAdding(params.cryptoCurrency.id) } .onLeft { changeAddButtonProgressStatus(isProgress = false) } diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 4315d06992..c580236ad8 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -40,6 +40,8 @@ internal class ReferralInteractorImpl( } val cryptoCurrency = repository.getCryptoCurrency(userWalletId = userWallet.walletId, tokenData = tokenData) + ?: error("Failed to create crypto currency") + derivePublicKeysUseCase(userWallet.walletId, listOfNotNull(cryptoCurrency)).getOrElse { Timber.e("Failed to derive public keys: $it") throw it.mapToDomainError() @@ -47,12 +49,12 @@ internal class ReferralInteractorImpl( addCryptoCurrenciesUseCase( userWalletId = userWallet.walletId, - currencies = listOfNotNull(cryptoCurrency), + currency = cryptoCurrency, ) val publicAddress = userWalletManager.getWalletAddress( networkId = tokenData.networkId, - derivationPath = cryptoCurrency?.network?.derivationPath?.value, + derivationPath = cryptoCurrency.network.derivationPath.value, ) return repository.startReferral( From 21d37bf95a46a0ba8037d348620a832ae9c0caa7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 8 Apr 2025 14:12:55 +0000 Subject: [PATCH 127/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 32be17375b..f33bae32b2 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "release-app_5.22-997" +tangemBlockchainSdk = "releases-5.23-1007" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.22-440" +tangemCardSdk = "releases-5.23-452" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From ad0f9678d7d5378ad243e3b5f95ad7d1a0e1b5ea Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 11:08:28 +0500 Subject: [PATCH 128/138] Updated on 2026-08-14 --- .../usecase/ValidateWalletAddressUseCase.kt | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt index c5a93fdc80..026dca2e67 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt @@ -26,15 +26,14 @@ class ValidateWalletAddressUseCase( address: String, currencyAddress: Set?, ): AddressValidationResult { + val decodedXAddress = BlockchainUtils.decodeRippleXAddress(address, network.id.value) val isUtxoConsolidationAvailable = walletManagersFacade.checkUtxoConsolidationAvailability(userWalletId, network) - val isCurrentAddress = currencyAddress?.any { it.value == address } ?: true + + val addressToValidate = decodedXAddress?.address ?: address + val isCurrentAddress = currencyAddress?.any { it.value == addressToValidate } ?: true val isForbidSelfSend = isCurrentAddress && !isUtxoConsolidationAvailable - - val decodedXAddress = BlockchainUtils.decodeRippleXAddress(address, network.id.value) - val addressToValidate = decodedXAddress?.address ?: address - val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, addressToValidate) return when { @@ -51,15 +50,14 @@ class ValidateWalletAddressUseCase( address: String, senderAddresses: List, ): AddressValidationResult { + val decodedXAddress = BlockchainUtils.decodeRippleXAddress(address, network.id.value) val isUtxoConsolidationAvailable = walletManagersFacade.checkUtxoConsolidationAvailability(userWalletId, network) - val isCurrentAddress = senderAddresses.any { it.address == address } + + val addressToValidate = decodedXAddress?.address ?: address + val isCurrentAddress = senderAddresses.any { it.address == addressToValidate } val isForbidSelfSend = isCurrentAddress && !isUtxoConsolidationAvailable - - val decodedXAddress = BlockchainUtils.decodeRippleXAddress(address, network.id.value) - val addressToValidate = decodedXAddress?.address ?: address - val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, addressToValidate) return when { From 8fef5574249de3b49f2cdf08c8c2d60f9b05a8c4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 11:08:51 +0500 Subject: [PATCH 129/138] Updated on 2026-08-14 --- .../v2/subcomponents/amount/SendAmountComponentParams.kt | 5 +++-- .../send/v2/subcomponents/amount/model/SendAmountModel.kt | 5 +---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt index 7185402604..0df4012c9d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt @@ -16,6 +16,7 @@ internal sealed class SendAmountComponentParams { abstract val userWallet: UserWallet abstract val appCurrency: AppCurrency abstract val cryptoCurrencyStatus: CryptoCurrencyStatus + abstract val predefinedAmountValue: String? data class AmountParams( override val state: AmountState, @@ -23,10 +24,10 @@ internal sealed class SendAmountComponentParams { override val userWallet: UserWallet, override val appCurrency: AppCurrency, override val cryptoCurrencyStatus: CryptoCurrencyStatus, + override val predefinedAmountValue: String?, val isEditMode: Boolean, val callback: ModelCallback, val currentRoute: Flow, - val predefinedAmountValue: String?, val isBalanceHidingFlow: StateFlow, ) : SendAmountComponentParams() @@ -36,8 +37,8 @@ internal sealed class SendAmountComponentParams { override val userWallet: UserWallet, override val appCurrency: AppCurrency, override val cryptoCurrencyStatus: CryptoCurrencyStatus, + override val predefinedAmountValue: String?, val blockClickEnableFlow: StateFlow, - val predefinedAmountValue: String?, val isPredefinedValues: Boolean, ) : SendAmountComponentParams() } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index 5f37e5f84f..40b5688362 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -98,10 +98,7 @@ internal class SendAmountModel @Inject constructor( ), ) } - val params = params as? SendAmountComponentParams.AmountBlockParams - if (params?.predefinedAmountValue != null) { - onAmountValueChange(params.predefinedAmountValue) - } + params.predefinedAmountValue?.let(::onAmountValueChange) } } From 98ba7092e3627ba2f77f9b2edb1d7abea404f2e1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 11:09:23 +0500 Subject: [PATCH 130/138] Updated on 2026-08-14 --- .../ui/notifications/NotificationsFactory.kt | 9 ++++----- .../send/v2/send/confirm/SendConfirmComponent.kt | 1 + .../v2/send/confirm/model/SendConfirmModel.kt | 3 +++ .../notifications/NotificationsComponent.kt | 1 + .../notifications/model/NotificationData.kt | 1 + .../notifications/model/NotificationsModel.kt | 16 ++++++++++++---- 6 files changed, 22 insertions(+), 9 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt index 55ee3a6906..008a2665bd 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -4,7 +4,6 @@ import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.amountScreen.utils.getFiatString -import com.tangem.common.ui.notifications.NotificationsFactory.addRequireDestinationFlagErrorNotification import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format @@ -115,10 +114,6 @@ object NotificationsFactory { } } - fun MutableList.addRequireDestinationFlagErrorNotification() { - add(NotificationUM.Error.DestinationMemoRequired) - } - fun MutableList.addMinimumAmountErrorNotification( minimumSendAmount: BigDecimal?, sendingAmount: BigDecimal, @@ -516,4 +511,8 @@ object NotificationsFactory { is CryptoCurrency.Token -> isChangeLowerThanDust } } + + private fun MutableList.addRequireDestinationFlagErrorNotification() { + add(NotificationUM.Error.DestinationMemoRequired) + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt index 3ed30ca608..ff6ba04840 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt @@ -94,6 +94,7 @@ internal class SendConfirmComponent( feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, appCurrency = params.appCurrency, destinationAddress = model.enteredDestination.orEmpty(), + memo = model.enteredMemo, amountValue = model.enteredAmount.orZero(), reduceAmountBy = model.reduceAmountBy.orZero(), isIgnoreReduce = model.isIgnoreReduce, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 7e089f9d66..e09a1175f2 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -131,6 +131,8 @@ internal class SendConfirmModel @Inject constructor( get() = amountState?.isIgnoreReduce == true val enteredDestination: String? get() = destinationUM?.addressTextField?.value + val enteredMemo: String? + get() = destinationUM?.memoTextField?.value val fee: Fee? get() = feeSelectorUM?.selectedFee val feeError: GetFeeError? @@ -459,6 +461,7 @@ internal class SendConfirmModel @Inject constructor( notificationsUpdateTrigger.triggerUpdate( data = NotificationData( destinationAddress = enteredDestination.orEmpty(), + memo = enteredMemo, amountValue = enteredAmount.orZero(), reduceAmountBy = reduceAmountBy.orZero(), isIgnoreReduce = isIgnoreReduce, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsComponent.kt index a7a941c816..14760575ab 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsComponent.kt @@ -46,6 +46,7 @@ internal class NotificationsComponent( val feeCryptoCurrencyStatus: CryptoCurrencyStatus, val appCurrency: AppCurrency, val destinationAddress: String, + val memo: String?, val amountValue: BigDecimal, val reduceAmountBy: BigDecimal, val isIgnoreReduce: Boolean, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationData.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationData.kt index adf6dc6e5f..bf7e440869 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationData.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationData.kt @@ -6,6 +6,7 @@ import java.math.BigDecimal data class NotificationData( val destinationAddress: String, + val memo: String?, val amountValue: BigDecimal, val reduceAmountBy: BigDecimal, val isIgnoreReduce: Boolean, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt index 9ff6f21955..b27fefd5e3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt @@ -79,6 +79,7 @@ internal class NotificationsModel @Inject constructor( private val appCurrency = params.appCurrency private var destinationAddress = params.destinationAddress + private var memo = params.memo private var amountValue = params.amountValue private var reduceAmountBy = params.reduceAmountBy private var isIgnoreReduce = params.isIgnoreReduce @@ -110,6 +111,7 @@ internal class NotificationsModel @Inject constructor( private suspend fun updateState(data: NotificationData) { destinationAddress = data.destinationAddress + memo = data.memo amountValue = data.amountValue reduceAmountBy = data.reduceAmountBy isIgnoreReduce = data.isIgnoreReduce @@ -134,6 +136,7 @@ internal class NotificationsModel @Inject constructor( ) addDomainNotifications( destinationAddress = destinationAddress, + memo = memo, amountValue = amountValue, reduceAmountBy = reduceAmountBy, fee = fee, @@ -160,6 +163,7 @@ internal class NotificationsModel @Inject constructor( private suspend fun MutableList.addDomainNotifications( destinationAddress: String, + memo: String?, amountValue: BigDecimal, reduceAmountBy: BigDecimal, fee: Fee?, @@ -199,6 +203,8 @@ internal class NotificationsModel @Inject constructor( currencyCheck = currencyCheck, ) addWarningNotifications( + destinationAddress = destinationAddress, + memo = memo, enteredAmount = amountValue, fee = fee, feeValue = feeValue, @@ -288,6 +294,8 @@ internal class NotificationsModel @Inject constructor( } private suspend fun MutableList.addWarningNotifications( + destinationAddress: String, + memo: String?, enteredAmount: BigDecimal, sendingAmount: BigDecimal, fee: Fee?, @@ -297,11 +305,11 @@ internal class NotificationsModel @Inject constructor( ) { val validationError = validateTransactionUseCase( userWalletId = userWalletId, - amount = enteredAmount.convertToSdkAmount(currency), + amount = amountValue.convertToSdkAmount(cryptoCurrencyStatus.currency), fee = fee, - memo = null, - destination = "", - network = currency.network, + memo = memo, + destination = destinationAddress, + network = cryptoCurrencyStatus.currency.network, ).leftOrNull() addRentExemptionNotification( From 13a9dba8ab000ce17df017052fc85d418720b52f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 8 Apr 2025 20:09:32 +0400 Subject: [PATCH 131/138] Updated on 2026-08-14 --- .../multi/DefaultMultiNetworkStatusFetcher.kt | 9 ++++ .../DefaultMultiNetworkStatusProducer.kt | 9 ++-- .../DefaultSingleNetworkStatusFetcher.kt | 7 ++- .../DefaultSingleNetworkStatusProducer.kt | 5 ++ .../store/DefaultNetworksStatusesStoreV2.kt | 32 +++++++++++-- .../networks/store/NetworksStatusesStoreV2.kt | 3 ++ .../DefaultMultiNetworkStatusFetcherTest.kt | 16 ++++++- .../DefaultMultiNetworkStatusProducerTest.kt | 3 ++ .../DefaultSingleNetworkStatusFetcherTest.kt | 47 +++++++++++++++++-- .../DefaultSingleNetworkStatusProducerTest.kt | 3 ++ .../NetworkStatusesStoreUpdateMethodsTest.kt | 45 +++++++++++++++++- .../com/tangem/domain/core/utils/EitherExt.kt | 13 +++++ .../single/SingleNetworkStatusFetcher.kt | 13 ++++- .../CachedCurrenciesStatusesOperations.kt | 25 ++++++++-- 14 files changed, 206 insertions(+), 24 deletions(-) diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt index 71d51a5793..c807f56761 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt @@ -3,6 +3,7 @@ package com.tangem.data.networks.multi import arrow.core.Either import arrow.core.raise.either import arrow.core.raise.ensure +import com.tangem.data.networks.store.NetworksStatusesStoreV2 import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import kotlinx.coroutines.async @@ -14,14 +15,21 @@ import javax.inject.Inject * Default implementation of [MultiNetworkStatusFetcher] * * @property singleNetworkStatusFetcher single network status fetcher + * @property networksStatusesStore networks statuses store * [REDACTED_AUTHOR] */ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, + private val networksStatusesStore: NetworksStatusesStoreV2, ) : MultiNetworkStatusFetcher { override suspend fun invoke(params: MultiNetworkStatusFetcher.Params): Either = either { + // Optimization! + // Every singleNetworkStatusFetcher with applyRefresh as true will refresh every network in the store. + // So if we update all networks at once, it will be more efficient. + networksStatusesStore.refresh(userWalletId = params.userWalletId, networks = params.networks) + val result = coroutineScope { params.networks .map { @@ -30,6 +38,7 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( params = SingleNetworkStatusFetcher.Params( userWalletId = params.userWalletId, network = it, + applyRefresh = false, ), ) } diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt index 2e060a8d75..84acf20972 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt @@ -6,13 +6,11 @@ import com.tangem.data.networks.store.NetworksStatusesStoreV2 import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEmpty +import kotlinx.coroutines.flow.* import timber.log.Timber /** @@ -22,12 +20,14 @@ import timber.log.Timber * @property networksStatusesStore networks statuses store * @property userWalletsStore user wallets store * @property excludedBlockchains excluded blockchains + * @property dispatchers dispatchers */ internal class DefaultMultiNetworkStatusProducer @AssistedInject constructor( @Assisted val params: MultiNetworkStatusProducer.Params, private val networksStatusesStore: NetworksStatusesStoreV2, private val userWalletsStore: UserWalletsStore, private val excludedBlockchains: ExcludedBlockchains, + private val dispatchers: CoroutineDispatcherProvider, ) : MultiNetworkStatusProducer { override val fallback: Set @@ -57,6 +57,7 @@ internal class DefaultMultiNetworkStatusProducer @AssistedInject constructor( } .distinctUntilChanged() .onEmpty { emit(value = hashSetOf()) } + .flowOn(dispatchers.default) } @AssistedFactory diff --git a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt index 9da7afbce4..6ded8b6524 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcher.kt @@ -14,6 +14,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.core.utils.catchOn import com.tangem.domain.demo.DemoConfig import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.tokens.model.CryptoCurrency @@ -51,8 +52,10 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor( private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(excludedBlockchains) private val networkStatusFactory = NetworkStatusFactory() - override suspend fun invoke(params: SingleNetworkStatusFetcher.Params): Either = Either.catch { - networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network) + override suspend fun invoke(params: SingleNetworkStatusFetcher.Params) = Either.catchOn(dispatchers.default) { + if (params.applyRefresh) { + networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network) + } val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) val networkCurrencies = createCurrencies(userWallet = userWallet, network = params.network) diff --git a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducer.kt b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducer.kt index a91cf178d7..5438949c19 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducer.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducer.kt @@ -4,11 +4,13 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusProducer import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapNotNull /** @@ -16,12 +18,14 @@ import kotlinx.coroutines.flow.mapNotNull * * @property params params * @property multiNetworkStatusSupplier multi network status supplier + * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ internal class DefaultSingleNetworkStatusProducer @AssistedInject constructor( @Assisted val params: SingleNetworkStatusProducer.Params, private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, + private val dispatchers: CoroutineDispatcherProvider, ) : SingleNetworkStatusProducer { override val fallback: NetworkStatus @@ -35,6 +39,7 @@ internal class DefaultSingleNetworkStatusProducer @AssistedInject constructor( statuses.firstOrNull { it.network == params.network } } .distinctUntilChanged() + .flowOn(dispatchers.default) } @AssistedFactory diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt index 776b446e54..dedbcb34e5 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStoreV2.kt @@ -58,6 +58,29 @@ internal class DefaultNetworksStatusesStoreV2( updateStatusSourceInRuntime(userWalletId = userWalletId, network = network, source = StatusSource.CACHE) } + override suspend fun refresh(userWalletId: UserWalletId, networks: Set) { + val simpleStatusIds = networks.map(SimpleNetworkStatus::Id) + + runtimeStore.update(default = emptyMap()) { stored -> + stored.toMutableMap().apply { + val storedStatuses = this[userWalletId.stringValue].orEmpty() + + val statuses = simpleStatusIds.mapTo(hashSetOf()) { simpleStatusId -> + val status = storedStatuses.firstOrNull { it.id == simpleStatusId } + ?: createDefaultStatus(id = simpleStatusId) + + status.copy( + value = status.value.copySealed(source = StatusSource.CACHE), + ) + } + + val updatedStatuses = storedStatuses.addOrReplace(statuses) { old, new -> old.id == new.id } + + put(key = userWalletId.stringValue, value = updatedStatuses) + } + } + } + override suspend fun storeActual(userWalletId: UserWalletId, value: NetworkStatus) { if (value.value.source != StatusSource.ACTUAL) { error("Method storeActual can be called only with StatusSource.ACTUAL") @@ -84,10 +107,7 @@ internal class DefaultNetworksStatusesStoreV2( stored.toMutableMap().apply { val status = this[userWalletId.stringValue].orEmpty() .firstOrNull { it.id == simpleStatusId } - ?: SimpleNetworkStatus( - id = simpleStatusId, - value = NetworkStatus.Unreachable(address = null), - ) + ?: createDefaultStatus(id = simpleStatusId) val updatedStatus = status.copy( value = status.value.copySealed(source = source), @@ -130,4 +150,8 @@ internal class DefaultNetworksStatusesStoreV2( } } } + + private fun createDefaultStatus(id: SimpleNetworkStatus.Id): SimpleNetworkStatus { + return SimpleNetworkStatus(id = id, value = NetworkStatus.Unreachable(address = null)) + } } \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt b/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt index 356df1dbf1..3810940ad9 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStoreV2.kt @@ -15,6 +15,9 @@ internal interface NetworksStatusesStoreV2 { /** Refresh status of [network] by [userWalletId] */ suspend fun refresh(userWalletId: UserWalletId, network: Network) + /** Refresh statuses of [networks] by [userWalletId] */ + suspend fun refresh(userWalletId: UserWalletId, networks: Set) + /** Store actual [NetworkStatus] by [userWalletId] */ suspend fun storeActual(userWalletId: UserWalletId, value: NetworkStatus) diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt index f74995432d..ab79da0dd1 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt @@ -4,6 +4,7 @@ import arrow.core.left import arrow.core.right import com.google.common.truth.Truth import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.networks.store.NetworksStatusesStoreV2 import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.wallets.models.UserWalletId @@ -19,8 +20,12 @@ import org.junit.Test internal class DefaultMultiNetworkStatusFetcherTest { private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher = mockk() + private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxed = true) - private val fetcher = DefaultMultiNetworkStatusFetcher(singleNetworkStatusFetcher = singleNetworkStatusFetcher) + private val fetcher = DefaultMultiNetworkStatusFetcher( + singleNetworkStatusFetcher = singleNetworkStatusFetcher, + networksStatusesStore = networksStatusesStore, + ) @Test fun `fetch networks statuses successfully`() = runTest { @@ -29,11 +34,13 @@ internal class DefaultMultiNetworkStatusFetcherTest { val ethParams = SingleNetworkStatusFetcher.Params( userWalletId = userWalletId, network = ethereumAndStellar.first(), + applyRefresh = false, ) val stellarParams = SingleNetworkStatusFetcher.Params( userWalletId = userWalletId, network = ethereumAndStellar.last(), + applyRefresh = false, ) coEvery { singleNetworkStatusFetcher(ethParams) } returns Unit.right() @@ -42,6 +49,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { val actual = fetcher(params) coVerify { + networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) singleNetworkStatusFetcher(ethParams) singleNetworkStatusFetcher(stellarParams) } @@ -56,11 +64,13 @@ internal class DefaultMultiNetworkStatusFetcherTest { val ethParams = SingleNetworkStatusFetcher.Params( userWalletId = userWalletId, network = ethereumAndStellar.first(), + applyRefresh = false, ) val stellarParams = SingleNetworkStatusFetcher.Params( userWalletId = userWalletId, network = ethereumAndStellar.last(), + applyRefresh = false, ) val ethException = IllegalStateException("eth") @@ -70,6 +80,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { val actual = fetcher(params) coVerify { + networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) singleNetworkStatusFetcher(ethParams) singleNetworkStatusFetcher(stellarParams) } @@ -88,11 +99,13 @@ internal class DefaultMultiNetworkStatusFetcherTest { val ethParams = SingleNetworkStatusFetcher.Params( userWalletId = userWalletId, network = ethereumAndStellar.first(), + applyRefresh = false, ) val stellarParams = SingleNetworkStatusFetcher.Params( userWalletId = userWalletId, network = ethereumAndStellar.last(), + applyRefresh = false, ) coEvery { singleNetworkStatusFetcher(ethParams) } returns IllegalStateException("eth").left() @@ -101,6 +114,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { val actual = fetcher(params) coVerify { + networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar) singleNetworkStatusFetcher(ethParams) singleNetworkStatusFetcher(stellarParams) } diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt index ebed6000a6..274e82eae5 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt @@ -14,6 +14,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.configs.GenericCardConfig import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -32,12 +33,14 @@ internal class DefaultMultiNetworkStatusProducerTest { private val networksStatusesStore = mockk() private val userWalletsStore = mockk() private val excludedBlockchains = mockk() + private val dispatchers = TestingCoroutineDispatcherProvider() private val producer = DefaultMultiNetworkStatusProducer( params = params, networksStatusesStore = networksStatusesStore, userWalletsStore = userWalletsStore, excludedBlockchains = excludedBlockchains, + dispatchers = dispatchers, ) @Before diff --git a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt index b448cddeec..051cbca91b 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusFetcherTest.kt @@ -10,9 +10,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.Ordering import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.coVerifyOrder import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Test @@ -37,14 +37,18 @@ internal class DefaultSingleNetworkStatusFetcherTest { @Test fun `fetch network status successfully`() = runTest { - val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network) + val params = SingleNetworkStatusFetcher.Params( + userWalletId = userWalletId, + network = network, + applyRefresh = true, + ) val result = UpdateWalletManagerResult.MissedDerivation coEvery { walletManagersFacade.update(userWalletId, network, emptySet()) } returns result val actual = fetcher(params) - coVerify(ordering = Ordering.SEQUENCE) { + coVerifyOrder { networksStatusesStore.refresh(userWalletId = userWalletId, network = network) userWalletsStore.getSyncStrict(key = userWalletId) walletManagersFacade.update(userWalletId, network, emptySet()) @@ -59,14 +63,18 @@ internal class DefaultSingleNetworkStatusFetcherTest { @Test fun `fetch network status failure`() = runTest { - val params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network) + val params = SingleNetworkStatusFetcher.Params( + userWalletId = userWalletId, + network = network, + applyRefresh = true, + ) val exception = IllegalStateException() coEvery { userWalletsStore.getSyncStrict(key = userWalletId) } throws exception val actual = fetcher(params) - coVerify(ordering = Ordering.SEQUENCE) { + coVerifyOrder { networksStatusesStore.refresh(userWalletId = userWalletId, network = network) userWalletsStore.getSyncStrict(key = userWalletId) networksStatusesStore.storeError(userWalletId = userWalletId, network = network) @@ -81,6 +89,35 @@ internal class DefaultSingleNetworkStatusFetcherTest { Truth.assertThat(actual.leftOrNull()).isEqualTo(exception) } + @Test + fun `fetch network status if applyRefresh is false`() = runTest { + val params = SingleNetworkStatusFetcher.Params( + userWalletId = userWalletId, + network = network, + applyRefresh = false, + ) + + val result = UpdateWalletManagerResult.MissedDerivation + coEvery { walletManagersFacade.update(userWalletId, network, emptySet()) } returns result + + val actual = fetcher(params) + + coVerifyOrder { + userWalletsStore.getSyncStrict(key = userWalletId) + walletManagersFacade.update(userWalletId, network, emptySet()) + networksStatusesStore.storeActual( + userWalletId = userWalletId, + value = NetworkStatus(network, NetworkStatus.MissedDerivation), + ) + } + + coVerify(inverse = true) { + networksStatusesStore.refresh(userWalletId = any(), network = any()) + } + + Truth.assertThat(actual.isRight()).isTrue() + } + private companion object { val userWalletId = UserWalletId("011") val network = MockCryptoCurrencyFactory().ethereum.network diff --git a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducerTest.kt b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducerTest.kt index 220e8b819f..98894ea109 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducerTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/single/DefaultSingleNetworkStatusProducerTest.kt @@ -9,6 +9,7 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusProducer import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -27,10 +28,12 @@ internal class DefaultSingleNetworkStatusProducerTest { ) private val multiNetworkStatusSupplier = mockk() + private val dispatchers = TestingCoroutineDispatcherProvider() private val producer = DefaultSingleNetworkStatusProducer( params = params, multiNetworkStatusSupplier = multiNetworkStatusSupplier, + dispatchers = dispatchers, ) @Test diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt index d2e5f3c533..57aea3c1e2 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/store/NetworkStatusesStoreUpdateMethodsTest.kt @@ -32,7 +32,7 @@ internal class NetworkStatusesStoreUpdateMethodsTest { ) @Test - fun `refresh if runtime store is empty`() = runTest { + fun `refresh the single network if runtime store is empty`() = runTest { store.refresh(userWalletId = userWalletId, network = network) val runtimeExpected = mapOf( @@ -49,7 +49,7 @@ internal class NetworkStatusesStoreUpdateMethodsTest { } @Test - fun `refresh if runtime store contains status with this network`() = runTest { + fun `refresh the single network if runtime store contains status with this network`() = runTest { val status = MockNetworkStatusFactory.createVerified().toSimple() runtimeStore.store( @@ -68,6 +68,45 @@ internal class NetworkStatusesStoreUpdateMethodsTest { Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) } + @Test + fun `refresh the multi networks if runtime store is empty`() = runTest { + store.refresh(userWalletId = userWalletId, networks = networks) + + val runtimeExpected = mapOf( + userWalletId.stringValue to networks.mapTo(hashSetOf()) { + SimpleNetworkStatus( + id = SimpleNetworkStatus.Id(it), + value = NetworkStatus.Unreachable(address = null), + ) + }, + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + + @Test + fun `refresh the multi networks if runtime store contains status with this network`() = runTest { + val firstStatus = MockNetworkStatusFactory.createVerified(network = networks.first()).toSimple() + val secondStatus = MockNetworkStatusFactory.createVerified(network = networks.last()).toSimple() + + runtimeStore.store( + value = mapOf(userWalletId.stringValue to setOf(firstStatus, secondStatus)), + ) + + store.refresh(userWalletId = userWalletId, networks = networks) + + val runtimeExpected = mapOf( + userWalletId.stringValue to setOf( + firstStatus.copy(value = firstStatus.value.copySealed(source = StatusSource.CACHE)), + secondStatus.copy(value = secondStatus.value.copySealed(source = StatusSource.CACHE)), + ), + ) + + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) + } + @Test fun `store actual with any status sources`() = runTest { val expectedErrorMessage = "Method storeActual can be called only with StatusSource.ACTUAL" @@ -210,5 +249,7 @@ internal class NetworkStatusesStoreUpdateMethodsTest { val userWalletId = UserWalletId(stringValue = "011") val network = MockCryptoCurrencyFactory().ethereum.network + + val networks = MockCryptoCurrencyFactory().ethereumAndStellar.mapTo(hashSetOf()) { it.network } } } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/utils/EitherExt.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/utils/EitherExt.kt index 457f3e4f5b..b961210633 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/utils/EitherExt.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/utils/EitherExt.kt @@ -2,7 +2,9 @@ package com.tangem.domain.core.utils import arrow.core.Either import com.tangem.domain.core.lce.Lce +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext /** * [Flow] of [Either] @@ -34,4 +36,15 @@ inline fun Either.toLce(isStillLoading: } } } +} + +@JvmName("tryCatchWithDispatch") +suspend inline fun Either.Companion.catchOn( + dispatcher: CoroutineDispatcher, + crossinline function: suspend () -> R, +): Either { + return withContext( + context = dispatcher, + block = { catch { function() } }, + ) } \ No newline at end of file diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt index 791e66ea05..d9ab0fbece 100644 --- a/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt +++ b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusFetcher.kt @@ -11,5 +11,16 @@ import com.tangem.domain.wallets.models.UserWalletId */ interface SingleNetworkStatusFetcher : FlowFetcher { - data class Params(val userWalletId: UserWalletId, val network: Network) + /** + * Params + * + * @property userWalletId wallet id + * @property network network + * @property applyRefresh flag that determines whether to apply refresh (see DefaultMultiNetworkStatusFetcher) + */ + data class Params( + val userWalletId: UserWalletId, + val network: Network, + val applyRefresh: Boolean = true, + ) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index b43e518a08..3334482cf9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -68,13 +68,12 @@ class CachedCurrenciesStatusesOperations( val prevStatuses = MutableStateFlow(value = emptyList()) val isUpdating = MutableStateFlow(value = true) - val isFetchingStarted = MutableStateFlow(value = false) val nonEmptyCurrencies = currenciesFlow.mapNotNull { it.getOrNull() }.firstOrNull()?.toNonEmptyListOrNull() - if (nonEmptyCurrencies != null) { + if (!isFetchingStarted(userWalletId) && nonEmptyCurrencies != null) { launch { - isFetchingStarted.value = true + setFetchStarted(userWalletId) val (networks, currenciesIds) = getIds(nonEmptyCurrencies) fetchComponents(userWalletId, networks, currenciesIds, nonEmptyCurrencies) @@ -132,9 +131,9 @@ class CachedCurrenciesStatusesOperations( ) } - if (!isFetchingStarted.value) { + if (!isFetchingStarted(userWalletId)) { launch { - isFetchingStarted.value = true + setFetchStarted(userWalletId) fetchComponents(userWalletId, networks, currenciesIds, currencies) } @@ -331,6 +330,7 @@ class CachedCurrenciesStatusesOperations( } // temporary code because token list is built using networks list + @OptIn(FlowPreview::class) private fun getNetworkStatusesUpdates( userWalletId: UserWalletId, networks: NonEmptySet, @@ -356,11 +356,26 @@ class CachedCurrenciesStatusesOperations( .onEach(::send) .launchIn(scope = this) } + .debounce(timeoutMillis = 500) .map, Either>> { it.right() } .distinctUntilChanged() } + private fun isFetchingStarted(userWalletId: UserWalletId): Boolean { + return fetchingState.value[userWalletId] ?: false + } + + private fun setFetchStarted(userWalletId: UserWalletId) { + fetchingState.update { + it.toMutableMap().apply { + put(key = userWalletId, value = true) + } + } + } + companion object { internal const val RETRY_DELAY = 2000L + + private val fetchingState = MutableStateFlow(value = emptyMap()) } } \ No newline at end of file From 95a1e6c76651694acdabd9478a775857cce2b417 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 14:59:58 +0400 Subject: [PATCH 132/138] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 5 ++++ .../local/datastore/RuntimeSharedStore.kt | 12 ++++++--- .../usecase/SendTransactionUseCase.kt | 25 +++++++++++++++---- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index a693b4963e..adc63cbd3d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -35,12 +36,16 @@ internal object TransactionDomainModule { cardSdkConfigRepository: CardSdkConfigRepository, transactionRepository: TransactionRepository, walletManagersFacade: WalletManagersFacade, + singleNetworkStatusFetcher: SingleNetworkStatusFetcher, + tokensFeatureToggles: TokensFeatureToggles, ): SendTransactionUseCase { return SendTransactionUseCase( demoConfig = DemoConfig(), cardSdkConfigRepository = cardSdkConfigRepository, transactionRepository = transactionRepository, walletManagersFacade = walletManagersFacade, + singleNetworkStatusFetcher = singleNetworkStatusFetcher, + tokensFeatureToggles = tokensFeatureToggles, ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeSharedStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeSharedStore.kt index 1636a37cd0..d32fa5215c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeSharedStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeSharedStore.kt @@ -4,6 +4,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock /** * Runtime shared store @@ -42,6 +44,7 @@ interface RuntimeSharedStore { operator fun invoke(): RuntimeSharedStore = object : RuntimeSharedStore { private val flow = MutableSharedFlow(replay = 1) + private val updateMutex = Mutex() init { flow.tryEmit(value = null) @@ -58,10 +61,13 @@ interface RuntimeSharedStore { } override suspend fun update(default: T, function: (T) -> T) { - val storedData = flow.firstOrNull() ?: default - val updatedData = function(storedData) + // required to synchronize get (firstOrNull) and update (emit) operations + updateMutex.withLock { + val storedData = flow.firstOrNull() ?: default + val updatedData = function(storedData) - flow.emit(value = updatedData) + flow.emit(value = updatedData) + } } } } diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index ba89fcc80f..15efbc7500 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -12,12 +12,14 @@ import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.network.ResultChecker import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.simple +import com.tangem.domain.card.models.TwinKey import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.DemoTransactionSender -import com.tangem.domain.card.models.TwinKey +import com.tangem.domain.networks.single.SingleNetworkStatusFetcher +import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.error.SendTransactionError @@ -30,6 +32,8 @@ class SendTransactionUseCase( private val cardSdkConfigRepository: CardSdkConfigRepository, private val transactionRepository: TransactionRepository, private val walletManagersFacade: WalletManagersFacade, + private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, + private val tokensFeatureToggles: TokensFeatureToggles, ) { suspend operator fun invoke( txsData: List, @@ -76,10 +80,21 @@ class SendTransactionUseCase( } cardSdkConfigRepository.setLinkedTerminal(linkedTerminal) - return sendResult.fold( - ifRight = { result -> result.hashes.right() }, - ifLeft = { it.left() }, - ) + return sendResult + .onRight { + if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { + singleNetworkStatusFetcher( + params = SingleNetworkStatusFetcher.Params( + userWalletId = userWallet.walletId, + network = network, + ), + ) + } + } + .fold( + ifRight = { result -> result.hashes.right() }, + ifLeft = { it.left() }, + ) } suspend operator fun invoke( From d11aac41089f2af5bc726c21ca9b2374bb14536d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 15:39:27 +0300 Subject: [PATCH 133/138] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/MainActivity.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 9508f53963..3eb6102ce8 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -299,9 +299,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } private fun setRootContent() { + // for now activity is singleTop and after going to ChromeCustomTab it calls onCreate but onDestroy + // doesn't calls. It lead to issue that decompose nav stack is not saved in bundle and to restore it + // we try to init component with previous stack val routingComponent = routingComponentFactory.create( context = rootComponentContext, - initialStack = null, + initialStack = appRouterConfig.stack, ) setContent { From b5ef5e6c4bf78b6d739112cb1f2ee91377154a2b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 19:01:17 +0500 Subject: [PATCH 134/138] Updated on 2026-08-14 --- .../v2/subcomponents/destination/model/SendDestinationModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 2f4e77b25d..14f2d7a777 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -104,7 +104,7 @@ internal class SendDestinationModel @Inject constructor( ), ) val params = params as? SendDestinationComponentParams.DestinationBlockParams - if (params?.predefinedAddressValue != null && params.predefinedMemoValue != null) { + if (params?.predefinedAddressValue != null) { _uiState.update( SendDestinationPredefinedStateTransformer( address = params.predefinedAddressValue, From 84070c0990df4450cdddf9a0b0f2209deaf8b04b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 17:11:38 +0400 Subject: [PATCH 135/138] Updated on 2026-08-14 --- features/wallet/impl/build.gradle.kts | 41 ++++++++++--------- .../intents/WalletWarningsClickIntents.kt | 25 ++++++++--- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 595cdad4de..da4e888dfe 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -66,33 +66,34 @@ dependencies { implementation(projects.libs.blockchainSdk) /** Domain modules */ - implementation(projects.domain.card) - implementation(projects.domain.demo) - implementation(projects.domain.legacy) - implementation(projects.domain.models) - implementation(projects.domain.settings) - implementation(projects.domain.tokens) - implementation(projects.domain.tokens.models) - implementation(projects.domain.txhistory) - implementation(projects.domain.txhistory.models) - implementation(projects.domain.wallets) - implementation(projects.domain.wallets.models) + implementation(projects.domain.analytics) implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) - implementation(projects.domain.analytics) - implementation(projects.domain.visa) - implementation(projects.domain.staking.models) - implementation(projects.domain.staking) - implementation(projects.domain.markets.models) + implementation(projects.domain.card) + implementation(projects.domain.demo) implementation(projects.domain.feedback) - implementation(projects.domain.onramp.models) - implementation(projects.domain.onramp) - implementation(projects.domain.promo) - implementation(projects.domain.promo.models) + implementation(projects.domain.legacy) + implementation(projects.domain.markets.models) + implementation(projects.domain.models) + implementation(projects.domain.networks) implementation(projects.domain.nft) implementation(projects.domain.nft.models) + implementation(projects.domain.onramp) + implementation(projects.domain.onramp.models) + implementation(projects.domain.promo) + implementation(projects.domain.promo.models) + implementation(projects.domain.settings) + implementation(projects.domain.staking) + implementation(projects.domain.staking.models) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.txhistory) + implementation(projects.domain.txhistory.models) + implementation(projects.domain.visa) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index aab061ab81..419db01ebc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -12,6 +12,7 @@ import com.tangem.domain.card.SetCardWasScannedUseCase import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.promo.ShouldShowSwapPromoWalletUseCase import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder @@ -19,6 +20,7 @@ import com.tangem.domain.settings.NeverToSuggestRateAppUseCase import com.tangem.domain.settings.RemindToRateAppLaterUseCase import com.tangem.domain.tokens.FetchTokenListUseCase import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode +import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType @@ -101,6 +103,8 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase, private val urlOpener: UrlOpener, + private val tokensFeatureToggles: TokensFeatureToggles, + private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { @@ -149,12 +153,21 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( ).fold( ifLeft = { Timber.e(it, "Failed to derive public keys") }, ifRight = { - fetchTokenListUseCase( - userWalletId = userWallet.walletId, - mode = RefreshMode.SKIP_CURRENCIES, - currencies = missedAddressCurrencies, - ).onLeft { - Timber.e("Unable to refresh token list: $it") + if (tokensFeatureToggles.isNetworksLoadingRefactoringEnabled) { + multiNetworkStatusFetcher( + params = MultiNetworkStatusFetcher.Params( + userWalletId = userWallet.walletId, + networks = missedAddressCurrencies.map(CryptoCurrency::network).toSet(), + ), + ) + } else { + fetchTokenListUseCase( + userWalletId = userWallet.walletId, + mode = RefreshMode.SKIP_CURRENCIES, + currencies = missedAddressCurrencies, + ).onLeft { + Timber.e("Unable to refresh token list: $it") + } } }, ) From 92440eee900538647197995d59ecee66b3bacae8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Apr 2025 18:41:20 +0300 Subject: [PATCH 136/138] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 12 +++++++++--- settings.gradle.kts | 12 ++++++------ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index f33bae32b2..4a299587fa 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,9 +1,15 @@ [versions] -tangemBlockchainSdk = "releases-5.23-1007" +# Blockchain SDK, Card SDK and Vico artifacts are built in a private namespace and then published to a +# public Maven repository, so anyone can build the app from source +# https://github.com/tangem/blockchain-sdk-kotlin/packages/ +# https://github.com/tangem/tangem-sdk-android/ +# https://github.com/tangem/vico + +tangemBlockchainSdk = "releases-5.23-1015" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.23-452" +tangemCardSdk = "releases-5.23-456" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ -tangemVico = "2.0.0-alpha.25-tangem-developments8" +tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/settings.gradle.kts b/settings.gradle.kts index cbcae1d8bb..ddcc0c9bc5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -47,7 +47,7 @@ dependencyResolutionManagement { } maven { // setting any repository from tangem project allows maven search all packages in the project - url = uri("https://maven.pkg.github.com/tangem-developments/tangem-sdk-android") + url = uri("https://maven.pkg.github.com/tangem/tangem-sdk-android") credentials { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") @@ -56,7 +56,7 @@ dependencyResolutionManagement { } maven { // setting any repository from tangem project allows maven search all packages in the project - url = uri("https://maven.pkg.github.com/tangem-developments/blst-android") + url = uri("https://maven.pkg.github.com/tangem/blst-android") credentials { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") @@ -65,7 +65,7 @@ dependencyResolutionManagement { } maven { // setting any repository from tangem project allows maven search all packages in the project - url = uri("https://maven.pkg.github.com/tangem-developments/blockchain-sdk-kotlin") + url = uri("https://maven.pkg.github.com/tangem/blockchain-sdk-kotlin") credentials { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") @@ -74,7 +74,7 @@ dependencyResolutionManagement { } maven { // setting any repository from tangem project allows maven search all packages in the project - url = uri("https://maven.pkg.github.com/tangem-developments/wallet-core") + url = uri("https://maven.pkg.github.com/tangem/wallet-core") credentials { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") @@ -86,7 +86,7 @@ dependencyResolutionManagement { } maven { // setting any repository from tangem project allows maven search all packages in the project - url = uri("https://maven.pkg.github.com/tangem-developments/vico") + url = uri("https://maven.pkg.github.com/tangem/vico") credentials { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") @@ -97,7 +97,7 @@ dependencyResolutionManagement { } maven { // setting any repository from tangem project allows maven search all packages in the project - url = uri("https://maven.pkg.github.com/tangem-developments/ic4j-agent") + url = uri("https://maven.pkg.github.com/tangem/ic4j-agent") credentials { username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") From ccfc8790996422d0e8497c25aebaf7402a3b8ec2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 10 Apr 2025 12:58:13 +0500 Subject: [PATCH 137/138] Updated on 2026-08-14 --- .../v2/subcomponents/notifications/model/NotificationsModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt index b27fefd5e3..e09d7ef110 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt @@ -366,7 +366,7 @@ internal class NotificationsModel @Inject constructor( }, onCloseClick = { modelScope.launch { - buildNotifications() + sendAmountReduceTrigger.triggerIgnoreReduce() } }, ) From 7d349893403202dcf44d61a4359c0486ae4508aa Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 10 Apr 2025 12:58:21 +0500 Subject: [PATCH 138/138] Updated on 2026-08-14 --- .../v2/send/confirm/model/SendConfirmModel.kt | 4 +- .../amount/model/SendAmountModel.kt | 13 +++++ .../send/v2/subcomponents/fee/SendFeeData.kt | 8 +++ .../subcomponents/fee/SendFeeReloadTrigger.kt | 35 ++++++++----- .../v2/subcomponents/fee/di/SendFeeModule.kt | 28 +++++----- .../subcomponents/fee/model/SendFeeModel.kt | 52 ++++++++++++------- .../notifications/model/NotificationsModel.kt | 10 +++- 7 files changed, 102 insertions(+), 48 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeData.kt diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index e09a1175f2..9d8f6b0816 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -51,6 +51,7 @@ import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM import com.tangem.features.send.v2.send.ui.state.ButtonsUM import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger import com.tangem.features.send.v2.subcomponents.fee.model.checkAndCalculateSubtractedAmount import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM @@ -94,6 +95,7 @@ internal class SendConfirmModel @Inject constructor( private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger, + private val sendFeeCheckReloadListener: SendFeeCheckReloadListener, private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter, private val notificationsUpdateTrigger: NotificationsUpdateTrigger, private val alertFactory: SendConfirmAlertFactory, @@ -445,7 +447,7 @@ internal class SendConfirmModel @Inject constructor( } private fun subscribeOnCheckFeeResultUpdates() { - sendFeeCheckReloadTrigger.checkReloadResultFlow.onEach { isFeeResultSuccess -> + sendFeeCheckReloadListener.checkReloadResultFlow.onEach { isFeeResultSuccess -> if (isFeeResultSuccess) { sendIdleTimer = SystemClock.elapsedRealtime() _uiState.update(SendConfirmSendingStateTransformer(isSending = true)) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index 40b5688362..ce05dce8b0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -26,6 +26,8 @@ import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParam import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents.SelectedCurrencyType +import com.tangem.features.send.v2.subcomponents.fee.SendFeeData +import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero import com.tangem.utils.isNullOrZero @@ -43,6 +45,7 @@ internal class SendAmountModel @Inject constructor( private val router: Router, private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, private val sendAmountReduceListener: SendAmountReduceListener, + private val feeReloadTrigger: SendFeeReloadTrigger, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model(), AmountScreenClickIntents { @@ -174,6 +177,11 @@ internal class SendAmountModel @Inject constructor( value = reduceTo, ), ) + feeReloadTrigger.triggerUpdate( + feeData = SendFeeData( + amount = (uiState.value as? AmountState.Data)?.amountTextField?.cryptoAmount?.value, + ), + ) } .launchIn(modelScope) } @@ -188,6 +196,11 @@ internal class SendAmountModel @Inject constructor( value = reduceByData, ), ) + feeReloadTrigger.triggerUpdate( + feeData = SendFeeData( + amount = (uiState.value as? AmountState.Data)?.amountTextField?.cryptoAmount?.value, + ), + ) } .launchIn(modelScope) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeData.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeData.kt new file mode 100644 index 0000000000..7db1e3d289 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeData.kt @@ -0,0 +1,8 @@ +package com.tangem.features.send.v2.subcomponents.fee + +import java.math.BigDecimal + +data class SendFeeData( + val amount: BigDecimal?, + val destinationAddress: String? = null, +) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeReloadTrigger.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeReloadTrigger.kt index 6502eb6fa2..369baf924e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeReloadTrigger.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeReloadTrigger.kt @@ -8,15 +8,24 @@ import javax.inject.Singleton interface SendFeeReloadTrigger { - /** Flow triggers fee reload */ - val reloadTriggerFlow: Flow - /** Trigger fee update */ - suspend fun triggerUpdate() + suspend fun triggerUpdate(feeData: SendFeeData) +} + +interface SendFeeReloadListener { + /** Flow triggers fee reload */ + val reloadTriggerFlow: Flow } interface SendFeeCheckReloadTrigger { + /** Trigger return callback with check result */ + suspend fun callbackCheckResult(isSuccess: Boolean) + /** Trigger fee check reload */ + suspend fun triggerCheckUpdate() +} + +interface SendFeeCheckReloadListener { /** * Flow triggers fee check reload before transaction. * Usually after significant time after fee was updated last time @@ -25,18 +34,16 @@ interface SendFeeCheckReloadTrigger { /** Flow return result of fee check update */ val checkReloadResultFlow: Flow - - /** Trigger return callback with check result */ - suspend fun callbackCheckResult(isSuccess: Boolean) - - /** Trigger fee check reload */ - suspend fun triggerCheckUpdate() } @Singleton -internal class DefaultSendFeeReloadTrigger @Inject constructor() : SendFeeReloadTrigger, SendFeeCheckReloadTrigger { +internal class DefaultSendFeeReloadTrigger @Inject constructor() : + SendFeeReloadTrigger, + SendFeeReloadListener, + SendFeeCheckReloadTrigger, + SendFeeCheckReloadListener { - private val _reloadTriggerFlow = MutableSharedFlow() + private val _reloadTriggerFlow = MutableSharedFlow() override val reloadTriggerFlow = _reloadTriggerFlow.asSharedFlow() private val _checkReloadTriggerFlow = MutableSharedFlow() @@ -45,8 +52,8 @@ internal class DefaultSendFeeReloadTrigger @Inject constructor() : SendFeeReload private val _checkReloadResultFlow = MutableSharedFlow() override val checkReloadResultFlow = _checkReloadResultFlow.asSharedFlow() - override suspend fun triggerUpdate() { - _reloadTriggerFlow.emit(Unit) + override suspend fun triggerUpdate(feeData: SendFeeData) { + _reloadTriggerFlow.emit(feeData) } override suspend fun triggerCheckUpdate() { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/di/SendFeeModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/di/SendFeeModule.kt index 27cc5cacd4..53f687dcea 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/di/SendFeeModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/di/SendFeeModule.kt @@ -1,27 +1,29 @@ package com.tangem.features.send.v2.subcomponents.fee.di -import com.tangem.features.send.v2.subcomponents.fee.DefaultSendFeeReloadTrigger -import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger -import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger +import com.tangem.features.send.v2.subcomponents.fee.* +import dagger.Binds import dagger.Module -import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module -internal object SendFeeModule { +internal interface SendFeeModule { - @Provides + @Binds @Singleton - fun provideSendFeeReloadTrigger(): SendFeeReloadTrigger { - return DefaultSendFeeReloadTrigger() - } + fun provideSendFeeReloadTrigger(impl: DefaultSendFeeReloadTrigger): SendFeeReloadTrigger - @Provides + @Binds @Singleton - fun provideSendFeeCheckReloadTrigger(): SendFeeCheckReloadTrigger { - return DefaultSendFeeReloadTrigger() - } + fun provideSendFeeReloadListener(impl: DefaultSendFeeReloadTrigger): SendFeeReloadListener + + @Binds + @Singleton + fun provideSendFeeCheckReloadTrigger(impl: DefaultSendFeeReloadTrigger): SendFeeCheckReloadTrigger + + @Binds + @Singleton + fun provideSendFeeCheckReloadListener(impl: DefaultSendFeeReloadTrigger): SendFeeCheckReloadListener } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt index 0b53128a92..6f2014fce0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/SendFeeModel.kt @@ -14,9 +14,10 @@ import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.features.send.v2.common.NavigationUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.ui.state.ButtonsUM +import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams -import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger +import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadListener import com.tangem.features.send.v2.subcomponents.fee.analytics.SendFeeAnalyticEvents import com.tangem.features.send.v2.subcomponents.fee.analytics.SendFeeAnalyticEvents.GasPriceInserter import com.tangem.features.send.v2.subcomponents.fee.model.transformers.* @@ -29,6 +30,7 @@ import com.tangem.utils.coroutines.saveIn import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import java.math.BigDecimal import java.util.Locale import javax.inject.Inject @@ -44,7 +46,8 @@ internal class SendFeeModel @Inject constructor( private val urlOpener: UrlOpener, private val analyticsEventHandler: AnalyticsEventHandler, private val sendFeeAlertFactory: SendFeeAlertFactory, - private val feeReloadTrigger: SendFeeReloadTrigger, + private val feeReloadListener: SendFeeReloadListener, + private val feeCheckReloadListener: SendFeeCheckReloadListener, private val feeCheckReloadTrigger: SendFeeCheckReloadTrigger, ) : Model(), SendFeeClickIntents { @@ -65,14 +68,19 @@ internal class SendFeeModel @Inject constructor( subscribeOnFeeReloadTriggerUpdates() subscribeOnFeeCheckReloadTriggerUpdates() initialState() - loadFee() + feeReload() } fun updateState(state: FeeUM) { _uiState.value = state } - override fun feeReload() = loadFee() + override fun feeReload() { + loadFee( + amountValue = params.sendAmount, + destinationAddress = params.destinationAddress, + ) + } override fun onFeeSelectorClick(feeType: FeeType) { _uiState.update( @@ -152,13 +160,18 @@ internal class SendFeeModel @Inject constructor( } private fun subscribeOnFeeReloadTriggerUpdates() { - feeReloadTrigger.reloadTriggerFlow - .onEach { feeReload() } + feeReloadListener.reloadTriggerFlow + .onEach { (amount, destination) -> + loadFee( + amountValue = amount ?: params.sendAmount, + destinationAddress = destination ?: params.destinationAddress, + ) + } .launchIn(modelScope) } private fun subscribeOnFeeCheckReloadTriggerUpdates() { - feeCheckReloadTrigger.checkReloadTriggerFlow + feeCheckReloadListener.checkReloadTriggerFlow .onEach { checkLoadFee() } .launchIn(modelScope) } @@ -175,13 +188,18 @@ internal class SendFeeModel @Inject constructor( params.callback.onFeeResult(uiState.value) } - private fun loadFee() { + private fun loadFee(amountValue: BigDecimal, destinationAddress: String) { modelScope.launch { val isShowLoading = (uiState.value as? FeeUM.Content)?.feeSelectorUM !is FeeSelectorUM.Content if (isShowLoading) { _uiState.update(SendFeeLoadingTransformer) } - callFeeUseCase().fold( + getFeeUseCase.invoke( + amount = amountValue, + destination = destinationAddress, + userWallet = params.userWallet, + cryptoCurrency = cryptoCurrencyStatus.currency, + ).fold( ifRight = { _uiState.update( SendFeeLoadedTransformer( @@ -206,7 +224,12 @@ internal class SendFeeModel @Inject constructor( private fun checkLoadFee() { modelScope.launch { - callFeeUseCase().fold( + getFeeUseCase.invoke( + amount = params.sendAmount, + destination = params.destinationAddress, + userWallet = params.userWallet, + cryptoCurrency = cryptoCurrencyStatus.currency, + ).fold( ifRight = { sendFeeAlertFactory.getFeeUpdatedAlert( newFee = it, @@ -236,20 +259,13 @@ internal class SendFeeModel @Inject constructor( ifLeft = { feeError -> feeCheckReloadTrigger.callbackCheckResult(false) _uiState.update(SendFeeFailedTransformer(feeError)) - sendFeeAlertFactory.getFeeUnreachableErrorState(::loadFee) + sendFeeAlertFactory.getFeeUnreachableErrorState(::feeReload) updateFeeNotifications() }, ) }.saveIn(feeJobHolder) } - private suspend fun callFeeUseCase() = getFeeUseCase.invoke( - amount = params.sendAmount, - destination = params.destinationAddress, - userWallet = params.userWallet, - cryptoCurrency = cryptoCurrencyStatus.currency, - ) - private fun isFeeApproximate(amountType: AmountType): Boolean { val networkId = feeCryptoCurrencyStatus.currency.network.id return isFeeApproximateUseCase( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt index e09d7ef110..6775e4a9b6 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt @@ -32,6 +32,7 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase import com.tangem.domain.utils.convertToSdkAmount import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceTrigger +import com.tangem.features.send.v2.subcomponents.fee.SendFeeData import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger import com.tangem.features.send.v2.subcomponents.fee.model.checkAndCalculateSubtractedAmount import com.tangem.features.send.v2.subcomponents.fee.model.checkFeeCoverage @@ -52,7 +53,7 @@ import kotlinx.coroutines.launch import java.math.BigDecimal import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @Stable @ModelScoped internal class NotificationsModel @Inject constructor( @@ -129,7 +130,12 @@ internal class NotificationsModel @Inject constructor( feeError = feeError, onReload = { modelScope.launch { - sendFeeReloadTrigger.triggerUpdate() + sendFeeReloadTrigger.triggerUpdate( + feeData = SendFeeData( + amount = amountValue, + destinationAddress = destinationAddress, + ), + ) } }, onClick = ::showTokenDetails,