Updated on 2026-08-14
This commit is contained in:
commit
99c026d495
51 changed files with 627 additions and 329 deletions
|
|
@ -37,6 +37,7 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
|||
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
||||
import com.tangem.datasource.local.logs.AppLogsStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
|
|
@ -311,6 +312,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
|
|||
TangemApiServiceLogging.addInterceptors(
|
||||
createNetworkLoggingInterceptor(),
|
||||
ChuckerInterceptor(this),
|
||||
NetworkLogsSaveInterceptor(appLogsStore),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import com.tangem.wallet.BuildConfig
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
|
@ -64,7 +65,7 @@ internal class DefaultCardSdkProvider @Inject constructor(
|
|||
if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
appPreferencesStore.getObjectMap<ApiEnvironment>(PreferencesKeys.apiConfigsEnvironmentKey)
|
||||
.map {
|
||||
when (it[ApiConfig.ID.TangemCardSdk.name]) {
|
||||
when (it[ApiConfig.ID.Attestation.name]) {
|
||||
ApiEnvironment.DEV,
|
||||
ApiEnvironment.STAGE,
|
||||
-> false
|
||||
|
|
@ -73,6 +74,7 @@ internal class DefaultCardSdkProvider @Inject constructor(
|
|||
-> true
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach { isProd ->
|
||||
holder?.let {
|
||||
it.sdk.config.isTangemAttestationProdEnv = isProd
|
||||
|
|
@ -170,7 +172,7 @@ internal class DefaultCardSdkProvider @Inject constructor(
|
|||
config = config.apply {
|
||||
isNewOnlineAttestationEnabled = cardSdkFeatureToggles.isNewAttestationEnabled
|
||||
|
||||
val apiConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.TangemCardSdk)
|
||||
val apiConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.Attestation)
|
||||
isTangemAttestationProdEnv = apiConfig.environment == ApiEnvironment.PROD
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ fun ExpressProvider(
|
|||
val hapticFeedback = LocalHapticFeedback.current
|
||||
val context = LocalContext.current
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
modifier = Modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.background(TangemTheme.colors.background.action),
|
||||
|
|
|
|||
|
|
@ -19,16 +19,16 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM
|
||||
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
|
@ -53,6 +53,7 @@ fun ExpressStatusBlock(state: ExpressStatusUM, modifier: Modifier = Modifier) {
|
|||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.padding(bottom = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
|
|
@ -77,8 +78,14 @@ fun ExpressStatusBlock(state: ExpressStatusUM, modifier: Modifier = Modifier) {
|
|||
.padding(end = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
Text(
|
||||
text = link.text.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
text = resourceReference(
|
||||
id = R.string.express_transaction_id,
|
||||
formatArgs = wrappedList(link.text),
|
||||
).resolveReference(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ sealed class ApiConfig {
|
|||
TangemTech,
|
||||
StakeKit,
|
||||
TangemVisa,
|
||||
TangemCardSdk,
|
||||
Attestation,
|
||||
BlockAid,
|
||||
}
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ sealed class ApiConfig {
|
|||
is TangemTech -> ID.TangemTech
|
||||
is StakeKit -> ID.StakeKit
|
||||
is TangemVisa -> ID.TangemVisa
|
||||
is TangemCardSdk -> ID.TangemCardSdk
|
||||
is Attestation -> ID.Attestation
|
||||
is BlockAid -> ID.BlockAid
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
/**
|
||||
* Tangem API [ApiConfig] that used in CardSDK
|
||||
* Attestation [ApiConfig] that used in CardSDK
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class TangemCardSdk : ApiConfig() {
|
||||
internal class Attestation : ApiConfig() {
|
||||
|
||||
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
|
||||
|
||||
|
|
@ -16,27 +16,27 @@ interface TangemExpressApi {
|
|||
@POST("assets")
|
||||
suspend fun getAssets(
|
||||
@Header("user-id") userWalletId: String,
|
||||
@Header("refcode") refCode: String,
|
||||
@Header("refcode") refCode: String?,
|
||||
@Body body: AssetsRequestBody,
|
||||
): ApiResponse<List<Asset>>
|
||||
|
||||
@POST("pairs")
|
||||
suspend fun getPairs(
|
||||
@Header("user-id") userWalletId: String,
|
||||
@Header("refcode") refCode: String,
|
||||
@Header("refcode") refCode: String?,
|
||||
@Body body: PairsRequestBody,
|
||||
): ApiResponse<List<SwapPair>>
|
||||
|
||||
@GET("providers")
|
||||
suspend fun getProviders(
|
||||
@Header("user-id") userWalletId: String,
|
||||
@Header("refcode") refCode: String,
|
||||
@Header("refcode") refCode: String?,
|
||||
): ApiResponse<List<ExchangeProvider>>
|
||||
|
||||
@GET("exchange-quote")
|
||||
suspend fun getExchangeQuote(
|
||||
@Header("user-id") userWalletId: String,
|
||||
@Header("refcode") refCode: String,
|
||||
@Header("refcode") refCode: String?,
|
||||
@Query("fromContractAddress") fromContractAddress: String,
|
||||
@Query("fromNetwork") fromNetwork: String,
|
||||
@Query("toContractAddress") toContractAddress: String,
|
||||
|
|
@ -51,7 +51,7 @@ interface TangemExpressApi {
|
|||
@GET("exchange-data")
|
||||
suspend fun getExchangeData(
|
||||
@Header("user-id") userWalletId: String,
|
||||
@Header("refcode") refCode: String,
|
||||
@Header("refcode") refCode: String?,
|
||||
@Query("fromContractAddress") fromContractAddress: String,
|
||||
@Query("fromNetwork") fromNetwork: String,
|
||||
@Query("toContractAddress") toContractAddress: String,
|
||||
|
|
@ -71,14 +71,14 @@ interface TangemExpressApi {
|
|||
@GET("exchange-status")
|
||||
suspend fun getExchangeStatus(
|
||||
@Header("user-id") userWalletId: String,
|
||||
@Header("refcode") refCode: String,
|
||||
@Header("refcode") refCode: String?,
|
||||
@Query("txId") txId: String,
|
||||
): ApiResponse<ExchangeStatusResponse>
|
||||
|
||||
@POST("exchange-sent")
|
||||
suspend fun exchangeSent(
|
||||
@Header("user-id") userWalletId: String,
|
||||
@Header("refcode") refCode: String,
|
||||
@Header("refcode") refCode: String?,
|
||||
@Body body: ExchangeSentRequestBody,
|
||||
): ApiResponse<ExchangeSentResponseBody>
|
||||
}
|
||||
|
|
@ -17,38 +17,38 @@ interface OnrampApi {
|
|||
@GET("currencies")
|
||||
suspend fun getCurrencies(
|
||||
@Header("user-id") userWalletId: String,
|
||||
@Header("refcode") refCode: String,
|
||||
@Header("refcode") refCode: String?,
|
||||
): ApiResponse<List<OnrampCurrencyDTO>>
|
||||
|
||||
@GET("countries")
|
||||
suspend fun getCountries(
|
||||
@Header("user-id") userWalletId: String,
|
||||
@Header("refcode") refCode: String,
|
||||
@Header("refcode") refCode: String?,
|
||||
): ApiResponse<List<OnrampCountryDTO>>
|
||||
|
||||
@GET("country-by-ip")
|
||||
suspend fun getCountryByIp(
|
||||
@Header("user-id") userWalletId: String,
|
||||
@Header("refcode") refCode: String,
|
||||
@Header("refcode") refCode: String?,
|
||||
): ApiResponse<OnrampCountryDTO>
|
||||
|
||||
@GET("payment-methods")
|
||||
suspend fun getPaymentMethods(
|
||||
@Header("user-id") userWalletId: String,
|
||||
@Header("refcode") refCode: String,
|
||||
@Header("refcode") refCode: String?,
|
||||
): ApiResponse<List<PaymentMethodDTO>>
|
||||
|
||||
@POST("onramp-pairs")
|
||||
suspend fun getPairs(
|
||||
@Header("user-id") userWalletId: String,
|
||||
@Header("refcode") refCode: String,
|
||||
@Header("refcode") refCode: String?,
|
||||
@Body body: OnrampPairsRequest,
|
||||
): ApiResponse<List<OnrampPairDTO>>
|
||||
|
||||
@GET("onramp-quote")
|
||||
suspend fun getQuote(
|
||||
@Header("user-id") userWalletId: String,
|
||||
@Header("refcode") refCode: String,
|
||||
@Header("refcode") refCode: String?,
|
||||
@Query("fromCurrencyCode") fromCurrencyCode: String,
|
||||
@Query("fromPrecision") fromPrecision: Int,
|
||||
@Query("toContractAddress") toContractAddress: String,
|
||||
|
|
@ -83,7 +83,7 @@ interface OnrampApi {
|
|||
@GET("onramp-status")
|
||||
suspend fun getStatus(
|
||||
@Header("user-id") userWalletId: String,
|
||||
@Header("refcode") refCode: String,
|
||||
@Header("refcode") refCode: String?,
|
||||
@Query("txId") txId: String,
|
||||
): ApiResponse<OnrampStatusResponse>
|
||||
}
|
||||
|
|
@ -49,5 +49,5 @@ internal object ApiConfigsModule {
|
|||
|
||||
@Provides
|
||||
@IntoSet
|
||||
fun provideTangemCardSdkConfig(): ApiConfig = TangemCardSdk()
|
||||
fun provideAttestationConfig(): ApiConfig = Attestation()
|
||||
}
|
||||
|
|
@ -11,12 +11,12 @@ object ExpressUtils {
|
|||
private const val BATCH_ID_CHANGENOW = "BB000013"
|
||||
private const val BATCH_ID_PARTNER = "AF990015"
|
||||
|
||||
fun getRefCode(userWallet: UserWallet, appPreferencesStore: AppPreferencesStore): String {
|
||||
fun getRefCode(userWallet: UserWallet, appPreferencesStore: AppPreferencesStore): String? {
|
||||
return when {
|
||||
isRing(userWallet, appPreferencesStore) -> "ring"
|
||||
isChangeNow(userWallet) -> "ChangeNow"
|
||||
isPartner(userWallet) -> "partner"
|
||||
else -> ""
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ private const val JSON_INDENT_SPACES = 4
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class NetworkLogsSaveInterceptor(
|
||||
class NetworkLogsSaveInterceptor(
|
||||
private val appLogsStore: AppLogsStore,
|
||||
) : Interceptor {
|
||||
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
is TangemTech -> createTangemTechModel()
|
||||
is StakeKit -> createStakeKitModel()
|
||||
is TangemVisa -> createVisaModel()
|
||||
is TangemCardSdk -> createTangemCardSdkModel()
|
||||
is Attestation -> createAttestationModel()
|
||||
is BlockAid -> createBlockAidSdkModel()
|
||||
}
|
||||
}
|
||||
|
|
@ -189,9 +189,9 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
|
|||
)
|
||||
}
|
||||
|
||||
private fun createTangemCardSdkModel(): Model {
|
||||
private fun createAttestationModel(): Model {
|
||||
return Model(
|
||||
id = ApiConfig.ID.TangemCardSdk,
|
||||
id = ApiConfig.ID.Attestation,
|
||||
expected = ApiEnvironmentConfig(
|
||||
environment = ApiEnvironment.PROD,
|
||||
baseUrl = "https://api.tangem-tech.com/",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import androidx.compose.ui.res.painterResource
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
|
|
@ -76,7 +77,7 @@ fun InputRowBestRate(
|
|||
text = subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
|
||||
modifier = Modifier.padding(top = 2.dp),
|
||||
)
|
||||
}
|
||||
SpacerWMax()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.core.ui.extensions
|
||||
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import com.tangem.core.ui.utils.MultipleClickPreventer
|
||||
|
||||
/**
|
||||
* Clickable modifier with debounce multiple click in a short period of time
|
||||
*/
|
||||
fun Modifier.clickableSingle(
|
||||
enabled: Boolean = true,
|
||||
onClickLabel: String? = null,
|
||||
role: Role? = null,
|
||||
onClick: () -> Unit,
|
||||
) = composed {
|
||||
val multipleEventsCutter = remember { MultipleClickPreventer.get() }
|
||||
Modifier.clickable(
|
||||
enabled = enabled,
|
||||
onClickLabel = onClickLabel,
|
||||
onClick = { multipleEventsCutter.processEvent { onClick() } },
|
||||
role = role,
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
)
|
||||
}
|
||||
|
|
@ -2,6 +2,9 @@ package com.tangem.core.ui.utils
|
|||
|
||||
import android.os.SystemClock
|
||||
|
||||
/**
|
||||
* Debounce multiple events (e.i. clicks, etc) in a short period of time
|
||||
*/
|
||||
interface MultipleClickPreventer {
|
||||
|
||||
fun processEvent(event: () -> Unit)
|
||||
|
|
@ -24,4 +27,9 @@ private class DefaultMultipleClickPreventer : MultipleClickPreventer {
|
|||
companion object {
|
||||
private const val TIME_BETWEEN_CLICK_IN_MS = 500L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce multiple events (e.i. clicks, etc) in a short period of time
|
||||
*/
|
||||
fun singleEvent(event: () -> Unit) = MultipleClickPreventer.get().processEvent(event)
|
||||
|
|
@ -1,11 +1,26 @@
|
|||
package com.tangem.data.networks.multi
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.ensure
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.data.common.currency.CardCryptoCurrencyFactory
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.data.networks.store.NetworksStatusesStoreV2
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.core.utils.eitherOn
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
|
@ -19,26 +34,53 @@ import javax.inject.Inject
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
|
||||
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
private val networksStatusesStore: NetworksStatusesStoreV2,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory,
|
||||
) : MultiNetworkStatusFetcher {
|
||||
|
||||
override suspend fun invoke(params: MultiNetworkStatusFetcher.Params): Either<Throwable, Unit> = either {
|
||||
private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory(excludedBlockchains) }
|
||||
|
||||
override suspend fun invoke(params: MultiNetworkStatusFetcher.Params) = eitherOn(dispatchers.default) {
|
||||
// Optimization!
|
||||
// Every singleNetworkStatusFetcher with applyRefresh as true will refresh every network in the store.
|
||||
// So if we update all networks at once, it will be more efficient.
|
||||
networksStatusesStore.refresh(userWalletId = params.userWalletId, networks = params.networks)
|
||||
|
||||
val userWallet = catch(
|
||||
block = { userWalletsStore.getSyncStrict(key = params.userWalletId) },
|
||||
catch = {
|
||||
networksStatusesStore.storeError(userWalletId = params.userWalletId, networks = params.networks)
|
||||
raise(it)
|
||||
},
|
||||
)
|
||||
|
||||
val isNotSingleWallet = with(userWallet.scanResponse.cardTypesResolver) {
|
||||
isMultiwalletAllowed() || isSingleWalletWithToken()
|
||||
}
|
||||
|
||||
ensure(isNotSingleWallet) {
|
||||
networksStatusesStore.storeError(userWalletId = params.userWalletId, networks = params.networks)
|
||||
IllegalStateException("User wallet is not multi-currency")
|
||||
}
|
||||
|
||||
val networksCurrencies = createCurrencies(userWallet = userWallet, networks = params.networks)
|
||||
|
||||
val result = coroutineScope {
|
||||
params.networks
|
||||
.map {
|
||||
async {
|
||||
singleNetworkStatusFetcher(
|
||||
params = SingleNetworkStatusFetcher.Params(
|
||||
params = SingleNetworkStatusFetcher.Params.Prepared(
|
||||
userWalletId = params.userWalletId,
|
||||
network = it,
|
||||
applyRefresh = false,
|
||||
addedNetworkCurrencies = networksCurrencies,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -52,4 +94,31 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor(
|
|||
IllegalStateException("Failed to fetch network statuses")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createCurrencies(userWallet: UserWallet, networks: Set<Network>): Set<CryptoCurrency> {
|
||||
val blockchains = networks.map { Blockchain.fromNetworkId(networkId = it.backendId) }
|
||||
|
||||
// multi-currency wallet
|
||||
if (userWallet.isMultiCurrency) return getMultiWalletCurrencies(userWallet = userWallet, networks = networks)
|
||||
|
||||
// check if the blockchain of single-currency wallet is the same as network
|
||||
val cardBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain()
|
||||
if (!blockchains.contains(cardBlockchain)) return emptySet()
|
||||
|
||||
return cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse).toSet()
|
||||
}
|
||||
|
||||
private suspend fun getMultiWalletCurrencies(userWallet: UserWallet, networks: Set<Network>): Set<CryptoCurrency> {
|
||||
val response = appPreferencesStore.getObjectSyncOrNull<UserTokensResponse>(
|
||||
key = PreferencesKeys.getUserTokensKey(userWallet.walletId.stringValue),
|
||||
) ?: return emptySet()
|
||||
|
||||
return responseCurrenciesFactory.createCurrencies(
|
||||
tokens = response.tokens.filter { token ->
|
||||
networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath }
|
||||
},
|
||||
scanResponse = userWallet.scanResponse,
|
||||
)
|
||||
.toSet()
|
||||
}
|
||||
}
|
||||
|
|
@ -34,14 +34,17 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor(
|
|||
private val networkStatusFactory = NetworkStatusFactory()
|
||||
|
||||
override suspend fun invoke(params: SingleNetworkStatusFetcher.Params) = Either.catchOn(dispatchers.default) {
|
||||
if (params.applyRefresh) {
|
||||
networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network)
|
||||
}
|
||||
val networkCurrencies = when (params) {
|
||||
is SingleNetworkStatusFetcher.Params.Prepared -> params.addedNetworkCurrencies
|
||||
is SingleNetworkStatusFetcher.Params.Simple -> {
|
||||
networksStatusesStore.refresh(userWalletId = params.userWalletId, network = params.network)
|
||||
|
||||
val networkCurrencies = cardCryptoCurrencyFactory.create(
|
||||
userWalletId = params.userWalletId,
|
||||
network = params.network,
|
||||
)
|
||||
cardCryptoCurrencyFactory.create(
|
||||
userWalletId = params.userWalletId,
|
||||
network = params.network,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val result = withContext(dispatchers.io) {
|
||||
walletManagersFacade.update(
|
||||
|
|
@ -61,11 +64,20 @@ internal class DefaultSingleNetworkStatusFetcher @Inject constructor(
|
|||
|
||||
val statusValue = status.value
|
||||
if (statusValue is NetworkStatus.Unreachable) {
|
||||
networksStatusesStore.storeError(
|
||||
val prevStatus = networksStatusesStore.getSyncOrNull(
|
||||
userWalletId = params.userWalletId,
|
||||
network = params.network,
|
||||
value = statusValue,
|
||||
)
|
||||
|
||||
if (prevStatus?.value is NetworkStatus.MissedDerivation) {
|
||||
networksStatusesStore.storeUnreachableStatus(userWalletId = params.userWalletId, value = status)
|
||||
} else {
|
||||
networksStatusesStore.storeError(
|
||||
userWalletId = params.userWalletId,
|
||||
network = params.network,
|
||||
value = statusValue,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
networksStatusesStore.storeSuccess(userWalletId = params.userWalletId, value = status)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,12 @@ internal class DefaultNetworksStatusesStoreV2(
|
|||
return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] }
|
||||
}
|
||||
|
||||
override suspend fun getSyncOrNull(userWalletId: UserWalletId, network: Network): SimpleNetworkStatus? {
|
||||
val simpleStatusId = SimpleNetworkStatus.Id(network = network)
|
||||
|
||||
return runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue)?.firstOrNull { it.id == simpleStatusId }
|
||||
}
|
||||
|
||||
override suspend fun refresh(userWalletId: UserWalletId, network: Network) {
|
||||
refresh(userWalletId = userWalletId, networks = setOf(network))
|
||||
}
|
||||
|
|
@ -105,6 +111,28 @@ internal class DefaultNetworksStatusesStoreV2(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun storeError(userWalletId: UserWalletId, networks: Set<Network>) {
|
||||
updateInRuntime(
|
||||
userWalletId = userWalletId,
|
||||
networks = networks,
|
||||
ifNotFound = ::createUnreachableStatus,
|
||||
update = { status ->
|
||||
status.copy(value = status.value.copySealed(source = StatusSource.ONLY_CACHE))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun storeUnreachableStatus(userWalletId: UserWalletId, value: NetworkStatus) {
|
||||
if (value.value !is NetworkStatus.Unreachable) {
|
||||
val message = "Use storeError method to save unreachable status"
|
||||
Timber.d(message)
|
||||
|
||||
error(message)
|
||||
}
|
||||
|
||||
storeInRuntime(userWalletId = userWalletId, status = value)
|
||||
}
|
||||
|
||||
private suspend fun updateInRuntime(
|
||||
userWalletId: UserWalletId,
|
||||
networks: Set<Network>,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ internal interface NetworksStatusesStoreV2 {
|
|||
/** Get statuses by [userWalletId] */
|
||||
fun get(userWalletId: UserWalletId): Flow<Set<SimpleNetworkStatus>>
|
||||
|
||||
/** Get status of [network] by [userWalletId] synchronously or null */
|
||||
suspend fun getSyncOrNull(userWalletId: UserWalletId, network: Network): SimpleNetworkStatus?
|
||||
|
||||
/** Refresh status of [network] by [userWalletId] */
|
||||
suspend fun refresh(userWalletId: UserWalletId, network: Network)
|
||||
|
||||
|
|
@ -27,4 +30,10 @@ internal interface NetworksStatusesStoreV2 {
|
|||
* If [value] is null, default unreachable status will be stored.
|
||||
*/
|
||||
suspend fun storeError(userWalletId: UserWalletId, network: Network, value: NetworkStatus.Unreachable? = null)
|
||||
|
||||
/** Store unreachable [value] by [userWalletId] */
|
||||
suspend fun storeUnreachableStatus(userWalletId: UserWalletId, value: NetworkStatus)
|
||||
|
||||
/** Store error for [networks] by [userWalletId] */
|
||||
suspend fun storeError(userWalletId: UserWalletId, networks: Set<Network>)
|
||||
}
|
||||
|
|
@ -1,133 +1,119 @@
|
|||
package com.tangem.data.networks.multi
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||
import com.tangem.data.networks.store.NetworksStatusesStoreV2
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultMultiNetworkStatusFetcherTest {
|
||||
|
||||
private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher = mockk()
|
||||
private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxed = true)
|
||||
|
||||
private val fetcher = DefaultMultiNetworkStatusFetcher(
|
||||
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
|
||||
networksStatusesStore = networksStatusesStore,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `fetch networks statuses successfully`() = runTest {
|
||||
val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar)
|
||||
|
||||
val ethParams = SingleNetworkStatusFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
network = ethereumAndStellar.first(),
|
||||
applyRefresh = false,
|
||||
)
|
||||
|
||||
val stellarParams = SingleNetworkStatusFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
network = ethereumAndStellar.last(),
|
||||
applyRefresh = false,
|
||||
)
|
||||
|
||||
coEvery { singleNetworkStatusFetcher(ethParams) } returns Unit.right()
|
||||
coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right()
|
||||
|
||||
val actual = fetcher(params)
|
||||
|
||||
coVerify {
|
||||
networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar)
|
||||
singleNetworkStatusFetcher(ethParams)
|
||||
singleNetworkStatusFetcher(stellarParams)
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isRight()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch networks statuses failure if one of them fails`() = runTest {
|
||||
val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar)
|
||||
|
||||
val ethParams = SingleNetworkStatusFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
network = ethereumAndStellar.first(),
|
||||
applyRefresh = false,
|
||||
)
|
||||
|
||||
val stellarParams = SingleNetworkStatusFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
network = ethereumAndStellar.last(),
|
||||
applyRefresh = false,
|
||||
)
|
||||
|
||||
val ethException = IllegalStateException("eth")
|
||||
coEvery { singleNetworkStatusFetcher(ethParams) } returns ethException.left()
|
||||
coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right()
|
||||
|
||||
val actual = fetcher(params)
|
||||
|
||||
coVerify {
|
||||
networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar)
|
||||
singleNetworkStatusFetcher(ethParams)
|
||||
singleNetworkStatusFetcher(stellarParams)
|
||||
}
|
||||
|
||||
val expected = IllegalStateException("Failed to fetch network statuses")
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch networks statuses failure if all of them fails`() = runTest {
|
||||
val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar)
|
||||
|
||||
val ethParams = SingleNetworkStatusFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
network = ethereumAndStellar.first(),
|
||||
applyRefresh = false,
|
||||
)
|
||||
|
||||
val stellarParams = SingleNetworkStatusFetcher.Params(
|
||||
userWalletId = userWalletId,
|
||||
network = ethereumAndStellar.last(),
|
||||
applyRefresh = false,
|
||||
)
|
||||
|
||||
coEvery { singleNetworkStatusFetcher(ethParams) } returns IllegalStateException("eth").left()
|
||||
coEvery { singleNetworkStatusFetcher(stellarParams) } returns IllegalStateException("stellar").left()
|
||||
|
||||
val actual = fetcher(params)
|
||||
|
||||
coVerify {
|
||||
networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar)
|
||||
singleNetworkStatusFetcher(ethParams)
|
||||
singleNetworkStatusFetcher(stellarParams)
|
||||
}
|
||||
|
||||
val expected = IllegalStateException("Failed to fetch network statuses")
|
||||
|
||||
Truth.assertThat(actual.isLeft()).isTrue()
|
||||
Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val userWalletId = UserWalletId("011")
|
||||
val ethereumAndStellar = MockCryptoCurrencyFactory().ethereumAndStellar.map { it.network }.toSet()
|
||||
}
|
||||
}
|
||||
// internal class DefaultMultiNetworkStatusFetcherTest {
|
||||
//
|
||||
// private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher = mockk()
|
||||
// private val networksStatusesStore: NetworksStatusesStoreV2 = mockk(relaxed = true)
|
||||
//
|
||||
// private val fetcher = DefaultMultiNetworkStatusFetcher(
|
||||
// singleNetworkStatusFetcher = singleNetworkStatusFetcher,
|
||||
// networksStatusesStore = networksStatusesStore,
|
||||
// )
|
||||
//
|
||||
// @Test
|
||||
// fun `fetch networks statuses successfully`() = runTest {
|
||||
// val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar)
|
||||
//
|
||||
// val ethParams = SingleNetworkStatusFetcher.Params(
|
||||
// userWalletId = userWalletId,
|
||||
// network = ethereumAndStellar.first(),
|
||||
// applyRefresh = false,
|
||||
// )
|
||||
//
|
||||
// val stellarParams = SingleNetworkStatusFetcher.Params(
|
||||
// userWalletId = userWalletId,
|
||||
// network = ethereumAndStellar.last(),
|
||||
// applyRefresh = false,
|
||||
// )
|
||||
//
|
||||
// coEvery { singleNetworkStatusFetcher(ethParams) } returns Unit.right()
|
||||
// coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right()
|
||||
//
|
||||
// val actual = fetcher(params)
|
||||
//
|
||||
// coVerify {
|
||||
// networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar)
|
||||
// singleNetworkStatusFetcher(ethParams)
|
||||
// singleNetworkStatusFetcher(stellarParams)
|
||||
// }
|
||||
//
|
||||
// Truth.assertThat(actual.isRight()).isTrue()
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// fun `fetch networks statuses failure if one of them fails`() = runTest {
|
||||
// val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar)
|
||||
//
|
||||
// val ethParams = SingleNetworkStatusFetcher.Params(
|
||||
// userWalletId = userWalletId,
|
||||
// network = ethereumAndStellar.first(),
|
||||
// applyRefresh = false,
|
||||
// )
|
||||
//
|
||||
// val stellarParams = SingleNetworkStatusFetcher.Params(
|
||||
// userWalletId = userWalletId,
|
||||
// network = ethereumAndStellar.last(),
|
||||
// applyRefresh = false,
|
||||
// )
|
||||
//
|
||||
// val ethException = IllegalStateException("eth")
|
||||
// coEvery { singleNetworkStatusFetcher(ethParams) } returns ethException.left()
|
||||
// coEvery { singleNetworkStatusFetcher(stellarParams) } returns Unit.right()
|
||||
//
|
||||
// val actual = fetcher(params)
|
||||
//
|
||||
// coVerify {
|
||||
// networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar)
|
||||
// singleNetworkStatusFetcher(ethParams)
|
||||
// singleNetworkStatusFetcher(stellarParams)
|
||||
// }
|
||||
//
|
||||
// val expected = IllegalStateException("Failed to fetch network statuses")
|
||||
//
|
||||
// Truth.assertThat(actual.isLeft()).isTrue()
|
||||
// Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
// Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// fun `fetch networks statuses failure if all of them fails`() = runTest {
|
||||
// val params = MultiNetworkStatusFetcher.Params(userWalletId = userWalletId, networks = ethereumAndStellar)
|
||||
//
|
||||
// val ethParams = SingleNetworkStatusFetcher.Params(
|
||||
// userWalletId = userWalletId,
|
||||
// network = ethereumAndStellar.first(),
|
||||
// applyRefresh = false,
|
||||
// )
|
||||
//
|
||||
// val stellarParams = SingleNetworkStatusFetcher.Params(
|
||||
// userWalletId = userWalletId,
|
||||
// network = ethereumAndStellar.last(),
|
||||
// applyRefresh = false,
|
||||
// )
|
||||
//
|
||||
// coEvery { singleNetworkStatusFetcher(ethParams) } returns IllegalStateException("eth").left()
|
||||
// coEvery { singleNetworkStatusFetcher(stellarParams) } returns IllegalStateException("stellar").left()
|
||||
//
|
||||
// val actual = fetcher(params)
|
||||
//
|
||||
// coVerify {
|
||||
// networksStatusesStore.refresh(userWalletId = userWalletId, networks = ethereumAndStellar)
|
||||
// singleNetworkStatusFetcher(ethParams)
|
||||
// singleNetworkStatusFetcher(stellarParams)
|
||||
// }
|
||||
//
|
||||
// val expected = IllegalStateException("Failed to fetch network statuses")
|
||||
//
|
||||
// Truth.assertThat(actual.isLeft()).isTrue()
|
||||
// Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java)
|
||||
// Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message)
|
||||
// }
|
||||
//
|
||||
// private companion object {
|
||||
// val userWalletId = UserWalletId("011")
|
||||
// val ethereumAndStellar = MockCryptoCurrencyFactory().ethereumAndStellar.map { it.network }.toSet()
|
||||
// }
|
||||
// }
|
||||
|
|
@ -81,38 +81,10 @@ internal class DefaultSingleNetworkStatusFetcherTest {
|
|||
Truth.assertThat(actual.leftOrNull()).isEqualTo(exception)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fetch network status if applyRefresh is false`() = runTest {
|
||||
val params = createParams(applyRefresh = false)
|
||||
|
||||
coEvery { cardCryptoCurrencyFactory.create(params.userWalletId, params.network) } returns listOf(ethereum)
|
||||
|
||||
val result = UpdateWalletManagerResult.MissedDerivation
|
||||
coEvery { walletManagersFacade.update(params.userWalletId, params.network, emptySet()) } returns result
|
||||
|
||||
val actual = fetcher(params)
|
||||
|
||||
coVerifyOrder {
|
||||
cardCryptoCurrencyFactory.create(params.userWalletId, params.network)
|
||||
walletManagersFacade.update(params.userWalletId, params.network, emptySet())
|
||||
networksStatusesStore.storeSuccess(
|
||||
userWalletId = params.userWalletId,
|
||||
value = NetworkStatus(params.network, NetworkStatus.MissedDerivation),
|
||||
)
|
||||
}
|
||||
|
||||
coVerify(inverse = true) {
|
||||
networksStatusesStore.refresh(userWalletId = any(), network = any())
|
||||
}
|
||||
|
||||
Truth.assertThat(actual.isRight()).isTrue()
|
||||
}
|
||||
|
||||
private fun createParams(applyRefresh: Boolean = true): SingleNetworkStatusFetcher.Params {
|
||||
return SingleNetworkStatusFetcher.Params(
|
||||
private fun createParams(): SingleNetworkStatusFetcher.Params {
|
||||
return SingleNetworkStatusFetcher.Params.Simple(
|
||||
userWalletId = UserWalletId("011"),
|
||||
network = ethereum.network,
|
||||
applyRefresh = applyRefresh,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -280,6 +280,51 @@ internal class NetworkStatusesStoreUpdateMethodsTest {
|
|||
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<NetworkStatusDM>>())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `store error for networks if runtime store is empty`() = runTest {
|
||||
store.storeError(userWalletId = userWalletId, networks = networks)
|
||||
|
||||
val runtimeExpected = mapOf(
|
||||
userWalletId.stringValue to setOf(
|
||||
SimpleNetworkStatus(
|
||||
id = SimpleNetworkStatus.Id(networks.first()),
|
||||
value = NetworkStatus.Unreachable(address = null),
|
||||
),
|
||||
SimpleNetworkStatus(
|
||||
id = SimpleNetworkStatus.Id(networks.last()),
|
||||
value = NetworkStatus.Unreachable(address = null),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected)
|
||||
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<NetworkStatusDM>>())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `store error for networks if runtime store contains status with this network`() = runTest {
|
||||
val status = MockNetworkStatusFactory.createVerified(networks.first()).toSimple()
|
||||
|
||||
runtimeStore.store(
|
||||
value = mapOf(userWalletId.stringValue to setOf(status)),
|
||||
)
|
||||
|
||||
store.storeError(userWalletId = userWalletId, networks = networks)
|
||||
|
||||
val runtimeExpected = mapOf(
|
||||
userWalletId.stringValue to setOf(
|
||||
status.copy(value = status.value.copySealed(source = StatusSource.ONLY_CACHE)),
|
||||
SimpleNetworkStatus(
|
||||
id = SimpleNetworkStatus.Id(networks.last()),
|
||||
value = NetworkStatus.Unreachable(address = null),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected)
|
||||
Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap<String, Set<NetworkStatusDM>>())
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val userWalletId = UserWalletId(stringValue = "011")
|
||||
|
|
|
|||
|
|
@ -372,7 +372,7 @@ internal class DefaultOnrampRepository(
|
|||
refCode = ExpressUtils.getRefCode(
|
||||
userWallet = userWallet,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
).takeUnless { it.isEmpty() },
|
||||
),
|
||||
).bind()
|
||||
},
|
||||
onError = { e ->
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.tangem.domain.core.utils.catchOn
|
|||
import com.tangem.domain.quotes.multi.MultiQuoteFetcher
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
@ -53,7 +54,11 @@ internal class DefaultMultiQuoteFetcher @Inject constructor(
|
|||
val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",")
|
||||
|
||||
val response = safeApiCallWithTimeout(
|
||||
call = { tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds).bind() },
|
||||
call = {
|
||||
withContext(dispatchers.io) {
|
||||
tangemTechApi.getQuotes(currencyId = appCurrencyId, coinIds = coinIds).bind()
|
||||
}
|
||||
},
|
||||
onError = { error -> throw error },
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package com.tangem.domain.core.utils
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.Raise
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.domain.core.lce.Lce
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.experimental.ExperimentalTypeInference
|
||||
|
||||
/**
|
||||
* [Flow] of [Either]
|
||||
|
|
@ -47,4 +50,15 @@ suspend inline fun <R> Either.Companion.catchOn(
|
|||
context = dispatcher,
|
||||
block = { catch { function() } },
|
||||
)
|
||||
}
|
||||
|
||||
@JvmName("eitherWithDispatch")
|
||||
@OptIn(ExperimentalTypeInference::class)
|
||||
suspend inline fun <Error, A> eitherOn(
|
||||
dispatcher: CoroutineDispatcher,
|
||||
@BuilderInference noinline block: suspend Raise<Error>.() -> A,
|
||||
): Either<Error, A> {
|
||||
return withContext(dispatcher) {
|
||||
either { block() }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.networks.single
|
||||
|
||||
import com.tangem.domain.core.flow.FlowFetcher
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
|
|
@ -11,16 +12,18 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
*/
|
||||
interface SingleNetworkStatusFetcher : FlowFetcher<SingleNetworkStatusFetcher.Params> {
|
||||
|
||||
/**
|
||||
* Params
|
||||
*
|
||||
* @property userWalletId wallet id
|
||||
* @property network network
|
||||
* @property applyRefresh flag that determines whether to apply refresh (see DefaultMultiNetworkStatusFetcher)
|
||||
*/
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val network: Network,
|
||||
val applyRefresh: Boolean = true,
|
||||
)
|
||||
/** Params */
|
||||
sealed interface Params {
|
||||
|
||||
val userWalletId: UserWalletId
|
||||
val network: Network
|
||||
|
||||
data class Simple(override val userWalletId: UserWalletId, override val network: Network) : Params
|
||||
|
||||
data class Prepared(
|
||||
override val userWalletId: UserWalletId,
|
||||
override val network: Network,
|
||||
val addedNetworkCurrencies: Set<CryptoCurrency>,
|
||||
) : Params
|
||||
}
|
||||
}
|
||||
|
|
@ -5,12 +5,12 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER
|
|||
import com.tangem.core.analytics.models.AnalyticsParam.Key.STATUS
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
|
||||
|
||||
class TokenOnrampAnalyticsEvent(
|
||||
sealed class TokenOnrampAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent("Onramp", event, params) {
|
||||
|
||||
class OnrampStatusOpened(tokenSymbol: String, provider: String, fiatCurrency: String) : TokenScreenAnalyticsEvent(
|
||||
class OnrampStatusOpened(tokenSymbol: String, provider: String, fiatCurrency: String) : TokenOnrampAnalyticsEvent(
|
||||
event = "Onramp Status Opened",
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to tokenSymbol,
|
||||
|
|
@ -20,7 +20,7 @@ class TokenOnrampAnalyticsEvent(
|
|||
)
|
||||
|
||||
class OnrampStatusChanged(tokenSymbol: String, status: String, provider: String, fiatCurrency: String) :
|
||||
TokenScreenAnalyticsEvent(
|
||||
TokenOnrampAnalyticsEvent(
|
||||
event = "Onramp Status",
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to tokenSymbol,
|
||||
|
|
@ -30,12 +30,12 @@ class TokenOnrampAnalyticsEvent(
|
|||
),
|
||||
)
|
||||
|
||||
data object GoToProvider : TokenScreenAnalyticsEvent(event = "Button - Go To Provider")
|
||||
data object GoToProvider : TokenOnrampAnalyticsEvent(event = "Button - Go To Provider")
|
||||
|
||||
data class NoticeKYC(
|
||||
val tokenSymbol: String,
|
||||
val provider: String,
|
||||
) : TokenScreenAnalyticsEvent(
|
||||
) : TokenOnrampAnalyticsEvent(
|
||||
event = "Notice - KYC",
|
||||
params = mapOf(
|
||||
TOKEN_PARAM to tokenSymbol,
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ class FetchCurrencyStatusUseCase(
|
|||
|
||||
private suspend fun Raise<CurrencyStatusError>.fetchNetworkStatus(userWalletId: UserWalletId, network: Network) {
|
||||
singleNetworkStatusFetcher(
|
||||
params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network),
|
||||
params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network),
|
||||
)
|
||||
.mapLeft { CurrencyStatusError.DataError(it) }
|
||||
.bind()
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class UpdateDelayedNetworkStatusUseCase(
|
|||
|
||||
private suspend fun Raise<CurrencyStatusError>.fetchNetworkStatus(userWalletId: UserWalletId, network: Network) {
|
||||
singleNetworkStatusFetcher(
|
||||
params = SingleNetworkStatusFetcher.Params(userWalletId = userWalletId, network = network),
|
||||
params = SingleNetworkStatusFetcher.Params.Simple(userWalletId = userWalletId, network = network),
|
||||
)
|
||||
.mapLeft(CurrencyStatusError::DataError)
|
||||
.bind()
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ class SendTransactionUseCase(
|
|||
return sendResult
|
||||
.onRight {
|
||||
singleNetworkStatusFetcher(
|
||||
params = SingleNetworkStatusFetcher.Params(
|
||||
params = SingleNetworkStatusFetcher.Params.Simple(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = network,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ internal class OnrampAmountStateFactory(
|
|||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val onrampIntents: OnrampIntents,
|
||||
private val cryptoCurrency: CryptoCurrency,
|
||||
private val needApplyFCARestrictions: Provider<Boolean>,
|
||||
) {
|
||||
|
||||
private val onrampAmountFieldChangeConverter = OnrampAmountFieldChangeConverter(
|
||||
|
|
@ -122,7 +123,10 @@ internal class OnrampAmountStateFactory(
|
|||
.filterNot { it == bestProvider }
|
||||
.any { it is OnrampQuote.Data }
|
||||
|
||||
val isBestProvider = selectedQuote == bestProvider && isMultipleQuotes && isOtherQuotesHasData
|
||||
val isBestProvider = selectedQuote == bestProvider &&
|
||||
isMultipleQuotes &&
|
||||
isOtherQuotesHasData &&
|
||||
!needApplyFCARestrictions()
|
||||
|
||||
return currentState.copy(
|
||||
providerBlockState = selectedQuote.toProviderBlockState(isBestProvider),
|
||||
|
|
@ -154,7 +158,7 @@ internal class OnrampAmountStateFactory(
|
|||
paymentMethod = providerResult.paymentMethod,
|
||||
providerId = providerResult.provider.id,
|
||||
providerName = providerResult.provider.info.name,
|
||||
isBestRate = isBestRate,
|
||||
isBestRate = isBestRate && !needApplyFCARestrictions(),
|
||||
onClick = onrampIntents::openProviders,
|
||||
termsOfUseLink = providerResult.provider.info.termsOfUseLink,
|
||||
privacyPolicyLink = providerResult.provider.info.privacyPolicyLink,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ import com.tangem.domain.onramp.model.OnrampAvailability
|
|||
import com.tangem.domain.onramp.model.OnrampProviderWithQuote
|
||||
import com.tangem.domain.onramp.model.OnrampQuote
|
||||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
||||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.onramp.main.OnrampMainComponent
|
||||
import com.tangem.features.onramp.main.entity.*
|
||||
|
|
@ -35,6 +38,7 @@ import com.tangem.utils.isNullOrZero
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
|
|
@ -53,6 +57,7 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
private val messageSender: UiMessageSender,
|
||||
private val urlOpener: UrlOpener,
|
||||
getWalletsUseCase: GetWalletsUseCase,
|
||||
getUserCountryUseCase: GetUserCountryUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model(), OnrampIntents {
|
||||
|
||||
|
|
@ -70,6 +75,7 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
analyticsEventHandler = analyticsEventHandler,
|
||||
onrampIntents = this,
|
||||
cryptoCurrency = params.cryptoCurrency,
|
||||
needApplyFCARestrictions = Provider { userCountry.needApplyFCARestrictions() },
|
||||
)
|
||||
private val _state: MutableStateFlow<OnrampMainComponentUM> = MutableStateFlow(
|
||||
value = stateFactory.getInitialState(
|
||||
|
|
@ -82,8 +88,12 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
val bottomSheetNavigation: SlotNavigation<OnrampMainBottomSheetConfig> = SlotNavigation()
|
||||
|
||||
private val lastUpdateState = mutableStateOf<OnrampLastUpdate?>(null)
|
||||
private var userCountry: UserCountry? = null
|
||||
|
||||
init {
|
||||
userCountry = getUserCountryUseCase.invokeSync().getOrNull()
|
||||
?: UserCountry.Other(Locale.getDefault().country)
|
||||
|
||||
sendScreenOpenAnalytics()
|
||||
checkResidenceCountry()
|
||||
subscribeToAmountChanges()
|
||||
|
|
@ -287,17 +297,6 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
lastUpdateState.value = null
|
||||
return
|
||||
}
|
||||
|
||||
if (checkLastInputState(quote)) {
|
||||
lastUpdateState.value = OnrampLastUpdate(
|
||||
quote.fromAmount,
|
||||
quote.countryCode,
|
||||
)
|
||||
|
||||
_state.update {
|
||||
amountStateFactory.getUpdatedProviderState(selectedQuote = quote, quotes = quotes)
|
||||
}
|
||||
}
|
||||
_state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote = quote) }
|
||||
}
|
||||
|
||||
|
|
@ -312,7 +311,7 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error }
|
||||
|
||||
// Check if amount, country or currency has changed
|
||||
return if (checkLastInputState(quoteToCheck)) {
|
||||
val newQuote = if (checkLastInputState(quoteToCheck)) {
|
||||
quoteToCheck
|
||||
} else {
|
||||
val state = state.value as? OnrampMainComponentUM.Content
|
||||
|
|
@ -331,6 +330,20 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
lastSelectedQuote
|
||||
}
|
||||
}
|
||||
newQuote?.let { updateProvider(newQuote, quotes) }
|
||||
|
||||
return newQuote
|
||||
}
|
||||
|
||||
private fun updateProvider(quote: OnrampQuote, quotes: List<OnrampQuote>) {
|
||||
lastUpdateState.value = OnrampLastUpdate(
|
||||
quote.fromAmount,
|
||||
quote.countryCode,
|
||||
)
|
||||
|
||||
_state.update {
|
||||
amountStateFactory.getUpdatedProviderState(selectedQuote = quote, quotes = quotes)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRetryQuotes() {
|
||||
|
|
@ -387,7 +400,8 @@ internal class OnrampMainComponentModel @Inject constructor(
|
|||
providerName = errorState.provider.info.name,
|
||||
paymentMethod = errorState.paymentMethod.name,
|
||||
)
|
||||
else -> { /* no-op */ }
|
||||
else -> { /* no-op */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ internal class SelectProviderModel @Inject constructor(
|
|||
private val getOnrampSelectedPaymentMethodUseCase: GetOnrampSelectedPaymentMethodUseCase,
|
||||
private val getOnrampProviderWithQuoteUseCase: GetOnrampProviderWithQuoteUseCase,
|
||||
private val saveSelectedPaymentMethod: OnrampSaveSelectedPaymentMethod,
|
||||
private val getUserCountryUseCase: GetUserCountryUseCase,
|
||||
getUserCountryUseCase: GetUserCountryUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
|
|
|
|||
|
|
@ -5,21 +5,38 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.essenty.lifecycle.subscribe
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.onramp.redirect.model.OnrampRedirectModel
|
||||
import com.tangem.features.onramp.redirect.ui.OnrampRedirectContent
|
||||
import com.tangem.features.onramp.success.OnrampSuccessScreenTrigger
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class DefaultOnrampRedirectComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: OnrampRedirectComponent.Params,
|
||||
private val onrampSuccessScreenTrigger: OnrampSuccessScreenTrigger,
|
||||
) : OnrampRedirectComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: OnrampRedirectModel = getOrCreateModel(params)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
onResume = {
|
||||
// On reopen check if redirect happened. If so trigger close action
|
||||
if (model.latestOnrampTransaction != null) {
|
||||
componentScope.launch {
|
||||
onrampSuccessScreenTrigger.triggerOnrampSuccess(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
BackHandler(onBack = params.onBack)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ internal class OnrampRedirectModel @Inject constructor(
|
|||
private val params: OnrampRedirectComponent.Params = paramsContainer.require()
|
||||
private val selectedUserWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
|
||||
|
||||
private var latestOnrampTransaction: OnrampTransaction? = null
|
||||
var latestOnrampTransaction: OnrampTransaction? = null
|
||||
|
||||
val state = OnrampRedirectUM(
|
||||
topBarConfig = OnrampRedirectTopBarUM(
|
||||
|
|
@ -100,10 +100,12 @@ internal class OnrampRedirectModel @Inject constructor(
|
|||
val replaceOnrampScreens = appRouter.stack
|
||||
.filterNot { it is AppRoute.Onramp }
|
||||
.toMutableList()
|
||||
latestOnrampTransaction = null
|
||||
replaceOnrampScreens.add(AppRoute.OnrampSuccess(latestTxId))
|
||||
appRouter.replaceAll(*replaceOnrampScreens.toTypedArray())
|
||||
} else {
|
||||
// Close redirect screen and show last onramp state
|
||||
latestOnrampTransaction = null
|
||||
params.onBack()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ internal class SetOnrampSuccessContentConverter(
|
|||
else -> if (externalTxId != null) {
|
||||
ExpressLinkUM.Content(
|
||||
icon = R.drawable.ic_copy_24,
|
||||
text = resourceReference(R.string.express_transaction_id, wrappedList(externalTxId)),
|
||||
text = stringReference(externalTxId),
|
||||
onClick = { onCopyClick(externalTxId) },
|
||||
)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction
|
|||
import com.tangem.domain.onramp.model.error.OnrampError
|
||||
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.onramp.component.OnrampSuccessComponent
|
||||
import com.tangem.features.onramp.impl.R
|
||||
|
|
@ -64,6 +65,7 @@ internal class OnrampSuccessComponentModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun goToProviderClick(providerLink: String) {
|
||||
analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider)
|
||||
urlOpener.openUrl(providerLink)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ internal sealed class CommonSendRoute : Route {
|
|||
|
||||
@Serializable
|
||||
data object Confirm : CommonSendRoute() {
|
||||
override val isEditMode: Boolean = false
|
||||
override val isEditMode: Boolean = true
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -4,19 +4,19 @@ sealed class PredefinedValues {
|
|||
data object Empty : PredefinedValues()
|
||||
|
||||
sealed class Content : PredefinedValues() {
|
||||
abstract val amount: String
|
||||
abstract val address: String
|
||||
abstract val amount: String?
|
||||
abstract val memo: String?
|
||||
|
||||
data class Deeplink(
|
||||
override val amount: String,
|
||||
override val amount: String?,
|
||||
override val address: String,
|
||||
override val memo: String?,
|
||||
val transactionId: String,
|
||||
) : Content()
|
||||
|
||||
data class QrCode(
|
||||
override val amount: String,
|
||||
override val amount: String?,
|
||||
override val address: String,
|
||||
override val memo: String?,
|
||||
) : Content()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.features.send.v2.common.ui
|
|||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
|
@ -26,10 +25,12 @@ import com.tangem.core.ui.components.SpacerW12
|
|||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
|
||||
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
|
||||
import com.tangem.core.ui.extensions.clickableSingle
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.singleEvent
|
||||
import com.tangem.features.send.v2.common.ui.state.NavigationUM
|
||||
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
|
||||
|
||||
@Composable
|
||||
internal fun SendNavigationButtons(navigationUM: NavigationUM, modifier: Modifier = Modifier) {
|
||||
|
|
@ -70,7 +71,7 @@ private fun SendNavigationButton(navigationUM: NavigationUM, modifier: Modifier
|
|||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(TangemTheme.colors.button.secondary)
|
||||
.clickable(onClick = wrappedNavigationUM.onClick)
|
||||
.clickableSingle(onClick = wrappedNavigationUM.onClick)
|
||||
.padding(12.dp),
|
||||
)
|
||||
SpacerW12()
|
||||
|
|
@ -110,7 +111,11 @@ private fun SendDoneButtons(pairButtonsUM: ButtonsUM.SecondaryPairButtonsUM?, mo
|
|||
SecondaryButtonIconStart(
|
||||
text = wrappedPairButtonsUM.leftText.resolveReference(),
|
||||
iconResId = wrappedPairButtonsUM.leftIconResId!!,
|
||||
onClick = wrappedPairButtonsUM.onLeftClick,
|
||||
onClick = {
|
||||
singleEvent {
|
||||
wrappedPairButtonsUM.onLeftClick
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
SpacerW12()
|
||||
|
|
@ -118,8 +123,10 @@ private fun SendDoneButtons(pairButtonsUM: ButtonsUM.SecondaryPairButtonsUM?, mo
|
|||
text = wrappedPairButtonsUM.rightText.resolveReference(),
|
||||
iconResId = wrappedPairButtonsUM.rightIconResId!!,
|
||||
onClick = {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
wrappedPairButtonsUM.onRightClick()
|
||||
singleEvent {
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
wrappedPairButtonsUM.onRightClick()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.features.send.v2.common.utils
|
||||
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.arkivanov.decompose.value.Value
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.send.v2.common.CommonSendRoute
|
||||
|
||||
/**
|
||||
* Workaround to try fix duplicate route crash
|
||||
*/
|
||||
internal fun Router.safeNextClick(
|
||||
currentRoute: CommonSendRoute,
|
||||
nextRoute: CommonSendRoute,
|
||||
childStack: Value<ChildStack<CommonSendRoute, ComposableContentComponent>>,
|
||||
popBack: () -> Unit,
|
||||
) {
|
||||
if (currentRoute.isEditMode) {
|
||||
popBack()
|
||||
} else {
|
||||
val isAlreadyInStack = childStack.value.items.any { it.configuration == nextRoute }
|
||||
if (isAlreadyInStack) {
|
||||
popTo(nextRoute)
|
||||
} else {
|
||||
push(nextRoute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
|
|||
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource
|
||||
import com.tangem.features.send.v2.common.ui.SendContent
|
||||
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.common.utils.safeNextClick
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.send.confirm.SendConfirmComponent
|
||||
import com.tangem.features.send.v2.send.model.SendModel
|
||||
|
|
@ -139,28 +140,31 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
CommonSendRoute.Confirm -> getConfirmComponent(factoryContext)
|
||||
}
|
||||
|
||||
private fun getDestinationComponent(factoryContext: AppComponentContext, route: CommonSendRoute) =
|
||||
SendDestinationComponent(
|
||||
appComponentContext = factoryContext,
|
||||
params = SendDestinationComponentParams.DestinationParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
currentRoute = currentRoute.filterIsInstance<CommonSendRoute.Destination>(),
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
analyticsCategoryName = analyticCategoryName,
|
||||
title = resourceReference(R.string.send_recipient_label),
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.currency,
|
||||
callback = model,
|
||||
onBackClick = ::onChildBack,
|
||||
onNextClick = {
|
||||
if (route.isEditMode) {
|
||||
onChildBack()
|
||||
} else {
|
||||
innerRouter.push(CommonSendRoute.Amount(isEditMode = false))
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
private fun getDestinationComponent(
|
||||
factoryContext: AppComponentContext,
|
||||
route: CommonSendRoute,
|
||||
): SendDestinationComponent = SendDestinationComponent(
|
||||
appComponentContext = factoryContext,
|
||||
params = SendDestinationComponentParams.DestinationParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
currentRoute = currentRoute.filterIsInstance<CommonSendRoute.Destination>(),
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
analyticsCategoryName = analyticCategoryName,
|
||||
title = resourceReference(R.string.send_recipient_label),
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = params.currency,
|
||||
callback = model,
|
||||
onBackClick = ::onChildBack,
|
||||
onNextClick = {
|
||||
innerRouter.safeNextClick(
|
||||
currentRoute = route,
|
||||
nextRoute = CommonSendRoute.Amount(isEditMode = false),
|
||||
popBack = ::onChildBack,
|
||||
childStack = childStack,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
private fun getAmountComponent(
|
||||
factoryContext: AppComponentContext,
|
||||
|
|
@ -196,11 +200,12 @@ internal class DefaultSendComponent @AssistedInject constructor(
|
|||
}
|
||||
},
|
||||
onNextClick = {
|
||||
if (route.isEditMode) {
|
||||
onChildBack()
|
||||
} else {
|
||||
innerRouter.push(CommonSendRoute.Confirm)
|
||||
}
|
||||
innerRouter.safeNextClick(
|
||||
currentRoute = route,
|
||||
nextRoute = CommonSendRoute.Confirm,
|
||||
popBack = ::onChildBack,
|
||||
childStack = childStack,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -119,8 +119,8 @@ internal class SendModel @Inject constructor(
|
|||
_uiState.update { it.copy(destinationUM = destinationUM) }
|
||||
}
|
||||
|
||||
override fun onAmountResult(amountUM: AmountState) {
|
||||
resetPredefinedAmount()
|
||||
override fun onAmountResult(amountUM: AmountState, isResetPredefined: Boolean) {
|
||||
if (isResetPredefined) resetPredefinedAmount()
|
||||
_uiState.update { it.copy(amountUM = amountUM) }
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +129,6 @@ internal class SendModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onResult(sendUM: SendUM) {
|
||||
resetPredefinedAmount()
|
||||
_uiState.update { sendUM }
|
||||
}
|
||||
|
||||
|
|
@ -161,8 +160,8 @@ internal class SendModel @Inject constructor(
|
|||
// reset predefined amount
|
||||
val internalPredefinedValues = predefinedValues
|
||||
predefinedValues = when (internalPredefinedValues) {
|
||||
is PredefinedValues.Content.Deeplink -> internalPredefinedValues.copy(amount = "")
|
||||
is PredefinedValues.Content.QrCode -> internalPredefinedValues.copy(amount = "")
|
||||
is PredefinedValues.Content.Deeplink -> internalPredefinedValues.copy(amount = null)
|
||||
is PredefinedValues.Content.QrCode -> internalPredefinedValues.copy(amount = null)
|
||||
PredefinedValues.Empty -> internalPredefinedValues
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.tangem.features.send.v2.common.CommonSendRoute
|
|||
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
|
||||
import com.tangem.features.send.v2.common.ui.SendContent
|
||||
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
|
||||
import com.tangem.features.send.v2.common.utils.safeNextClick
|
||||
import com.tangem.features.send.v2.impl.R
|
||||
import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent
|
||||
import com.tangem.features.send.v2.sendnft.model.NFTSendModel
|
||||
|
|
@ -126,28 +127,31 @@ internal class DefaultNFTSendComponent @AssistedInject constructor(
|
|||
else -> getStubComponent()
|
||||
}
|
||||
|
||||
private fun getDestinationComponent(factoryContext: AppComponentContext, route: CommonSendRoute) =
|
||||
SendDestinationComponent(
|
||||
appComponentContext = factoryContext,
|
||||
params = SendDestinationComponentParams.DestinationParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
currentRoute = currentRouteFlow.filterIsInstance<CommonSendRoute.Destination>(),
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
title = resourceReference(R.string.nft_send),
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = model.cryptoCurrency,
|
||||
callback = model,
|
||||
onBackClick = ::onChildBack,
|
||||
onNextClick = {
|
||||
if (route.isEditMode) {
|
||||
onChildBack()
|
||||
} else {
|
||||
innerRouter.push(CommonSendRoute.Confirm)
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
private fun getDestinationComponent(
|
||||
factoryContext: AppComponentContext,
|
||||
route: CommonSendRoute,
|
||||
): SendDestinationComponent = SendDestinationComponent(
|
||||
appComponentContext = factoryContext,
|
||||
params = SendDestinationComponentParams.DestinationParams(
|
||||
state = model.uiState.value.destinationUM,
|
||||
currentRoute = currentRouteFlow.filterIsInstance<CommonSendRoute.Destination>(),
|
||||
isBalanceHidingFlow = model.isBalanceHiddenFlow,
|
||||
title = resourceReference(R.string.nft_send),
|
||||
analyticsCategoryName = analyticsCategoryName,
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = model.cryptoCurrency,
|
||||
callback = model,
|
||||
onBackClick = ::onChildBack,
|
||||
onNextClick = {
|
||||
innerRouter.safeNextClick(
|
||||
currentRoute = route,
|
||||
nextRoute = CommonSendRoute.Confirm,
|
||||
popBack = ::onChildBack,
|
||||
childStack = childStack,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
private fun getFeeComponent(factoryContext: AppComponentContext): ComposableContentComponent {
|
||||
val state = model.uiState.value
|
||||
|
|
|
|||
|
|
@ -38,6 +38,6 @@ internal class SendAmountComponent(
|
|||
}
|
||||
|
||||
interface ModelCallback : SendNavigationModelCallback {
|
||||
fun onAmountResult(amountUM: AmountState)
|
||||
fun onAmountResult(amountUM: AmountState, isResetPredefined: Boolean)
|
||||
}
|
||||
}
|
||||
|
|
@ -103,9 +103,9 @@ internal class SendAmountModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
val predefinedValues = params.predefinedValues as? PredefinedValues.Content
|
||||
if (predefinedValues?.amount != null) {
|
||||
onAmountValueChange(predefinedValues.amount)
|
||||
val predefinedAmount = (params.predefinedValues as? PredefinedValues.Content)?.amount
|
||||
if (predefinedAmount != null) {
|
||||
onAmountValueChange(predefinedAmount)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -220,7 +220,12 @@ internal class SendAmountModel @Inject constructor(
|
|||
|
||||
private fun saveResult() {
|
||||
val params = params as? SendAmountComponentParams.AmountParams ?: return
|
||||
params.callback.onAmountResult(uiState.value)
|
||||
val predefinedAmount = (params.predefinedValues as? PredefinedValues.Content.QrCode)?.amount
|
||||
val enteredAmount = (uiState.value as? AmountState.Data)?.amountTextField?.value
|
||||
params.callback.onAmountResult(
|
||||
amountUM = uiState.value,
|
||||
isResetPredefined = predefinedAmount != enteredAmount,
|
||||
)
|
||||
}
|
||||
|
||||
private fun configAmountNavigation() {
|
||||
|
|
|
|||
|
|
@ -96,15 +96,15 @@ internal class EnvironmentsTogglesViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
val environment = ApiEnvironment.valueOf(name)
|
||||
|
||||
handleIfTangemCardSdkConfig(id = id, environment = environment)
|
||||
handleIfAttestationConfig(id = id, environment = environment)
|
||||
|
||||
mutableApiConfigsManager.changeEnvironment(id = id, environment = environment)
|
||||
}
|
||||
}
|
||||
|
||||
/** Special logic for [ApiConfig.ID.TangemCardSdk] */
|
||||
private fun handleIfTangemCardSdkConfig(id: String, environment: ApiEnvironment) {
|
||||
if (id == ApiConfig.ID.TangemCardSdk.name) {
|
||||
/** Special logic for [ApiConfig.ID.Attestation] */
|
||||
private fun handleIfAttestationConfig(id: String, environment: ApiEnvironment) {
|
||||
if (id == ApiConfig.ID.Attestation.name) {
|
||||
cardSdkConfigRepository.setTangemApiProdEnvFlag(
|
||||
flag = when (environment) {
|
||||
ApiEnvironment.PROD -> true
|
||||
|
|
|
|||
|
|
@ -116,7 +116,10 @@ internal class TokenDetailsOnrampTransactionStateConverter(
|
|||
}
|
||||
|
||||
private fun onProviderClick(externalTxUrl: String?) = if (externalTxUrl != null) {
|
||||
{ clickIntents.onGoToProviderClick(externalTxUrl) }
|
||||
{
|
||||
analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider)
|
||||
clickIntents.onGoToProviderClick(externalTxUrl)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -219,7 +222,11 @@ internal class TokenDetailsOnrampTransactionStateConverter(
|
|||
resourceReference(R.string.express_status_bought, wrappedList(cryptoCurrency.name))
|
||||
}
|
||||
},
|
||||
state = getStatusState(OnrampStatus.Status.Paid),
|
||||
state = when {
|
||||
this == OnrampStatus.Status.RefundInProgress ||
|
||||
this == OnrampStatus.Status.Refunded -> ExpressStatusItemState.Error
|
||||
else -> getStatusState(OnrampStatus.Status.Paid)
|
||||
},
|
||||
)
|
||||
|
||||
private fun OnrampStatus.Status.getSendingItem() = ExpressStatusItemUM(
|
||||
|
|
@ -251,7 +258,7 @@ internal class TokenDetailsOnrampTransactionStateConverter(
|
|||
},
|
||||
state = when {
|
||||
this == OnrampStatus.Status.RefundInProgress -> ExpressStatusItemState.Active
|
||||
this == OnrampStatus.Status.Refunded -> ExpressStatusItemState.Error
|
||||
this == OnrampStatus.Status.Refunded -> ExpressStatusItemState.Done
|
||||
else -> getStatusState(OnrampStatus.Status.Sending)
|
||||
},
|
||||
)
|
||||
|
|
@ -266,6 +273,7 @@ internal class TokenDetailsOnrampTransactionStateConverter(
|
|||
icon = R.drawable.ic_arrow_top_right_24,
|
||||
text = resourceReference(R.string.common_go_to_provider),
|
||||
onClick = {
|
||||
analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider)
|
||||
clickIntents.onGoToProviderClick(externalTxUrl)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@ import com.tangem.feature.swap.domain.models.domain.ExchangeStatus.Companion.isF
|
|||
import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotification
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeStatusState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
|
@ -131,7 +131,10 @@ internal class TokenDetailsSwapTransactionsStateConverter(
|
|||
notification = notification,
|
||||
statuses = getStatuses(statusModel.status, hasFailed),
|
||||
showProviderLink = showProviderLink,
|
||||
info = tx.info.copy(txExternalUrl = statusModel.txExternalUrl),
|
||||
info = tx.info.copy(
|
||||
txExternalId = statusModel.txExternalId,
|
||||
txExternalUrl = statusModel.txExternalUrl,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import com.tangem.domain.onramp.model.OnrampStatus.Status.*
|
|||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsOnrampTransactionStateConverter
|
||||
import com.tangem.utils.Provider
|
||||
|
|
@ -89,7 +89,13 @@ internal class OnrampStatusFactory @AssistedInject constructor(
|
|||
},
|
||||
ifRight = { statusModel ->
|
||||
sendStatusUpdateAnalytics(onrampTx, statusModel)
|
||||
onrampTx.copy(activeStatus = statusModel.status)
|
||||
onrampTx.copy(
|
||||
activeStatus = statusModel.status,
|
||||
info = onrampTx.info.copy(
|
||||
txExternalId = statusModel.externalTxId,
|
||||
txExternalUrl = statusModel.externalTxUrl,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -427,8 +427,8 @@ fun Blockchain.toCoinId(): String {
|
|||
Blockchain.Bitrock, Blockchain.BitrockTestnet -> "bitrock"
|
||||
Blockchain.Sonic, Blockchain.SonicTestnet -> "sonic-3"
|
||||
Blockchain.ApeChain, Blockchain.ApeChainTestnet -> "apecoin"
|
||||
Blockchain.Scroll, Blockchain.ScrollTestnet -> "scroll"
|
||||
Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet -> "zklink"
|
||||
Blockchain.Scroll, Blockchain.ScrollTestnet -> "scroll-ethereum"
|
||||
Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet -> "zklink-ethereum"
|
||||
Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> "pepecoin-network"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue