Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-24 19:55:22 +03:00
commit 975163984e
118 changed files with 1722 additions and 727 deletions

View file

@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
<!--<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />-->
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.INTERNET" />
@ -25,6 +25,13 @@
</intent>
</queries>
<queries>
<intent>
<action android:name=
"android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
<application
android:name="com.tangem.tap.TangemHiltApplication"
android:allowBackup="false"
@ -48,7 +55,7 @@
android:name="com.tangem.tap.MainActivity"
android:configChanges="uiMode"
android:exported="true"
android:launchMode="singleTask"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/SplashTheme"
android:windowSoftInputMode="adjustResize">

View file

@ -30,7 +30,7 @@ internal class LockUserWalletsTimer(
lifecycle.addObserver(this)
}
override fun onResume(owner: LifecycleOwner) {
override fun onStart(owner: LifecycleOwner) {
owner.lifecycleScope.launch {
val wasApplicationStopped = settingsRepository.wasApplicationStopped()
val shouldOpenWelcomeScreenOnResume = settingsRepository.shouldOpenWelcomeScreenOnResume()
@ -45,8 +45,6 @@ internal class LockUserWalletsTimer(
settingsRepository.setWasApplicationStopped(value = false)
start()
if (shouldOpenWelcomeScreenOnResume) {
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false)
@ -54,6 +52,10 @@ internal class LockUserWalletsTimer(
}
}
override fun onResume(owner: LifecycleOwner) {
start()
}
override fun onStop(owner: LifecycleOwner) {
Timber.i("Owner stopped")

View file

@ -253,9 +253,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
window.setHideOverlayWindows(true)
}
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// window.setHideOverlayWindows(true)
// }
splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown }
@ -279,6 +279,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
if (intent != null) {
deepLinksRegistry.launch(intent)
}
lifecycle.addObserver(WindowObscurationObserver)
}
private fun setRootContent() {
@ -550,20 +552,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
val isPartiallyObscured = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
event.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0
} else {
false
}
val result = WindowObscurationObserver.dispatchTouchEvent(event, analyticsEventsHandler)
val isFullyObscured = event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0
if (isPartiallyObscured || isFullyObscured) {
Timber.e("Window is partially or fully obscured")
return false
}
return super.dispatchTouchEvent(event)
return if (result) super.dispatchTouchEvent(event) else false
}
private fun showSnackbar(text: String, length: Int, buttonTitle: String?, action: View.OnClickListener?) {

View file

@ -0,0 +1,68 @@
package com.tangem.tap
import android.os.Build
import android.view.MotionEvent
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.TechAnalyticsEvent
import com.tangem.core.analytics.models.event.TechAnalyticsEvent.WindowObscured.ObscuredState
import timber.log.Timber
internal object WindowObscurationObserver : DefaultLifecycleObserver {
private var isWindowPartiallyObscuredAlreadySent: Boolean = false
private var isWindowFullyObscuredAlreadySent: Boolean = false
private var isReadyToProxy = false
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
isReadyToProxy = true
}
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
isReadyToProxy = false
}
fun dispatchTouchEvent(event: MotionEvent, analyticsEventHandler: AnalyticsEventHandler): Boolean {
if (!isReadyToProxy) return true
val isPartiallyObscured = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
event.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0
} else {
false
}
if (isPartiallyObscured) {
Timber.d("Window is partially obscured")
if (!isWindowPartiallyObscuredAlreadySent) {
analyticsEventHandler.send(
event = TechAnalyticsEvent.WindowObscured(state = ObscuredState.PARTIALLY),
)
isWindowPartiallyObscuredAlreadySent = true
}
}
val isFullyObscured = event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0
if (isFullyObscured) {
Timber.d("Window is partially or fully obscured")
if (!isWindowFullyObscuredAlreadySent) {
analyticsEventHandler.send(
event = TechAnalyticsEvent.WindowObscured(state = ObscuredState.FULLY),
)
isWindowFullyObscuredAlreadySent = true
}
return false
}
return true
}
}

View file

@ -1,8 +1,11 @@
package com.tangem.tap.common.url
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK
import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT
@ -12,6 +15,7 @@ import com.tangem.tap.common.extensions.getColorCompat
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.withForegroundActivity
import com.tangem.wallet.R
import timber.log.Timber
internal class CustomTabsUrlOpener : UrlOpener {
@ -34,6 +38,37 @@ internal class CustomTabsUrlOpener : UrlOpener {
)
.build()
customTabsIntent.launchUrl(context, Uri.parse(url))
customTabsIntent.intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
runCatching {
if (checkCustomTabsAvailability(context, browserIntent)) {
context.startActivity(browserIntent)
} else {
customTabsIntent.launchUrl(context, Uri.parse(url))
}
}.onFailure {
Timber.e(it.message)
}
}
/**
* Custom Tabs compatibility check. Returns flag whether custom tabs are supported
* @see "https://developer.chrome.com/docs/android/custom-tabs/howto-custom-tab-check"
*/
private fun checkCustomTabsAvailability(context: Context, browserIntent: Intent): Boolean {
// Get all apps that can handle VIEW intents and Custom Tab service connections.
val resolveInfos = context.packageManager.queryIntentActivities(browserIntent, PackageManager.MATCH_ALL)
// Extract package names from ResolveInfo objects
val packageNames = mutableListOf<String>()
for (info in resolveInfos) {
packageNames.add(info.activityInfo.packageName)
}
// Get a package that supports Custom Tabs
val packageName = CustomTabsClient.getPackageName(context, packageNames, true)
return packageName == null // Custom Tabs are not supported by any browser on the device
}
}

View file

@ -208,6 +208,7 @@ internal class MainViewModel @Inject constructor(
override fun onDismissBottomSheet() {
listenToFlipsUseCase.changeUpdateEnabled(true)
router.pop()
stateHolder.updateWithoutModalNotification()
stateHolder.updateWithHiddenBalancesToast(true)
}

View file

@ -46,8 +46,6 @@ object TradeCryptoMiddleware {
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
is TradeCryptoAction.Buy -> proceedBuyAction(action)
is TradeCryptoAction.Sell -> proceedSellAction(action)
is TradeCryptoAction.SendToken -> handleNewSendToken(action = action)
is TradeCryptoAction.SendCoin -> handleNewSendCoin(action = action)
}
}
@ -158,37 +156,4 @@ object TradeCryptoMiddleware {
transactionId = transactionId,
)?.let { store.dispatchOpenUrl(it) }
}
private fun handleNewSendToken(action: TradeCryptoAction.SendToken) {
handleNewSend(
userWalletId = action.userWallet.walletId,
txInfo = action.transactionInfo,
currency = action.tokenCurrency,
)
}
private fun handleNewSendCoin(action: TradeCryptoAction.SendCoin) {
handleNewSend(
userWalletId = action.userWallet.walletId,
txInfo = action.transactionInfo,
currency = action.coinStatus.currency,
)
}
private fun handleNewSend(
userWalletId: UserWalletId,
txInfo: TradeCryptoAction.TransactionInfo?,
currency: CryptoCurrency,
) {
val route = AppRoute.Send(
currency = currency,
userWalletId = userWalletId,
transactionId = txInfo?.transactionId,
destinationAddress = txInfo?.destinationAddress,
amount = txInfo?.amount,
tag = txInfo?.tag,
)
store.dispatchNavigationAction { push(route) }
}
}

View file

@ -178,9 +178,9 @@
android:id="@+id/tv_body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/home_bottom_tv_body_margin_start"
android:layout_marginStart="32dp"
android:layout_marginTop="@dimen/home_bottom_tv_body_margin_top"
android:layout_marginEnd="@dimen/home_bottom_tv_body_margin_end"
android:layout_marginEnd="32dp"
android:textAlignment="center"
android:textColor="@color/text_secondary"
android:textSize="16sp"

View file

@ -19,6 +19,7 @@ dependencies {
implementation(deps.compose.navigation)
implementation(deps.compose.navigation.hilt)
implementation(deps.compose.coil)
implementation(deps.compose.constraintLayout)
/** Deps */
implementation(deps.kotlin.immutable.collections)
@ -34,6 +35,7 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.transaction.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.onramp.models)
implementation(deps.tangem.card.core)
implementation(deps.tangem.blockchain) {

View file

@ -1,4 +1,4 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
package com.tangem.common.ui.expressStatus
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@ -7,17 +7,17 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import com.tangem.common.ui.R
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.inputrow.InputRowApprox
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.tokendetails.impl.R
@Suppress("LongParameterList")
@Composable
internal fun ExpressEstimate(
fun ExpressEstimate(
timestamp: TextReference,
fromTokenIconState: CurrencyIconState,
toTokenIconState: CurrencyIconState,

View file

@ -1,4 +1,4 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
package com.tangem.common.ui.expressStatus
import android.content.res.Configuration
import android.widget.Toast
@ -29,7 +29,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
internal fun ExpressProvider(
fun ExpressProvider(
providerName: TextReference,
providerType: TextReference,
providerTxId: String?,

View file

@ -0,0 +1,24 @@
package com.tangem.common.ui.expressStatus
import androidx.compose.runtime.Composable
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.res.TangemTheme
data class ExpressStatusBottomSheetConfig(
val value: ExpressTransactionStateUM,
) : TangemBottomSheetConfigContent
@Composable
fun ExpressStatusBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(
config = config,
containerColor = TangemTheme.colors.background.tertiary,
) { content: ExpressStatusBottomSheetConfig ->
when (val state = content.value) {
is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(state)
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
package com.tangem.common.ui.expressStatus
import android.content.res.Configuration
import androidx.annotation.DrawableRes
@ -18,6 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.constraintlayout.compose.*
import com.tangem.common.ui.R
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
@ -27,7 +28,6 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tokendetails.impl.R
@Suppress("DestructuringDeclarationWithTooManyEntries", "LongMethod", "LongParameterList")
@Composable

View file

@ -1,14 +1,14 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
package com.tangem.common.ui.expressStatus
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier
import com.tangem.common.ui.R
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateIconUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.PersistentList
internal fun LazyListScope.expressTransactionsItems(
fun LazyListScope.expressTransactionsItems(
expressTxs: PersistentList<ExpressTransactionStateUM>,
modifier: Modifier = Modifier,
) {

View file

@ -1,4 +1,4 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp
package com.tangem.common.ui.expressStatus
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@ -7,8 +7,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import com.tangem.common.ui.expressStatus.ExpressStatusBlock
import com.tangem.common.ui.expressStatus.ExpressStatusNotificationBlock
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH10
import com.tangem.core.ui.components.SpacerH12
@ -17,12 +16,9 @@ import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressEstimate
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressProvider
@Composable
internal fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) {
fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) {
Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) {
SpacerH10()
Text(

View file

@ -1,31 +1,13 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.express
package com.tangem.common.ui.expressStatus.state
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications
import kotlinx.collections.immutable.ImmutableList
internal sealed class ExpressTransactionStateUM {
interface ExpressTransactionStateUM {
abstract val info: ExpressTransactionStateInfoUM
data class ExchangeUM(
override val info: ExpressTransactionStateInfoUM,
val provider: SwapProvider,
val activeStatus: ExchangeStatus?,
val statuses: ImmutableList<ExchangeStatusState>,
val notification: ExchangeStatusNotifications? = null,
val showProviderLink: Boolean,
val isRefundTerminalStatus: Boolean,
val fromCryptoCurrency: CryptoCurrency,
val toCryptoCurrency: CryptoCurrency,
) : ExpressTransactionStateUM()
val info: ExpressTransactionStateInfoUM
data class OnrampUM(
override val info: ExpressTransactionStateInfoUM,
@ -34,10 +16,10 @@ internal sealed class ExpressTransactionStateUM {
val providerType: String, // todo onramp fix after SwapProvider moved to own module
val activeStatus: OnrampStatus.Status,
val fromCurrencyCode: String,
) : ExpressTransactionStateUM()
) : ExpressTransactionStateUM
}
internal data class ExpressTransactionStateInfoUM(
data class ExpressTransactionStateInfoUM(
val title: TextReference,
val status: ExpressStatusUM,
val notification: NotificationUM?,
@ -49,7 +31,6 @@ internal data class ExpressTransactionStateInfoUM(
val onGoToProviderClick: (String) -> Unit,
val onClick: () -> Unit,
val iconState: ExpressTransactionStateIconUM,
val toAmount: TextReference,
val toFiatAmount: TextReference?,
val toAmountSymbol: String,
@ -61,7 +42,7 @@ internal data class ExpressTransactionStateInfoUM(
val fromCurrencyIcon: CurrencyIconState,
)
internal enum class ExpressTransactionStateIconUM {
enum class ExpressTransactionStateIconUM {
Warning,
Error,
None,

View file

@ -0,0 +1,26 @@
package com.tangem.core.analytics.models.event
import com.tangem.core.analytics.models.AnalyticsEvent
/**
* Tech analytics event
*
* @param event event name
* @param params params
*/
sealed class TechAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Tech", event = event, params = params) {
class WindowObscured(state: ObscuredState) : TechAnalyticsEvent(
event = "Window Obscured",
params = mapOf("State" to state.name),
) {
enum class ObscuredState {
PARTIALLY,
FULLY,
}
}
}

View file

@ -8,7 +8,7 @@
"version": "undefined"
},
{
"name": "clore-ai",
"name": "NEXA/test",
"version": "undefined"
},
{

View file

@ -13,7 +13,7 @@
},
{
"name": "ONRAMP_ENABLED",
"version": "5.19.0"
"version": "undefined"
},
{
"name": "MAIN_ACTION_BUTTONS_ENABLED",

View file

@ -6,8 +6,8 @@ import com.tangem.core.configtoggle.utils.associateToggles
import com.tangem.core.configtoggle.version.VersionProvider
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
import com.tangem.datasource.local.preferences.utils.storeObjectMap
internal class DefaultExcludedBlockchainsManager(
private val localTogglesStorage: TogglesStorage,
@ -17,62 +17,64 @@ internal class DefaultExcludedBlockchainsManager(
private var isInitialized: Boolean = false
private lateinit var currentExcludedBlockchainsIds: Set<String>
private lateinit var localExcludedBlockchainsIds: Set<String>
private lateinit var currentExcludedBlockchains: MutableMap<String, Boolean>
private lateinit var localExcludedBlockchains: Map<String, Boolean>
override val excludedBlockchainsIds: Set<String>
get() {
if (!isInitialized) error("ExcludedBlockchainsManager is not initialized")
return currentExcludedBlockchainsIds
return currentExcludedBlockchains
.filterValues { it }
.keys
}
override suspend fun init() {
localTogglesStorage.populate(path = "configs/excluded_blockchains_config")
val storedExcludedBlockchainsIds = appPreferencesStore.getSyncOrDefault(
val storedExcludedBlockchainsIds = appPreferencesStore.getObjectMapSync<Boolean>(
key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY,
default = emptySet(),
)
localExcludedBlockchainsIds = localTogglesStorage.toggles
localExcludedBlockchains = localTogglesStorage.toggles
.associateToggles(currentVersion = versionProvider.get().orEmpty())
.filterValues { isIncluded -> !isIncluded }
.keys
.mapValues { (_, isIncluded) -> !isIncluded }
if (storedExcludedBlockchainsIds.isEmpty()) {
recoverLocalConfig()
} else {
currentExcludedBlockchainsIds = storedExcludedBlockchainsIds
}
currentExcludedBlockchains = (localExcludedBlockchains.keys + storedExcludedBlockchainsIds.keys)
.fold(mutableMapOf()) { acc, blockchainId ->
val isExcluded = storedExcludedBlockchainsIds[blockchainId] ?: localExcludedBlockchains[blockchainId]
requireNotNull(isExcluded) {
"Unable to find $blockchainId in local or stored excluded blockchains"
}
acc[blockchainId] = isExcluded
acc
}
isInitialized = true
}
override suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean) {
if (isExcluded) {
currentExcludedBlockchainsIds += mainnetId
} else {
currentExcludedBlockchainsIds -= mainnetId
}
currentExcludedBlockchains[mainnetId] = isExcluded
storeCurrent()
}
override fun isMatchLocalConfig(): Boolean {
return currentExcludedBlockchainsIds == localExcludedBlockchainsIds
return currentExcludedBlockchains == localExcludedBlockchains
}
override suspend fun recoverLocalConfig() {
currentExcludedBlockchainsIds = localExcludedBlockchainsIds
currentExcludedBlockchains = localExcludedBlockchains.toMutableMap()
storeCurrent()
}
private suspend fun storeCurrent() {
appPreferencesStore.store(
PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY,
currentExcludedBlockchainsIds,
appPreferencesStore.storeObjectMap(
key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY,
value = currentExcludedBlockchains,
)
}
}

View file

@ -12,7 +12,7 @@ data class OnrampCurrencyDTO(
val code: String,
@Json(name = "image")
val image: String,
val image: String?,
@Json(name = "precision")
val precision: Int,

View file

@ -55,7 +55,7 @@ object PreferencesKeys {
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringSetPreferencesKey(name = "excludedBlockchains") }
val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringPreferencesKey(name = "excludedBlockchainsV2") }
val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") }

View file

@ -3,15 +3,10 @@ package com.tangem.core.deeplink.global
import com.tangem.core.deeplink.DeepLink
class BuyCurrencyDeepLink(
isOnrampFeatureEnabled: Boolean,
val onReceive: (externalTxId: String) -> Unit,
) : DeepLink {
override val uri: String = if (isOnrampFeatureEnabled) {
ONRAMP_REDIRECT_DEEPLINK
} else {
BUY_REDIRECT_DEEPLINK
}
override val uri = BUY_REDIRECT_DEEPLINK
override fun onReceive(params: Map<String, String>) {
onReceive(
@ -20,7 +15,7 @@ class BuyCurrencyDeepLink(
}
companion object {
const val ONRAMP_REDIRECT_DEEPLINK = "tangem://onramp-success?"
const val ONRAMP_REDIRECT_DEEPLINK = "https://tangem.com/success?action=dismissBrowser"
private const val BUY_REDIRECT_DEEPLINK = "tangem://redirect?action=dismissBrowser"
}
}

View file

@ -583,6 +583,8 @@
<string name="onramp_choose_provider_title_hint">Der Kauf von Kryptowährungen bei Tangem wird von Drittanbietern zu deren Bedingungen durchgeführt.</string>
<string name="onramp_country_search">Suche nach Land</string>
<string name="onramp_country_unavailable">Nicht verfügbar</string>
<string name="onramp_currency_other">Andere Währungen</string>
<string name="onramp_currency_popular">Beliebte Fiats</string>
<string name="onramp_currency_search">Suche nach Währung</string>
<string name="onramp_legal">Durch die Nutzung der Onramp-Funktionalität stimmst Du den %1$s und %2$s des Anbieters zu.</string>
<string name="onramp_max_amount_restriction">Der Kaufbetrag sollte nicht höher sein als %s</string>

View file

@ -6,10 +6,15 @@
<string name="action_buttons_service_loading_alert_message">Esto puede tardar unos segundos. Inténtelo de nuevo más tarde.</string>
<string name="action_buttons_service_loading_alert_title">Los datos aún no se han cargado.</string>
<string name="action_buttons_something_wrong_alert_message">La acción no está disponible actualmente. Inténtalo de nuevo más tarde o actualiza los datos deslizando el dedo hacia abajo en la pantalla.</string>
<string name="action_buttons_something_wrong_alert_title">No disponible</string>
<string name="action_buttons_swap_choose_token">Elige el token</string>
<string name="action_buttons_swap_empty_search_message">¿No encuentras el token en tu billetera? Consulte los mercados para encontrarlo y agregarlo al intercambio</string>
<string name="action_buttons_swap_empty_search_message">¿No encuentra el token en tu billetera? Consulte los mercados para encontrarlo y agregarlo al intercambio</string>
<string name="action_buttons_swap_no_available_pair_notification_message">No hay tokens disponibles para intercambiar con el token seleccionado. Por favor elige otro.</string>
<string name="action_buttons_swap_no_available_pair_notification_title">No hay par disponible</string>
<string name="action_buttons_swap_no_tokens_added_alert_message">Para utilizar la función de intercambio, su billetera debe contener al menos 2 tokens</string>
<string name="action_buttons_swap_no_tokens_added_alert_title">Añada tokens</string>
<string name="action_buttons_swap_not_enough_tokens_alert_message">Sólo tienes 1 token añadido a tu portafolio. Para utilizar la función de intercambio, necesita añadir al menos 2 tokens</string>
<string name="action_buttons_swap_not_enough_tokens_alert_title">Añada tokens</string>
<string name="action_buttons_you_want_to_receive">Seleccione el token que desea recibir</string>
<string name="action_buttons_you_want_to_swap">Seleccione el token que desea intercambiar</string>
<string name="add_custom_token_choose_network">Elige red</string>
@ -25,7 +30,7 @@
<string name="alert_manage_tokens_unsupported_blockchain_by_card_message">El seleccionado no admite la red %1$s</string>
<string name="alert_manage_tokens_unsupported_curve_message">Para activar el %1$s cifrado criptográfico de la blockchain, necesitarás reiniciar la billetera a los ajustes de fábrica. Por favor, retire sus fondos antes de hacerlo para asegurarte de no perderlos, y luego complete el proceso de reinicio. El acceso a la billetera actual no será posible después del reinicio.</string>
<string name="alert_manage_tokens_unsupported_message">Esta tarjeta no admite %1$s tokens de red debido a una limitación del firmware.</string>
<string name="alert_troubleshooting_scan_card_title">¿Tiene dificultades para escanear su tarjeta?</string>
<string name="alert_troubleshooting_scan_card_title">¿Tiene dificultades para escanear su tarjeta/anillo?</string>
<string name="alert_unsupported_card">Esta tarjeta no está diseñada para funcionar con Tangem</string>
<string name="app_settings_default_fee">Tarifa por defecto</string>
<string name="app_settings_default_fee_footer">Habilite las Tarifas predeterminadas para establecer automáticamente las tarifas de transacción y omitir la página de Tarifas al enviar fondos. Siempre puede volver a esta página si es necesario.</string>
@ -65,10 +70,10 @@
<string name="card_settings_change_access_code_footer">El código de acceso se cambiará solo en esta tarjeta o este anillo</string>
<string name="card_settings_completed_reset_alert_message">Todas las tarjetas de la billetera seleccionada se han restablecido a la configuración de fábrica. Ahora puedes crear una nueva billetera.</string>
<string name="card_settings_completed_reset_alert_title">Reinicio completado</string>
<string name="card_settings_continue_reset_alert_message">¿Quiere restablecer la siguiente tarjeta en esta billetera?</string>
<string name="card_settings_continue_reset_alert_message">¿Quiere restablecer el siguiente dispositivo en esta billetera?</string>
<string name="card_settings_continue_reset_alert_title">Reinicio de tarjeta</string>
<string name="card_settings_interrupted_reset_alert_message">Recomendamos completar el proceso de reinicio para todas las tarjetas en esta billetera.</string>
<string name="card_settings_interrupted_reset_alert_title">No ha reseteado todas sus tarjetas</string>
<string name="card_settings_interrupted_reset_alert_title">No ha reseteado todas sus dispositivos Tangem</string>
<string name="card_settings_reset_card_to_factory">Restablecer a ajustes de fábrica</string>
<string name="card_settings_security_mode">Modo de seguridad</string>
<string name="card_settings_title">Ajustes del dispositivo</string>
@ -266,6 +271,9 @@
<string name="express_exchange_notification_refund_title">El monto fue reembolsado en %1$s (red %2$s)</string>
<string name="express_exchange_notification_verification_text">Visite el sitio web del proveedor para la verificación</string>
<string name="express_exchange_notification_verification_title">Verificación KYC requerida por el proveedor</string>
<string name="express_exchange_status_bought">Comprado</string>
<string name="express_exchange_status_buying">Comprando</string>
<string name="express_exchange_status_buying_active">Comprando...</string>
<string name="express_exchange_status_canceled">Cancelado</string>
<string name="express_exchange_status_confirmed">Confirmado</string>
<string name="express_exchange_status_confirming">Esperando confirmación</string>
@ -305,15 +313,15 @@
<string name="express_token_list_empty_search">No se encontraron fichas. Por favor intenta con otra solicitud</string>
<string name="express_transaction_id">ID : %s</string>
<string name="express_transaction_id_copied">ID de transacción copiado</string>
<string name="exсhange_token_description">Convierta una de sus monedas a otra</string>
<string name="exсhange_token_description">Convierta una de sus activos de su portafolio por este token</string>
<string name="feedback_data_collection_message">La siguiente información es opcional. Puede borrarla si no quiere compartirla.</string>
<string name="feedback_preface_rate_negative">Cuéntenos qué funciones echa de menos y trataremos de ayudarle.</string>
<string name="feedback_preface_scan_failed">Por favor, díganos qué tarjeta tiene</string>
<string name="feedback_preface_scan_failed">Por favor, díganos qué tarjeta o anillo tiene</string>
<string name="feedback_preface_support">Hola equipo de soporte,</string>
<string name="feedback_preface_tx_failed">Por favor, cuéntenos más sobre tu problema. Cada pequeño detalle puede ayudar.</string>
<string name="feedback_subject_pre_activated_wallet">Billetera previamente activada</string>
<string name="feedback_subject_rate_negative">Mis recomendaciones</string>
<string name="feedback_subject_scan_failed">No se puede escanear una tarjeta</string>
<string name="feedback_subject_scan_failed">No se puede escanear una tarjeta/anillo</string>
<string name="feedback_subject_support">Comentarios</string>
<string name="feedback_subject_support_tangem">Comentarios en Tangem</string>
<string name="feedback_subject_tx_failed">No se puede completar una transacción</string>
@ -329,14 +337,14 @@
<string name="home_button_order">Ordenar </string>
<string name="home_button_scan">Escanee</string>
<string name="information_generated_with_ai">Esta información fue generada con IA.\nPulse aquí si encuentra algún error.</string>
<string name="initial_message_change_access_code_body">Toque para cambiar la contraseña</string>
<string name="initial_message_change_access_code_body">Para cambiar el código de acceso coloque la tarjeta o el anillo como se muestra arriba y no lo retire hasta el fin de la operación</string>
<string name="initial_message_change_passcode_body">Toque para cambiar la contraseña</string>
<string name="initial_message_create_wallet_body">Para crear la billetera, toque la tarjeta como se muestra arriba y no la retire hasta el final de la operación</string>
<string name="initial_message_create_wallet_body">Para crear la billetera, coloque la tarjeta o el anillo como se muestra arriba y no la retire hasta el fin de la operación</string>
<string name="initial_message_create_wallet_body_ring">Para crear la billetera, ponga el anillo como se muestra arriba y manténgala hasta que termine la operación.</string>
<string name="initial_message_reset_backup_card_header">Toque la tarjeta no. %s de la billetera</string>
<string name="initial_message_scan_header">Coloque para escanear</string>
<string name="initial_message_sign_header">Toque para firmar</string>
<string name="initial_message_tap_header">Coloque la tarjeta</string>
<string name="initial_message_tap_header">Coloque la tarjeta o el anillo</string>
<string name="key_invalidated_warning_description">Ha actualizado la biometría, escanee su tarjeta o anillo para entrar</string>
<string name="koinos_insufficient_balance_to_send_koin_description">Su saldo debe ser mayor que el valor de la tarifa para hacer una transferencia</string>
<string name="koinos_insufficient_balance_to_send_koin_title">Saldo insuficiente</string>
@ -379,7 +387,7 @@
<string name="markets_add_to_my_portfolio_unavailable_description">Este activo no está disponible en la billetera</string>
<string name="markets_add_to_my_portfolio_unavailable_for_wallet_description">Este activo no está disponible para esta billetera</string>
<string name="markets_add_to_portfolio_button">Añadir al portafolio</string>
<string name="markets_add_token">Agregue un token</string>
<string name="markets_add_token">Añadir</string>
<string name="markets_available_networks">Redes disponibles</string>
<string name="markets_common_my_portfolio">Mi portafolio</string>
<string name="markets_common_title">Mercado</string>
@ -391,7 +399,7 @@
<string name="markets_quick_actions">Acciones rápidas</string>
<string name="markets_search_header_title">Busque en el mercado</string>
<string name="markets_search_result_title">Resultado</string>
<string name="markets_search_see_tokens_under_100k">Ver tokens con una capitalización de mercado de menos de $100,000</string>
<string name="markets_search_see_tokens_under_100k">Ver tokens con una capitalización de mercado inferior a 100,000$</string>
<string name="markets_search_show_tokens">Mostrar tokens</string>
<string name="markets_search_token_no_result_title">Sin resultado</string>
<string name="markets_select_network">Seleccione una red</string>
@ -478,9 +486,9 @@
<string name="nfc_error_unavailable">NFC no está disponible en su dispositivo</string>
<string name="onboarding_access_code_feature_1_description">Deba configurar un único código de acceso para proteger todss sus dispositivos.</string>
<string name="onboarding_access_code_feature_1_title">Proteger</string>
<string name="onboarding_access_code_feature_2_description">Puede configurar un código de acceso individual en cada tarjeta más tarde</string>
<string name="onboarding_access_code_feature_2_description">Puede configurar un código de acceso individual en cada tarjeta más adelante</string>
<string name="onboarding_access_code_feature_2_title">Personalizar</string>
<string name="onboarding_access_code_feature_3_description">El código de acceso se puede recuperar con una tarjeta o un anillo vinculada(o). No guarde todas las tarjetas en un mismo lugar.</string>
<string name="onboarding_access_code_feature_3_description">El código de acceso se puede recuperar con una tarjeta o un anillo vinculada(o). No guarde todos los dispositivos en el mismo lugar.</string>
<string name="onboarding_access_code_feature_3_title">Restaurar</string>
<string name="onboarding_access_code_hint">Elige cualquier palabra, frase o número que quieras como su código de acceso</string>
<string name="onboarding_access_code_intro_title">Crear un código de acceso</string>
@ -491,12 +499,12 @@
<string name="onboarding_activation_error_message">Por favor repita la operación. La tarjeta se restablecerá a la configuración de fábrica.</string>
<string name="onboarding_activation_error_title">Error de activación</string>
<string name="onboarding_add_tokens">Agregar tokens</string>
<string name="onboarding_alert_message_not_max_backup_cards_added">Ha agregado una tarjeta de backup. Cuando el proceso de backup finalice, no podrá añadir más tarjetas de backup. Si tiene otra tarjeta, agréguela al backup. ¿Quiere continuar el proceso de backup?</string>
<string name="onboarding_alert_message_not_max_backup_cards_added">Ha agregado una tarjeta/anillo de backup. Cuando el proceso de backup finalice, no podrá añadir más dispositivos de backup. Si tiene otra tarjeta o anillo, agréguela al backup. ¿Quiere continuar el proceso de backup?</string>
<string name="onboarding_backup_exit_warning">El proceso de backup está parcialmente completo. No puede salir ahora.</string>
<string name="onboarding_bottom_sheet_passphrase_description">La frase de contraseña es una característica de seguridad avanzada utilizada por las billeteras criptográficas. Agrega una palabra o frase adicional de su elección a su frase de recuperación ya existente para desbloquear un conjunto completamente nuevo de direcciones.</string>
<string name="onboarding_button_add_backup_card">Agregar una tarjeta o un anillo de backup</string>
<string name="onboarding_button_backup_card">Escanee la tarjeta</string>
<string name="onboarding_button_backup_card_format">Escanee la tarjeta no. %d</string>
<string name="onboarding_button_backup_card_format">Escanee la tarjeta núm. %d</string>
<string name="onboarding_button_backup_now">Hacer backup ahora</string>
<string name="onboarding_button_backup_origin">Escanee la tarjeta principal</string>
<string name="onboarding_button_backup_ring">Escanee el anillo</string>
@ -506,7 +514,7 @@
<string name="onboarding_button_scan_origin_card">Escanee la tarjeta principal</string>
<string name="onboarding_button_skip_backup">Finalizar más tarde</string>
<string name="onboarding_button_what_does_it_mean">¿Cómo funciona?</string>
<string name="onboarding_create_wallet_body">Vamos a generar todas las claves en su tarjeta y crear una billetera segura</string>
<string name="onboarding_create_wallet_body">Vamos a generar todas las claves en su tarjeta o anillos y crear una billetera segura</string>
<string name="onboarding_create_wallet_button_create_wallet">Crear una billetera</string>
<string name="onboarding_create_wallet_header">Crear una billetera</string>
<string name="onboarding_create_wallet_options_button_options">Otras opciones</string>
@ -539,23 +547,23 @@
<string name="onboarding_seed_phrase_intro_legacy">Legado</string>
<string name="onboarding_seed_user_validation_message">Para comprobar si has escrito correctamente su seed phrase, por favor introduce las palabras 2ª, 7ª y 11ª</string>
<string name="onboarding_seed_user_validation_title">Bien, vamos a comprobar</string>
<string name="onboarding_subtitle_no_backup_cards">Para iniciar el proceso de backup, agregue hasta dos tarjetas de backup.</string>
<string name="onboarding_subtitle_one_backup_card">Puede agregar una tarjeta más o finalizar el proceso de backup</string>
<string name="onboarding_subtitle_no_backup_cards">Para iniciar el proceso de backup, agregue hasta dos tarjetas/anillos de backup.</string>
<string name="onboarding_subtitle_one_backup_card">Puede agregar una tarjeta/anillo más o finalizar el proceso de backup</string>
<string name="onboarding_subtitle_scan_backup_card_format">Prepare su tarjeta de backup con el no.%s</string>
<string name="onboarding_subtitle_scan_primary">Escanee la tarjeta principal o el anillo para iniciar el proceso de backup.</string>
<string name="onboarding_subtitle_scan_primary_card_format">Prepare la tarjeta principal con el número %s</string>
<string name="onboarding_subtitle_scan_ring">Prepare el anillo y presione el botón de escaneo a continuación</string>
<string name="onboarding_subtitle_success_tangem_wallet_onboarding">Su tarjeta billetera está configurada y lista para usar.</string>
<string name="onboarding_subtitle_two_backup_cards">Número máximo de tarjetas añadido. Finalice el proceso de backup.</string>
<string name="onboarding_title">Activando tarjeta</string>
<string name="onboarding_subtitle_success_tangem_wallet_onboarding">Su billetera está configurada y lista para usar.</string>
<string name="onboarding_subtitle_two_backup_cards">Número máximo de dispositivos añadido. Finalice el proceso de backup.</string>
<string name="onboarding_title">Activando billetera</string>
<string name="onboarding_title_backup_card">Tarjeta de backup</string>
<string name="onboarding_title_backup_card_format">Tarjeta de backup no.%d</string>
<string name="onboarding_title_backup_ring">Anillo de backup</string>
<string name="onboarding_title_no_backup_cards">Ningunos dispositivos de backup</string>
<string name="onboarding_title_notifications">Notificaciones</string>
<string name="onboarding_title_one_backup_card">Una tarjeta de backup agregada</string>
<string name="onboarding_title_one_backup_card">Un dispositivo de backup agregado</string>
<string name="onboarding_title_scan_origin_card">Prepare su tarjeta</string>
<string name="onboarding_title_two_backup_cards">Dos tarjetas de backup agregadas</string>
<string name="onboarding_title_two_backup_cards">Dos dispositivos de backup agregados</string>
<string name="onboarding_top_up_body">Para empezar, simplemente recargue la billetera con cualquier cantidad</string>
<string name="onboarding_top_up_body_no_account_error">Para empezar, simplemente recargue la billetera con más de %1$s %2$s</string>
<string name="onboarding_top_up_button_but_crypto">Comprar cripto</string>
@ -563,7 +571,7 @@
<string name="onboarding_topup_title">Activar una billetera</string>
<string name="onboarding_twin_exit_warning">El proceso de emparejamiento está parcialmente completo. No puede salir ahora.</string>
<string name="onboarding_twins_interrupt_warning">Si el proceso de creación del wallet se interrumpe de alguna manera, tendrá que empezar de nuevo</string>
<string name="onboarding_wallet_info_subtitle_first">Puede hacer backup de sus claves en hasta dos tarjetas Tangem Wallet en blanco.</string>
<string name="onboarding_wallet_info_subtitle_first">Puede hacer backup de sus claves en hasta dos tarjetas o anillos Tangem no activadas.</string>
<string name="onboarding_wallet_info_subtitle_fourth">El código de acceso se puede restaurar con una de las tarjetas de backup.</string>
<string name="onboarding_wallet_info_subtitle_second">Todas las tarjetas de backup se pueden usar como funcionales con las claves idénticas.</string>
<string name="onboarding_wallet_info_subtitle_third">Podrá establecer un código de acceso para proteger sus billeteras.</string>
@ -572,8 +580,11 @@
<string name="onboarding_wallet_info_title_second">Tarjetas idénticas</string>
<string name="onboarding_wallet_info_title_third">Código de acceso</string>
<string name="onramp_avaiable_with_payment_methods">Disponible con %s</string>
<string name="onramp_choose_provider_title_hint">La compra de criptomonedas en Tangem se lleva a cabo utilizando proveedores externos usando sus propios términos.</string>
<string name="onramp_country_search">Buscar por país</string>
<string name="onramp_country_unavailable">Indisponible</string>
<string name="onramp_currency_other">Otras monedas</string>
<string name="onramp_currency_popular">Monedas Fiats Populares</string>
<string name="onramp_currency_search">Buscar por moneda</string>
<string name="onramp_legal">Al utilizar la funcionalidad onramp, acepta %1$s y %2$s del proveedor.</string>
<string name="onramp_max_amount_restriction">El monto de la compra no debe ser mayor a %s</string>
@ -604,7 +615,8 @@
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) en la red %3$s</string>
<string name="receive_bottom_sheet_warning_message_description">El envío de cualquier otra moneda supondrá una pérdida irreversible.</string>
<string name="receive_bottom_sheet_warning_message_full">Envíe solo %s a esta dirección. Enviar cualquier otra moneda resultará en su pérdida irreversible.</string>
<string name="receive_token_description">Transfiera fondos desde otra billetera o intercambio</string>
<string name="receive_bottom_sheet_warning_title">Envíe solo %1$s en la red %2$s</string>
<string name="receive_token_description">Transfiera fondos desde otra billetera o exchange</string>
<string name="referral_button_participate">Participar</string>
<string name="referral_error_failed_to_load_info">Error al cargar la información sobre el programa de referidos. Por favor, inténtelo de nuevo más tarde.</string>
<string name="referral_error_failed_to_load_info_with_reason">Error al cargar la información sobre el programa de referidos. Código de error: %s. Por favor, inténtelo de nuevo más tarde.</string>
@ -806,17 +818,17 @@
<string name="staking_revote">Revotar</string>
<string name="staking_reward_claiming_auto">Auto</string>
<string name="staking_reward_claiming_manual">Manual</string>
<string name="staking_reward_schedule_block">Bloquear</string>
<string name="staking_reward_schedule_day">Día</string>
<string name="staking_reward_schedule_block">Por Bloque</string>
<string name="staking_reward_schedule_day">Diario</string>
<string name="staking_reward_schedule_each">Cada %s</string>
<string name="staking_reward_schedule_each_day">Cada día</string>
<string name="staking_reward_schedule_each_days">Cada %s</string>
<string name="staking_reward_schedule_each_days">Cada %s días</string>
<string name="staking_reward_schedule_each_plural">Cada</string>
<string name="staking_reward_schedule_epoch">Época</string>
<string name="staking_reward_schedule_epoch">Por Época</string>
<string name="staking_reward_schedule_era">Era</string>
<string name="staking_reward_schedule_hour">Hora</string>
<string name="staking_reward_schedule_month">Mes</string>
<string name="staking_reward_schedule_week">Semana</string>
<string name="staking_reward_schedule_hour">Cada Hora</string>
<string name="staking_reward_schedule_month">Mensual</string>
<string name="staking_reward_schedule_week">Semanal</string>
<string name="staking_rewards">Recompensas</string>
<string name="staking_stake_locked">El stake está bloqueado</string>
<string name="staking_stake_more">Hacer más staking</string>
@ -843,7 +855,7 @@
<string name="staking_vote_locked">Votar</string>
<string name="staking_withdraw">Retirar</string>
<string name="staking_your_stakes">Sus stakes</string>
<string name="story_awe_description">Almacene sus activos crypto de forma segura manteniendo las claves privadas contenidas en su tarjeta</string>
<string name="story_awe_description">Almacene sus criptomonedas de forma segura manteniendo las claves privadas almacenadas en su tarjeta</string>
<string name="story_awe_title">Billetera de hardware revolucionaria</string>
<string name="story_backup_description">Hasta 3 tarjetas físicas o anillos por billetera</string>
<string name="story_backup_title">Backup ultra seguro</string>
@ -857,7 +869,7 @@
<string name="swap_give_permission_fee_footer">La red cobrará una tarifa de aprobación de token para verificar que está autorizando el uso de su token para el swap.</string>
<string name="swap_promo_text">Intercambie más tokens a mejores tasas directamente en su billetera.</string>
<string name="swap_promo_title">¡Nuevo proveedor de intercambio disponible!</string>
<string name="swapping_alert_cex_description">El monto incluye:\n• Tarifas del proveedor de servicios\n• Tarifas de red por enviar %s desde el intercambio a la dirección del usuario.</string>
<string name="swapping_alert_cex_description">La cantidad incluye:\n• Tarifas del proveedor de servicios\n• Tarifas de red por enviar %s desde el intercambio a la dirección del usuario.</string>
<string name="swapping_alert_cex_description_with_slippage">El monto incluye:\n• tarifas del proveedor de servicios\n• tarifas de red por enviar %1$s desde el intercambio a la dirección del usuario. \n\nEl slippage del proveedor puede alcanzar el %2$s</string>
<string name="swapping_alert_dex_description">El importe incluye los honorarios del proveedor de servicios.</string>
<string name="swapping_alert_dex_description_with_slippage">El importe incluye las tarifas del proveedor de servicios. \n\nEl slippage del proveedor puede alcanzar el %s</string>
@ -920,7 +932,7 @@
<string name="twin_error_wrong_twin">Ha escaneado la tarjeta gemela incorrecta. Por favor, intente con otra</string>
<string name="twins_onboarding_description_format">Esta que tiene en sus manos y la otra con el número %s.\n\nAmbas tarjetas pueden usarse para extraer fondos de esta billetera.</string>
<string name="twins_onboarding_subtitle">Una billetera. Dos tarjetas.</string>
<string name="twins_recreate_button_format">Escanee la tarjeta no. %s</string>
<string name="twins_recreate_button_format">Escanee la tarjeta núm. %s</string>
<string name="twins_recreate_title_creating_wallet">Creando la billetera</string>
<string name="twins_recreate_title_format">Escanear tarjeta gemela no. %s</string>
<string name="twins_recreate_title_preparing">Preparando la tarjeta</string>

View file

@ -580,8 +580,11 @@
<string name="onboarding_wallet_info_title_second">Cartes identiques</string>
<string name="onboarding_wallet_info_title_third">Code d\'accès</string>
<string name="onramp_avaiable_with_payment_methods">Disponible avec %s</string>
<string name="onramp_choose_provider_title_hint">L\'achat de crypto-monnaies sur Tangem est assuré par des fournisseurs tiers selon leurs conditions.</string>
<string name="onramp_country_search">Recherche par pays</string>
<string name="onramp_country_unavailable">Indisponible</string>
<string name="onramp_currency_other">Autres devises</string>
<string name="onramp_currency_popular">Devises Fiats Populaires</string>
<string name="onramp_currency_search">Recherche par devise</string>
<string name="onramp_legal">En utilisant la fonctionnalité onramp, vous acceptez %1$s et %2$s du fournisseur</string>
<string name="onramp_max_amount_restriction">Le montant de l\'achat ne doit pas dépasser %s</string>

View file

@ -575,6 +575,8 @@
<string name="onramp_choose_provider_title_hint">Tangemでの暗号通貨の買付は、サードパーティプロバイダーの条件に基づいて行われます。</string>
<string name="onramp_country_search">国で検索</string>
<string name="onramp_country_unavailable">利用不可</string>
<string name="onramp_currency_other">その他の通貨</string>
<string name="onramp_currency_popular">人気の法定通貨</string>
<string name="onramp_currency_search">通貨で検索</string>
<string name="onramp_legal">オンランプ機能を使用することにより、プロバイダの%1$sおよび%2$sに同意するものとします</string>
<string name="onramp_max_amount_restriction">買付金額は%s以下にしてください</string>

View file

@ -5,7 +5,7 @@
<string name="action_buttons_sell_navigation_bar_title">Продать</string>
<string name="action_buttons_service_loading_alert_message">Это может занять несколько секунд. Пожалуйста, попробуйте позже.</string>
<string name="action_buttons_service_loading_alert_title">Данные ещё не загрузились</string>
<string name="action_buttons_something_wrong_alert_message">Это действие в данный момент недоступно, пожалуйста попробуйте позже или обновите данные, сделав свайп экрана вниз.</string>
<string name="action_buttons_something_wrong_alert_message">Это действие в данный момент недоступно, пожалуйста, попробуйте позже или обновите данные, сделав свайп экрана вниз.</string>
<string name="action_buttons_something_wrong_alert_title">Кнопка недоступна</string>
<string name="action_buttons_swap_choose_token">Выберите токен</string>
<string name="action_buttons_swap_empty_search_message">Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для обмена.</string>
@ -51,6 +51,8 @@
<string name="balance_hidden_do_not_show_button">Больше не показывать</string>
<string name="balance_hidden_got_it_button">Понятно</string>
<string name="balance_hidden_title">Балансы скрыты</string>
<string name="beta_mode_warning_message">Согласно информации от разработчиков сети, токены Kaspa находятся в режиме бета. Следите за обновлениями!</string>
<string name="beta_mode_warning_title">Бета режим</string>
<string name="biometric_lockout_permanent_warning_description">Пожалуйста, отсканируйте карту или кольцо</string>
<string name="biometric_lockout_warning_description">Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту или кольцо</string>
<string name="biometric_lockout_warning_title">Слишком много попыток</string>
@ -227,7 +229,7 @@
<string name="custom_token_network_input_not_selected">Не выбрано</string>
<string name="custom_token_network_input_title">Сеть</string>
<string name="custom_token_network_selector_title">Сеть</string>
<string name="custom_token_subtitle">Вы можете добавить токен в ручную, если он не поддерживается Tangem</string>
<string name="custom_token_subtitle">Вы можете добавить токен вручную, если он не поддерживается Tangem</string>
<string name="custom_token_token_symbol_input_placeholder">Например, USDC</string>
<string name="custom_token_token_symbol_input_title">Символ</string>
<string name="custom_token_token_symbol_input_title_old">Символ токена</string>
@ -597,6 +599,8 @@
<string name="onramp_choose_provider_title_hint">Tangem предоставляет доступ к покупке через сторонних провайдеров в соответствии с их правилами</string>
<string name="onramp_country_search">Поиск по стране</string>
<string name="onramp_country_unavailable">Недоступно</string>
<string name="onramp_currency_other">Другие валюты</string>
<string name="onramp_currency_popular">Популярные фиаты</string>
<string name="onramp_currency_search">Поиск по валюте</string>
<string name="onramp_legal">Пользуясь сервисом покупки, вы соглашаетесь с %1$s и %2$s</string>
<string name="onramp_max_amount_restriction">Сумма покупки не может быть больше, чем %s</string>

View file

@ -12,6 +12,7 @@
<string name="action_buttons_swap_no_available_pair_notification_message">Немає доступних токенів для обміну з обраним токеном. Будь ласка, оберіть інший.</string>
<string name="action_buttons_swap_no_available_pair_notification_title">Немає вільної пари</string>
<string name="action_buttons_swap_no_tokens_added_alert_message">Щоб скористатися функцією обміну, у вашому портфелі має бути щонайменше 2 токени.</string>
<string name="action_buttons_swap_no_tokens_added_alert_title">Додайте токени</string>
<string name="action_buttons_swap_not_enough_tokens_alert_message">У вас додано лише 1 токен. Щоб скористатися функцією обміну, потрібно додати мінімум 2 токени.</string>
<string name="action_buttons_swap_not_enough_tokens_alert_title">Додайте токени</string>
<string name="action_buttons_you_want_to_receive">Оберіть токен для отримання</string>
@ -324,6 +325,7 @@
<string name="feedback_preface_scan_failed">Розкажіть, будь ласка, яку картку або кільце ви маєте?</string>
<string name="feedback_preface_support">Привіт, команда підтримки,</string>
<string name="feedback_preface_tx_failed">Будь ласка, розкажіть нам більше про вашу проблему. Кожна дрібниця може допомогти.</string>
<string name="feedback_subject_pre_activated_wallet">Раніше активований гаманець</string>
<string name="feedback_subject_rate_negative">Мої пропозиції</string>
<string name="feedback_subject_scan_failed">Не вдається відсканувати картку/кільце</string>
<string name="feedback_subject_support">Звернення в підтримку</string>
@ -597,7 +599,10 @@
<string name="onramp_choose_provider_title_hint">Tangem забезпечує доступ к покупці через сторонніх провайдерів згідно з їхніми умовами</string>
<string name="onramp_country_search">Пошук за країною</string>
<string name="onramp_country_unavailable">Недоступно</string>
<string name="onramp_currency_other">Інші валюти</string>
<string name="onramp_currency_popular">Популярні фіати</string>
<string name="onramp_currency_search">Пошук по валюті</string>
<string name="onramp_legal">Використовуючи сервіс покупки, ви погоджуєтесь з %1$s та %2$s</string>
<string name="onramp_max_amount_restriction">Сума покупки не може бути більше ніж %s</string>
<string name="onramp_min_amount_restriction">Сума покупки повинна бути не менше %s</string>
<string name="onramp_no_available_providers">Для данної валюти немає доступних провайдерів</string>

View file

@ -51,6 +51,8 @@
<string name="balance_hidden_do_not_show_button">Don\'t show again</string>
<string name="balance_hidden_got_it_button">Got it</string>
<string name="balance_hidden_title">Balances are hidden</string>
<string name="beta_mode_warning_message">According to the blockchain developers, Kaspa tokens are currently in beta. Stay tuned for updates!</string>
<string name="beta_mode_warning_title">Beta Mode</string>
<string name="biometric_lockout_permanent_warning_description">Please scan the card or ring</string>
<string name="biometric_lockout_warning_description">Please try again in 30 seconds or scan the card or ring</string>
<string name="biometric_lockout_warning_title">Too many attempts</string>
@ -583,6 +585,8 @@
<string name="onramp_choose_provider_title_hint">Buying crypto in Tangem is powered by third-party providers on their terms.</string>
<string name="onramp_country_search">Search by country</string>
<string name="onramp_country_unavailable">Unavailable</string>
<string name="onramp_currency_other">Other currencies</string>
<string name="onramp_currency_popular">Popular Fiats</string>
<string name="onramp_currency_search">Search by currency</string>
<string name="onramp_legal">By using onramp functionality, you agree with providers %1$s and %2$s</string>
<string name="onramp_max_amount_restriction">The purchase amount should be no more than %s</string>

View file

@ -32,10 +32,8 @@ import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrDefault
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.onramp.model.*
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.model.error.OnrampError
@ -251,6 +249,7 @@ internal class DefaultOnrampRepository(
maxFromAmount = convertToAmount(response.maxFromAmount, cryptoCurrency),
paymentMethod = paymentMethod,
provider = provider,
countryCode = response.countryCode,
)
},
onError = { error ->
@ -259,6 +258,7 @@ internal class DefaultOnrampRepository(
paymentMethod = paymentMethod,
provider = provider,
fromOnrampAmount = fromOnrampAmount,
countryCode = country.code,
)
},
)
@ -281,6 +281,7 @@ internal class DefaultOnrampRepository(
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
quote: OnrampProviderWithQuote.Data,
isDarkTheme: Boolean,
): OnrampTransaction = withContext(dispatchers.io) {
try {
val address = requireNotNull(
@ -306,7 +307,7 @@ internal class DefaultOnrampRepository(
toAddress = address,
redirectUrl = BuyCurrencyDeepLink.ONRAMP_REDIRECT_DEEPLINK,
language = null,
theme = getTheme(),
theme = getTheme(isDarkTheme),
requestId = requestId,
).bind()
},
@ -415,7 +416,7 @@ internal class DefaultOnrampRepository(
fromCurrency = currency,
toAmount = quote.toAmount.value,
toCurrencyId = cryptoCurrency.id.value,
status = OnrampStatus.Status.Expired,
status = OnrampStatus.Status.Created,
externalTxUrl = onrampDataJson.externalTxUrl,
externalTxId = onrampDataJson.externalTxId,
timestamp = DateTime.now().millis,
@ -442,17 +443,10 @@ internal class DefaultOnrampRepository(
)
}
private suspend fun getTheme(): String {
val appTheme = appPreferencesStore.getObjectSyncOrDefault(
key = PreferencesKeys.APP_THEME_MODE_KEY,
default = AppThemeMode.DEFAULT,
)
return when (appTheme) {
AppThemeMode.FORCE_DARK -> PROVIDER_THEME_DARK
AppThemeMode.FORCE_LIGHT,
AppThemeMode.FOLLOW_SYSTEM,
-> PROVIDER_THEME_LIGHT
}
private fun getTheme(isDarkTheme: Boolean): String = if (isDarkTheme) {
PROVIDER_THEME_DARK
} else {
PROVIDER_THEME_LIGHT
}
private fun List<PaymentMethodDTO>.removeApplePay(): List<PaymentMethodDTO> = filterNot { it.id == "apple-pay" }
@ -462,18 +456,26 @@ internal class DefaultOnrampRepository(
paymentMethod: OnrampPaymentMethod,
provider: OnrampProvider,
fromOnrampAmount: OnrampAmount,
countryCode: String,
) = if (error is ApiResponseError.HttpException) {
val onrampError = onrampErrorConverter.convert(value = error.errorBody.orEmpty())
if (onrampError is OnrampError.AmountError) {
OnrampQuote.AmountError(
paymentMethod = paymentMethod,
provider = provider,
fromAmount = fromOnrampAmount,
error = onrampError,
countryCode = countryCode,
)
} else {
Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error")
OnrampQuote.Error(
paymentMethod = paymentMethod,
provider = provider,
fromAmount = fromOnrampAmount,
error = onrampError,
countryCode = countryCode,
)
} else {
Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error")
null
}
} else {
Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error")

View file

@ -138,32 +138,32 @@ internal class DefaultCurrenciesRepository(
}
private fun populateCurrenciesWithMissedCoins(currencies: List<CryptoCurrency>): List<CryptoCurrency> {
val networkToCoin = mutableMapOf<Network, CryptoCurrency.Coin>()
val networkToTokens = mutableMapOf<Network, List<CryptoCurrency.Token>>()
val currenciesSequence = currencies.asSequence()
currencies.forEach { currency ->
when (currency) {
is CryptoCurrency.Coin -> {
networkToCoin[currency.network] = currency
}
is CryptoCurrency.Token -> {
val tokens = networkToTokens.getOrElse(currency.network, ::emptyList)
networkToTokens[currency.network] = tokens + currency
}
}
}
val networksWithTokens = currenciesSequence
.filterIsInstance<CryptoCurrency.Token>()
.map { it.network }
.distinct()
val networksWithCoins = currenciesSequence
.filterIsInstance<CryptoCurrency.Coin>()
.map { it.network }
.distinct()
val networksNeedingCoins = (networksWithTokens - networksWithCoins.toSet()).toMutableList()
if (networksNeedingCoins.isEmpty()) return currencies
return buildList {
if (networkToTokens.isEmpty()) {
// add only coin here
addAll(networkToCoin.values)
} else {
networkToTokens.forEach { (network, networkTokens) ->
val networkCoin = networkToCoin[network] ?: cryptoCurrencyFactory.createCoin(network)
currencies.forEach { currency ->
if (currency is CryptoCurrency.Token && currency.network in networksNeedingCoins) {
val coin = cryptoCurrencyFactory.createCoin(currency.network)
add(coin)
add(networkCoin)
addAll(networkTokens)
networksNeedingCoins.remove(currency.network)
}
add(currency)
}
}
}

View file

@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
data class OnrampCurrency(
val name: String,
val code: String,
val image: String,
val image: String?,
val precision: Int,
val unit: String,
)

View file

@ -23,7 +23,7 @@ sealed interface OnrampProviderWithQuote {
data class Error(
override val provider: OnrampProvider,
val quoteError: OnrampQuote.Error,
val quoteError: OnrampQuote.AmountError,
) : Unavailable
}
}

View file

@ -6,20 +6,32 @@ sealed class OnrampQuote {
abstract val paymentMethod: OnrampPaymentMethod
abstract val provider: OnrampProvider
abstract val fromAmount: OnrampAmount
abstract val countryCode: String
data class Data(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
val fromAmount: OnrampAmount,
override val fromAmount: OnrampAmount,
override val countryCode: String,
val toAmount: OnrampAmount,
val minFromAmount: OnrampAmount,
val maxFromAmount: OnrampAmount,
) : OnrampQuote()
data class AmountError(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
override val fromAmount: OnrampAmount,
override val countryCode: String,
val error: OnrampError.AmountError,
) : OnrampQuote()
data class Error(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
val fromAmount: OnrampAmount,
val error: OnrampError.AmountError,
override val fromAmount: OnrampAmount,
override val countryCode: String,
val error: OnrampError,
) : OnrampQuote()
}

View file

@ -31,8 +31,8 @@ class GetOnrampProviderWithQuoteUseCase(
private fun List<OnrampQuote>.quoteWithProvider(
provider: OnrampProvider,
selectedPaymentMethod: OnrampPaymentMethod,
): OnrampProviderWithQuote {
val matchedQuote = firstOrNull { it.paymentMethod == selectedPaymentMethod }
): OnrampProviderWithQuote? {
val matchedQuote = firstOrNull { it.paymentMethod.id == selectedPaymentMethod.id }
return when (matchedQuote) {
is OnrampQuote.Data -> OnrampProviderWithQuote.Data(
provider = matchedQuote.provider,
@ -40,14 +40,23 @@ class GetOnrampProviderWithQuoteUseCase(
toAmount = matchedQuote.toAmount,
fromAmount = matchedQuote.fromAmount,
)
is OnrampQuote.Error -> Unavailable.Error(
is OnrampQuote.AmountError -> Unavailable.Error(
provider = matchedQuote.provider,
quoteError = matchedQuote,
)
null -> Unavailable.NotSupportedPaymentMethod(
provider = provider,
availablePaymentMethods = provider.paymentMethods,
)
is OnrampQuote.Error -> null
null -> {
val availablePaymentMethods = getAvailablePaymentMethods(provider)
Unavailable.NotSupportedPaymentMethod(
provider = provider,
availablePaymentMethods = availablePaymentMethods,
).takeIf { availablePaymentMethods.isNotEmpty() }
}
}
}
private fun List<OnrampQuote>.getAvailablePaymentMethods(provider: OnrampProvider) = provider.paymentMethods
.filter { pm ->
filter { it !is OnrampQuote.Error }.any { it.paymentMethod.id == pm.id }
}
}

View file

@ -47,9 +47,9 @@ class GetOnrampQuotesUseCase(
grouped.value.sortedByDescending {
when (it) {
is OnrampQuote.Data -> it.toAmount.value
is OnrampQuote.Error -> null
// negative difference to sort both when data and unavailable is present
is OnrampQuote.Error -> {
is OnrampQuote.AmountError -> {
when (val error = it.error) {
is OnrampError.AmountError.TooSmallError -> it.fromAmount.value - error.requiredAmount
is OnrampError.AmountError.TooBigError -> error.requiredAmount - it.fromAmount.value

View file

@ -19,12 +19,14 @@ class GetOnrampRedirectUrlUseCase(
userWalletId: UserWalletId,
quote: OnrampProviderWithQuote.Data,
cryptoCurrency: CryptoCurrency,
isDarkTheme: Boolean,
): Either<OnrampError, String> {
return Either.catch {
val transaction = repository.getOnrampData(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
quote = quote,
isDarkTheme = isDarkTheme,
)
transactionRepository.storeTransaction(transaction)
transaction.redirectUrl

View file

@ -11,9 +11,9 @@ class GetOnrampStatusUseCase(
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(externalTxId: String): Either<OnrampError, OnrampStatus> {
suspend operator fun invoke(txId: String): Either<OnrampError, OnrampStatus> {
return Either.catch {
onrampRepository.getStatus(externalTxId)
onrampRepository.getStatus(txId)
}.mapLeft(errorResolver::resolve)
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.domain.onramp.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.CURRENCY
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_DESCRIPTION
import com.tangem.core.analytics.models.AnalyticsParam.Key.PAYMENT_METHOD
@ -37,7 +36,7 @@ sealed class OnrampAnalyticsEvent(
private val currency: String,
) : OnrampAnalyticsEvent(
event = "Currency Chosen",
params = mapOf(CURRENCY to currency),
params = mapOf("Currency Type" to currency),
)
data object CloseOnramp : OnrampAnalyticsEvent(event = "Button - Close")
@ -114,7 +113,7 @@ sealed class OnrampAnalyticsEvent(
event = "Button - Buy",
params = mapOf(
PROVIDER to providerName,
CURRENCY to currency,
"Currency Type" to currency,
TOKEN_PARAM to tokenSymbol,
),
)
@ -131,7 +130,7 @@ sealed class OnrampAnalyticsEvent(
TOKEN_PARAM to tokenSymbol,
PROVIDER to providerName,
RESIDENCE to residence,
CURRENCY to currency,
"Currency Type" to currency,
PAYMENT_METHOD to paymentMethod,
),
)
@ -143,11 +142,13 @@ sealed class OnrampAnalyticsEvent(
private val tokenSymbol: String,
private val errorCode: String,
private val providerName: String?,
private val paymentMethod: String?,
) : OnrampAnalyticsEvent(
event = "Errors",
params = buildMap {
put(TOKEN_PARAM, tokenSymbol)
providerName?.let { put(PROVIDER, providerName) }
paymentMethod?.let { put(PAYMENT_METHOD, paymentMethod) }
put(ERROR_CODE, errorCode)
},
)

View file

@ -24,6 +24,7 @@ interface OnrampRepository {
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
quote: OnrampProviderWithQuote.Data,
isDarkTheme: Boolean,
): OnrampTransaction
// cache

View file

@ -74,6 +74,16 @@ sealed class TokenScreenAnalyticsEvent(
params = mapOf("Token" to tokenSymbol, "Blockchain" to blockchain),
)
class RevealTryAgain(tokenSymbol: String, blockchain: String) : TokenScreenAnalyticsEvent(
event = "Button - Reveal Try Again",
params = mapOf("Token" to tokenSymbol, "Blockchain" to blockchain),
)
class RevealCancel(tokenSymbol: String, blockchain: String) : TokenScreenAnalyticsEvent(
event = "Button - Reveal Cancel",
params = mapOf("Token" to tokenSymbol, "Blockchain" to blockchain),
)
data class StakingClicked(val token: String) : TokenScreenAnalyticsEvent(
event = "Staking Clicked",
params = mapOf("Token" to token),

View file

@ -57,4 +57,6 @@ sealed class CryptoCurrencyWarning {
val amount: BigDecimal,
val maxAmount: BigDecimal?,
) : CryptoCurrencyWarning()
data object TokensInBetaWarning : CryptoCurrencyWarning()
}

View file

@ -173,6 +173,7 @@ class GetCurrencyWarningsUseCase(
when {
tokenStatus != null && coinStatus != null -> {
buildList {
getIsBetaTokensWarning(tokenStatus.currency)?.let(::add)
getFeeWarning(
userWalletId = userWalletId,
coinStatus = coinStatus,
@ -219,6 +220,15 @@ class GetCurrencyWarningsUseCase(
}
}
private fun getIsBetaTokensWarning(currency: CryptoCurrency): CryptoCurrencyWarning? {
val isTokenBetaFunctionality = BlockchainUtils.isTokenBetaFunctionality(currency.network.id.value)
return if (currency is CryptoCurrency.Token && isTokenBetaFunctionality) {
CryptoCurrencyWarning.TokensInBetaWarning
} else {
null
}
}
private suspend fun constructTokenBalanceNotEnoughWarning(
userWalletId: UserWalletId,
tokenStatus: CryptoCurrencyStatus,

View file

@ -1,11 +1,9 @@
package com.tangem.domain.tokens.legacy
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import org.rekotlin.Action
import java.math.BigDecimal
sealed class TradeCryptoAction : Action {
@ -23,29 +21,4 @@ sealed class TradeCryptoAction : Action {
val cryptoCurrencyStatus: CryptoCurrencyStatus,
val appCurrencyCode: String,
) : TradeCryptoAction()
@Deprecated("Use AppRoute instead")
data class SendToken(
val userWallet: UserWallet,
val tokenCurrency: CryptoCurrency.Token,
val tokenFiatRate: BigDecimal?,
val coinFiatRate: BigDecimal?,
val feeCurrencyStatus: CryptoCurrencyStatus?,
val transactionInfo: TransactionInfo? = null,
) : TradeCryptoAction()
@Deprecated("Use AppRoute instead")
data class SendCoin(
val userWallet: UserWallet,
val coinStatus: CryptoCurrencyStatus,
val feeCurrencyStatus: CryptoCurrencyStatus?,
val transactionInfo: TransactionInfo? = null,
) : TradeCryptoAction()
data class TransactionInfo(
val transactionId: String,
val destinationAddress: String,
val amount: String,
val tag: String? = null,
)
}

View file

@ -47,11 +47,16 @@ import java.nio.charset.StandardCharsets
@Composable
internal fun DisclaimerScreen(state: DisclaimerUM) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val bottomPadding = bottomBarHeight + TangemTheme.dimens.size16
val bottomPadding = if (state.isTosAccepted) {
bottomBarHeight + TangemTheme.dimens.size16
} else {
bottomBarHeight + TangemTheme.dimens.size64
}
val backgroundColor = TangemTheme.colors.background.primary
val (textColor, iconColor) = TangemTheme.colors.text.primary1 to TangemTheme.colors.icon.primary1
Box(
modifier = Modifier
.background(TangemTheme.colors.background.primary)
.background(backgroundColor)
.statusBarsPadding()
.testTag(DISCLAIMER_SCREEN_CONTAINER),
) {
@ -67,6 +72,8 @@ internal fun DisclaimerScreen(state: DisclaimerUM) {
onIconClicked = state.popBack,
).takeIf { state.isTosAccepted },
titleAlignment = Alignment.CenterHorizontally,
textColor = textColor,
iconTint = iconColor,
)
DisclaimerContent(state.url)
}
@ -74,7 +81,7 @@ internal fun DisclaimerScreen(state: DisclaimerUM) {
if (!state.isTosAccepted) {
BottomFade(
modifier = Modifier.align(Alignment.BottomCenter),
backgroundColor = TangemTheme.colors.background.primary,
backgroundColor = backgroundColor,
)
DisclaimerButton(state.onAccept)
} else {
@ -85,6 +92,8 @@ internal fun DisclaimerScreen(state: DisclaimerUM) {
@Composable
private fun DisclaimerContent(url: String) {
val backgroundColor = TangemTheme.colors.background.primary
val webViewStateUrl = rememberWebViewState(url)
val webViewStateData = rememberWebViewStateWithHTMLData(
data = localTermsOfServices,
@ -119,12 +128,12 @@ private fun DisclaimerContent(url: String) {
exit = fadeOut(),
modifier = Modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary),
.background(backgroundColor),
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary),
.background(backgroundColor),
) {
CircularProgressIndicator(
color = TangemTheme.colors.icon.informative,

View file

@ -14,6 +14,7 @@ android {
dependencies {
/* Project - API */
api(projects.features.markets.api)
api(projects.features.onramp.api)
implementation(projects.core.navigation)
/* Domain */

View file

@ -23,6 +23,7 @@ import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.impl.loader.PortfolioData
import com.tangem.features.markets.portfolio.impl.ui.WarningDialog
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.utils.Provider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -41,12 +42,15 @@ internal class TokenActionsHandler @AssistedInject constructor(
@Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val messageSender: UiMessageSender,
private val onrampFeatureToggles: OnrampFeatureToggles,
) {
private val disabledActionsInDemoMode = setOf(
TokenActionsBSContentUM.Action.Buy,
TokenActionsBSContentUM.Action.Sell,
)
private val disabledActionsInDemoMode = buildSet {
if (!onrampFeatureToggles.isFeatureEnabled) {
add(TokenActionsBSContentUM.Action.Buy)
}
add(TokenActionsBSContentUM.Action.Sell)
}
fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
onHandleQuickAction(

View file

@ -10,7 +10,7 @@ internal interface ConfirmResidencyComponent : ComposableBottomSheetComponent {
data class Params(
val cryptoCurrency: CryptoCurrency,
val country: OnrampCountry,
val onDismiss: (OnrampCountry) -> Unit,
val onDismiss: () -> Unit,
)
interface Factory : ComponentFactory<Params, ConfirmResidencyComponent>

View file

@ -66,11 +66,17 @@ internal class DefaultConfirmResidencyComponent @AssistedInject constructor(
config: ConfirmResidencyBottomSheetConfig,
componentContext: ComponentContext,
): ComposableBottomSheetComponent = when (config) {
ConfirmResidencyBottomSheetConfig.SelectCountry -> selectCountryComponentFactory.create(
is ConfirmResidencyBottomSheetConfig.SelectCountry -> selectCountryComponentFactory.create(
context = childByContext(componentContext),
params = SelectCountryComponent.Params(
cryptoCurrency = params.cryptoCurrency,
onDismiss = model.bottomSheetNavigation::dismiss,
onDismiss = {
// Dismiss country select sheet
model.bottomSheetNavigation.dismiss()
// Dismiss confirm residency sheet
config.onDismiss()
},
),
)
}

View file

@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable
internal sealed class ConfirmResidencyBottomSheetConfig {
@Serializable
data object SelectCountry : ConfirmResidencyBottomSheetConfig()
data class SelectCountry(val onDismiss: () -> Unit) : ConfirmResidencyBottomSheetConfig()
}

View file

@ -10,6 +10,7 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.onramp.OnrampSaveDefaultCountryUseCase
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent
import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyBottomSheetConfig
import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyUM
@ -35,7 +36,7 @@ internal class ConfirmResidencyModel @Inject constructor(
country = params.country.name,
countryFlagUrl = params.country.image,
isCountrySupported = params.country.onrampAvailable,
primaryButtonConfig = getPrimaryButtonConfig(),
primaryButtonConfig = getPrimaryButtonConfig(params.country),
secondaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig(
onClick = ::onChangeClick,
text = resourceReference(R.string.common_change),
@ -47,11 +48,14 @@ internal class ConfirmResidencyModel @Inject constructor(
analyticsEventHandler.send(OnrampAnalyticsEvent.ResidenceConfirmScreenOpened(params.country.name))
}
private fun getPrimaryButtonConfig() = if (params.country.onrampAvailable) {
private fun getPrimaryButtonConfig(country: OnrampCountry) = if (country.onrampAvailable) {
ConfirmResidencyUM.ActionButtonConfig(
onClick = {
analyticsEventHandler.send(OnrampAnalyticsEvent.OnResidenceConfirm(params.country.name))
modelScope.launch { saveDefaultCountryUseCase.invoke(params.country) }
analyticsEventHandler.send(OnrampAnalyticsEvent.OnResidenceConfirm(country.name))
modelScope.launch {
saveDefaultCountryUseCase.invoke(country)
params.onDismiss()
}
},
text = resourceReference(R.string.common_confirm),
)
@ -67,6 +71,6 @@ internal class ConfirmResidencyModel @Inject constructor(
private fun onChangeClick() {
analyticsEventHandler.send(OnrampAnalyticsEvent.OnResidenceChange)
bottomSheetNavigation.activate(ConfirmResidencyBottomSheetConfig.SelectCountry)
bottomSheetNavigation.activate(ConfirmResidencyBottomSheetConfig.SelectCountry(params.onDismiss))
}
}

View file

@ -58,7 +58,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor(
country = config.country,
onDismiss = {
model.bottomSheetNavigation.dismiss()
model.handleOnrampAvailable(it.defaultCurrency)
model.handleOnrampAvailable()
},
),
)

View file

@ -10,7 +10,12 @@ internal data class OnrampAmountBlockUM(
val secondaryFieldModel: OnrampAmountSecondaryFieldUM,
)
internal data class OnrampCurrencyUM(val code: String, val iconUrl: String, val precision: Int, val onClick: () -> Unit)
internal data class OnrampCurrencyUM(
val code: String,
val iconUrl: String?,
val precision: Int,
val onClick: () -> Unit,
)
@Immutable
internal sealed interface OnrampAmountSecondaryFieldUM {

View file

@ -0,0 +1,8 @@
package com.tangem.features.onramp.main.entity
import com.tangem.domain.onramp.model.OnrampAmount
data class OnrampLastUpdate(
val lastAmount: OnrampAmount,
val lastCountryString: String,
)

View file

@ -49,8 +49,11 @@ internal class OnrampStateFactory(
fun getOnrampErrorState(onrampError: OnrampError): OnrampMainComponentUM {
return when (onrampError) {
OnrampError.PairsNotFound -> getNoPairsErrorState()
is OnrampError.DataError -> getErrorState(onrampError.code)
is OnrampError.DomainError -> getErrorState()
is OnrampError.DataError -> getErrorState(
errorCode = onrampError.code,
onRefresh = onrampIntents::onRefresh,
)
is OnrampError.DomainError -> getErrorState(onRefresh = onrampIntents::onRefresh)
is OnrampError.AmountError.TooBigError,
is OnrampError.AmountError.TooSmallError,
OnrampError.RedirectError.VerificationFailed,
@ -74,7 +77,7 @@ internal class OnrampStateFactory(
)
}
fun getErrorState(errorCode: String? = null): OnrampMainComponentUM {
fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampMainComponentUM {
val state = currentStateProvider()
val endButton = state.topBarConfig.endButtonUM.copy(enabled = true)
@ -89,13 +92,13 @@ internal class OnrampStateFactory(
providerBlockState = OnrampProviderBlockUM.Empty,
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
errorCode = errorCode,
onRefresh = onrampIntents::onRefresh,
onRefresh = onRefresh,
),
)
is OnrampMainComponentUM.InitialLoading -> state.copy(
errorNotification = NotificationUM.Warning.OnrampErrorNotification(
errorCode = errorCode,
onRefresh = onrampIntents::onRefresh,
onRefresh = onRefresh,
),
)
}

View file

@ -14,15 +14,18 @@ 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.tokens.model.AmountType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.entity.*
import com.tangem.features.onramp.providers.entity.SelectProviderResult
import com.tangem.utils.Provider
import com.tangem.utils.extensions.isSingleItem
internal class OnrampAmountStateFactory(
private val currentStateProvider: Provider<OnrampMainComponentUM>,
private val analyticsEventHandler: AnalyticsEventHandler,
private val onrampIntents: OnrampIntents,
private val cryptoCurrency: CryptoCurrency,
) {
private val onrampAmountFieldChangeConverter = OnrampAmountFieldChangeConverter(
@ -62,11 +65,12 @@ internal class OnrampAmountStateFactory(
return currentState.copy(
amountBlockState = amountState.copy(secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading),
providerBlockState = OnrampProviderBlockUM.Loading,
buyButtonConfig = currentState.buyButtonConfig.copy(enabled = false),
)
}
fun getAmountSecondaryUpdatedState(quote: OnrampQuote, isBestRate: Boolean): OnrampMainComponentUM {
fun getAmountSecondaryUpdatedState(quote: OnrampQuote): OnrampMainComponentUM {
val currentState = currentStateProvider()
if (currentState !is OnrampMainComponentUM.Content) return currentState
@ -75,9 +79,9 @@ internal class OnrampAmountStateFactory(
return currentState.copy(
amountBlockState = amountState.copy(
secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState),
amountFieldModel = amountState.amountFieldModel.copy(isError = false),
secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState) ?: amountState.secondaryFieldModel,
),
providerBlockState = quote.toProviderBlockState(isBestRate),
buyButtonConfig = currentState.buyButtonConfig.copy(
enabled = quote is OnrampQuote.Data,
onClick = {
@ -96,6 +100,32 @@ internal class OnrampAmountStateFactory(
)
}
fun getUpdatedProviderState(selectedQuote: OnrampQuote, quotes: List<OnrampQuote>): OnrampMainComponentUM {
val currentState = currentStateProvider()
if (currentState !is OnrampMainComponentUM.Content) return currentState
analyticsEventHandler.send(
OnrampAnalyticsEvent.ProviderCalculated(
providerName = selectedQuote.provider.info.name,
tokenSymbol = cryptoCurrency.symbol,
paymentMethod = selectedQuote.paymentMethod.name,
),
)
val bestProvider = selectedQuote as? OnrampQuote.Data
val isMultipleQuotes = !quotes.isSingleItem()
val isOtherQuotesHasData = quotes
.filter { it.paymentMethod == selectedQuote.paymentMethod }
.filterNot { it == bestProvider }
.any { it is OnrampQuote.Data }
val isBestProvider = selectedQuote == bestProvider && isMultipleQuotes && isOtherQuotesHasData
return currentState.copy(
providerBlockState = selectedQuote.toProviderBlockState(isBestProvider),
)
}
fun getAmountSecondaryUpdatedState(
providerResult: SelectProviderResult,
isBestRate: Boolean,
@ -175,19 +205,20 @@ internal class OnrampAmountStateFactory(
)
}
private fun OnrampQuote.toSecondaryFieldUiModel(amountState: OnrampAmountBlockUM): OnrampAmountSecondaryFieldUM {
private fun OnrampQuote.toSecondaryFieldUiModel(amountState: OnrampAmountBlockUM): OnrampAmountSecondaryFieldUM? {
return when (this) {
is OnrampQuote.Error -> null
is OnrampQuote.Data -> {
val amount = toAmount.value.format {
crypto(symbol = toAmount.symbol, decimals = toAmount.decimals)
}
OnrampAmountSecondaryFieldUM.Content(stringReference(amount))
}
is OnrampQuote.Error -> this.toSecondaryFieldUiModel(amountState)
is OnrampQuote.AmountError -> this.toSecondaryFieldUiModel(amountState)
}
}
private fun OnrampQuote.Error.toSecondaryFieldUiModel(
private fun OnrampQuote.AmountError.toSecondaryFieldUiModel(
amountState: OnrampAmountBlockUM,
): OnrampAmountSecondaryFieldUM.Error {
val amount = error.requiredAmount.format {
@ -203,7 +234,7 @@ internal class OnrampAmountStateFactory(
R.string.onramp_max_amount_restriction
}
is OnrampError.AmountError.TooSmallError -> {
analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError)
analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError)
R.string.onramp_min_amount_restriction
}
}

View file

@ -19,17 +19,13 @@ import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.onramp.*
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.OnrampAvailability
import com.tangem.domain.onramp.model.OnrampCurrency
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.wallets.usecase.GetWalletsUseCase
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.OnrampMainComponent
import com.tangem.features.onramp.main.entity.OnrampIntents
import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig
import com.tangem.features.onramp.main.entity.OnrampMainComponentUM
import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM
import com.tangem.features.onramp.main.entity.*
import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory
import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory
import com.tangem.features.onramp.providers.entity.SelectProviderResult
@ -39,15 +35,13 @@ import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.PeriodicTask
import com.tangem.utils.coroutines.SingleTaskScheduler
import com.tangem.utils.extensions.isSingleItem
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
internal class OnrampMainComponentModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
@ -76,19 +70,20 @@ internal class OnrampMainComponentModel @Inject constructor(
currentStateProvider = Provider { _state.value },
analyticsEventHandler = analyticsEventHandler,
onrampIntents = this,
cryptoCurrency = params.cryptoCurrency,
)
private val selectedUserWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }
private val _state: MutableStateFlow<OnrampMainComponentUM> = MutableStateFlow(
value = stateFactory.getInitialState(
currency = params.cryptoCurrency.name,
onClose = router::pop,
onClose = ::onCloseClick,
),
)
private val quotesTaskScheduler = SingleTaskScheduler<Unit>()
val state: StateFlow<OnrampMainComponentUM> get() = _state.asStateFlow()
val bottomSheetNavigation: SlotNavigation<OnrampMainBottomSheetConfig> = SlotNavigation()
private val lastAmount = mutableStateOf(BigDecimal.ZERO)
private val lastUpdateState = mutableStateOf<OnrampLastUpdate?>(null)
init {
sendScreenOpenAnalytics()
@ -105,8 +100,7 @@ internal class OnrampMainComponentModel @Inject constructor(
)
}
fun handleOnrampAvailable(currency: OnrampCurrency) {
_state.update { stateFactory.getReadyState(currency) }
fun handleOnrampAvailable() {
subscribeToCountryAndCurrencyUpdates()
subscribeToQuotesUpdate()
}
@ -125,7 +119,7 @@ internal class OnrampMainComponentModel @Inject constructor(
private fun handleOnrampAvailability(availability: OnrampAvailability) {
when (availability) {
is OnrampAvailability.Available -> handleOnrampAvailable(availability.currency)
is OnrampAvailability.Available -> handleOnrampAvailable()
is OnrampAvailability.ConfirmResidency,
is OnrampAvailability.NotSupported,
-> bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.ConfirmResidency(availability.country))
@ -139,7 +133,13 @@ internal class OnrampMainComponentModel @Inject constructor(
ifLeft = ::handleOnrampError,
ifRight = { country ->
if (country == null) return@onEach
_state.update { amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) }
_state.update {
if (it is OnrampMainComponentUM.InitialLoading) {
stateFactory.getReadyState(country.defaultCurrency)
} else {
amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency)
}
}
updatePairsAndQuotes()
},
)
@ -157,7 +157,11 @@ internal class OnrampMainComponentModel @Inject constructor(
}
private suspend fun updatePairsAndQuotes() {
_state.update { amountStateFactory.getAmountSecondaryLoadingState() }
val state = state.value as? OnrampMainComponentUM.Content
if (!state?.amountBlockState?.amountFieldModel?.fiatValue.isNullOrEmpty()) {
_state.update { amountStateFactory.getAmountSecondaryLoadingState() }
}
fetchPairsUseCase.invoke(params.cryptoCurrency).fold(
ifLeft = ::handleOnrampError,
ifRight = { _state.update { amountStateFactory.getAmountSecondaryResetState() } },
@ -196,6 +200,7 @@ internal class OnrampMainComponentModel @Inject constructor(
private fun subscribeToQuotesUpdate() {
getOnrampQuotesUseCase.invoke()
.conflate()
.onEach { maybeQuotes ->
maybeQuotes.fold(
ifLeft = ::handleOnrampError,
@ -268,44 +273,75 @@ internal class OnrampMainComponentModel @Inject constructor(
super.onDestroy()
}
private fun handleQuoteResult(quotes: List<OnrampQuote>) {
quotes.filterIsInstance<OnrampQuote.Error>().forEach { errorState ->
sendOnrampErrorAnalytic(errorState.error)
}
private fun onCloseClick() {
analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp)
router.pop()
}
val quote = quotes.firstOrNull()
private fun handleQuoteResult(quotes: List<OnrampQuote>) {
sendOnrampQuotesErrorAnalytic(quotes)
val quote = selectOrUpdateQuote(quotes)
if (quote == null) {
_state.update { stateFactory.getErrorState() }
_state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) }
lastUpdateState.value = null
return
}
val bestProvider = quote as? OnrampQuote.Data
val isMultipleQuotes = !quotes.isSingleItem()
val isOtherQuotesHasData = quotes
.filter { it.paymentMethod == quote.paymentMethod }
.filterNot { it == bestProvider }
.any { it is OnrampQuote.Data }
val hasBestProvider = isMultipleQuotes && isOtherQuotesHasData
val isBestProvider = quote == bestProvider && hasBestProvider
if (quote is OnrampQuote.Data && lastAmount.value != quote.fromAmount.value) {
lastAmount.value = quote.fromAmount.value
analyticsEventHandler.send(
OnrampAnalyticsEvent.ProviderCalculated(
providerName = quote.provider.info.name,
tokenSymbol = params.cryptoCurrency.symbol,
paymentMethod = quote.paymentMethod.name,
),
if (checkLastInputState(quote)) {
lastUpdateState.value = OnrampLastUpdate(
quote.fromAmount,
quote.countryCode,
)
_state.update {
amountStateFactory.getUpdatedProviderState(selectedQuote = quote, quotes = quotes)
}
}
_state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote = quote) }
}
/**
* !!! Important quote selection logic !!!
* Selects or updated quote based on input data (amount, country, currency).
* If input data has changed select new best quote, otherwise last selected quote.
* If last selected quote on same input data is in an error state, select next best quote
* If new best quote or next best quote does not exist (i.e. Error state) select nothing.
*/
private fun selectOrUpdateQuote(quotes: List<OnrampQuote>): OnrampQuote? {
val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error }
// Check if amount, country or currency has changed
return if (checkLastInputState(quoteToCheck)) {
quoteToCheck
} else {
val state = state.value as? OnrampMainComponentUM.Content
val providerState = state?.providerBlockState as? OnrampProviderBlockUM.Content
// Get current selected quote to update
val lastSelectedQuote = quotes.firstOrNull {
it.provider.id == providerState?.providerId &&
it.paymentMethod.id == providerState.paymentMethod.id
}
// Check if selected updated quote is not error
if (lastSelectedQuote is OnrampQuote.Error) {
quoteToCheck
} else {
lastSelectedQuote
}
}
}
private fun onRetryQuotes() {
_state.update {
amountStateFactory.getAmountSecondaryUpdatedState(
quote = quote,
isBestRate = isBestProvider,
)
(it as? OnrampMainComponentUM.Content)?.copy(
errorNotification = null,
providerBlockState = OnrampProviderBlockUM.Loading,
) ?: it
}
startLoadingQuotes()
}
private fun showDemoWarning() {
@ -344,9 +380,35 @@ internal class OnrampMainComponentModel @Inject constructor(
error = error,
tokenSymbol = params.cryptoCurrency.symbol,
providerName = providerContent?.providerName,
paymentMethod = providerContent?.paymentMethod?.name,
)
}
private fun sendOnrampQuotesErrorAnalytic(quotes: List<OnrampQuote>) {
quotes.forEach { errorState ->
when (errorState) {
is OnrampQuote.Error -> analyticsEventHandler.sendOnrampErrorEvent(
error = errorState.error,
tokenSymbol = params.cryptoCurrency.symbol,
providerName = errorState.provider.info.name,
paymentMethod = errorState.paymentMethod.name,
)
is OnrampQuote.AmountError -> analyticsEventHandler.sendOnrampErrorEvent(
error = errorState.error,
tokenSymbol = params.cryptoCurrency.symbol,
providerName = errorState.provider.info.name,
paymentMethod = errorState.paymentMethod.name,
)
else -> { /* no-op */ }
}
}
}
private fun checkLastInputState(quote: OnrampQuote?): Boolean {
return lastUpdateState.value?.lastAmount != quote?.fromAmount ||
lastUpdateState.value?.lastCountryString != quote?.countryCode
}
private companion object {
const val UPDATE_DELAY = 10_000L
}

View file

@ -6,30 +6,42 @@ internal sealed interface ProviderListItemUM {
val providerId: String
val name: String
val imageUrl: String
data class Available(
override val providerId: String,
override val name: String,
val imageUrl: String,
val rate: String,
val isBestRate: Boolean,
val diffRate: TextReference?,
val isSelected: Boolean,
val onClick: () -> Unit,
) : ProviderListItemUM
sealed interface Available : ProviderListItemUM {
val onClick: () -> Unit
val providerResult: SelectProviderResult
val isBestRate: Boolean
val isSelected: Boolean
data class AvailableWithError(
override val providerId: String,
override val name: String,
val imageUrl: String,
val subtitle: TextReference,
val onClick: () -> Unit,
) : ProviderListItemUM
data class Content(
override val providerId: String,
override val name: String,
override val onClick: () -> Unit,
override val imageUrl: String,
override val providerResult: SelectProviderResult,
override val isBestRate: Boolean,
override val isSelected: Boolean,
val rate: String,
val diffRate: TextReference?,
) : Available
data class WithError(
override val providerId: String,
override val name: String,
override val onClick: () -> Unit,
override val imageUrl: String,
override val providerResult: SelectProviderResult,
override val isBestRate: Boolean,
override val isSelected: Boolean,
val subtitle: TextReference,
) : Available
}
data class Unavailable(
override val providerId: String,
override val name: String,
val imageUrl: String,
override val imageUrl: String,
val subtitle: TextReference,
) : ProviderListItemUM
}

View file

@ -4,13 +4,14 @@ import com.tangem.domain.onramp.model.OnrampPaymentMethod
import kotlinx.collections.immutable.ImmutableList
internal data class SelectPaymentAndProviderUM(
val paymentMethods: ImmutableList<OnrampPaymentMethod>,
val selectedPaymentMethod: SelectProviderUM,
val paymentMethods: ImmutableList<PaymentProviderUM>,
val selectedPaymentMethod: PaymentProviderUM,
val selectedProviderId: String,
val isPaymentMethodClickEnabled: Boolean,
val onPaymentMethodClick: () -> Unit,
)
internal data class SelectProviderUM(
internal data class PaymentProviderUM(
val paymentMethod: OnrampPaymentMethod,
val providers: ImmutableList<ProviderListItemUM>,
)

View file

@ -20,6 +20,6 @@ sealed class SelectProviderResult {
data class ProviderWithError(
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
val quoteError: OnrampQuote.Error,
val quoteError: OnrampQuote.AmountError,
) : SelectProviderResult()
}

View file

@ -72,9 +72,21 @@ internal class SelectProviderModel @Inject constructor(
},
ifRight = { it },
)
val filteredEmptyMethods = methods.mapNotNull { method ->
val providers = getOnrampProviderWithQuoteUseCase(method).getOrNull()
if (!providers.isNullOrEmpty()) {
PaymentProviderUM(
paymentMethod = method,
providers = providers.toProvidersListItems(),
)
} else {
null
}
}
_state.value = state.value.copy(
paymentMethods = methods.toPersistentList(),
isPaymentMethodClickEnabled = methods.isNotEmpty(),
paymentMethods = filteredEmptyMethods.toPersistentList(),
isPaymentMethodClickEnabled = filteredEmptyMethods.isNotEmpty(),
)
}
}
@ -111,13 +123,19 @@ internal class SelectProviderModel @Inject constructor(
private fun getInitialState(): SelectPaymentAndProviderUM {
return SelectPaymentAndProviderUM(
paymentMethods = persistentListOf(params.selectedPaymentMethod),
paymentMethods = persistentListOf(
PaymentProviderUM(
paymentMethod = params.selectedPaymentMethod,
providers = emptyList<ProviderListItemUM>().toImmutableList(),
),
),
isPaymentMethodClickEnabled = false,
onPaymentMethodClick = ::openPaymentMethods,
selectedPaymentMethod = SelectProviderUM(
selectedPaymentMethod = PaymentProviderUM(
paymentMethod = params.selectedPaymentMethod,
providers = emptyList<ProviderListItemUM>().toImmutableList(),
),
selectedProviderId = params.selectedProviderId,
)
}
@ -131,16 +149,18 @@ internal class SelectProviderModel @Inject constructor(
)
}
private fun ImmutableList<OnrampPaymentMethod>.toPaymentUMList(): ImmutableList<PaymentMethodUM> = map { method ->
private fun ImmutableList<PaymentProviderUM>.toPaymentUMList(): ImmutableList<PaymentMethodUM> = map { method ->
PaymentMethodUM(
id = method.id,
imageUrl = method.imageUrl,
name = method.name,
id = method.paymentMethod.id,
imageUrl = method.paymentMethod.imageUrl,
name = method.paymentMethod.name,
onSelect = { onPaymentMethodSelected(method) },
)
}.toPersistentList()
private fun onPaymentMethodSelected(paymentMethod: OnrampPaymentMethod) {
private fun onPaymentMethodSelected(methodContainer: PaymentProviderUM) {
val paymentMethod = methodContainer.paymentMethod
val firstProvider = methodContainer.providers.firstOrNull()
analyticsEventHandler.send(OnrampAnalyticsEvent.OnPaymentMethodChosen(paymentMethod = paymentMethod.name))
modelScope.launch {
saveSelectedPaymentMethod.invoke(paymentMethod)
@ -151,6 +171,10 @@ internal class SelectProviderModel @Inject constructor(
),
)
}
if (firstProvider is ProviderListItemUM.Available) {
onProviderSelected(firstProvider.providerResult, firstProvider.isBestRate)
}
bottomSheetNavigation.dismiss()
}
}
@ -165,6 +189,8 @@ internal class SelectProviderModel @Inject constructor(
val hasBestProvider = isMultipleQuotes && isOtherQuotesHasData
return sorted.map { quote ->
val isSelectedProvider = state.value.selectedProviderId == quote.provider.id
when (quote) {
is OnrampProviderWithQuote.Data -> {
val rate = quote.toAmount.value.format {
@ -173,28 +199,29 @@ internal class SelectProviderModel @Inject constructor(
val rateDiff = bestProvider?.toAmount?.value?.let { bestRate ->
BigDecimal.ONE - quote.toAmount.value / bestRate
}
val isSelectedProvider = quote.provider.id == params.selectedProviderId
val isSelectedPayment = quote.paymentMethod.id == params.selectedPaymentMethod.id
val isBestProvider = quote == bestProvider && hasBestProvider
ProviderListItemUM.Available(
val providerResult = SelectProviderResult.ProviderWithQuote(
paymentMethod = quote.paymentMethod,
provider = quote.provider,
fromAmount = quote.fromAmount,
toAmount = quote.toAmount,
)
ProviderListItemUM.Available.Content(
providerId = quote.provider.id,
imageUrl = quote.provider.info.imageLarge,
name = quote.provider.info.name,
rate = rate,
isSelected = isSelectedProvider && isSelectedPayment,
isBestRate = isBestProvider,
diffRate = stringReference("$MINUS${rateDiff.format { percent() }}")
.takeIf { hasBestProvider },
providerResult = providerResult,
isSelected = isSelectedProvider,
onClick = {
onProviderSelected(
result = SelectProviderResult.ProviderWithQuote(
paymentMethod = quote.paymentMethod,
provider = quote.provider,
fromAmount = quote.fromAmount,
toAmount = quote.toAmount,
),
result = providerResult,
isBestRate = isBestProvider,
)
params.onDismiss()
},
)
}
@ -207,21 +234,25 @@ internal class SelectProviderModel @Inject constructor(
is OnrampError.AmountError.TooBigError -> R.string.express_provider_max_amount
is OnrampError.AmountError.TooSmallError -> R.string.express_provider_min_amount
}
ProviderListItemUM.AvailableWithError(
val providerResult = SelectProviderResult.ProviderWithError(
paymentMethod = quoteError.paymentMethod,
provider = quote.provider,
quoteError = quote.quoteError,
)
ProviderListItemUM.Available.WithError(
providerId = quote.provider.id,
imageUrl = quote.provider.info.imageLarge,
name = quote.provider.info.name,
subtitle = resourceReference(errorSubtitleRes, wrappedList(amount)),
providerResult = providerResult,
isBestRate = false,
isSelected = isSelectedProvider,
onClick = {
onProviderSelected(
result = SelectProviderResult.ProviderWithError(
paymentMethod = quoteError.paymentMethod,
provider = quote.provider,
quoteError = quote.quoteError,
),
result = providerResult,
isBestRate = bestProvider == quote,
)
params.onDismiss()
},
)
}
@ -247,8 +278,10 @@ internal class SelectProviderModel @Inject constructor(
tokenSymbol = params.cryptoCurrency.symbol,
),
)
_state.update {
it.copy(selectedProviderId = result.provider.id)
}
params.onProviderClick(result, isBestRate)
params.onDismiss()
}
private fun sendOnrampErrorEvent(error: OnrampError) {
@ -259,6 +292,7 @@ internal class SelectProviderModel @Inject constructor(
error = error,
tokenSymbol = params.cryptoCurrency.symbol,
providerName = selectedProvider?.name,
paymentMethod = state.value.selectedPaymentMethod.paymentMethod.name,
)
}

View file

@ -2,59 +2,83 @@ package com.tangem.features.onramp.providers.model.previewData
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.onramp.model.OnrampPaymentMethod
import com.tangem.domain.onramp.model.PaymentMethodType
import com.tangem.domain.onramp.model.*
import com.tangem.features.onramp.providers.entity.PaymentProviderUM
import com.tangem.features.onramp.providers.entity.ProviderListItemUM
import com.tangem.features.onramp.providers.entity.SelectPaymentAndProviderUM
import com.tangem.features.onramp.providers.entity.SelectProviderUM
import com.tangem.features.onramp.providers.entity.SelectProviderResult
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
internal object SelectProviderPreviewData {
private val paymentMethods = persistentListOf(
OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
),
private val paymentMethod = OnrampPaymentMethod(
id = "card",
name = "Card",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
type = PaymentMethodType.CARD,
)
private val providerListUM = SelectProviderUM(
paymentMethod = paymentMethods.first(),
private val provider = OnrampProvider(
id = "mercuryo",
info = OnrampProviderInfo(
name = "Mercuryo",
imageLarge = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/mercuryo1024.png",
termsOfUseLink = null,
privacyPolicyLink = null,
),
paymentMethods = listOf(),
)
private val providerListUM = PaymentProviderUM(
paymentMethod = paymentMethod,
providers = persistentListOf(
ProviderListItemUM.Available(
providerId = "mercuryo",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/mercuryo1024.png",
name = "Mercuryo",
ProviderListItemUM.Available.Content(
providerId = provider.id,
imageUrl = provider.info.imageLarge,
name = provider.info.name,
rate = "1 BTC",
isSelected = true,
isBestRate = true,
diffRate = TextReference.EMPTY,
providerResult = SelectProviderResult.ProviderWithQuote(
paymentMethod = paymentMethod,
provider = provider,
fromAmount = OnrampAmount(symbol = "$", value = BigDecimal.ONE, decimals = 2),
toAmount = OnrampAmount(symbol = "BTC", value = BigDecimal.ONE, decimals = 18),
),
onClick = {},
),
ProviderListItemUM.Available(
providerId = "mercuryo",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/mercuryo1024.png",
name = "Mercuryo",
ProviderListItemUM.Available.Content(
providerId = provider.id,
imageUrl = provider.info.imageLarge,
name = provider.info.name,
rate = "1 BTC",
isSelected = false,
isBestRate = false,
diffRate = stringReference("-0.03%"),
providerResult = SelectProviderResult.ProviderWithQuote(
paymentMethod = paymentMethod,
provider = provider,
fromAmount = OnrampAmount(symbol = "$", value = BigDecimal.ONE, decimals = 2),
toAmount = OnrampAmount(symbol = "BTC", value = BigDecimal.ONE, decimals = 18),
),
onClick = {},
),
ProviderListItemUM.Unavailable(
providerId = "mercuryo",
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/mercuryo1024.png",
name = "Mercuryo",
providerId = provider.id,
imageUrl = provider.info.imageLarge,
name = provider.info.name,
subtitle = stringReference("Avaiable from 0,00413 BTC"),
),
),
)
val state = SelectPaymentAndProviderUM(
paymentMethods = paymentMethods,
paymentMethods = persistentListOf(providerListUM),
selectedPaymentMethod = providerListUM,
selectedProviderId = provider.id,
isPaymentMethodClickEnabled = false,
onPaymentMethodClick = {},
)

View file

@ -124,16 +124,17 @@ private fun PaymentMethodBlock(
@Composable
private fun ProviderItem(state: ProviderListItemUM, modifier: Modifier = Modifier) {
when (state) {
is ProviderListItemUM.Available -> AvailableProviderItem(
is ProviderListItemUM.Available.Content -> AvailableProviderItem(
modifier = modifier,
state = state,
)
is ProviderListItemUM.AvailableWithError -> {
is ProviderListItemUM.Available.WithError -> {
UnavailableProviderItem(
modifier = modifier.clickable(onClick = state.onClick),
imageUrl = state.imageUrl,
providerName = state.name,
subtitle = state.subtitle,
isSelected = state.isSelected,
)
}
is ProviderListItemUM.Unavailable -> UnavailableProviderItem(
@ -141,12 +142,13 @@ private fun ProviderItem(state: ProviderListItemUM, modifier: Modifier = Modifie
imageUrl = state.imageUrl,
providerName = state.name,
subtitle = state.subtitle,
isSelected = false,
)
}
}
@Composable
private fun AvailableProviderItem(state: ProviderListItemUM.Available, modifier: Modifier = Modifier) {
private fun AvailableProviderItem(state: ProviderListItemUM.Available.Content, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.then(
@ -220,10 +222,19 @@ private fun UnavailableProviderItem(
imageUrl: String,
providerName: String,
subtitle: TextReference,
isSelected: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier.padding(all = TangemTheme.dimens.spacing12),
modifier = modifier
.then(
if (isSelected) {
Modifier.selectedBorder()
} else {
Modifier.clip(RoundedCornerShape(16.dp))
},
)
.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {

View file

@ -1,7 +1,9 @@
package com.tangem.features.onramp.redirect
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
@ -22,6 +24,11 @@ internal class DefaultOnrampRedirectComponent @AssistedInject constructor(
override fun Content(modifier: Modifier) {
BackHandler(onBack = params.onBack)
OnrampRedirectContent(modifier = modifier, state = model.state)
val isDarkTheme = isSystemInDarkTheme()
LaunchedEffect(model.state) {
model.getRedirectUrl(isDarkTheme = isDarkTheme)
}
}
@AssistedFactory

View file

@ -58,16 +58,13 @@ internal class OnrampRedirectModel @Inject constructor(
),
)
init {
getRedirectUrl()
}
private fun getRedirectUrl() {
fun getRedirectUrl(isDarkTheme: Boolean) {
modelScope.launch {
getOnrampRedirectUrlUseCase.invoke(
userWalletId = params.userWalletId,
quote = params.onrampProviderWithQuote,
cryptoCurrency = params.cryptoCurrency,
isDarkTheme = isDarkTheme,
)
.onLeft(::handleError)
.onRight {
@ -83,6 +80,7 @@ internal class OnrampRedirectModel @Inject constructor(
error = error,
tokenSymbol = params.cryptoCurrency.symbol,
providerName = params.onrampProviderWithQuote.provider.info.name,
paymentMethod = params.onrampProviderWithQuote.paymentMethod.name,
)
val contentMessage = ContentMessage { onDismiss ->
BasicDialog(

View file

@ -185,7 +185,7 @@ private fun UnavailableCountryItem(state: CountryItemState.WithContent.Unavailab
color = TangemTheme.colors.text.disabled,
)
Text(
text = "Unavailable",
text = stringResourceSafe(R.string.onramp_country_unavailable),
maxLines = 1,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,

View file

@ -1,10 +1,11 @@
package com.tangem.features.onramp.selectcurrency.entity.transformer
import arrow.core.Either
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.onramp.model.OnrampCurrencies
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.selectcurrency.entity.CurrenciesListUM
import com.tangem.features.onramp.selectcurrency.entity.CurrenciesSection
import com.tangem.features.onramp.selectcurrency.entity.CurrencyItemState
@ -28,7 +29,7 @@ internal class UpdateCurrencyItemsTransformer(
if (populars.isNotEmpty()) {
add(
CurrenciesSection(
title = stringReference("Popular fiats"),
title = resourceReference(R.string.onramp_currency_popular),
items = populars.map(::convertCurrencyToUiModel).toImmutableList(),
),
)
@ -36,7 +37,7 @@ internal class UpdateCurrencyItemsTransformer(
if (others.isNotEmpty()) {
add(
CurrenciesSection(
title = stringReference("Other currencies"),
title = resourceReference(R.string.onramp_currency_other),
items = others.map(::convertCurrencyToUiModel).toImmutableList(),
),
)

View file

@ -6,7 +6,6 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.onramp.FetchOnrampCurrenciesUseCase
import com.tangem.domain.onramp.GetOnrampCurrenciesUseCase
import com.tangem.domain.onramp.OnrampSaveDefaultCurrencyUseCase
@ -136,8 +135,11 @@ internal class OnrampSelectCurrencyModel @Inject constructor(
private companion object {
val loadingSections = listOf(
CurrenciesSection(stringReference("Popular fiats"), items = createLoadingItems("popular")),
CurrenciesSection(stringReference("Other currencies"), items = createLoadingItems("other")),
CurrenciesSection(
resourceReference(R.string.onramp_currency_popular),
items = createLoadingItems("popular"),
),
CurrenciesSection(resourceReference(R.string.onramp_currency_other), items = createLoadingItems("other")),
).toImmutableList()
private fun createLoadingItems(prefix: String, size: Int = 5): ImmutableList<CurrencyItemState.Loading> =

View file

@ -11,7 +11,7 @@ sealed class OnrampSuccessComponentUM {
data class Content(
val txId: String,
val timestamp: Long,
val currencyImageUrl: String,
val currencyImageUrl: String?,
val fromAmount: TextReference,
val toAmount: TextReference,
val statusBlock: ExpressStatusUM,

View file

@ -1,19 +1,28 @@
package com.tangem.features.onramp.success.model
import androidx.compose.ui.res.stringResource
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.ContentMessage
import com.tangem.domain.onramp.GetOnrampStatusUseCase
import com.tangem.domain.onramp.GetOnrampTransactionUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent
import com.tangem.domain.onramp.model.OnrampStatus
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.features.onramp.component.OnrampSuccessComponent
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.success.entity.OnrampSuccessClickIntents
import com.tangem.features.onramp.success.entity.OnrampSuccessComponentUM
import com.tangem.features.onramp.success.entity.conterter.SetOnrampSuccessContentConverter
@ -36,6 +45,8 @@ internal class OnrampSuccessComponentModel @Inject constructor(
private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val messageSender: UiMessageSender,
private val router: Router,
paramsContainer: ParamsContainer,
) : Model(), OnrampSuccessClickIntents {
@ -58,8 +69,9 @@ internal class OnrampSuccessComponentModel @Inject constructor(
modelScope.launch {
getOnrampTransactionUseCase(externalTxId = params.externalTxId)
.fold(
ifLeft = {
Timber.e(it.toString())
ifLeft = { error ->
Timber.e(error.toString())
showErrorAlert(error)
},
ifRight = { transaction ->
loadTransactionStatus(transaction)
@ -74,15 +86,17 @@ internal class OnrampSuccessComponentModel @Inject constructor(
transaction.toCurrencyId,
).getOrElse { error("Crypto currency not found") }
getOnrampStatusUseCase(externalTxId = params.externalTxId)
getOnrampStatusUseCase(txId = transaction.txId)
.fold(
ifLeft = { error ->
analyticsEventHandler.sendOnrampErrorEvent(
error = error,
tokenSymbol = cryptoCurrency.symbol,
providerName = transaction.providerName,
paymentMethod = transaction.paymentMethod,
)
Timber.e(error.toString())
showErrorAlert(error)
},
ifRight = { status ->
analyticsEventHandler.send(
@ -101,14 +115,46 @@ internal class OnrampSuccessComponentModel @Inject constructor(
goToProviderClick = ::goToProviderClick,
).convert(status)
}
removeTransactionIfTerminalStatus(cryptoCurrency, transaction.providerName, status)
removeTransactionIfTerminalStatus(
cryptoCurrency = cryptoCurrency,
providerName = transaction.providerName,
paymentMethod = transaction.paymentMethod,
status = status,
)
},
)
}
private fun showErrorAlert(error: OnrampError) {
val contentMessage = ContentMessage { onDismiss ->
val errorCode = (error as? OnrampError.DataError)?.code
val message = if (errorCode.isNullOrBlank()) {
stringResource(R.string.common_unknown_error)
} else {
stringResource(R.string.express_error_code, wrappedList(errorCode))
}
BasicDialog(
message = message,
confirmButton = DialogButtonUM(
title = stringResource(id = R.string.common_ok),
onClick = {
router.pop()
onDismiss()
},
),
onDismissDialog = {
router.pop()
onDismiss()
},
)
}
messageSender.send(contentMessage)
}
private fun removeTransactionIfTerminalStatus(
cryptoCurrency: CryptoCurrency,
providerName: String,
paymentMethod: String,
status: OnrampStatus,
) {
modelScope.launch {
@ -118,6 +164,7 @@ internal class OnrampSuccessComponentModel @Inject constructor(
error = error,
tokenSymbol = cryptoCurrency.symbol,
providerName = providerName,
paymentMethod = paymentMethod,
)
}
}

View file

@ -8,11 +8,13 @@ internal fun AnalyticsEventHandler.sendOnrampErrorEvent(
error: OnrampError,
tokenSymbol: String,
providerName: String? = null,
paymentMethod: String? = null,
) = when (error) {
is OnrampError.DataError -> send(
OnrampAnalyticsEvent.Errors(
tokenSymbol = tokenSymbol,
providerName = providerName,
paymentMethod = paymentMethod,
errorCode = error.code,
),
)

View file

@ -25,6 +25,7 @@ dependencies {
implementation(deps.androidx.appCompat)
implementation(deps.androidx.fragment.ktx)
implementation(deps.lifecycle.viewModel.ktx)
implementation(deps.androidx.activity.compose)
/** Compose */
implementation(deps.compose.foundation)

View file

@ -2,6 +2,7 @@ package com.tangem.feature.referral.ui
import android.content.res.Configuration
import android.content.res.Resources
import androidx.activity.compose.BackHandler
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@ -51,6 +52,8 @@ import kotlinx.coroutines.launch
internal fun ReferralScreen(stateHolder: ReferralStateHolder) {
val snackbarHostState = remember(::SnackbarHostState)
BackHandler(onBack = stateHolder.headerState.onBackClicked)
Scaffold(
topBar = {
AppBarWithBackButton(

View file

@ -380,10 +380,9 @@ internal class SendViewModel @Inject constructor(
when {
uiState.value.sendState?.isSuccess == true -> return
transactionId != null && amount != null && destinationAddress != null -> {
loadFee()
uiState.value = stateFactory.getReadyState(amount, destinationAddress, memo)
stateRouter.showSend()
updateNotifications()
loadFee()
}
else -> {
uiState.value = stateFactory.getReadyState()

View file

@ -8,6 +8,7 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import java.lang.ref.WeakReference
@Deprecated("Replace with CustomTabsUrlOpener")
class CustomTabsManager(private val context: WeakReference<Context>) {
fun openUrl(url: String) {

View file

@ -184,6 +184,14 @@ internal class SwapViewModel @Inject constructor(
}.launchIn(viewModelScope)
}
override fun onStart(owner: LifecycleOwner) {
startLoadingQuotesFromLastState(true)
}
override fun onStop(owner: LifecycleOwner) {
singleTaskScheduler.cancelTask()
}
override fun onCleared() {
singleTaskScheduler.cancelTask()
super.onCleared()

View file

@ -50,6 +50,13 @@ internal class DefaultTokenDetailsRouter(
}
override fun openOnrampSuccess(externalTxId: String) {
router.push(AppRoute.OnrampSuccess(externalTxId))
// finish current onramp flow and show onramp success screen
val replaceOnrampScreens = router.stack
.filterNot { it is AppRoute.Onramp }
.toMutableList()
replaceOnrampScreens.add(AppRoute.OnrampSuccess(externalTxId))
router.replaceAll(*replaceOnrampScreens.toTypedArray())
}
}

View file

@ -152,6 +152,7 @@ internal object TokenDetailsPreviewData {
formatArgs = wrappedList("Solana"),
),
iconState = iconState,
isEnabled = true,
onStakeClicked = {},
)
@ -308,6 +309,7 @@ internal object TokenDetailsPreviewData {
dialogConfig = null,
pendingTxs = persistentListOf(),
expressTxs = persistentListOf(),
expressTxsToDisplay = persistentListOf(),
pullToRefreshConfig = pullToRefreshConfig,
bottomSheetConfig = null,
isBalanceHidden = false,
@ -338,6 +340,7 @@ internal object TokenDetailsPreviewData {
dialogConfig = null,
pendingTxs = persistentListOf(),
expressTxs = persistentListOf(),
expressTxsToDisplay = persistentListOf(),
pullToRefreshConfig = pullToRefreshConfig,
bottomSheetConfig = null,
isBalanceHidden = false,

View file

@ -23,5 +23,10 @@ internal open class TokenDetailsAnalyticsEvent(
event = "Not Enough Fee",
params = currency.toAnalyticsParams(),
)
class Reveal(currency: CryptoCurrency) : Notice(
event = "Reveal Transaction",
params = currency.toAnalyticsParams(),
)
}
}

View file

@ -41,6 +41,9 @@ internal class TokenDetailsNotificationsAnalyticsSender(
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
source = AnalyticsParam.ScreensSources.Token,
)
is TokenDetailsNotification.KaspaIncompleteTransactionWarning -> TokenDetailsAnalyticsEvent.Notice.Reveal(
currency = cryptoCurrency,
)
is TokenDetailsNotification.NetworksUnreachable,
is TokenDetailsNotification.ExistentialDeposit,
is TokenDetailsNotification.NetworksNoAccount,
@ -48,9 +51,9 @@ internal class TokenDetailsNotificationsAnalyticsSender(
is TokenDetailsNotification.RentInfo,
is TokenDetailsNotification.NetworkShutdown,
is TokenDetailsNotification.HederaAssociateWarning,
is TokenDetailsNotification.KaspaIncompleteTransactionWarning,
is TokenDetailsNotification.KoinosMana,
is TokenDetailsNotification.MigrationMaticToPol,
is TokenDetailsNotification.TokensInBeta,
-> null
}
}

View file

@ -5,6 +5,6 @@ import com.tangem.domain.tokens.model.CryptoCurrency
internal fun CryptoCurrency.toAnalyticsParams(): Map<String, String> {
return mapOf(
"Token" to symbol,
"Blockchain" to network.currencySymbol,
"Blockchain" to network.name,
)
}

View file

@ -24,6 +24,7 @@ internal sealed interface StakingBlockUM {
val iconState: IconState,
val titleText: TextReference,
val subtitleText: TextReference,
val isEnabled: Boolean,
val onStakeClicked: () -> Unit,
) : StakingBlockUM
}

View file

@ -9,7 +9,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
@ -21,6 +21,7 @@ internal data class TokenDetailsState(
val stakingBlocksState: StakingBlockUM?,
val notifications: ImmutableList<TokenDetailsNotification>,
val pendingTxs: PersistentList<TransactionState>,
val expressTxsToDisplay: PersistentList<ExpressTransactionStateUM>,
val expressTxs: PersistentList<ExpressTransactionStateUM>,
val txHistoryState: TxHistoryState,
val dialogConfig: TokenDetailsDialogConfig?,

View file

@ -229,4 +229,9 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
title = resourceReference(id = R.string.warning_matic_migration_title),
subtitle = resourceReference(id = R.string.warning_matic_migration_message),
)
data object TokensInBeta : Warning(
title = resourceReference(id = R.string.beta_mode_warning_title),
subtitle = resourceReference(id = R.string.beta_mode_warning_message),
)
}

View file

@ -0,0 +1,21 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.express
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications
import kotlinx.collections.immutable.ImmutableList
internal data class ExchangeUM(
override val info: ExpressTransactionStateInfoUM,
val provider: SwapProvider,
val activeStatus: ExchangeStatus?,
val statuses: ImmutableList<ExchangeStatusState>,
val notification: ExchangeStatusNotifications? = null,
val showProviderLink: Boolean,
val isRefundTerminalStatus: Boolean,
val fromCryptoCurrency: CryptoCurrency,
val toCryptoCurrency: CryptoCurrency,
) : ExpressTransactionStateUM

View file

@ -158,7 +158,7 @@ internal class TokenDetailsLoadedBalanceConverter(
return when {
stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> {
if (pendingBalances.isEmpty()) {
getStakeAvailableState(stakingEntryInfo, iconState)
getStakeAvailableState(stakingEntryInfo, iconState, isStakingButtonEnabled(status))
} else {
getStakedBlockWithFiatAmount(status, pendingBalances.sumOf { it.amount }, null)
}
@ -170,6 +170,12 @@ internal class TokenDetailsLoadedBalanceConverter(
}
}
private fun isStakingButtonEnabled(status: CryptoCurrencyStatus): Boolean {
return status.value is CryptoCurrencyStatus.Loaded ||
status.value is CryptoCurrencyStatus.NoQuote ||
status.value is CryptoCurrencyStatus.Custom
}
private fun getStakedBlockWithFiatAmount(
status: CryptoCurrencyStatus,
stakingAmount: BigDecimal?,
@ -210,6 +216,7 @@ internal class TokenDetailsLoadedBalanceConverter(
private fun getStakeAvailableState(
stakingEntryInfo: StakingEntryInfo,
iconState: IconState,
isEnabled: Boolean,
): StakingBlockUM.StakeAvailable {
val apr = stakingEntryInfo.apr.format { percent() }
return StakingBlockUM.StakeAvailable(
@ -222,6 +229,7 @@ internal class TokenDetailsLoadedBalanceConverter(
formatArgs = wrappedList(stakingEntryInfo.tokenSymbol),
),
iconState = iconState,
isEnabled = isEnabled,
onStakeClicked = clickIntents::onStakeBannerClick,
)
}

View file

@ -135,6 +135,7 @@ internal class TokenDetailsNotificationConverter(
},
)
is CryptoCurrencyWarning.MigrationMaticToPol -> MigrationMaticToPol
is CryptoCurrencyWarning.TokensInBetaWarning -> TokensInBeta
}
}

View file

@ -23,9 +23,9 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction
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.state.express.ExpressTransactionStateIconUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateInfoUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider

View file

@ -67,6 +67,7 @@ internal class TokenDetailsSkeletonStateConverter(
notifications = persistentListOf(),
pendingTxs = persistentListOf(),
expressTxs = persistentListOf(),
expressTxsToDisplay = persistentListOf(),
txHistoryState = TxHistoryState.Content(
contentItems = MutableStateFlow(
value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick),

View file

@ -23,9 +23,9 @@ import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListMode
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeStatusState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateIconUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateInfoUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
@ -45,20 +45,20 @@ internal class TokenDetailsSwapTransactionsStateConverter(
private val cryptoCurrency: CryptoCurrency,
private val analyticsEventsHandler: AnalyticsEventHandler,
appCurrencyProvider: Provider<AppCurrency>,
) : Converter<Unit, PersistentList<ExpressTransactionStateUM.ExchangeUM>> {
) : Converter<Unit, PersistentList<ExchangeUM>> {
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
private val appCurrency = appCurrencyProvider()
override fun convert(value: Unit): PersistentList<ExpressTransactionStateUM.ExchangeUM> {
override fun convert(value: Unit): PersistentList<ExchangeUM> {
return persistentListOf()
}
fun convert(
savedTransactions: List<SavedSwapTransactionListModel>,
quotes: Set<Quote>,
): PersistentList<ExpressTransactionStateUM.ExchangeUM> {
val result = mutableListOf<ExpressTransactionStateUM.ExchangeUM>()
): PersistentList<ExchangeUM> {
val result = mutableListOf<ExchangeUM>()
savedTransactions
.forEach { swapTransaction ->
@ -84,7 +84,7 @@ internal class TokenDetailsSwapTransactionsStateConverter(
getNotification(transaction.status?.status, transaction.status?.txExternalUrl, null)
val showProviderLink = getShowProviderLink(notifications, transaction.status)
result.add(
ExpressTransactionStateUM.ExchangeUM(
ExchangeUM(
provider = transaction.provider,
statuses = getStatuses(transaction.status?.status),
notification = notifications,
@ -108,11 +108,11 @@ internal class TokenDetailsSwapTransactionsStateConverter(
}
fun updateTxStatus(
tx: ExpressTransactionStateUM.ExchangeUM,
tx: ExchangeUM,
statusModel: ExchangeStatusModel?,
refundToken: CryptoCurrency?,
isRefundTerminalStatus: Boolean,
): ExpressTransactionStateUM.ExchangeUM {
): ExchangeUM {
if (statusModel == null || tx.activeStatus == statusModel.status) {
Timber.e("UpdateTxStatus isn't required. Current status isn't changed")
return tx

View file

@ -1,5 +1,6 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus
import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore
@ -15,9 +16,8 @@ import com.tangem.feature.swap.domain.SwapTransactionRepository
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.Provider
import dagger.assisted.Assisted
@ -55,7 +55,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
)
}
suspend operator fun invoke(): Flow<PersistentList<ExpressTransactionStateUM.ExchangeUM>> {
suspend operator fun invoke(): Flow<PersistentList<ExchangeUM>> {
val selectedWallet = getSelectedWalletSyncUseCase().fold(
ifLeft = { return emptyFlow() },
ifRight = { it },
@ -82,7 +82,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
suspend fun removeTransactionOnBottomSheetClosed(isForceTerminal: Boolean = false) {
val state = currentStateProvider()
val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return
val selectedTx = bottomSheetConfig.value as? ExpressTransactionStateUM.ExchangeUM ?: return
val selectedTx = bottomSheetConfig.value as? ExchangeUM ?: return
val shouldTerminate = selectedTx.activeStatus.isTerminal(selectedTx.isRefundTerminalStatus) || isForceTerminal
if (shouldTerminate) {
@ -95,7 +95,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
}
}
suspend fun updateSwapTxStatus(swapTx: ExpressTransactionStateUM.ExchangeUM): ExpressTransactionStateUM.ExchangeUM {
suspend fun updateSwapTxStatus(swapTx: ExchangeUM): ExchangeUM {
return if (swapTx.activeStatus.isTerminal(swapTx.isRefundTerminalStatus)) {
swapTx
} else {
@ -163,7 +163,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
private fun getExchangeStatusState(
savedTransactions: List<SavedSwapTransactionListModel>?,
quotes: Set<Quote>,
): PersistentList<ExpressTransactionStateUM.ExchangeUM> {
): PersistentList<ExchangeUM> {
if (savedTransactions == null) {
return persistentListOf()
}

View file

@ -1,5 +1,7 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.appcurrency.model.AppCurrency
@ -10,8 +12,7 @@ import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -79,11 +80,13 @@ internal class ExpressStatusFactory @AssistedInject constructor(
expressTxs.map { tx ->
async {
when (tx) {
is ExpressTransactionStateUM.ExchangeUM -> exchangeStatusFactory.updateSwapTxStatus(tx)
is ExchangeUM -> exchangeStatusFactory.updateSwapTxStatus(tx)
is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.updateOnrmapTxStatus(tx)
else -> null
}
}
}.awaitAll()
.filterNotNull()
.toPersistentList()
}
@ -95,11 +98,18 @@ internal class ExpressStatusFactory @AssistedInject constructor(
val config = state.bottomSheetConfig
val expressBottomSheet = config?.content as? ExpressStatusBottomSheetConfig
val currentTx = expressTxs.firstOrNull { it.info.txId == expressBottomSheet?.value?.info?.txId }
if (currentTx is ExpressTransactionStateUM.ExchangeUM && currentTx.activeStatus == ExchangeStatus.Finished) {
if (currentTx is ExchangeUM && currentTx.activeStatus == ExchangeStatus.Finished) {
updateBalance(currentTx.toCryptoCurrency)
}
val expressTxsToDisplay = expressTxs.filterNot {
when (it) {
is ExpressTransactionStateUM.OnrampUM -> it.activeStatus.isHidden
else -> false
}
}.toPersistentList()
return state.copy(
expressTxs = expressTxs,
expressTxsToDisplay = expressTxsToDisplay,
bottomSheetConfig = currentTx?.let(
::updateStateWithExpressStatusBottomSheet,
) ?: config,
@ -108,7 +118,7 @@ internal class ExpressStatusFactory @AssistedInject constructor(
fun getStateWithExpressStatusBottomSheet(expressState: ExpressTransactionStateUM): TokenDetailsState {
val analyticEvent = when (expressState) {
is ExpressTransactionStateUM.ExchangeUM -> TokenExchangeAnalyticsEvent.CexTxStatusOpened(
is ExchangeUM -> TokenExchangeAnalyticsEvent.CexTxStatusOpened(
cryptoCurrency.symbol,
)
is ExpressTransactionStateUM.OnrampUM -> TokenOnrampAnalyticsEvent.OnrampStatusOpened(
@ -116,6 +126,7 @@ internal class ExpressStatusFactory @AssistedInject constructor(
provider = expressState.providerName,
fiatCurrency = expressState.fromCurrencyCode,
)
else -> return currentStateProvider()
}
analyticsEventsHandler.send(analyticEvent)
@ -149,9 +160,7 @@ internal class ExpressStatusFactory @AssistedInject constructor(
isForceTerminal: Boolean = false,
) {
when (expressState) {
is ExpressTransactionStateUM.ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed(
isForceTerminal,
)
is ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed(isForceTerminal)
is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.removeTransactionOnBottomSheetClosed()
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus
import com.tangem.domain.appcurrency.model.AppCurrency
@ -14,9 +16,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsOnrampTransactionStateConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.Provider
import dagger.assisted.Assisted
@ -61,7 +61,7 @@ internal class OnrampStatusFactory @AssistedInject constructor(
ifRight = { onrampTxs ->
val transactions = onrampTransactionStateConverter.convertList(onrampTxs)
transactions.clearHiddenTerminal()
transactions.filterNot { it.activeStatus.isHidden }
transactions
},
ifLeft = { persistentListOf() },
)

View file

@ -22,6 +22,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.paging.compose.collectAsLazyPagingItems
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.expressTransactionsItems
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
@ -49,8 +51,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.expressTransactionsItems
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
@ -178,7 +178,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon
}
expressTransactionsItems(
expressTxs = state.expressTxs,
expressTxs = state.expressTxsToDisplay,
modifier = itemModifier,
)

View file

@ -1,17 +1,14 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
import androidx.compose.runtime.Composable
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.OnrampStatusBottomSheetContent
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange.ExchangeStatusBottomSheetContent
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp.OnrampStatusBottomSheetContent
internal data class ExpressStatusBottomSheetConfig(
val value: ExpressTransactionStateUM,
) : TangemBottomSheetConfigContent
@Composable
internal fun ExpressStatusBottomSheet(config: TangemBottomSheetConfig) {
@ -21,7 +18,7 @@ internal fun ExpressStatusBottomSheet(config: TangemBottomSheetConfig) {
) { content: ExpressStatusBottomSheetConfig ->
when (val state = content.value) {
is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(state)
is ExpressTransactionStateUM.ExchangeUM -> ExchangeStatusBottomSheetContent(state)
is ExchangeUM -> ExchangeStatusBottomSheetContent(state)
}
}
}

View file

@ -8,6 +8,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import com.tangem.common.ui.expressStatus.ExpressEstimate
import com.tangem.common.ui.expressStatus.ExpressProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH10
import com.tangem.core.ui.components.SpacerH12
@ -19,12 +21,10 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressEstimate
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressProvider
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
@Composable
internal fun ExchangeStatusBottomSheetContent(state: ExpressTransactionStateUM.ExchangeUM) {
internal fun ExchangeStatusBottomSheetContent(state: ExchangeUM) {
Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) {
SpacerH10()
Text(

View file

@ -114,6 +114,7 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(id = R.string.common_stake),
enabled = state.isEnabled,
onClick = state.onStakeClicked,
)
}

View file

@ -41,7 +41,6 @@ import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkAddress
@ -68,10 +67,10 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.Token
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.ExpressStatusFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
@ -84,7 +83,6 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList", "LargeClass", "TooManyFunctions")
@ -99,8 +97,6 @@ internal class TokenDetailsViewModel @Inject constructor(
private val getExploreUrlUseCase: GetExploreUrlUseCase,
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
@ -199,7 +195,6 @@ internal class TokenDetailsViewModel @Inject constructor(
viewModel = this,
deepLinks = listOf(
BuyCurrencyDeepLink(
isOnrampFeatureEnabled = onrampFeatureToggles.isFeatureEnabled,
onReceive = ::onBuyCurrencyDeepLink,
),
),
@ -337,6 +332,7 @@ internal class TokenDetailsViewModel @Inject constructor(
expressTxStatusTaskScheduler.scheduleTask(
viewModelScope,
PeriodicTask(
isDelayFirst = false,
delay = EXPRESS_STATUS_UPDATE_DELAY,
task = {
runCatching {
@ -463,9 +459,8 @@ internal class TokenDetailsViewModel @Inject constructor(
return
}
showErrorIfDemoModeOrElse {
val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse
val status = cryptoCurrencyStatus ?: return
if (onrampFeatureToggles.isFeatureEnabled) {
viewModelScope.launch(dispatchers.main) {
reduxStateHolder.dispatch(
TradeCryptoAction.Buy(
@ -476,6 +471,19 @@ internal class TokenDetailsViewModel @Inject constructor(
),
)
}
} else {
showErrorIfDemoModeOrElse {
viewModelScope.launch(dispatchers.main) {
reduxStateHolder.dispatch(
TradeCryptoAction.Buy(
userWallet = userWallet,
source = OnrampSource.TOKEN_DETAILS,
cryptoCurrencyStatus = status,
appCurrencyCode = selectedAppCurrencyFlow.value.code,
),
)
}
}
}
}
@ -507,68 +515,16 @@ internal class TokenDetailsViewModel @Inject constructor(
return
}
sendCurrency(status = cryptoCurrencyStatus ?: return)
sendCurrency()
}
private fun sendCurrency(status: CryptoCurrencyStatus, transactionInfo: TransactionInfo? = null) {
viewModelScope.launch(dispatchers.main) {
val maybeFeeCurrencyStatus =
getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull()
private fun sendCurrency() {
val route = AppRoute.Send(
currency = cryptoCurrency,
userWalletId = userWallet.walletId,
)
when (val currency = status.currency) {
is CryptoCurrency.Coin -> {
reduxStateHolder.dispatch(
action = TradeCryptoAction.SendCoin(
userWallet = userWallet,
coinStatus = status,
feeCurrencyStatus = maybeFeeCurrencyStatus,
transactionInfo = transactionInfo,
),
)
}
is CryptoCurrency.Token -> {
sendToken(
tokenCurrency = currency,
tokenFiatRate = status.value.fiatRate,
feeCurrencyStatus = maybeFeeCurrencyStatus,
transactionInfo = transactionInfo,
)
}
}
}
}
private fun sendToken(
tokenCurrency: CryptoCurrency.Token,
tokenFiatRate: BigDecimal?,
feeCurrencyStatus: CryptoCurrencyStatus?,
transactionInfo: TransactionInfo?,
) {
viewModelScope.launch(dispatchers.main) {
val maybeCoinStatus = getNetworkCoinStatusUseCase(
userWalletId = userWalletId,
networkId = tokenCurrency.network.id,
derivationPath = tokenCurrency.network.derivationPath,
isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
)
.conflate()
.distinctUntilChanged()
.firstOrNull()
reduxStateHolder.dispatchWithMain(
action = TradeCryptoAction.SendToken(
userWallet = userWallet,
tokenCurrency = tokenCurrency,
tokenFiatRate = tokenFiatRate,
coinFiatRate = maybeCoinStatus?.fold(
ifLeft = { null },
ifRight = { it.value.fiatRate },
),
feeCurrencyStatus = feeCurrencyStatus,
transactionInfo = transactionInfo,
),
)
}
appRouter.push(route)
}
override fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason) {
@ -808,7 +764,7 @@ internal class TokenDetailsViewModel @Inject constructor(
}
override fun onExpressTransactionClick(txId: String) {
val expressTxState = internalUiState.value.expressTxs.first { it.info.txId == txId }
val expressTxState = internalUiState.value.expressTxsToDisplay.first { it.info.txId == txId }
internalUiState.value = expressStatusFactory.getStateWithExpressStatusBottomSheet(expressTxState)
}
@ -873,6 +829,12 @@ internal class TokenDetailsViewModel @Inject constructor(
}
override fun onRetryIncompleteTransactionClick() {
analyticsEventsHandler.send(
TokenScreenAnalyticsEvent.RevealTryAgain(
tokenSymbol = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
),
)
viewModelScope.launch {
retryIncompleteTransactionUseCase(
userWalletId = userWalletId,
@ -908,6 +870,12 @@ internal class TokenDetailsViewModel @Inject constructor(
}
override fun onDismissIncompleteTransactionClick() {
analyticsEventsHandler.send(
TokenScreenAnalyticsEvent.RevealCancel(
tokenSymbol = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
),
)
viewModelScope.launch {
internalUiState.value = stateFactory.getStateWithDismissIncompleteTransactionConfirmDialog()
}

View file

@ -52,19 +52,19 @@ dependencies {
implementation(projects.core.utils)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.common.routing)
implementation(projects.core.deepLinks)
implementation(projects.core.deepLinks.global)
implementation(projects.core.decompose)
implementation(projects.core.datasource)
implementation(projects.core.res)
implementation(projects.libs.crypto)
implementation(projects.libs.blockchainSdk)
/** Domain modules */
implementation(projects.domain.card)
implementation(projects.domain.demo)
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.models)
implementation(projects.domain.settings)
implementation(projects.domain.tokens)
@ -83,6 +83,7 @@ dependencies {
implementation(projects.domain.markets.models)
implementation(projects.domain.feedback)
implementation(projects.domain.onramp.models)
implementation(projects.domain.onramp)
//TODO: Create api/impl modules for onboarding [REDACTED_JIRA]
implementation(projects.features.onboarding)
@ -101,6 +102,7 @@ dependencies {
/** Common modules */
implementation(projects.common.ui)
implementation(projects.common.routing)
/** Test libraries */
implementation(deps.test.junit)

Some files were not shown because too many files have changed in this diff Show more