Updated on 2026-08-14
This commit is contained in:
commit
975163984e
118 changed files with 1722 additions and 727 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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?) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -208,6 +208,7 @@ internal class MainViewModel @Inject constructor(
|
|||
|
||||
override fun onDismissBottomSheet() {
|
||||
listenToFlipsUseCase.changeUpdateEnabled(true)
|
||||
router.pop()
|
||||
stateHolder.updateWithoutModalNotification()
|
||||
stateHolder.updateWithHiddenBalancesToast(true)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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?,
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
) {
|
||||
|
|
@ -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(
|
||||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "clore-ai",
|
||||
"name": "NEXA/test",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
{
|
||||
"name": "ONRAMP_ENABLED",
|
||||
"version": "5.19.0"
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "MAIN_ACTION_BUTTONS_ENABLED",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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") }
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 provider’s %1$s and %2$s</string>
|
||||
<string name="onramp_max_amount_restriction">The purchase amount should be no more than %s</string>
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -23,7 +23,7 @@ sealed interface OnrampProviderWithQuote {
|
|||
|
||||
data class Error(
|
||||
override val provider: OnrampProvider,
|
||||
val quoteError: OnrampQuote.Error,
|
||||
val quoteError: OnrampQuote.AmountError,
|
||||
) : Unavailable
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ interface OnrampRepository {
|
|||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
quote: OnrampProviderWithQuote.Data,
|
||||
isDarkTheme: Boolean,
|
||||
): OnrampTransaction
|
||||
|
||||
// cache
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -57,4 +57,6 @@ sealed class CryptoCurrencyWarning {
|
|||
val amount: BigDecimal,
|
||||
val maxAmount: BigDecimal?,
|
||||
) : CryptoCurrencyWarning()
|
||||
|
||||
data object TokensInBetaWarning : CryptoCurrencyWarning()
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ android {
|
|||
dependencies {
|
||||
/* Project - API */
|
||||
api(projects.features.markets.api)
|
||||
api(projects.features.onramp.api)
|
||||
implementation(projects.core.navigation)
|
||||
|
||||
/* Domain */
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable
|
|||
internal sealed class ConfirmResidencyBottomSheetConfig {
|
||||
|
||||
@Serializable
|
||||
data object SelectCountry : ConfirmResidencyBottomSheetConfig()
|
||||
data class SelectCountry(val onDismiss: () -> Unit) : ConfirmResidencyBottomSheetConfig()
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor(
|
|||
country = config.country,
|
||||
onDismiss = {
|
||||
model.bottomSheetNavigation.dismiss()
|
||||
model.handleOnrampAvailable(it.defaultCurrency)
|
||||
model.handleOnrampAvailable()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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> =
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ internal sealed interface StakingBlockUM {
|
|||
val iconState: IconState,
|
||||
val titleText: TextReference,
|
||||
val subtitleText: TextReference,
|
||||
val isEnabled: Boolean,
|
||||
val onStakeClicked: () -> Unit,
|
||||
) : StakingBlockUM
|
||||
}
|
||||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ internal class TokenDetailsNotificationConverter(
|
|||
},
|
||||
)
|
||||
is CryptoCurrencyWarning.MigrationMaticToPol -> MigrationMaticToPol
|
||||
is CryptoCurrencyWarning.TokensInBetaWarning -> TokensInBeta
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue