diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index 5708a79d8b..42450edff8 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -1,7 +1,9 @@ package com.tangem.tap.data import android.content.Context +import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi +import com.squareup.moshi.Types import com.tangem.data.pay.entity.WithdrawStoreData import com.tangem.data.pay.util.WithdrawStateConverter import com.tangem.data.pay.util.WithdrawStoreDataConverter @@ -19,6 +21,7 @@ import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.withContext import java.util.UUID import javax.inject.Inject @@ -137,28 +140,56 @@ internal class DefaultTangemPayStorage @Inject constructor( return appPreferencesStore.getSyncOrNull(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId)) } - override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) { + override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) { appPreferencesStore.editData { mutablePreferences -> - val orders = mutablePreferences.getObjectMap( - PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, - ) - .plus(createWithdrawOrderIdKey(userWalletId) to withdrawStoreDataConverter.convert(data)) + val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + .plus(createWithdrawOrderIdKey(userWalletId) to orderId) mutablePreferences.setObjectMap( - key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, + key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, value = orders, ) } } - override suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState? { - val orders = appPreferencesStore.getObjectMapSync( - PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, - ) - val data = orders[createWithdrawOrderIdKey(userWalletId)] ?: return null - return withdrawStateConverter.convert(data) + override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) { + val listType = Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java) + val mapType = Types.newParameterizedType(Map::class.java, String::class.java, listType) + val adapter: JsonAdapter>> = appPreferencesStore.moshi.adapter(mapType) + appPreferencesStore.editData { prefs -> + val walletKey = createWithdrawOrderIdKey(userWalletId) + val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]?.let(adapter::fromJson).orEmpty() + val updatedList = currentMap[walletKey].orEmpty() + withdrawStoreDataConverter.convert(data) + prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = adapter + .toJson(currentMap + (walletKey to updatedList)) + } } - override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) { + override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? { + val orders = appPreferencesStore.getObjectMapSync(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + return orders[createWithdrawOrderIdKey(userWalletId)] + } + + override suspend fun getWithdrawOrders(userWalletId: UserWalletId): List { + val listType = Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java) + val mapType = Types.newParameterizedType(Map::class.java, String::class.java, listType) + val adapter: JsonAdapter>> = appPreferencesStore.moshi.adapter(mapType) + val map = appPreferencesStore.data.firstOrNull() + ?.get(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)?.let(adapter::fromJson).orEmpty() + return map[createWithdrawOrderIdKey(userWalletId)].orEmpty().map(withdrawStateConverter::convert) + } + + override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) { + appPreferencesStore.editData { mutablePreferences -> + val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + .minus(createWithdrawOrderIdKey(userWalletId)) + mutablePreferences.setObjectMap( + key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, + value = orders, + ) + } + } + + override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) { appPreferencesStore.editData { mutablePreferences -> val orders = mutablePreferences.getObjectMap( PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, 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 fa95151e3b..5e55ebdd54 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 @@ -1,9 +1,9 @@ package com.tangem.tap.di.domain import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase @@ -272,11 +272,11 @@ internal object WalletsDomainModule { @Singleton fun providesSetNotificationsEnabledUseCase( walletsRepository: WalletsRepository, - currenciesRepository: CurrenciesRepository, + accountsCRUDRepository: AccountsCRUDRepository, ): SetNotificationsEnabledUseCase { return SetNotificationsEnabledUseCase( walletsRepository = walletsRepository, - currenciesRepository = currenciesRepository, + accountsCRUDRepository = accountsCRUDRepository, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index 95d2b8fe04..ad63237780 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -19,15 +19,15 @@ import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList import com.tangem.core.ui.message.dialog.Dialogs -import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.card.common.util.twinsIsTwinned +import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.sdk.extensions.localizedDescriptionRes import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor -import com.tangem.tap.common.extensions.* -import com.tangem.tap.common.redux.AppDialog +import com.tangem.tap.common.extensions.dispatchNavigationAction +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.onboarding.OnboardingHelper @@ -113,7 +113,6 @@ internal class LegacyScanProcessor @Inject constructor( onProgressStateChange = onProgressStateChange, onSuccess = onSuccess, onWalletNotCreated = onWalletNotCreated, - onCancel = onCancel, ) }, ) @@ -198,90 +197,38 @@ internal class LegacyScanProcessor @Inject constructor( scanResponse: ScanResponse, crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit, crossinline onWalletNotCreated: suspend () -> Unit, - crossinline onCancel: suspend () -> Unit, crossinline onSuccess: suspend (ScanResponse) -> Unit, ) { - checkCardWasUsedInApp( - scanResponse = scanResponse, - onCancel = { - mainScope.launch { - onProgressStateChange.invoke(false) - onCancel() - } - }, - ) { - if (OnboardingHelper.isOnboardingCase(scanResponse)) { - trackingContextProxy.addContext(scanResponse) + if (OnboardingHelper.isOnboardingCase(scanResponse)) { + trackingContextProxy.addContext(scanResponse) + onWalletNotCreated() + navigateTo( + AppRoute.Onboarding( + scanResponse = scanResponse, + mode = AppRoute.Onboarding.Mode.Onboarding, + ), + ) { onProgressStateChange(it) } + } else { + trackingContextProxy.setContext(scanResponse) + + val wasTwinsOnboardingShown = + store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync() + + if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) { onWalletNotCreated() navigateTo( AppRoute.Onboarding( scanResponse = scanResponse, - mode = AppRoute.Onboarding.Mode.Onboarding, + mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, ), ) { onProgressStateChange(it) } } else { - trackingContextProxy.setContext(scanResponse) - - val wasTwinsOnboardingShown = - store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync() - - if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) { - onWalletNotCreated() - navigateTo( - AppRoute.Onboarding( - scanResponse = scanResponse, - mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, - ), - ) { onProgressStateChange(it) } - } else { - delay(DELAY_SDK_DIALOG_CLOSE) - onSuccess(scanResponse) - } + delay(DELAY_SDK_DIALOG_CLOSE) + onSuccess(scanResponse) } } } - /** - * Checks if card has password and never login at this app - * Show alert in this case - */ - private suspend fun checkCardWasUsedInApp( - scanResponse: ScanResponse, - onCancel: () -> Unit, - onSuccess: suspend () -> Unit, - ) { - val userWalletId = runCatching { UserWalletIdBuilder.card(scanResponse.card).build() }.getOrNull() - if (userWalletId == null) { - onSuccess() - return - } - - val userTokensResponseStore = store.inject(DaggerGraphState::userTokensResponseStore) - val tokens = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - - if (scanResponse.card.isAccessCodeSet && tokens == null) { - store.dispatchDialogShow( - AppDialog.WalletAlreadyWasUsedDialog( - onOk = { mainScope.launch { onSuccess() } }, - onSupportClick = { - val cardInfo = - store.inject(DaggerGraphState::getWalletMetaInfoUseCase).invoke(scanResponse).getOrNull() - ?: error("CardInfo must be not null") - - scope.launch { - store.inject(DaggerGraphState::sendFeedbackEmailUseCase) - .invoke(type = FeedbackEmailType.PreActivatedWallet(cardInfo)) - } - onCancel() - }, - onCancel = { onCancel() }, - ), - ) - } else { - onSuccess() - } - } - private suspend inline fun navigateTo(route: AppRoute, onProgressStateChange: (showProgress: Boolean) -> Unit) { delay(DELAY_SDK_DIALOG_CLOSE) store.dispatchNavigationAction { push(route) } 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 39cae1ec11..4db814b36c 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 @@ -173,6 +173,7 @@ internal class DefaultTangemSdkManager( allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, visaCardScanHandler = visaCardScanHandler, visaCoroutineScope = this, + shouldCheckIsAlreadyActivated = true, onboardingV2FeatureToggles = onboardingV2FeatureToggles, ), cardId = cardId, diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt index bb0fafa039..f1a1683517 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt @@ -27,6 +27,7 @@ internal class ResetBackupCardTask( PreflightReadTask( readMode = PreflightReadMode.FullCardRead, filter = UserWalletIdPreflightReadFilter(expectedUserWalletId = userWalletId), + secureStorage = session.environment.secureStorage, ).run(session) { result -> when (result) { is CompletionResult.Success -> resetCard(session, callback) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index e33ebc491c..eb14c89bb4 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -27,6 +27,7 @@ import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles +import com.tangem.operations.PreflightReadMode import com.tangem.operations.ScanTask import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.backup.StartPrimaryCardLinkingTask @@ -49,6 +50,7 @@ internal class ScanProductTask( private val visaCardScanHandler: VisaCardScanHandler?, private val visaCoroutineScope: CoroutineScope?, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?, + private val shouldCheckIsAlreadyActivated: Boolean, override val allowsRequestAccessCodeFromRepository: Boolean = false, ) : CardSessionRunnable { @@ -106,6 +108,14 @@ internal class ScanProductTask( } } + override fun preflightReadMode(): PreflightReadMode { + return if (shouldCheckIsAlreadyActivated) { + PreflightReadMode.FullCardReadWithAccessCodeCheck + } else { + return super.preflightReadMode() + } + } + private fun getErrorIfExcludedCard(cardDto: CardDTO, card: Card): TangemError? { if (cardDto.isExcluded) return TapSdkError.CardForDifferentApp() if (cardDto.isNotSupportedInThatRelease) return TapSdkError.CardNotSupportedByRelease() diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt index 792b932178..c851c06bdf 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt @@ -20,7 +20,10 @@ class FinalizeTwinTask( WriteProtectedIssuerDataTask(twinPublicKey, issuerKeys).run(session) { result -> when (result) { is CompletionResult.Success -> - PreflightReadTask(PreflightReadMode.FullCardRead).run(session) { readResult -> + PreflightReadTask( + readMode = PreflightReadMode.FullCardRead, + secureStorage = session.environment.secureStorage, + ).run(session) { readResult -> when (readResult) { is CompletionResult.Success -> ScanProductTask( @@ -28,6 +31,7 @@ class FinalizeTwinTask( blockchainToDeriveFinder = null, visaCardScanHandler = null, visaCoroutineScope = null, + shouldCheckIsAlreadyActivated = false, onboardingV2FeatureToggles = null, ).run(session, callback) is CompletionResult.Failure -> diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 083fa85275..ae0d6a4253 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -97,6 +97,7 @@ sealed class AnalyticsParam { data object NewsList : ScreensSources("News List") data object NewsLink : ScreensSources("News Link") data object NewsPage : ScreensSources("News Page") + data object Portfolio : ScreensSources("Portfolio") } sealed class TxSentFrom(val value: String) { @@ -292,6 +293,7 @@ sealed class AnalyticsParam { const val ACCOUNT_DERIVATION = "Account Derivation" const val REFERRAL = "Referral" const val REFERRAL_ID = "Referral_ID" + const val SEARCHED = "Searched" } } diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SwapAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SwapAnalyticsEvent.kt new file mode 100644 index 0000000000..e6e5864f91 --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SwapAnalyticsEvent.kt @@ -0,0 +1,29 @@ +package com.tangem.core.analytics.models.event + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.SEARCHED +import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM +import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources + +/** +[REDACTED_AUTHOR] + */ +sealed class SwapAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent("Swap", event, params) { + + data class TokenSelected( + val token: String, + val source: ScreensSources, + val isSearched: Boolean, + ) : SwapAnalyticsEvent( + event = "Token Selected", + params = mapOf( + TOKEN_PARAM to token, + SOURCE to source.value, + SEARCHED to if (isSearched) "True" else "False", + ), + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt index 9b96730278..21f255653b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt @@ -24,7 +24,11 @@ data class SwapPairProvider( @Json(name = "rateTypes") val rateTypes: List, -) +) { + fun hasOnlyFixedRateType(): Boolean { + return rateTypes.isNotEmpty() && rateTypes.all { it == RateType.FIXED } + } +} @JsonClass(generateAdapter = false) enum class RateType { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index e0c785a2e8..c70f061ee1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -161,7 +161,10 @@ object PreferencesKeys { intPreferencesKey(name = "tronNetworkFeeNotificationShowCount") } - val TANGEM_PAY_WITHDRAW_ORDERS_KEY by lazy { stringPreferencesKey(name = "tangemPayWithdrawOrders") } + val TANGEM_PAY_WITHDRAW_ORDERS_KEY by lazy { stringPreferencesKey(name = "tangemPayWithdrawOrdersKey") } + val TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY by lazy { + stringPreferencesKey(name = "tangemPayActiveWithdrawOrdersKey") + } val TANGEM_PAY_ELIGIBILITY_KEY by lazy { booleanPreferencesKey(name = "tangemPayEligibility") } fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key") diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt index bd29637e98..25a33b8970 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -29,11 +29,25 @@ interface TangemPayStorage { suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? + /** Called after creating withdraw order, active order id */ + suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) + + /** Called after creating withdraw order, saves order data */ suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) - suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState? + /** Returns single active order id. Once the order is completed, deletes id from storage. + * Only one active order allowed for a wallet */ + suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? - suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) + /** Returns all withdraw orders saved. + * Once we get tx hash for an order, it gets deleted from this storage */ + suspend fun getWithdrawOrders(userWalletId: UserWalletId): List? + + /** Deletes active withdraw order. Called after order is completed */ + suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) + + /** Deletes withdraw order data. Called after getting its tx hash */ + suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean 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 60747a0d0c..b46d6ea594 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 @@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.staking.store.StakeKitBalancesStore import com.tangem.domain.card.common.TapWorkarounds.isWallet2 import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -136,6 +137,10 @@ internal class DefaultStakingRepository( return false } + if (userWallet.scanResponse.productType == ProductType.Note) { + return true + } + val blockchainId = cryptoCurrency.network.rawId return when { isSolana(blockchainId) -> INVALID_BATCHES_FOR_SOLANA.contains(userWallet.scanResponse.card.batchId) diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index b229533df3..080f06bacf 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -94,9 +94,10 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( currencyStatus.currency.network.backendId == pair.to.network } - val mappedProviders = pair.providers.mapNotNull { - expressProviders[it.providerId] - }.filterYieldSupplyProvider(statusFrom) + val mappedProviders = pair.providers + .filterNot { it.hasOnlyFixedRateType() } + .mapNotNull { expressProviders[it.providerId] } + .filterYieldSupplyProvider(statusFrom) if (statusFrom != null && statusTo != null && mappedProviders.isNotEmpty()) { SwapPairModel( @@ -151,9 +152,10 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( val currencyStatusFrom = createSendWithSwapCryptoCurrencyStatus(statusFromDeferred.await()) val currencyStatusTo = createSendWithSwapCryptoCurrencyStatus(statusToDeferred.await()) - val mappedProvider = pair.providers.mapNotNull { - mappedProviders[it.providerId] - }.filterYieldSupplyProvider(currencyStatusFrom) + val mappedProvider = pair.providers + .filterNot { it.hasOnlyFixedRateType() } + .mapNotNull { mappedProviders[it.providerId] } + .filterYieldSupplyProvider(currencyStatusFrom) if (currencyStatusFrom != null && currencyStatusTo != null && mappedProvider.isNotEmpty()) { SwapPairModel( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt index cea8e61d60..922b617842 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt @@ -2,7 +2,6 @@ package com.tangem.data.pay.repository import arrow.core.Either import arrow.core.left -import arrow.core.right import com.tangem.core.error.UniversalError import com.tangem.data.common.quote.QuotesFetcher import com.tangem.datasource.api.pay.TangemPayApi @@ -17,7 +16,6 @@ import com.tangem.domain.pay.TangemPayWithdrawState import com.tangem.domain.pay.WithdrawalResult import com.tangem.domain.pay.WithdrawalSignatureResult import com.tangem.domain.pay.datasource.TangemPayAuthDataSource -import com.tangem.domain.pay.model.OrderData import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.TangemPayWithdrawRepository @@ -25,13 +23,7 @@ import com.tangem.domain.visa.error.VisaApiError import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.utils.extensions.addHexPrefix -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber @@ -40,7 +32,6 @@ import java.math.RoundingMode import java.util.Currency import java.util.Locale import javax.inject.Inject -import kotlin.collections.set import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration.Companion.seconds @@ -113,23 +104,18 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( ) { val orderId = response.result?.orderId if (orderId != null) { - val orderData = orderRepository - .getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() - val withdrawTxHash = orderData?.withdrawTxHash + tangemPayStorage.storeActiveWithdrawOrderId(userWalletId = userWallet.walletId, orderId = orderId) + val order = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + val withdrawTxHash = order?.withdrawTxHash val storeData = TangemPayWithdrawState( orderId = orderId, exchangeData = exchangeData, ) - if (orderData != null && !withdrawTxHash.isNullOrEmpty()) { - finalizeWithdraw( - userWallet = userWallet, - withdrawTxHash = withdrawTxHash, - orderId = orderId, - exchangeData = exchangeData, - order = orderData, - ).onLeft { - tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) - } + if (order != null && !withdrawTxHash.isNullOrEmpty()) { + finalizeWithdraw(userWallet = userWallet, txHash = withdrawTxHash, exchangeData = exchangeData) + .onLeft { + tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) + } } else { tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) } @@ -138,10 +124,8 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( private suspend fun finalizeWithdraw( userWallet: UserWallet, - withdrawTxHash: String, - orderId: String, + txHash: String, exchangeData: TangemPayWithdrawExchangeState, - order: OrderData, ): Either { return swapRepository.exchangeSent( userWallet = userWallet, @@ -149,89 +133,63 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( fromNetwork = exchangeData.fromNetwork, fromAddress = exchangeData.fromAddress, payInAddress = exchangeData.payInAddress, - txHash = withdrawTxHash, + txHash = txHash, payInExtraId = exchangeData.payInExtraId, ) - .onRight { - val isActive = order.status == OrderStatus.NEW || order.status == OrderStatus.PROCESSING - if (!isActive) { - tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId) - } else { - tangemPayStorage.storeWithdrawOrder( - userWalletId = userWallet.walletId, - data = TangemPayWithdrawState(orderId = orderId, exchangeData = null), - ) - } - } .onLeft { error -> Timber.tag(TAG).e(error.toString()) } } override suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean { - val orderExchangeData = tangemPayStorage.getWithdrawOrder(userWallet.walletId) - if (orderExchangeData == null) return false - - val exchangeData = orderExchangeData.exchangeData - val orderData = orderRepository - .getOrderData(userWallet.walletId, orderId = orderExchangeData.orderId).getOrNull() - val withdrawTxHash = orderData?.withdrawTxHash - - if (exchangeData != null && orderData != null && withdrawTxHash != null) { - finalizeWithdraw( - userWallet = userWallet, - withdrawTxHash = withdrawTxHash, - orderId = orderExchangeData.orderId, - exchangeData = exchangeData, - order = orderData, - ) + val orderId = tangemPayStorage.getActiveWithdrawOrderId(userWallet.walletId) + if (orderId.isNullOrEmpty()) return false + val orderData = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + val isActive = orderData?.status == OrderStatus.NEW || orderData?.status == OrderStatus.PROCESSING + if (!isActive) { + tangemPayStorage.deleteActiveWithdrawOrder(userWalletId = userWallet.walletId) } - - return orderData?.status == OrderStatus.NEW || orderData?.status == OrderStatus.PROCESSING + return isActive } - override suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either { - val storeData = tangemPayStorage.getWithdrawOrder(userWallet.walletId) ?: return Unit.right() - val exchangeData = storeData.exchangeData ?: return Unit.right() - - val orderId = storeData.orderId - val order = orderRepository - .getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() - ?: return Unit.right() + override suspend fun pollWithdrawOrdersIfNeeds(userWallet: UserWallet) { + tangemPayStorage.getWithdrawOrders(userWalletId = userWallet.walletId)?.forEach { state -> + withdrawPollingScope.launch { + try { + pollWithdrawOrderIfNeeds(userWallet = userWallet, data = state) + } catch (exception: CancellationException) { + throw exception + } catch (exception: Exception) { + Timber.tag(TAG).e(exception) + } + } + } + } + private suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet, data: TangemPayWithdrawState) { + val exchangeData = data.exchangeData ?: return + val orderId = data.orderId + val order = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + ?: return val txHash = order.withdrawTxHash if (!txHash.isNullOrEmpty()) { - finalizeWithdraw( - userWallet = userWallet, - withdrawTxHash = txHash, - orderId = storeData.orderId, - exchangeData = exchangeData, - order = order, - ).onLeft { - startWithdrawOrderPolling( - userWallet = userWallet, - orderId = orderId, - storeData = storeData, - exchangeData = exchangeData, - ) - } + finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData) + .onRight { + tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) + } + .onLeft { + startWithdrawOrderPolling(userWallet = userWallet, orderId = orderId, exchangeData = exchangeData) + } } else { - startWithdrawOrderPolling( - userWallet = userWallet, - orderId = orderId, - storeData = storeData, - exchangeData = exchangeData, - ) + startWithdrawOrderPolling(userWallet = userWallet, orderId = orderId, exchangeData = exchangeData) } - - return Unit.right() + return } private suspend fun startWithdrawOrderPolling( userWallet: UserWallet, orderId: String, - storeData: TangemPayWithdrawState, exchangeData: TangemPayWithdrawExchangeState, ) { withdrawPollingMutex.withLock { @@ -239,33 +197,33 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( val pollingJob = withdrawPollingScope.launch { try { - while (isActive && withdrawPollingJobs.containsKey(orderId)) { + while (isActive) { delay(duration = 5.seconds) - val orderData = orderRepository - .getOrderData(userWalletId = userWallet.walletId, orderId = orderId) - orderData.onRight { order -> - if (order.status != OrderStatus.NEW && order.status != OrderStatus.PROCESSING) { - tangemPayStorage.deleteWithdrawOrder(userWallet.walletId) - withdrawPollingJobs.remove(key = orderId) + orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId) + .onRight { order -> + val txHash = order.withdrawTxHash + if (txHash.isNullOrEmpty()) return@onRight + finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData) + .onRight { + tangemPayStorage.deleteWithdrawOrder( + userWalletId = userWallet.walletId, + orderId = orderId, + ) + withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } + return@launch + } + .onLeft { error -> + Timber.tag(TAG).e("finalizeWithdraw error: $error") + withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } + return@launch + } + } + .onLeft { error -> + Timber.tag(TAG).e("getOrderData error ${error.errorCode}") + withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } return@launch } - val txHash = order.withdrawTxHash - if (!txHash.isNullOrEmpty()) { - finalizeWithdraw( - userWallet = userWallet, - withdrawTxHash = txHash, - orderId = storeData.orderId, - exchangeData = exchangeData, - order = order, - ).onRight { - withdrawPollingJobs.remove(key = orderId) - return@launch - } - } - }.onLeft { error -> - Timber.tag(TAG).e("error ${error.errorCode}") - } } } catch (exception: CancellationException) { throw exception diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt index d41defe9be..2bef64e9b1 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt @@ -59,14 +59,21 @@ class ManageCryptoCurrenciesUseCase( accountId: AccountId, add: CryptoCurrency? = null, remove: CryptoCurrency? = null, + skipDerivationErrors: Boolean = true, ): Either { - return invoke(accountId = accountId, add = listOfNotNull(add), remove = listOfNotNull(remove)) + return invoke( + accountId = accountId, + add = listOfNotNull(add), + remove = listOfNotNull(remove), + skipDerivationErrors = skipDerivationErrors, + ) } suspend operator fun invoke( accountId: AccountId, add: List = emptyList(), remove: List = emptyList(), + skipDerivationErrors: Boolean = true, ): Either = eitherOn(dispatchers.default) { if (add.isEmpty() && remove.isEmpty()) { Timber.d("No currencies to add or remove, skipping") @@ -89,7 +96,7 @@ class ManageCryptoCurrenciesUseCase( account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total), ) - derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) + val result = derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) parallelUpdatingScope.launch { syncTokens(userWalletId, modifiedCurrencyList) @@ -98,6 +105,10 @@ class ManageCryptoCurrenciesUseCase( refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total) clearMetadata(userWalletId = userWalletId, currencies = modifiedCurrencyList.removed) } + + if (!skipDerivationErrors) { + result.bind() + } } } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt index 18b09d331e..6079a4e874 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt @@ -6,7 +6,6 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayWithdrawExchangeState import com.tangem.domain.pay.WithdrawalResult -import com.tangem.domain.visa.error.VisaApiError import java.math.BigDecimal interface TangemPayWithdrawRepository { @@ -21,5 +20,5 @@ interface TangemPayWithdrawRepository { suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean - suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either + suspend fun pollWithdrawOrdersIfNeeds(userWallet: UserWallet) } \ No newline at end of file diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index b7b3798d96..f11102091a 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(projects.domain.walletManager) implementation(projects.libs.blockchainSdk) implementation(projects.libs.tangemSdkApi) + implementation(projects.domain.account) implementation(projects.domain.models) implementation(projects.domain.tokens) implementation(projects.domain.card) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt index 5a66ceb5ab..c749d56049 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt @@ -1,13 +1,13 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either -import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.repository.WalletsRepository class SetNotificationsEnabledUseCase( private val walletsRepository: WalletsRepository, - private val currenciesRepository: CurrenciesRepository, + private val accountsCRUDRepository: AccountsCRUDRepository, ) { suspend operator fun invoke(userWalletId: UserWalletId, isEnabled: Boolean): Either = @@ -16,7 +16,7 @@ class SetNotificationsEnabledUseCase( userWalletId = userWalletId, isEnabled = isEnabled, ) - currenciesRepository.syncTokens(userWalletId) + accountsCRUDRepository.syncTokens(userWalletId) }.onLeft { walletsRepository.setNotificationsEnabled( userWalletId = userWalletId, diff --git a/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCaseTest.kt b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCaseTest.kt new file mode 100644 index 0000000000..73d291ba22 --- /dev/null +++ b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCaseTest.kt @@ -0,0 +1,131 @@ +package com.tangem.domain.wallets.usecase + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifyOrder +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class SetNotificationsEnabledUseCaseTest { + + private lateinit var useCase: SetNotificationsEnabledUseCase + private lateinit var walletsRepository: WalletsRepository + private lateinit var accountsCRUDRepository: AccountsCRUDRepository + + @Before + fun setup() { + walletsRepository = mockk() + accountsCRUDRepository = mockk() + useCase = SetNotificationsEnabledUseCase( + walletsRepository = walletsRepository, + accountsCRUDRepository = accountsCRUDRepository, + ) + } + + @Test + fun `GIVEN notifications enabled successfully WHEN invoke THEN return Right with Unit`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = true + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } just runs + coEvery { accountsCRUDRepository.syncTokens(userWalletId) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isRight()).isTrue() + coVerifyOrder { + walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) + accountsCRUDRepository.syncTokens(userWalletId) + } + } + + @Test + fun `GIVEN notifications disabled successfully WHEN invoke THEN return Right with Unit`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = false + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } just runs + coEvery { accountsCRUDRepository.syncTokens(userWalletId) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isRight()).isTrue() + coVerifyOrder { + walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) + accountsCRUDRepository.syncTokens(userWalletId) + } + } + + @Test + fun `GIVEN setNotificationsEnabled throws exception WHEN invoke THEN return Left and revert notifications`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = true + val exception = RuntimeException("Network error") + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } throws exception + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isLeft()).isTrue() + result.onLeft { throwable -> + assertThat(throwable).isEqualTo(exception) + } + coVerify { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } + } + + @Test + fun `GIVEN syncTokens throws exception WHEN invoke THEN return Left and revert notifications`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = true + val exception = RuntimeException("Sync error") + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } just runs + coEvery { accountsCRUDRepository.syncTokens(userWalletId) } throws exception + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isLeft()).isTrue() + result.onLeft { throwable -> + assertThat(throwable).isEqualTo(exception) + } + coVerify { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } + } + + @Test + fun `GIVEN disabling notifications fails WHEN invoke THEN return Left and revert to enabled`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = false + val exception = RuntimeException("Network error") + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } throws exception + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isLeft()).isTrue() + result.onLeft { throwable -> + assertThat(throwable).isEqualTo(exception) + } + coVerify { walletsRepository.setNotificationsEnabled(userWalletId, true) } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt index e234d1d2f6..0d733ab09f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -157,6 +157,8 @@ internal class AddToPortfolioModel @Inject constructor( allRequireForAdd.first() // line of navigation to AddToken screen is finished; cancel the job, select a new root screen firstPartOfNavigation.cancel() + + analyticsEventHandler.send(event = eventBuilder.popupToConfirm()) navigation.replaceAll(AddToPortfolioRoutes.AddToken) var middleNavigationJob: Job? = null diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt index cd16df3f04..17d27f1aad 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt @@ -88,6 +88,7 @@ internal class AddTokenModel @Inject constructor( val blockchainNames = listOf(selectedNetwork.selectedNetwork) .mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name } analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames)) + analyticsEventHandler.send(analyticsEventBuilder.addButtonClick()) manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency) .onLeft { throwable -> @@ -110,6 +111,11 @@ internal class AddTokenModel @Inject constructor( } is Account.Payment -> TODO("[REDACTED_JIRA]") } + + analyticsEventHandler.send( + event = analyticsEventBuilder.tokenAdded(status.status.currency.network.name), + ) + params.callbacks.onTokenAdded(status.status) } uiState.value = um.toggleProgress(false) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt index b75d67abf4..87ed9607b7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt @@ -15,28 +15,64 @@ internal class PortfolioAnalyticsEvent( fun addToPortfolioClicked() = PortfolioAnalyticsEvent( event = "Button - Add To Portfolio", - params = mapOf( - "Token" to tokenSymbol, - ), + params = buildMap { + put("Token", tokenSymbol) + if (source != null) put("Source", source) + }, ) fun popupToChooseAccount() = PortfolioAnalyticsEvent( event = "Choose Account Opened", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun popupToConfirm() = PortfolioAnalyticsEvent( + event = "Add Token Screen Opened", + params = buildMap { + if (source != null) put("Source", source) + }, ) fun addToNotMainAccount() = PortfolioAnalyticsEvent( event = "Button - Add To Account", + params = buildMap { + if (source != null) put("Source", source) + }, ) - fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(event = "Wallet Selected") + fun addButtonClick() = PortfolioAnalyticsEvent( + event = "Button - Add Token", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent( + event = "Wallet Selected", + params = buildMap { + if (source != null) put("Source", source) + }, + ) fun addToPortfolioContinue(blockchainNames: List) = PortfolioAnalyticsEvent( event = "Token Network Selected", - params = mapOf( - "Count" to blockchainNames.size.toString(), - "Token" to tokenSymbol, - "blockchain" to blockchainNames.joinToString(separator = ", "), - ), + params = buildMap { + put("Count", blockchainNames.size.toString()) + put("Token", tokenSymbol) + put("blockchain", blockchainNames.joinToString(separator = ", ")) + if (source != null) put("Source", source) + }, + ) + + fun tokenAdded(blockchainName: String) = PortfolioAnalyticsEvent( + event = "Token Added", + params = buildMap { + put("Token", tokenSymbol) + put("Blockchain", blockchainName) + if (source != null) put("Source", source) + }, ) fun quickActionClick(actionUM: TokenActionsBSContentUM.Action, blockchainName: String) = @@ -51,7 +87,7 @@ internal class PortfolioAnalyticsEvent( }, params = buildMap { put("Token", tokenSymbol) - source?.let { put("Source", it) } + if (source != null) put("Source", source) put("blockchain", blockchainName) }, ) @@ -64,10 +100,16 @@ internal class PortfolioAnalyticsEvent( TokenActionsBSContentUM.Action.Stake -> "Popup Get token - Button Stake" else -> "error" }, + params = buildMap { + if (source != null) put("Source", source) + }, ) fun getTokenLater() = PortfolioAnalyticsEvent( event = "Popup Get token - Button Later", + params = buildMap { + if (source != null) put("Source", source) + }, ) } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt index 03d7c218c2..636c9f4f32 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt @@ -43,8 +43,8 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor( componentScope.launch { val cardInfo = getWalletMetaInfoUseCase(params.scanResponse).getOrNull() ?: return@launch - val userWalletId = cardInfo.userWalletId ?: return@launch - val visaCustomerId = getTangemPayCustomerIdUseCase(userWalletId).getOrNull() + val userWalletId = cardInfo.userWalletId + val visaCustomerId = userWalletId?.let { id -> getTangemPayCustomerIdUseCase(id).getOrNull() } sendFeedbackEmailUseCase( if (params.scanResponse.card.isVisa && !visaCustomerId.isNullOrEmpty()) { FeedbackEmailType.Visa.Activation(walletMetaInfo = cardInfo, customerId = visaCustomerId) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt deleted file mode 100644 index f5a57df2b5..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.features.onramp.swap.availablepairs.model - -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN -import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM - -private const val SWAP_CATEGORY = "Swap" - -internal sealed class AvailableSwapPairsAnalyticsEvent( - event: String, - params: Map = emptyMap(), -) : AnalyticsEvent(SWAP_CATEGORY, event, params) { - - class TokenSelected( - val token: String, - val source: String, - val isSearched: Boolean, - ) : AvailableSwapPairsAnalyticsEvent( - event = "Token Selected", - params = mapOf( - TOKEN_PARAM to token, - SOURCE to source, - SEARCHED to if (isSearched) "True" else "False", - ), - ) { - companion object { - const val SOURCE = "Source" - const val SEARCHED = "Searched" - const val SOURCE_PORTFOLIO = "Portfolio" - const val SOURCE_MARKETS = "Markets" - } - } - - class TokenAdded( - val token: String, - val blockchain: String, - ) : AvailableSwapPairsAnalyticsEvent( - event = "Token Added", - params = mapOf( - TOKEN_PARAM to token, - BLOCKCHAIN to blockchain, - ), - ) -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index 62822d4108..363f0712d5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -1,11 +1,17 @@ package com.tangem.features.onramp.swap.availablepairs.model +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources +import com.tangem.core.analytics.models.event.SwapAnalyticsEvent import com.tangem.core.decompose.model.Model -import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.fields.InputManager -import com.tangem.core.ui.R as CoreUiR +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.resourceReference @@ -43,9 +49,7 @@ import com.tangem.feature.swap.domain.models.domain.SwapPairLeast import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent -import com.tangem.features.swap.SwapFeatureToggles import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetLoadingTokenItemsTransformer import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformer @@ -58,24 +62,22 @@ import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMController import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer import com.tangem.features.onramp.tokenlist.entity.transformer.* +import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.features.swap.SwapFeatureToggles import com.tangem.lib.crypto.BlockchainUtils -import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate -import com.arkivanov.decompose.router.slot.dismiss import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.runSuspendCatching -import kotlinx.coroutines.ExperimentalCoroutinesApi import com.tangem.utils.coroutines.saveIn +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +import com.tangem.core.ui.R as CoreUiR private typealias AvailablePairsState = Lce> @@ -567,9 +569,9 @@ internal class AvailableSwapPairsModel @Inject constructor( private fun onPortfolioTokenClick(tokenItem: TokenItemState, status: CryptoCurrencyStatus) { analyticsEventHandler.send( - AvailableSwapPairsAnalyticsEvent.TokenSelected( + SwapAnalyticsEvent.TokenSelected( token = status.currency.symbol, - source = AvailableSwapPairsAnalyticsEvent.TokenSelected.SOURCE_PORTFOLIO, + source = ScreensSources.Portfolio, isSearched = state.value.searchBarUM.query.isNotEmpty(), ), ) @@ -686,15 +688,9 @@ internal class AvailableSwapPairsModel @Inject constructor( modelScope.launch { bottomSheetNavigation.dismiss() analyticsEventHandler.send( - AvailableSwapPairsAnalyticsEvent.TokenAdded( + SwapAnalyticsEvent.TokenSelected( token = addedToken.symbol, - blockchain = addedToken.network.name, - ), - ) - analyticsEventHandler.send( - AvailableSwapPairsAnalyticsEvent.TokenSelected( - token = addedToken.symbol, - source = AvailableSwapPairsAnalyticsEvent.TokenSelected.SOURCE_MARKETS, + source = ScreensSources.Markets, isSearched = state.value.searchBarUM.query.isNotEmpty(), ), ) @@ -746,7 +742,7 @@ internal class AvailableSwapPairsModel @Inject constructor( .create( scope = modelScope, token = param, - analyticsParams = null, + analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value), ).apply { setTokenNetworks(networks) } diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index cfbdeea741..930418c920 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -73,7 +73,17 @@ internal class ReferralInteractorImpl( when (portfolioId) { is PortfolioId.Account -> { - manageCryptoCurrenciesUseCase(accountId = portfolioId.accountId, add = cryptoCurrency) + manageCryptoCurrenciesUseCase( + accountId = portfolioId.accountId, + add = cryptoCurrency, + skipDerivationErrors = false, + ).mapLeft { + it.mapToDomainError() + }.onLeft { error -> + if (error is ReferralError.UserCancelledException) { + throw error + } + } } is PortfolioId.Wallet -> { derivePublicKeysUseCase(userWallet.walletId, listOf(cryptoCurrency)).getOrElse { throwable -> diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt index cf27f09d02..8be7f8fc02 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt @@ -5,6 +5,9 @@ import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.swap.models.SwapDataModel +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import kotlinx.collections.immutable.ImmutableList @Immutable @@ -37,6 +40,9 @@ internal sealed class ConfirmUM { val txUrl: String, val swapDataModel: SwapDataModel, val provider: ExpressProvider, + val amountUM: SwapAmountUM, + val destinationUM: DestinationUM, + val feeSelectorUM: FeeSelectorUM, ) : ConfirmUM() data object Empty : ConfirmUM() { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmSentStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmSentStateTransformer.kt index 970c3f1f93..d0ade6d256 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmSentStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmSentStateTransformer.kt @@ -20,6 +20,9 @@ internal class SendWithSwapConfirmSentStateTransformer( txUrl = txUrl, provider = provider, swapDataModel = swapDataModel, + amountUM = prevState.amountUM, + destinationUM = prevState.destinationUM, + feeSelectorUM = prevState.feeSelectorUM, ), ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt index 5657f00361..834289a971 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt @@ -93,10 +93,10 @@ internal fun SendWithSwapSuccessContent(sendWithSwapUM: SendWithSwapUM) { @Composable private fun SuccessContent(sendWithSwapUM: SendWithSwapUM, modifier: Modifier = Modifier) { val confirmUM = sendWithSwapUM.confirmUM as? ConfirmUM.Success ?: return - val amountUM = sendWithSwapUM.amountUM as? SwapAmountUM.Content ?: return + val amountUM = confirmUM.amountUM as? SwapAmountUM.Content ?: return val quoteUM = amountUM.selectedQuote as? SwapQuoteUM.Content ?: return - val destinationUM = sendWithSwapUM.destinationUM as? DestinationUM.Content ?: return - val feeSelectorUM = sendWithSwapUM.feeSelectorUM as? FeeSelectorUM.Content ?: return + val destinationUM = confirmUM.destinationUM as? DestinationUM.Content ?: return + val feeSelectorUM = confirmUM.feeSelectorUM as? FeeSelectorUM.Content ?: return Column( modifier = modifier @@ -393,6 +393,64 @@ private fun SendWithSwapSuccessContent_Preview() { txExtraIdName = "Jeffry Blackwell", ), ), + amountUM = SwapAmountContentPreview.defaultState, + destinationUM = DestinationUM.Content( + isPrimaryButtonEnabled = false, + addressTextField = DestinationTextFieldUM.RecipientAddress( + value = "0x391316d97a07027a0702c8A002c8A0C25d8470", + keyboardOptions = KeyboardOptions(), + placeholder = TextReference.EMPTY, + label = resourceReference(R.string.send_recipient), + isError = false, + error = null, + isValuePasted = false, + blockchainAddress = "0x391316d97a07027a0702c8A002c8A0C25d8470", + ), + memoTextField = DestinationTextFieldUM.RecipientMemo( + value = "123123123", + keyboardOptions = KeyboardOptions(), + placeholder = TextReference.EMPTY, + label = resourceReference(R.string.send_recipient), + isError = false, + error = null, + isValuePasted = false, + isEnabled = true, + disabledText = TextReference.EMPTY, + ), + recent = persistentListOf(), + wallets = persistentListOf(), + networkName = "Polygon", + isValidating = false, + isInitialized = false, + isRecentHidden = false, + isAccountsMode = false, + ), + feeSelectorUM = FeeSelectorUM.Content( + fees = TransactionFee.Single( + normal = Fee.Common( + BigDecimal.ONE.convertToSdkAmount( + SwapAmountContentPreview.cryptoCurrencyStatus, + ), + ), + ), + feeItems = persistentListOf(), + selectedFeeItem = FeeItem.Market( + Fee.Common( + BigDecimal.ONE.convertToSdkAmount( + SwapAmountContentPreview.cryptoCurrencyStatus, + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + feeCryptoCurrencyStatus = SwapAmountContentPreview.cryptoCurrencyStatus, + ), + feeFiatRateUM = null, + feeNonce = FeeNonce.None, + isPrimaryButtonEnabled = false, + ), ), navigationUM = NavigationUM.Content( source = SendWithSwapRoute.Success.javaClass.simpleName, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index 1080578a05..414a2649d7 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -25,9 +25,9 @@ class SwapPairInfoConverter : Converter get() = MutableStateFlow(FeeSelectorUM.Loading) + val forceUpdateState: SharedFlow + get() = MutableStateFlow(FeeSelectorUM.Loading) + fun onResult(newState: FeeSelectorUM) suspend fun loadFee(): Either 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 b929ec77f6..1f5c403663 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 @@ -4,7 +4,6 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue - import arrow.core.Either import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation @@ -19,13 +18,16 @@ import com.tangem.common.ui.markets.models.MarketsListItemUM 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.AnalyticsParam.ScreensSources import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.models.event.SwapAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles +import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference @@ -100,7 +102,6 @@ import com.tangem.feature.swap.models.UiActions import com.tangem.feature.swap.models.market.SwapMarketsListBatchFlowManager import com.tangem.feature.swap.models.market.state.SwapMarketState import com.tangem.feature.swap.models.states.SwapNotificationUM -import com.tangem.core.ui.R import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder @@ -117,12 +118,8 @@ import com.tangem.utils.Provider import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode @@ -311,17 +308,13 @@ internal class SwapModel @Inject constructor( modelScope.launch { bottomSheetNavigation.dismiss() analyticsEventHandler.send( - SwapEvents.ChooseTokenScreenResult( - isTokenChosen = true, - token = addedToken.symbol, - source = SwapEvents.ChooseTokenScreenResult.SOURCE_MARKETS, - isSearched = searchQueryState.value.isNotEmpty(), - ), + SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = addedToken.symbol), ) analyticsEventHandler.send( - SwapEvents.TokenAdded( + SwapAnalyticsEvent.TokenSelected( token = addedToken.symbol, - blockchain = addedToken.network.name, + source = ScreensSources.Markets, + isSearched = searchQueryState.value.isNotEmpty(), ), ) searchQueryState.value = "" @@ -530,14 +523,7 @@ internal class SwapModel @Inject constructor( dataState.fromCryptoCurrency } - fromCryptoCurrency?.let { cryptoCurrency -> - dataState = dataState.copy( - feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = userWalletId, - cryptoCurrencyStatus = cryptoCurrency, - ).getOrNull(), - ) - } + if (fromCryptoCurrency != null) updateFeePaidCryptoCurrencyFor(fromCryptoCurrency) subscribeToCoinBalanceUpdatesIfNeeded() }.onFailure { error -> @@ -774,6 +760,15 @@ internal class SwapModel @Inject constructor( } } + private suspend fun updateFeePaidCryptoCurrencyFor(fromToken: CryptoCurrencyStatus) { + dataState = dataState.copy( + feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = fromToken, + ).getOrNull(), + ) + } + private fun loadQuotesTask( fromToken: CryptoCurrencyStatus, fromAccount: Account.CryptoPortfolio?, @@ -1332,10 +1327,13 @@ internal class SwapModel @Inject constructor( foundToken?.currency?.symbol?.let { symbol -> analyticsEventHandler.send( - SwapEvents.ChooseTokenScreenResult( - isTokenChosen = true, + SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = symbol), + ) + + analyticsEventHandler.send( + SwapAnalyticsEvent.TokenSelected( token = symbol, - source = SwapEvents.ChooseTokenScreenResult.SOURCE_PORTFOLIO, + source = ScreensSources.Portfolio, isSearched = searchQueryState.value.isNotEmpty(), ), ) @@ -1399,6 +1397,9 @@ internal class SwapModel @Inject constructor( toAccount = toAccount, selectedProvider = null, ) + modelScope.launch { + updateFeePaidCryptoCurrencyFor(fromToken) + } startLoadingQuotes( fromToken = fromToken, fromAccount = fromAccount, @@ -1541,6 +1542,7 @@ internal class SwapModel @Inject constructor( toAccount = newToAccount, ) isOrderReversed = !isOrderReversed + updateFeePaidCryptoCurrencyFor(newFromToken) dataState.tokensDataState?.let { tokensDataState -> updateTokensState(tokensDataState) } @@ -2290,7 +2292,7 @@ internal class SwapModel @Inject constructor( .create( scope = modelScope, token = param, - analyticsParams = null, + analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value), ).apply { setTokenNetworks(networks) } @@ -2372,6 +2374,8 @@ internal class SwapModel @Inject constructor( FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true), ) + override val forceUpdateState = MutableSharedFlow() + override suspend fun loadFeeExtended( selectedToken: CryptoCurrencyStatus?, ): Either { @@ -2404,18 +2408,13 @@ internal class SwapModel @Inject constructor( } override fun onResult(newState: FeeSelectorUM) { - if (isPermissionNotificationShown()) { - state.value = FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true) - return - } - if (newState is FeeSelectorUM.Error) { - state.value = newState.copy(isHidden = true) + modelScope.launch { + forceUpdateState.emit(newState.copy(isHidden = true)) + } return } - state.value = newState - // If fee currency is same as from currency, we need to reload quotes to update fee info val isFeeCurrencySameAsFromCurrency = newState is FeeSelectorUM.Content && dataState.fromCryptoCurrency?.currency?.id == newState.feeExtraInfo.feeCryptoCurrencyStatus.currency.id diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index ed0a2dcfe8..a30871f920 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -163,10 +163,10 @@ internal class StateBuilder( amountTextFieldValue = null, amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", token = fromToken, - tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, - coinId = uiStateHolder.sendCardData.coinId, - isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, - tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, + tokenIconUrl = fromToken.currency.iconUrl, + coinId = fromToken.currency.network.backendId, + isNotNativeToken = fromToken.currency is CryptoCurrency.Token, + tokenCurrency = fromToken.currency.symbol, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, balance = fromToken.getFormattedAmount(isNeedSymbol = false), networkIconRes = getActiveIconRes(fromToken.currency.network.rawId), @@ -206,10 +206,10 @@ internal class StateBuilder( amountTextFieldValue = null, amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", token = fromToken, - tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, - coinId = uiStateHolder.sendCardData.coinId, - isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, - tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, + tokenIconUrl = fromToken.currency.iconUrl, + coinId = fromToken.currency.network.backendId, + isNotNativeToken = fromToken.currency is CryptoCurrency.Token, + tokenCurrency = fromToken.currency.symbol, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, balance = fromToken.getFormattedAmount(isNeedSymbol = false), networkIconRes = getActiveIconRes(fromToken.currency.network.rawId), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index 793549861d..beb2bed482 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -153,7 +153,7 @@ internal class TangemPayDetailsModel @Inject constructor( modelScope.launch { val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull() ?: return@launch - tangemPayWithdrawRepository.pollWithdrawOrderIfNeeds(userWallet) + tangemPayWithdrawRepository.pollWithdrawOrdersIfNeeds(userWallet) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt index e888745a6f..950bd50f41 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt @@ -25,14 +25,12 @@ internal class ExpressStateFactory( fun getStateWithClosedDialog(): ExpressTransactionsBlockState { val state = currentStateProvider() - val slot = state.dialogSlot ?: return state - return state.copy(dialogSlot = slot.copy(config = slot.config.copy(isShow = false))) + return state.copy(dialogSlot = null) } fun getStateWithClosedBottomSheet(): ExpressTransactionsBlockState { val state = currentStateProvider() - val slot = state.bottomSheetSlot ?: return state - return state.copy(bottomSheetSlot = slot.copy(config = slot.config.copy(isShown = false))) + return state.copy(bottomSheetSlot = null) } fun getStateWithConfirmHideExpressStatus(): ExpressTransactionsBlockState { 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 f92476350d..11aaab2b47 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 @@ -33,7 +33,7 @@ sealed class WalletScreenAnalyticsEvent { put(AnalyticsParam.BALANCE, balance.value) tokensCount?.let { put(AnalyticsParam.TOKENS_COUNT, it.toString()) } }, - ) + ), AppsFlyerIncludedEvent class TokenBalance(balance: AnalyticsParam.EmptyFull, token: String) : Basic( event = "Token Balance", diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index 5cfa978a2a..d4d08e2e70 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -48,7 +48,8 @@ internal class TokenListAnalyticsSender @Inject constructor( if (screenLifecycleProvider.isBackgroundState.value) return if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return - if (totalFiatBalance is TotalFiatBalance.Loading) { + val isFlickering = totalFiatBalance is TotalFiatBalance.Loaded && totalFiatBalance.source == StatusSource.CACHE + if (totalFiatBalance is TotalFiatBalance.Loading || isFlickering) { startLoadingTraceIfNeeded(userWallet.walletId, flattenCurrencies) return } @@ -57,12 +58,10 @@ internal class TokenListAnalyticsSender @Inject constructor( stopLoadingTraceIfNeeded(userWallet.walletId, totalFiatBalance) } - val currenciesStatuses = flattenCurrencies - - sendBalanceLoadedEventIfNeeded(totalFiatBalance, currenciesStatuses) - sendToppedUpEventIfNeeded(userWallet, totalFiatBalance, currenciesStatuses) - sendUnreachableNetworksEventIfNeeded(currenciesStatuses) - sendTokenBalancesIfNeeded(currenciesStatuses) + sendBalanceLoadedEventIfNeeded(totalFiatBalance, flattenCurrencies) + sendToppedUpEventIfNeeded(userWallet, totalFiatBalance, flattenCurrencies) + sendUnreachableNetworksEventIfNeeded(flattenCurrencies) + sendTokenBalancesIfNeeded(flattenCurrencies) } private suspend fun startLoadingTraceIfNeeded( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListAnalyticsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListAnalyticsSubscriber.kt new file mode 100644 index 0000000000..7fb410838e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListAnalyticsSubscriber.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach + +/** + * Subscriber that monitors account list changes and sends token list analytics + * when the total fiat balance changes. + * +[REDACTED_AUTHOR] + */ +internal class TokenListAnalyticsSubscriber @AssistedInject constructor( + @Assisted override val userWallet: UserWallet, + override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val stateHolder: WalletStateController, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, +) : BasicWalletSubscriber() { + + override fun create(coroutineScope: CoroutineScope): Flow<*> = getAccountStatusListFlow() + .distinctUntilChanged { old, new -> old.totalFiatBalance == new.totalFiatBalance } + .onEach(::sendTokenListAnalytics) + + private suspend fun sendTokenListAnalytics(accountStatusList: AccountStatusList) { + val displayedState = stateHolder.getWalletStateIfSelected(userWallet.walletId) + + val flattenCurrencies = accountStatusList.flattenCurrencies() + tokenListAnalyticsSender.send( + displayedUiState = displayedState, + userWallet = userWallet, + flattenCurrencies = flattenCurrencies, + totalFiatBalance = accountStatusList.totalFiatBalance, + ) + } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): TokenListAnalyticsSubscriber + } +} \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index c84c960be6..6934faad61 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "develop-1437" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-578" +tangemCardSdk = "develop-582" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/tangem-android-tools b/tangem-android-tools index 35755ad66f..43fab6f690 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 35755ad66f5f8fa6bcb1c84b443826cd315495d0 +Subproject commit 43fab6f690538391cae17e046ffb2ec9fe08b0c7