From f8d0276d456ebf861b3249d935ca2662b789e813 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Mar 2025 18:03:37 +0300 Subject: [PATCH 01/42] 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 02/42] 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 03/42] 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 04/42] 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 05/42] 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 06/42] 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 07/42] 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 08/42] 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 09/42] 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 10/42] 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 11/42] 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 12/42] 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 13/42] 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 14/42] 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 15/42] 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 16/42] 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 17/42] 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 18/42] 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 19/42] 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 20/42] 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 21/42] 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 22/42] 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 23/42] 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 24/42] 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 25/42] 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 26/42] 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 27/42] 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 28/42] 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 29/42] 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 30/42] 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 31/42] 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 32/42] 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 33/42] 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 34/42] 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 35/42] 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 36/42] 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 37/42] 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 a774347c704b8eeb4cb3bcce0956444a8578f946 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 26 Mar 2025 13:10:44 +0500 Subject: [PATCH 38/42] 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 39/42] 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 40/42] 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 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 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 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 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 41/42] 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 42/42] 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