Updated on 2026-08-14
This commit is contained in:
commit
9b043d6703
42 changed files with 648 additions and 382 deletions
|
|
@ -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<TangemPayWithdrawState>(
|
||||
PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,
|
||||
)
|
||||
.plus(createWithdrawOrderIdKey(userWalletId) to withdrawStoreDataConverter.convert(data))
|
||||
val orders = mutablePreferences.getObjectMap<String>(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<WithdrawStoreData>(
|
||||
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<Map<String, List<WithdrawStoreData>>> = 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<String>(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY)
|
||||
return orders[createWithdrawOrderIdKey(userWalletId)]
|
||||
}
|
||||
|
||||
override suspend fun getWithdrawOrders(userWalletId: UserWalletId): List<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<Map<String, List<WithdrawStoreData>>> = 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<String>(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<WithdrawStoreData>(
|
||||
PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ internal class DefaultTangemSdkManager(
|
|||
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
|
||||
visaCardScanHandler = visaCardScanHandler,
|
||||
visaCoroutineScope = this,
|
||||
shouldCheckIsAlreadyActivated = true,
|
||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||
),
|
||||
cardId = cardId,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<ScanResponse> {
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String, String> = 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",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -24,7 +24,11 @@ data class SwapPairProvider(
|
|||
|
||||
@Json(name = "rateTypes")
|
||||
val rateTypes: List<RateType>,
|
||||
)
|
||||
) {
|
||||
fun hasOnlyFixedRateType(): Boolean {
|
||||
return rateTypes.isNotEmpty() && rateTypes.all { it == RateType.FIXED }
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class RateType {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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<TangemPayWithdrawState>?
|
||||
|
||||
/** 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<ExpressDataError, Unit> {
|
||||
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<VisaApiError, Unit> {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -59,14 +59,21 @@ class ManageCryptoCurrenciesUseCase(
|
|||
accountId: AccountId,
|
||||
add: CryptoCurrency? = null,
|
||||
remove: CryptoCurrency? = null,
|
||||
skipDerivationErrors: Boolean = true,
|
||||
): Either<Throwable, Unit> {
|
||||
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<CryptoCurrency> = emptyList(),
|
||||
remove: List<CryptoCurrency> = emptyList(),
|
||||
skipDerivationErrors: Boolean = true,
|
||||
): Either<Throwable, Unit> = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<VisaApiError, Unit>
|
||||
suspend fun pollWithdrawOrdersIfNeeds(userWallet: UserWallet)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<Throwable, Unit> =
|
||||
|
|
@ -16,7 +16,7 @@ class SetNotificationsEnabledUseCase(
|
|||
userWalletId = userWalletId,
|
||||
isEnabled = isEnabled,
|
||||
)
|
||||
currenciesRepository.syncTokens(userWalletId)
|
||||
accountsCRUDRepository.syncTokens(userWalletId)
|
||||
}.onLeft {
|
||||
walletsRepository.setNotificationsEnabled(
|
||||
userWalletId = userWalletId,
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<String>) = 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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<String, String> = 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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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<Throwable, List<SwapPairLeast>>
|
||||
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ internal class SendWithSwapConfirmSentStateTransformer(
|
|||
txUrl = txUrl,
|
||||
provider = provider,
|
||||
swapDataModel = swapDataModel,
|
||||
amountUM = prevState.amountUM,
|
||||
destinationUM = prevState.destinationUM,
|
||||
feeSelectorUM = prevState.feeSelectorUM,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
|
|||
contractAddress = pair.to.contractAddress,
|
||||
network = pair.to.network,
|
||||
),
|
||||
providers = pair.providers.mapNotNull {
|
||||
convertProvider(it, providersAdditionalMap)
|
||||
},
|
||||
providers = pair.providers
|
||||
.filterNot { it.hasOnlyFixedRateType() }
|
||||
.mapNotNull { convertProvider(it, providersAdditionalMap) },
|
||||
)
|
||||
}
|
||||
return PairsWithProviders(
|
||||
|
|
|
|||
|
|
@ -46,31 +46,12 @@ sealed class SwapEvents(
|
|||
class ChooseTokenScreenResult(
|
||||
val isTokenChosen: Boolean,
|
||||
val token: String? = null,
|
||||
val source: String? = null,
|
||||
val isSearched: Boolean? = null,
|
||||
) : SwapEvents(
|
||||
event = "Choose Token Screen Result",
|
||||
params = buildMap {
|
||||
put("Token Chosen", if (isTokenChosen) "Yes" else "No")
|
||||
token?.let { put("Token", it) }
|
||||
source?.let { put(TOKEN_SELECTED_SOURCE, it) }
|
||||
isSearched?.let { put(SEARCHED, if (it) "True" else "False") }
|
||||
},
|
||||
) {
|
||||
companion object {
|
||||
const val TOKEN_SELECTED_SOURCE = "Token Selected Source"
|
||||
const val SEARCHED = "Searched"
|
||||
const val SOURCE_PORTFOLIO = "Portfolio"
|
||||
const val SOURCE_MARKETS = "Markets"
|
||||
}
|
||||
}
|
||||
|
||||
class TokenAdded(val token: String, val blockchain: String) : SwapEvents(
|
||||
event = "Token Added",
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to token,
|
||||
BLOCKCHAIN to blockchain,
|
||||
),
|
||||
)
|
||||
|
||||
class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import dagger.assisted.Assisted
|
|||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -55,7 +56,11 @@ class SwapFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
|
||||
init {
|
||||
params.repository.state
|
||||
.onEach(feeSelectorBlockComponent::updateState)
|
||||
.onEach { feeSelectorBlockComponent.updateState(it) }
|
||||
.launchIn(componentScope)
|
||||
|
||||
params.repository.forceUpdateState
|
||||
.onEach { feeSelectorBlockComponent.updateState(it) }
|
||||
.launchIn(componentScope)
|
||||
}
|
||||
|
||||
|
|
@ -68,6 +73,9 @@ class SwapFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
val state: StateFlow<FeeSelectorUM>
|
||||
get() = MutableStateFlow<FeeSelectorUM>(FeeSelectorUM.Loading)
|
||||
|
||||
val forceUpdateState: SharedFlow<FeeSelectorUM>
|
||||
get() = MutableStateFlow<FeeSelectorUM>(FeeSelectorUM.Loading)
|
||||
|
||||
fun onResult(newState: FeeSelectorUM)
|
||||
|
||||
suspend fun loadFee(): Either<GetFeeError, TransactionFee>
|
||||
|
|
|
|||
|
|
@ -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<FeeSelectorUM>()
|
||||
|
||||
override suspend fun loadFeeExtended(
|
||||
selectedToken: CryptoCurrencyStatus?,
|
||||
): Either<GetFeeError, TransactionFeeExtended> {
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ^
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 35755ad66f5f8fa6bcb1c84b443826cd315495d0
|
||||
Subproject commit 43fab6f690538391cae17e046ffb2ec9fe08b0c7
|
||||
Loading…
Add table
Add a link
Reference in a new issue