Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-19 21:29:23 +03:00
commit 7dbefe9430
54 changed files with 1046 additions and 475 deletions

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
package com.tangem.common.ui.expressStatus
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@ -8,16 +8,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
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.res.TangemTheme
import com.tangem.features.tokendetails.impl.R
@Suppress("LongParameterList")
@Composable
internal fun ExpressEstimate(
fun ExpressEstimate(
timestamp: TextReference,
fromTokenIconState: CurrencyIconState,
toTokenIconState: CurrencyIconState,

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp
package com.tangem.common.ui.expressStatus
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@ -8,8 +8,7 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
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.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressEstimate
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressProvider
@Composable
internal fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) {
fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) {
Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) {
SpacerH10()
Text(

View file

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

View file

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

View file

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

View file

@ -582,6 +582,8 @@
<string name="onramp_avaiable_with_payment_methods">Disponible avec %s</string>
<string name="onramp_country_search">Recherche par pays</string>
<string name="onramp_country_unavailable">Indisponible</string>
<string name="onramp_currency_other">Autres devises</string>
<string name="onramp_currency_popular">Devises Fiats Populaires</string>
<string name="onramp_currency_search">Recherche par devise</string>
<string name="onramp_legal">En utilisant la fonctionnalité onramp, vous acceptez %1$s et %2$s du fournisseur</string>
<string name="onramp_max_amount_restriction">Le montant de l\'achat ne doit pas dépasser %s</string>

View file

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

View file

@ -597,6 +597,8 @@
<string name="onramp_choose_provider_title_hint">Tangem предоставляет доступ к покупке через сторонних провайдеров в соответствии с их правилами</string>
<string name="onramp_country_search">Поиск по стране</string>
<string name="onramp_country_unavailable">Недоступно</string>
<string name="onramp_currency_other">Другие валюты</string>
<string name="onramp_currency_popular">Популярные фиаты</string>
<string name="onramp_currency_search">Поиск по валюте</string>
<string name="onramp_legal">Пользуясь сервисом покупки, вы соглашаетесь с %1$s и %2$s</string>
<string name="onramp_max_amount_restriction">Сумма покупки не может быть больше, чем %s</string>

View file

@ -599,6 +599,8 @@
<string name="onramp_choose_provider_title_hint">Tangem забезпечує доступ к покупці через сторонніх провайдерів згідно з їхніми умовами</string>
<string name="onramp_country_search">Пошук за країною</string>
<string name="onramp_country_unavailable">Недоступно</string>
<string name="onramp_currency_other">Інші валюти</string>
<string name="onramp_currency_popular">Популярні фіати</string>
<string name="onramp_currency_search">Пошук по валюті</string>
<string name="onramp_legal">Використовуючи сервіс покупки, ви погоджуєтесь з %1$s та %2$s</string>
<string name="onramp_max_amount_restriction">Сума покупки не може бути більше ніж %s</string>

View file

@ -583,6 +583,8 @@
<string name="onramp_choose_provider_title_hint">Buying crypto in Tangem is powered by third-party providers on their terms.</string>
<string name="onramp_country_search">Search by country</string>
<string name="onramp_country_unavailable">Unavailable</string>
<string name="onramp_currency_other">Other currencies</string>
<string name="onramp_currency_popular">Popular Fiats</string>
<string name="onramp_currency_search">Search by currency</string>
<string name="onramp_legal">By using onramp functionality, you agree with providers %1$s and %2$s</string>
<string name="onramp_max_amount_restriction">The purchase amount should be no more than %s</string>

View file

@ -249,6 +249,7 @@ internal class DefaultOnrampRepository(
maxFromAmount = convertToAmount(response.maxFromAmount, cryptoCurrency),
paymentMethod = paymentMethod,
provider = provider,
countryCode = response.countryCode,
)
},
onError = { error ->
@ -257,6 +258,7 @@ internal class DefaultOnrampRepository(
paymentMethod = paymentMethod,
provider = provider,
fromOnrampAmount = fromOnrampAmount,
countryCode = country.code,
)
},
)
@ -454,6 +456,7 @@ 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) {
@ -462,6 +465,7 @@ internal class DefaultOnrampRepository(
provider = provider,
fromAmount = fromOnrampAmount,
error = onrampError,
countryCode = countryCode,
)
} else {
Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error")
@ -470,6 +474,7 @@ internal class DefaultOnrampRepository(
provider = provider,
fromAmount = fromOnrampAmount,
error = onrampError,
countryCode = countryCode,
)
}
} else {

View file

@ -7,11 +7,13 @@ 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,
override val fromAmount: OnrampAmount,
override val countryCode: String,
val toAmount: OnrampAmount,
val minFromAmount: OnrampAmount,
val maxFromAmount: OnrampAmount,
@ -21,6 +23,7 @@ sealed class OnrampQuote {
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
override val fromAmount: OnrampAmount,
override val countryCode: String,
val error: OnrampError.AmountError,
) : OnrampQuote()
@ -28,6 +31,7 @@ sealed class OnrampQuote {
override val paymentMethod: OnrampPaymentMethod,
override val provider: OnrampProvider,
override val fromAmount: OnrampAmount,
override val countryCode: String,
val error: OnrampError,
) : OnrampQuote()
}

View file

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

View file

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

View file

@ -52,8 +52,10 @@ internal class ConfirmResidencyModel @Inject constructor(
ConfirmResidencyUM.ActionButtonConfig(
onClick = {
analyticsEventHandler.send(OnrampAnalyticsEvent.OnResidenceConfirm(country.name))
modelScope.launch { saveDefaultCountryUseCase.invoke(country) }
params.onDismiss()
modelScope.launch {
saveDefaultCountryUseCase.invoke(country)
params.onDismiss()
}
},
text = resourceReference(R.string.common_confirm),
)

View file

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

View file

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

View file

@ -14,15 +14,18 @@ import com.tangem.domain.onramp.model.OnrampProviderWithQuote
import com.tangem.domain.onramp.model.OnrampQuote
import com.tangem.domain.onramp.model.error.OnrampError
import com.tangem.domain.tokens.model.AmountType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.main.entity.*
import com.tangem.features.onramp.providers.entity.SelectProviderResult
import com.tangem.utils.Provider
import com.tangem.utils.extensions.isSingleItem
internal class OnrampAmountStateFactory(
private val currentStateProvider: Provider<OnrampMainComponentUM>,
private val analyticsEventHandler: AnalyticsEventHandler,
private val onrampIntents: OnrampIntents,
private val cryptoCurrency: CryptoCurrency,
) {
private val onrampAmountFieldChangeConverter = OnrampAmountFieldChangeConverter(
@ -62,11 +65,12 @@ internal class OnrampAmountStateFactory(
return currentState.copy(
amountBlockState = amountState.copy(secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading),
providerBlockState = OnrampProviderBlockUM.Loading,
buyButtonConfig = currentState.buyButtonConfig.copy(enabled = false),
)
}
fun getAmountSecondaryUpdatedState(quote: OnrampQuote, isBestRate: Boolean): OnrampMainComponentUM {
fun getAmountSecondaryUpdatedState(quote: OnrampQuote): OnrampMainComponentUM {
val currentState = currentStateProvider()
if (currentState !is OnrampMainComponentUM.Content) return currentState
@ -75,9 +79,9 @@ internal class OnrampAmountStateFactory(
return currentState.copy(
amountBlockState = amountState.copy(
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,

View file

@ -25,10 +25,7 @@ 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
@ -38,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,
@ -75,6 +70,7 @@ 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(
@ -87,7 +83,7 @@ internal class OnrampMainComponentModel @Inject constructor(
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()
@ -161,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() } },
@ -200,7 +200,6 @@ internal class OnrampMainComponentModel @Inject constructor(
private fun subscribeToQuotesUpdate() {
getOnrampQuotesUseCase.invoke()
.distinctUntilChanged()
.conflate()
.onEach { maybeQuotes ->
maybeQuotes.fold(
@ -282,42 +281,67 @@ internal class OnrampMainComponentModel @Inject constructor(
private fun handleQuoteResult(quotes: List<OnrampQuote>) {
sendOnrampQuotesErrorAnalytic(quotes)
val quote = quotes.firstOrNull { it !is OnrampQuote.Error }
val quote = selectOrUpdateQuote(quotes)
if (quote == null) {
_state.update { stateFactory.getErrorState() }
lastAmount.value = BigDecimal.ZERO
_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
if (checkLastInputState(quote)) {
lastUpdateState.value = OnrampLastUpdate(
quote.fromAmount,
quote.countryCode,
)
val isBestProvider = quote == bestProvider && hasBestProvider
if (lastAmount.value != quote.fromAmount.value) {
lastAmount.value = quote.fromAmount.value
if (quote is OnrampQuote.Data) {
analyticsEventHandler.send(
OnrampAnalyticsEvent.ProviderCalculated(
providerName = quote.provider.info.name,
tokenSymbol = params.cryptoCurrency.symbol,
paymentMethod = quote.paymentMethod.name,
),
)
}
_state.update {
amountStateFactory.getAmountSecondaryUpdatedState(
quote = quote,
isBestRate = isBestProvider,
)
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 {
(it as? OnrampMainComponentUM.Content)?.copy(
errorNotification = null,
providerBlockState = OnrampProviderBlockUM.Loading,
) ?: it
}
startLoadingQuotes()
}
private fun showDemoWarning() {
@ -380,6 +404,11 @@ internal class OnrampMainComponentModel @Inject constructor(
}
}
private fun checkLastInputState(quote: OnrampQuote?): Boolean {
return lastUpdateState.value?.lastAmount != quote?.fromAmount ||
lastUpdateState.value?.lastCountryString != quote?.countryCode
}
private companion object {
const val UPDATE_DELAY = 10_000L
}

View file

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

View file

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

View file

@ -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,7 +86,7 @@ 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(
@ -84,6 +96,7 @@ internal class OnrampSuccessComponentModel @Inject constructor(
paymentMethod = transaction.paymentMethod,
)
Timber.e(error.toString())
showErrorAlert(error)
},
ifRight = { status ->
analyticsEventHandler.send(
@ -112,6 +125,32 @@ internal class OnrampSuccessComponentModel @Inject constructor(
)
}
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,

View file

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

View file

@ -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

View file

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

View file

@ -23,9 +23,9 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateIconUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateInfoUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider

View file

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

View file

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

View file

@ -1,5 +1,7 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.appcurrency.model.AppCurrency
@ -10,8 +12,7 @@ import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -79,11 +80,13 @@ internal class ExpressStatusFactory @AssistedInject constructor(
expressTxs.map { tx ->
async {
when (tx) {
is ExpressTransactionStateUM.ExchangeUM -> exchangeStatusFactory.updateSwapTxStatus(tx)
is ExchangeUM -> exchangeStatusFactory.updateSwapTxStatus(tx)
is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.updateOnrmapTxStatus(tx)
else -> null
}
}
}.awaitAll()
.filterNotNull()
.toPersistentList()
}
@ -95,13 +98,13 @@ 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.ExchangeUM -> false
is ExpressTransactionStateUM.OnrampUM -> it.activeStatus.isHidden
else -> false
}
}.toPersistentList()
return state.copy(
@ -115,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(
@ -123,6 +126,7 @@ internal class ExpressStatusFactory @AssistedInject constructor(
provider = expressState.providerName,
fiatCurrency = expressState.fromCurrencyCode,
)
else -> return currentStateProvider()
}
analyticsEventsHandler.send(analyticEvent)
@ -156,9 +160,7 @@ internal class ExpressStatusFactory @AssistedInject constructor(
isForceTerminal: Boolean = false,
) {
when (expressState) {
is ExpressTransactionStateUM.ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed(
isForceTerminal,
)
is ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed(isForceTerminal)
is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.removeTransactionOnBottomSheetClosed()
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus
import com.tangem.domain.appcurrency.model.AppCurrency
@ -14,9 +16,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsOnrampTransactionStateConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.Provider
import dagger.assisted.Assisted

View file

@ -22,6 +22,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.paging.compose.collectAsLazyPagingItems
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.expressTransactionsItems
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet
import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet
@ -49,8 +51,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.expressTransactionsItems
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock
import com.tangem.features.markets.token.block.TokenMarketBlockComponent

View file

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

View file

@ -9,6 +9,8 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
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.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressEstimate
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressProvider
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
@Composable
internal fun ExchangeStatusBottomSheetContent(state: ExpressTransactionStateUM.ExchangeUM) {
internal fun ExchangeStatusBottomSheetContent(state: ExchangeUM) {
Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) {
SpacerH10()
Text(

View file

@ -5,7 +5,6 @@ import androidx.lifecycle.*
import androidx.paging.cachedIn
import arrow.core.getOrElse
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.core.TangemSdkError
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.bundle.unbundle
@ -41,7 +40,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 +66,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 +82,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 +96,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,
@ -518,68 +513,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) {

View file

@ -52,18 +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.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)
@ -82,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)
@ -100,6 +102,7 @@ dependencies {
/** Common modules */
implementation(projects.common.ui)
implementation(projects.common.routing)
/** Test libraries */
implementation(deps.test.junit)

View file

@ -2,25 +2,22 @@ package com.tangem.feature.wallet.presentation.deeplink
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.deeplink.DeepLink
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
import com.tangem.core.deeplink.global.SellCurrencyDeepLink
import com.tangem.domain.redux.ReduxStateHolder
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.GetCryptoCurrencyUseCase
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import com.tangem.features.onramp.OnrampFeatureToggles
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ -28,13 +25,9 @@ internal class WalletDeepLinksHandler @Inject constructor(
private val deepLinksRegistry: DeepLinksRegistry,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase,
private val getCryptoCurrencyStatusesSyncUseCase: GetCryptoCurrencyStatusesSyncUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
private val reduxStateHolder: ReduxStateHolder,
private val clickIntents: WalletClickIntents,
private val onrampFeatureToggles: OnrampFeatureToggles,
private val router: AppRouter,
) {
private var deepLinksMap = mutableMapOf<UserWalletId, List<DeepLink>>()
@ -75,35 +68,23 @@ internal class WalletDeepLinksHandler @Inject constructor(
}
private suspend fun onSellCurrencyDeepLink(userWallet: UserWallet, data: SellCurrencyDeepLink.Data) {
val cryptoCurrencyStatus = findCryptoCurrencyStatus(userWallet, data.currencyId) ?: return
val feeCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(
userWallet.walletId,
cryptoCurrencyStatus,
).getOrNull()
val cryptoCurrency = getCryptoCurrencyUseCase(userWallet.walletId, data.currencyId).getOrNull()
val transactionInfo = data.let {
TransactionInfo(
amount = it.baseCurrencyAmount,
destinationAddress = it.depositWalletAddress,
transactionId = it.transactionId,
tag = it.depositWalletAddressTag,
)
if (cryptoCurrency == null) {
Timber.e("onSellCurrencyDeepLink cryptoCurrency is null")
return
}
when (cryptoCurrencyStatus.currency) {
is CryptoCurrency.Coin -> sendCoin(
userWallet = userWallet,
cryptoCurrencyStatus = cryptoCurrencyStatus,
feeCurrencyStatus = feeCurrencyStatus,
transactionInfo = transactionInfo,
)
is CryptoCurrency.Token -> sendToken(
userWallet = userWallet,
cryptoCurrencyStatus = cryptoCurrencyStatus,
feeCurrencyStatus = feeCurrencyStatus,
transactionInfo = transactionInfo,
)
}
val route = AppRoute.Send(
currency = cryptoCurrency,
userWalletId = userWallet.walletId,
transactionId = data.transactionId,
destinationAddress = data.depositWalletAddress,
amount = data.baseCurrencyAmount,
tag = data.depositWalletAddressTag,
)
router.push(route)
}
private suspend fun onBuyCurrencyDeepLink(externalTxId: String, userWallet: UserWallet) {
@ -114,62 +95,4 @@ internal class WalletDeepLinksHandler @Inject constructor(
analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol))
}
}
private suspend fun findCryptoCurrencyStatus(
userWallet: UserWallet,
currencyIdValue: String,
): CryptoCurrencyStatus? {
return if (userWallet.isMultiCurrency) {
getCryptoCurrencyStatusesSyncUseCase(userWallet.walletId).getOrNull()?.let { currencies ->
currencies.find { currencyIdValue == it.currency.id.value }
}
} else {
getCryptoCurrencyStatusSyncUseCase(userWallet.walletId).getOrNull()
}
}
private fun sendCoin(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
feeCurrencyStatus: CryptoCurrencyStatus?,
transactionInfo: TransactionInfo,
) {
reduxStateHolder.dispatch(
action = TradeCryptoAction.SendCoin(
userWallet = userWallet,
coinStatus = cryptoCurrencyStatus,
feeCurrencyStatus = feeCurrencyStatus,
transactionInfo = transactionInfo,
),
)
}
private suspend fun sendToken(
userWallet: UserWallet,
cryptoCurrencyStatus: CryptoCurrencyStatus,
feeCurrencyStatus: CryptoCurrencyStatus?,
transactionInfo: TransactionInfo,
) {
val cryptoCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return
val coinStatus = getNetworkCoinStatusUseCase(
userWalletId = userWallet.walletId,
networkId = cryptoCurrency.network.id,
derivationPath = cryptoCurrency.network.derivationPath,
isSingleWalletWithTokens = false,
)
.firstOrNull()
?.getOrNull()
?: return
reduxStateHolder.dispatch(
action = TradeCryptoAction.SendToken(
userWallet = userWallet,
tokenCurrency = cryptoCurrency,
tokenFiatRate = cryptoCurrencyStatus.value.fiatRate,
coinFiatRate = coinStatus.value.fiatRate,
feeCurrencyStatus = feeCurrencyStatus,
transactionInfo = transactionInfo,
),
)
}
}

View file

@ -0,0 +1,99 @@
package com.tangem.feature.wallet.presentation.wallet.domain
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.onramp.GetOnrampStatusUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.onramp.OnrampUpdateTransactionStatusUseCase
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.OnrampStatus.Status.*
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@ViewModelScoped
internal class OnrampStatusFactory @Inject constructor(
private val stateHolder: WalletStateController,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
private val getOnrampStatusUseCase: GetOnrampStatusUseCase,
private val onrampUpdateTransactionStatusUseCase: OnrampUpdateTransactionStatusUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val dispatchers: CoroutineDispatcherProvider,
) {
suspend fun removeTransactionOnBottomSheetClosed() {
val state = stateHolder.getSelectedWallet()
val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return
val selectedTx = bottomSheetConfig.value as? ExpressTransactionStateUM.OnrampUM ?: return
if (selectedTx.activeStatus.isTerminal) {
onrampRemoveTransactionUseCase(externalTxId = selectedTx.info.txExternalId)
}
}
suspend fun updateOnrmapTransactionStatuses() = withContext(dispatchers.io) {
val singleWalletState = stateHolder.getSelectedWallet() as? WalletState.SingleCurrency.Content
?: return@withContext
singleWalletState.expressTxs.map { tx ->
async {
if (tx is ExpressTransactionStateUM.OnrampUM) {
updateOnrampTxStatus(tx)
}
}
}.awaitAll()
}
private suspend fun updateOnrampTxStatus(onrampTx: ExpressTransactionStateUM.OnrampUM) {
if (!onrampTx.activeStatus.isTerminal) {
getOnrampStatusUseCase(onrampTx.info.txId).fold(
ifLeft = {
Timber.e("Couldn't update onramp status. $it")
},
ifRight = { statusModel ->
val externalTxId = statusModel.externalTxId
val status = toAnalyticStatus(statusModel.status) ?: return
if (statusModel.status != onrampTx.activeStatus) {
analyticsEventHandler.send(
TokenOnrampAnalyticsEvent.OnrampStatusChanged(
tokenSymbol = onrampTx.info.toAmountSymbol,
status = status.name,
provider = onrampTx.providerName,
fiatCurrency = onrampTx.fromCurrencyCode,
),
)
onrampUpdateTransactionStatusUseCase(externalTxId = externalTxId, statusModel.status)
}
},
)
}
}
private fun toAnalyticStatus(status: OnrampStatus.Status?): ExpressAnalyticsStatus? {
return when (status) {
Expired,
Paused,
-> ExpressAnalyticsStatus.Cancelled
Created,
WaitingForPayment,
PaymentProcessing,
Paid,
Sending,
-> ExpressAnalyticsStatus.InProgress
Verifying -> ExpressAnalyticsStatus.KYC
Failed -> ExpressAnalyticsStatus.Fail
Finished -> ExpressAnalyticsStatus.Done
null -> null
}
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.onramp.GetOnrampTransactionsUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
@ -27,6 +29,8 @@ internal class SingleWalletContentLoader(
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
) : WalletContentLoader(id = userWallet.walletId) {
@ -55,6 +59,16 @@ internal class SingleWalletContentLoader(
getSingleWalletWarningsFactory = getSingleWalletWarningsFactory,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
),
SingleWalletExpressStatusesSubscriber(
userWallet = userWallet,
stateHolder = stateHolder,
clickIntents = clickIntents,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
analyticsEventHandler = analyticsEventHandler,
getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase,
getOnrampTransactionsUseCase = getOnrampTransactionsUseCase,
onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase,
),
TxHistorySubscriber(
userWallet = userWallet,
isRefresh = isRefresh,

View file

@ -2,6 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.onramp.GetOnrampTransactionsUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
@ -26,6 +28,8 @@ internal class SingleWalletContentLoaderFactory @Inject constructor(
private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase,
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
) {
@ -45,6 +49,8 @@ internal class SingleWalletContentLoaderFactory @Inject constructor(
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
analyticsEventHandler = analyticsEventHandler,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
getOnrampTransactionsUseCase = getOnrampTransactionsUseCase,
onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase,
)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.state.model
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
@ -59,6 +60,8 @@ internal sealed interface WalletState : WalletStateHolder {
override val buttons: PersistentList<WalletManageButton>,
override val marketPriceBlockState: MarketPriceBlockState,
override val txHistoryState: TxHistoryState,
val expressTxsToDisplay: PersistentList<ExpressTransactionStateUM>,
val expressTxs: PersistentList<ExpressTransactionStateUM>,
) : SingleCurrency()
data class Locked(

View file

@ -0,0 +1,76 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletOnrampTransactionConverter
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import kotlinx.collections.immutable.toPersistentList
import timber.log.Timber
internal class SetExpressStatusesTransformer(
userWalletId: UserWalletId,
private val onrampTxs: List<OnrampTransaction>,
private val clickIntents: WalletClickIntents,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency,
private val analyticsEventHandler: AnalyticsEventHandler,
) : WalletStateTransformer(userWalletId) {
override fun transform(prevState: WalletState): WalletState {
return when (prevState) {
is WalletState.SingleCurrency.Content -> {
val expressTxs = SingleWalletOnrampTransactionConverter(
clickIntents = clickIntents,
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrency = appCurrency,
analyticsEventHandler = analyticsEventHandler,
).convertList(onrampTxs).toPersistentList()
val expressTxsToDisplay = expressTxs.filterNot {
it.activeStatus.isHidden
}.toPersistentList()
val expressBottomSheet = prevState.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig
val currentTx = expressTxs.firstOrNull { it.info.txId == expressBottomSheet?.value?.info?.txId }
prevState.copy(
expressTxs = expressTxs,
expressTxsToDisplay = expressTxsToDisplay,
bottomSheetConfig = prevState.bottomSheetConfig?.updateStateWithExpressStatusBottomSheet(currentTx),
)
}
is WalletState.SingleCurrency.Locked -> {
Timber.w("Impossible to load express statuses for locked wallet")
prevState
}
is WalletState.Visa -> {
Timber.w("Impossible to load express statuses for visa wallet")
prevState
}
is WalletState.MultiCurrency -> {
Timber.w("Impossible to load express statuses for multi-currency wallet")
prevState
}
}
}
private fun TangemBottomSheetConfig.updateStateWithExpressStatusBottomSheet(
expressState: ExpressTransactionStateUM?,
): TangemBottomSheetConfig {
val currentConfig = this.content as? ExpressStatusBottomSheetConfig ?: return this
if (expressState == null) return this
return copy(
content = if (currentConfig.value != expressState) {
ExpressStatusBottomSheetConfig(expressState)
} else {
currentConfig
},
)
}
}

View file

@ -0,0 +1,279 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.common.ui.notifications.ExpressNotificationsUM
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent
import com.tangem.feature.wallet.impl.R
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.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
internal class SingleWalletOnrampTransactionConverter(
private val clickIntents: WalletClickIntents,
cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Converter<OnrampTransaction, ExpressTransactionStateUM.OnrampUM> {
private val iconStateConverter = CryptoCurrencyToIconStateConverter()
private val currency = cryptoCurrencyStatus.currency
private val status = cryptoCurrencyStatus.value
override fun convert(value: OnrampTransaction): ExpressTransactionStateUM.OnrampUM {
return ExpressTransactionStateUM.OnrampUM(
info = ExpressTransactionStateInfoUM(
title = resourceReference(
id = R.string.express_status_buying,
wrappedList(currency.name),
),
status = convertStatuses(value.status, value.externalTxUrl),
notification = getNotification(value.status, value.externalTxUrl, value.providerName),
txId = value.txId,
txExternalId = value.externalTxId,
txExternalUrl = value.externalTxUrl,
timestamp = value.timestamp,
timestampFormatted = resourceReference(
R.string.send_date_format,
wrappedList(
value.timestamp.toDateFormatWithTodayYesterday(),
value.timestamp.toTimeFormat(),
),
),
toAmount = stringReference(
value.toAmount.format { crypto(currency) },
),
toFiatAmount = stringReference(
status.fiatRate?.multiply(value.toAmount).format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
),
toAmountSymbol = currency.symbol,
toCurrencyIcon = iconStateConverter.convert(currency),
fromAmount = stringReference(
value.fromAmount.format {
fiat(
fiatCurrencyCode = value.fromCurrency.name,
fiatCurrencySymbol = value.fromCurrency.code,
)
},
),
fromFiatAmount = null,
fromAmountSymbol = value.fromCurrency.code,
fromCurrencyIcon = CurrencyIconState.FiatIcon(
url = value.fromCurrency.image,
fallbackResId = R.drawable.ic_currency_24,
),
iconState = getIconState(value.status),
onGoToProviderClick = {
analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider)
clickIntents.onGoToProviderClick(it)
},
onClick = {
val analyticEvent = TokenOnrampAnalyticsEvent.OnrampStatusOpened(
tokenSymbol = currency.symbol,
provider = value.providerName,
fiatCurrency = value.fromCurrency.code,
)
analyticsEventHandler.send(analyticEvent)
clickIntents.onExpressTransactionClick(value.txId)
},
),
providerName = value.providerName,
providerImageUrl = value.providerImageUrl,
providerType = value.providerType,
activeStatus = value.status,
fromCurrencyCode = value.fromCurrency.code,
)
}
private fun getNotification(
status: OnrampStatus.Status,
externalTxUrl: String?,
providerName: String,
): NotificationUM? {
if (externalTxUrl == null) return null
return when (status) {
OnrampStatus.Status.Verifying -> {
analyticsEventHandler.send(
TokenOnrampAnalyticsEvent.NoticeKYC(currency.symbol, providerName),
)
ExpressNotificationsUM.NeedVerification {
clickIntents.onGoToProviderClick(externalTxUrl)
}
}
OnrampStatus.Status.Failed -> {
ExpressNotificationsUM.FailedByProvider {
clickIntents.onGoToProviderClick(externalTxUrl)
}
}
else -> null
}
}
private fun getIconState(status: OnrampStatus.Status): ExpressTransactionStateIconUM {
return when (status) {
OnrampStatus.Status.Verifying -> ExpressTransactionStateIconUM.Warning
OnrampStatus.Status.Failed -> ExpressTransactionStateIconUM.Error
else -> ExpressTransactionStateIconUM.None
}
}
private fun convertStatuses(status: OnrampStatus.Status, externalTxUrl: String?): ExpressStatusUM {
val statuses = with(status) {
persistentListOf(
getAwaitingDepositItem(),
getPaymentProcessingItem(),
getBuyingItem(),
getSendingItem(),
)
}
return ExpressStatusUM(
title = resourceReference(R.string.common_transaction_status),
link = getStatusLink(status, externalTxUrl),
statuses = statuses,
)
}
private fun OnrampStatus.Status.getAwaitingDepositItem() = ExpressStatusItemUM(
text = when {
order < OnrampStatus.Status.WaitingForPayment.order -> {
resourceReference(R.string.express_exchange_status_receiving)
}
this == OnrampStatus.Status.WaitingForPayment -> {
resourceReference(R.string.express_exchange_status_receiving_active)
}
else -> {
resourceReference(R.string.express_exchange_status_received)
}
},
state = getStatusState(OnrampStatus.Status.WaitingForPayment),
)
private fun OnrampStatus.Status.getPaymentProcessingItem() = ExpressStatusItemUM(
text = when {
order < OnrampStatus.Status.PaymentProcessing.order -> {
resourceReference(R.string.express_exchange_status_confirming)
}
this == OnrampStatus.Status.PaymentProcessing -> {
resourceReference(R.string.express_exchange_status_confirming_active)
}
this == OnrampStatus.Status.Verifying -> {
resourceReference(R.string.express_exchange_status_verifying)
}
this == OnrampStatus.Status.Failed -> {
resourceReference(R.string.express_exchange_status_failed)
}
else -> resourceReference(R.string.express_exchange_status_confirmed)
},
state = when {
order < OnrampStatus.Status.PaymentProcessing.order -> {
ExpressStatusItemState.Default
}
this == OnrampStatus.Status.PaymentProcessing -> {
ExpressStatusItemState.Active
}
this == OnrampStatus.Status.Verifying -> {
ExpressStatusItemState.Warning
}
this == OnrampStatus.Status.Failed -> {
ExpressStatusItemState.Error
}
else -> {
ExpressStatusItemState.Done
}
},
)
private fun OnrampStatus.Status.getBuyingItem() = ExpressStatusItemUM(
text = when {
order < OnrampStatus.Status.Paid.order -> {
resourceReference(R.string.express_status_buying, wrappedList(currency.name))
}
this == OnrampStatus.Status.Paid -> {
resourceReference(
R.string.express_status_buying_active,
wrappedList(currency.name),
)
}
else -> {
resourceReference(R.string.express_status_bought, wrappedList(currency.name))
}
},
state = getStatusState(OnrampStatus.Status.Paid),
)
private fun OnrampStatus.Status.getSendingItem() = ExpressStatusItemUM(
text = when {
order < OnrampStatus.Status.Sending.order -> {
resourceReference(
R.string.express_exchange_status_sending,
wrappedList(currency.name),
)
}
this == OnrampStatus.Status.Sending -> {
resourceReference(
R.string.express_exchange_status_sending_active,
wrappedList(currency.name),
)
}
else -> {
resourceReference(
R.string.express_exchange_status_sent,
wrappedList(currency.name),
)
}
},
state = getStatusState(OnrampStatus.Status.Sending),
)
private fun getStatusLink(status: OnrampStatus.Status, externalTxUrl: String?): ExpressLinkUM {
if (externalTxUrl == null) return ExpressLinkUM.Empty
return when (status) {
OnrampStatus.Status.Verifying,
OnrampStatus.Status.Failed,
-> {
ExpressLinkUM.Content(
icon = R.drawable.ic_arrow_top_right_24,
text = resourceReference(R.string.common_go_to_provider),
onClick = {
clickIntents.onGoToProviderClick(externalTxUrl)
},
)
}
else -> ExpressLinkUM.Empty
}
}
private fun OnrampStatus.Status.getStatusState(targetState: OnrampStatus.Status) = when {
order < targetState.order -> ExpressStatusItemState.Default
this == targetState -> ExpressStatusItemState.Active
else -> ExpressStatusItemState.Done
}
}

View file

@ -58,6 +58,8 @@ internal class WalletLoadingStateFactory(
value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick),
),
),
expressTxsToDisplay = persistentListOf(),
expressTxs = persistentListOf(),
)
}

View file

@ -0,0 +1,92 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.onramp.GetOnrampTransactionsUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetExpressStatusesTransformer
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import timber.log.Timber
@Suppress("LongParameterList")
internal class SingleWalletExpressStatusesSubscriber(
private val userWallet: UserWallet,
private val stateHolder: WalletStateController,
private val clickIntents: WalletClickIntents,
private val analyticsEventHandler: AnalyticsEventHandler,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
) : WalletSubscriber() {
override fun create(
coroutineScope: CoroutineScope,
): Flow<Pair<Either<CurrencyStatusError, CryptoCurrencyStatus>, AppCurrency>> {
return combine(
flow = getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWallet.walletId)
.conflate()
.distinctUntilChanged(),
flow2 = getSelectedAppCurrencyUseCase()
.conflate()
.distinctUntilChanged()
.map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } },
transform = { maybeCurrencyStatus, appCurrency -> maybeCurrencyStatus to appCurrency },
).onEach { maybeCurrencyStatusAndAppCurrency ->
val status = maybeCurrencyStatusAndAppCurrency.first.getOrElse {
Timber.e("Unable to get primary currency status: $it")
return@onEach
}
getOnrampTransactionsUseCase(
userWalletId = userWallet.walletId,
cryptoCurrencyId = status.currency.id,
).onEach { maybeTransaction ->
maybeTransaction.fold(
ifRight = { onrampTxs ->
onrampTxs.clearHiddenTerminal()
stateHolder.update(
SetExpressStatusesTransformer(
userWalletId = userWallet.walletId,
onrampTxs = onrampTxs,
clickIntents = clickIntents,
cryptoCurrencyStatus = status,
appCurrency = maybeCurrencyStatusAndAppCurrency.second,
analyticsEventHandler = analyticsEventHandler,
),
)
},
ifLeft = {
stateHolder.update(
SetExpressStatusesTransformer(
userWalletId = userWallet.walletId,
onrampTxs = listOf(),
clickIntents = clickIntents,
cryptoCurrencyStatus = status,
appCurrency = maybeCurrencyStatusAndAppCurrency.second,
analyticsEventHandler = analyticsEventHandler,
),
)
},
)
}
.launchIn(coroutineScope)
}
}
private suspend fun List<OnrampTransaction>.clearHiddenTerminal() {
this.filter { it.status.isHidden && it.status.isTerminal }
.forEach { onrampRemoveTransactionUseCase(externalTxId = it.externalTxId) }
}
}

View file

@ -45,6 +45,9 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheet
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.expressTransactionsItems
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.components.atoms.handComposableComponentHeight
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
@ -216,6 +219,12 @@ private fun WalletContent(
walletState.marketPriceBlockState?.let { marketPriceBlockState ->
marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier)
}
if (walletState is WalletState.SingleCurrency.Content) {
expressTransactionsItems(
expressTxs = walletState.expressTxsToDisplay,
modifier = itemModifier,
)
}
}
(selectedWallet as? WalletState.Visa.Content)?.let {
@ -716,6 +725,7 @@ private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) {
is BalancesAndLimitsBottomSheetConfig -> BalancesAndLimitsBottomSheet(config = bottomSheetConfig)
is VisaTxDetailsBottomSheetConfig -> VisaTxDetailsBottomSheet(config = bottomSheetConfig)
is PushNotificationsBottomSheetConfig -> PushNotificationsBottomSheet(config = bottomSheetConfig)
is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet(config = bottomSheetConfig)
}
}
}

View file

@ -17,6 +17,7 @@ import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWalletAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
@ -33,9 +34,7 @@ import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.coroutines.*
import com.tangem.utils.extensions.indexOfFirstOrNull
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
@ -70,6 +69,7 @@ internal class WalletViewModel @Inject constructor(
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
private val walletImageResolver: WalletImageResolver,
private val tokenListStore: MultiWalletTokenListStore,
private val onrampStatusFactory: OnrampStatusFactory,
private val walletFeatureToggles: WalletFeatureToggles,
analyticsEventsHandler: AnalyticsEventHandler,
) : ViewModel() {
@ -81,6 +81,8 @@ internal class WalletViewModel @Inject constructor(
private val refreshWalletJobHolder = JobHolder()
private var needToRefreshWallet = false
private var expressTxStatusTaskScheduler = SingleTaskScheduler<Unit>()
init {
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened)
@ -93,6 +95,7 @@ internal class WalletViewModel @Inject constructor(
subscribeOnSelectedWalletFlow()
subscribeToScreenBackgroundState()
subscribeOnPushNotificationsPermission()
subscribeOnExpressTransactionsUpdates()
}
private fun maybeMigrateNames() {
@ -239,6 +242,24 @@ internal class WalletViewModel @Inject constructor(
.launchIn(viewModelScope)
}
private fun subscribeOnExpressTransactionsUpdates() {
viewModelScope.launch(dispatchers.main) {
expressTxStatusTaskScheduler.cancelTask()
expressTxStatusTaskScheduler.scheduleTask(
viewModelScope,
PeriodicTask(
isDelayFirst = false,
delay = EXPRESS_STATUS_UPDATE_DELAY,
task = {
runCatching { onrampStatusFactory.updateOnrmapTransactionStatuses() }
},
onSuccess = { /* no-op */ },
onError = { /* no-op */ },
),
)
}
}
private fun needToRefreshTimer() {
viewModelScope.launch {
delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS)
@ -430,5 +451,6 @@ internal class WalletViewModel @Inject constructor(
private companion object {
const val REFRESH_WALLET_BACKGROUND_TIMER_MILLIS = 10000L
const val EXPRESS_STATUS_UPDATE_DELAY = 10000L
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
import arrow.core.getOrElse
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
@ -11,10 +12,14 @@ import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory
import com.tangem.feature.wallet.presentation.wallet.domain.unwrap
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBottomSheetConfig
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCurrencyActionsConverter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.scopes.ViewModelScoped
@ -43,6 +48,12 @@ internal interface WalletContentClickIntents {
fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
fun onTransactionClick(txHash: String)
fun onDissmissBottomSheet()
fun onGoToProviderClick(externalTxId: String)
fun onExpressTransactionClick(txId: String)
}
@Suppress("LongParameterList")
@ -51,6 +62,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
private val stateHolder: WalletStateController,
private val currencyActionsClickIntents: WalletCurrencyActionsClickIntentsImplementor,
private val walletWarningsClickIntents: WalletWarningsClickIntentsImplementor,
private val onrampStatusFactory: OnrampStatusFactory,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
@ -164,4 +176,39 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
)
}
}
override fun onGoToProviderClick(externalTxUrl: String) {
router.openUrl(externalTxUrl)
}
override fun onDissmissBottomSheet() {
val userWalletId = stateHolder.getSelectedWalletId()
if (stateHolder.getSelectedWallet().bottomSheetConfig?.content is ExpressStatusBottomSheetConfig) {
viewModelScope.launch(dispatchers.main) {
onrampStatusFactory.removeTransactionOnBottomSheetClosed()
}
}
stateHolder.update(CloseBottomSheetTransformer(userWalletId))
}
override fun onExpressTransactionClick(txId: String) {
viewModelScope.launch {
val userWalletId = stateHolder.getSelectedWalletId()
val singleWalletState = stateHolder.getSelectedWallet() as? WalletState.SingleCurrency.Content
?: return@launch
val expressTransaction = singleWalletState.expressTxsToDisplay.firstOrNull { it.info.txId == txId }
?: return@launch
stateHolder.update(
OpenBottomSheetTransformer(
userWalletId = userWalletId,
content = ExpressStatusBottomSheetConfig(
value = expressTransaction,
),
onDismissBottomSheet = ::onDissmissBottomSheet,
),
)
}
}
}

View file

@ -21,7 +21,6 @@ import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.extenstions.unwrap
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.demo.IsDemoCardUseCase
@ -32,13 +31,14 @@ import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase
import com.tangem.domain.tokens.RemoveCurrencyUseCase
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
@ -53,9 +53,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import java.math.BigDecimal
import javax.inject.Inject
@ -108,8 +106,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase,
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val getExploreUrlUseCase: GetExploreUrlUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
@ -136,63 +132,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
if (handleUnavailabilityReason(unavailabilityReason)) return
stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWallet.walletId))
viewModelScope.launch(dispatchers.main) {
val maybeFeeCurrencyStatus =
getFeePaidCryptoCurrencyStatusSyncUseCase(userWallet.walletId, cryptoCurrencyStatus).getOrNull()
when (val currency = cryptoCurrencyStatus.currency) {
is CryptoCurrency.Coin -> sendCoin(cryptoCurrencyStatus, userWallet, maybeFeeCurrencyStatus)
is CryptoCurrency.Token -> sendToken(
cryptoCurrency = currency,
cryptoCurrencyStatus = cryptoCurrencyStatus.value,
feeCurrencyStatus = maybeFeeCurrencyStatus,
userWallet = userWallet,
)
}
}
}
private fun sendCoin(
cryptoCurrencyStatus: CryptoCurrencyStatus,
userWallet: UserWallet,
feeCurrencyStatus: CryptoCurrencyStatus?,
) {
reduxStateHolder.dispatch(
action = TradeCryptoAction.SendCoin(
userWallet = userWallet,
coinStatus = cryptoCurrencyStatus,
feeCurrencyStatus = feeCurrencyStatus,
),
val route = AppRoute.Send(
currency = cryptoCurrencyStatus.currency,
userWalletId = userWallet.walletId,
)
}
private fun sendToken(
cryptoCurrency: CryptoCurrency.Token,
cryptoCurrencyStatus: CryptoCurrencyStatus.Value,
feeCurrencyStatus: CryptoCurrencyStatus?,
userWallet: UserWallet,
) {
viewModelScope.launch(dispatchers.main) {
getNetworkCoinStatusUseCase(
userWalletId = userWallet.walletId,
networkId = cryptoCurrency.network.id,
derivationPath = cryptoCurrency.network.derivationPath,
isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(),
)
.take(count = 1)
.collectLatest {
it.onRight { coinStatus ->
reduxStateHolder.dispatch(
action = TradeCryptoAction.SendToken(
userWallet = userWallet,
tokenCurrency = cryptoCurrency,
tokenFiatRate = cryptoCurrencyStatus.fiatRate,
coinFiatRate = coinStatus.value.fiatRate,
feeCurrencyStatus = feeCurrencyStatus,
),
)
}
}
}
appRouter.push(route)
}
override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) {