Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-20 14:00:12 +05:00
commit d16c443ee2
55 changed files with 1581 additions and 67 deletions

View file

@ -1,7 +1,9 @@
package com.tangem.tap.di.domain
import com.tangem.domain.onramp.*
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -48,9 +50,50 @@ internal object OnrampDomainModule {
return CheckOnrampAvailabilityUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideGetOnrampStatusUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampStatusUseCase {
return GetOnrampStatusUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideGetOnrampCurrencyUseCase(onrampRepository: OnrampRepository): GetOnrampCurrencyUseCase {
return GetOnrampCurrencyUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideGetOnrampTransactionsUseCase(
onrampTransactionRepository: OnrampTransactionRepository,
): GetOnrampTransactionsUseCase {
return GetOnrampTransactionsUseCase(onrampTransactionRepository)
}
@Provides
@Singleton
fun provideGetOnrampTransactionUseCase(
onrampTransactionRepository: OnrampTransactionRepository,
): GetOnrampTransactionUseCase {
return GetOnrampTransactionUseCase(onrampTransactionRepository)
}
@Provides
@Singleton
fun provideOnrampRemoveTransactionUseCase(
onrampTransactionRepository: OnrampTransactionRepository,
): OnrampRemoveTransactionUseCase {
return OnrampRemoveTransactionUseCase(onrampTransactionRepository)
}
@Provides
@Singleton
fun provideOnrampSaveTransactionUseCase(
onrampTransactionRepository: OnrampTransactionRepository,
): OnrampSaveTransactionUseCase {
return OnrampSaveTransactionUseCase(onrampTransactionRepository)
}
}

View file

@ -12,10 +12,7 @@ import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent
import com.tangem.features.onramp.component.BuyCryptoComponent
import com.tangem.features.onramp.component.OnrampComponent
import com.tangem.features.onramp.component.SellCryptoComponent
import com.tangem.features.onramp.component.SwapSelectTokensComponent
import com.tangem.features.onramp.component.*
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.staking.api.navigation.StakingRouter
@ -52,6 +49,7 @@ internal class ChildFactory @Inject constructor(
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
private val onrampComponentFactory: OnrampComponent.Factory,
private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory,
private val buyCryptoComponentFactory: BuyCryptoComponent.Factory,
private val sellCryptoComponentFactory: SellCryptoComponent.Factory,
private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory,
@ -203,6 +201,13 @@ internal class ChildFactory @Inject constructor(
componentFactory = onrampComponentFactory,
)
}
is AppRoute.OnrampSuccess -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = OnrampSuccessComponent.Params(route.txId),
componentFactory = onrampSuccessComponentFactory,
)
}
is AppRoute.BuyCrypto -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),

View file

@ -299,6 +299,13 @@ sealed class AppRoute(val path: String) : Route {
override fun getBundle(): Bundle = bundle(serializer())
}
@Serializable
data class OnrampSuccess(
val txId: String,
) : AppRoute(path = "/onramp/success/$txId"), RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
}
@Serializable
data class BuyCrypto(
val userWalletId: UserWalletId,

View file

@ -31,10 +31,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
@ -118,7 +115,7 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh
@Composable
private fun ApprovalBottomSheetInfo(data: GiveTxPermissionState.ReadyForRequest) {
FooterContainer(
footer = stringResource(id = R.string.give_permission_policy_type_footer),
footer = resourceReference(R.string.give_permission_policy_type_footer),
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
AmountItem(
@ -130,7 +127,7 @@ private fun ApprovalBottomSheetInfo(data: GiveTxPermissionState.ReadyForRequest)
}
SpacerH16()
FooterContainer(
footer = data.footerText.resolveReference(),
footer = data.footerText,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
FeeItem(fee = data.fee)

View file

@ -0,0 +1,228 @@
package com.tangem.common.ui.expressStatus
import androidx.annotation.DrawableRes
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.R
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.persistentListOf
/**
* Block with express statuses
*
* @param state ui holder
* @param modifier modifier
* @see [Figma](https://www.figma.com/design/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?node-id=18459-26521&t=4jox7bfqUiXnm2h1-4)
*/
@Composable
fun ExpressStatusBlock(state: ExpressStatusUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.padding(
vertical = TangemTheme.dimens.spacing14,
horizontal = TangemTheme.dimens.spacing12,
),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing16),
) {
Text(
text = state.title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
SpacerWMax()
AnimatedVisibility(visible = state.link is ExpressLinkUM.Content) {
val link = remember(this) { state.link as ExpressLinkUM.Content }
Row(
modifier = Modifier.clickable { link.onClick() },
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(id = link.icon),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.spacing16)
.padding(end = TangemTheme.dimens.spacing2),
)
Text(
text = link.text.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
Column {
state.statuses.forEachIndexed { index, item ->
ExpressStatusStep(item, index == state.statuses.lastIndex)
}
}
}
}
@Composable
private fun ExpressStatusStep(status: ExpressStatusItemUM, isLast: Boolean) {
AnimatedContent(
targetState = status,
label = "Exchange Step Change Success",
transitionSpec = {
fadeIn(tween(durationMillis = 220)) togetherWith
fadeOut(tween(durationMillis = 220))
},
) { content ->
Row {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
when (content.state) {
ExpressStatusItemState.Active -> StepInProgress()
ExpressStatusItemState.Default -> StepDefault()
ExpressStatusItemState.Done -> Step(
iconRes = R.drawable.ic_check_24,
iconColor = TangemTheme.colors.icon.primary1,
borderColor = TangemTheme.colors.field.focused,
)
ExpressStatusItemState.Error -> Step(
iconRes = R.drawable.ic_close_24,
iconColor = TangemTheme.colors.icon.warning,
)
ExpressStatusItemState.Warning -> Step(
iconRes = R.drawable.ic_close_24,
iconColor = TangemTheme.colors.icon.attention,
)
}
if (!isLast) {
StepSeparator()
}
}
val textColor = when (status.state) {
ExpressStatusItemState.Active -> TangemTheme.colors.text.primary1
ExpressStatusItemState.Default -> TangemTheme.colors.text.disabled
ExpressStatusItemState.Done -> TangemTheme.colors.text.primary1
ExpressStatusItemState.Error -> TangemTheme.colors.text.warning
ExpressStatusItemState.Warning -> TangemTheme.colors.text.attention
}
Text(
text = content.text.resolveReference(),
style = TangemTheme.typography.body2,
color = textColor,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12),
)
}
}
}
@Composable
private fun StepDefault() {
Box(
modifier = Modifier
.size(TangemTheme.dimens.size20)
.border(
width = TangemTheme.dimens.size1_5,
color = TangemTheme.colors.field.focused,
shape = CircleShape,
)
.padding(TangemTheme.dimens.spacing2),
)
}
@Composable
private fun Step(iconColor: Color, @DrawableRes iconRes: Int, borderColor: Color = iconColor) {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconColor,
modifier = Modifier
.size(TangemTheme.dimens.size20)
.border(
width = TangemTheme.dimens.size1_5,
color = borderColor,
shape = CircleShape,
)
.padding(TangemTheme.dimens.spacing2),
)
}
@Composable
private fun StepInProgress() {
CircularProgressIndicator(
color = TangemTheme.colors.icon.primary1,
strokeWidth = TangemTheme.dimens.size2,
modifier = Modifier
.padding(TangemTheme.dimens.spacing2)
.size(TangemTheme.dimens.size14),
)
}
@Composable
private fun StepSeparator() {
Box(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing2)
.size(
width = TangemTheme.dimens.size1_5,
height = TangemTheme.dimens.size10,
)
.background(
color = TangemTheme.colors.field.focused,
shape = CircleShape,
),
)
}
@Preview
@Composable
private fun Preview_ExchangeStatusBlock() {
val state = ExpressStatusUM(
title = resourceReference(R.string.express_exchange_status_title),
link = ExpressLinkUM.Content(
icon = R.drawable.ic_alert_24,
text = resourceReference(R.string.common_go_to_provider),
onClick = {},
),
statuses = persistentListOf(
ExpressStatusItemUM(text = stringReference("Done"), state = ExpressStatusItemState.Done),
ExpressStatusItemUM(text = stringReference("Active"), state = ExpressStatusItemState.Active),
ExpressStatusItemUM(text = stringReference("Warning"), state = ExpressStatusItemState.Warning),
ExpressStatusItemUM(text = stringReference("Error"), state = ExpressStatusItemState.Error),
ExpressStatusItemUM(text = stringReference("Default"), state = ExpressStatusItemState.Default),
),
)
TangemThemePreview {
ExpressStatusBlock(state = state)
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.common.ui.expressStatus
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.ExpressNotificationsUM
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.res.TangemTheme
@Composable
fun ExpressStatusNotificationBlock(state: NotificationUM?) {
AnimatedContent(
targetState = state,
modifier = Modifier.padding(top = 12.dp),
label = "Express Status Notification Change",
) { notification ->
if (notification?.config != null) {
Notification(
config = notification.config,
iconTint = when (state) {
is ExpressNotificationsUM.NeedVerification -> TangemTheme.colors.icon.attention
is ExpressNotificationsUM.FailedByProvider -> TangemTheme.colors.icon.warning
else -> null
},
containerColor = TangemTheme.colors.background.action,
)
}
}
}

View file

@ -0,0 +1,54 @@
package com.tangem.common.ui.expressStatus.state
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Stable
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
/**
* UI data holder for express status block
*
* @property title block title
* @property link provider web link
* @property statuses list of possible and active statuses
*/
data class ExpressStatusUM(
val title: TextReference,
val link: ExpressLinkUM,
val statuses: ImmutableList<ExpressStatusItemUM>,
)
/**
* Provider web link for express status block.
* [Empty] if no link needed
* [Content] if link is provided and displayed
*/
@Stable
sealed class ExpressLinkUM {
data object Empty : ExpressLinkUM()
data class Content(
@DrawableRes val icon: Int,
val text: TextReference,
val onClick: () -> Unit,
) : ExpressLinkUM()
}
/**
* Single status item in express status block
*/
data class ExpressStatusItemUM(
val text: TextReference,
val state: ExpressStatusItemState,
)
/**
* Available status states for express status block
*/
enum class ExpressStatusItemState {
Active,
Default,
Done,
Warning,
Error,
;
}

View file

@ -0,0 +1,28 @@
package com.tangem.common.ui.notifications
import com.tangem.common.ui.R
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.resourceReference
object ExpressNotificationsUM {
data class NeedVerification(val onGoToProviderClick: () -> Unit) : NotificationUM.Warning(
title = resourceReference(R.string.express_exchange_notification_verification_title),
subtitle = resourceReference(R.string.express_exchange_notification_verification_text),
iconResId = R.drawable.ic_alert_triangle_20,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.common_go_to_provider),
onClick = onGoToProviderClick,
),
)
data class FailedByProvider(val onGoToProviderClick: () -> Unit) : NotificationUM.Error(
title = resourceReference(R.string.express_exchange_notification_failed_title),
subtitle = resourceReference(R.string.express_exchange_notification_failed_text),
iconResId = R.drawable.ic_alert_circle_24,
buttonState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.common_go_to_provider),
onClick = onGoToProviderClick,
),
)
}

View file

@ -14,8 +14,8 @@ data class OnrampStatusResponse(
@Json(name = "payoutAddress")
val payoutAddress: String,
// @Json(name = "status")
// val status: ???
@Json(name = "status")
val status: Status,
@Json(name = "failReason")
val failReason: String?,
@ -48,14 +48,46 @@ data class OnrampStatusResponse(
val toDecimals: String,
@Json(name = "toAmount")
val toAmount: String,
val toAmount: String?,
@Json(name = "toActualAmount")
val toActualAmount: String,
val toActualAmount: String?,
@Json(name = "paymentMethod")
val paymentMethod: String,
@Json(name = "countryCode")
val countryCode: String,
)
)
enum class Status {
@Json(name = "created")
Created,
@Json(name = "expired")
Expired,
@Json(name = "waiting-for-payment")
WaitingForPayment,
@Json(name = "payment-processing")
PaymentProcessing,
@Json(name = "verifying")
Verifying,
@Json(name = "failed")
Failed,
@Json(name = "paid")
Paid,
@Json(name = "sending")
Sending,
@Json(name = "finished")
Finished,
@Json(name = "paused")
Paused,
}

View file

@ -107,6 +107,8 @@ object PreferencesKeys {
val ONRAMP_DEFAULT_COUNTRY by lazy { stringPreferencesKey(name = "onrampDefaultCountry") }
val ONRAMP_TRANSACTIONS_STATUSES_KEY by lazy { stringPreferencesKey(name = "onrampTransactionsStatuses") }
// region Permission
fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission")

View file

@ -150,4 +150,12 @@ suspend inline fun <reified T> AppPreferencesStore.getObjectSetSync(key: Prefere
?.get(key)
?.let(adapter::fromJson)
.orEmpty()
}
/** Get flow of set of [T] by string [key], or empty if data is not found */
inline fun <reified T> AppPreferencesStore.getObjectSet(key: Preferences.Key<String>): Flow<Set<T>> {
val adapter = moshi.adapter<Set<T>>(Types.newParameterizedType(Set::class.java, T::class.java))
return data.map {
it[key]?.let(adapter::fromJson) ?: emptySet()
}
}

View file

@ -568,7 +568,7 @@
<string name="onramp_no_available_providers">Keine verfügbaren Anbieter für diese Währung</string>
<string name="onramp_pay_with">Bezahlen mit</string>
<string name="onramp_redirecting_to_provider_subtitle">Du kannst Deine Transaktion beim Drittanbieter %s abschließen.</string>
<string name="onramp_redirecting_to_provider_title">Umleitung auf %s </string>
<string name="onramp_redirecting_to_provider_title">Umleitung auf %s…</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Unsere Dienstleistungen sind in diesem Land nicht verfügbar</string>
<string name="onramp_residency_bottomsheet_country_subtitle">Änder oder bestätige bitte</string>
<string name="onramp_residency_bottomsheet_title">Dein Wohnsitz wurde identifiziert als</string>

View file

@ -858,14 +858,14 @@
<string name="toast_balances_shown">Saldos mostrados</string>
<string name="toast_undo">Cancelar</string>
<string name="token_button_unavailability_generic_description">Esta operación no está disponible actualmente. Por favor, inténtalo de nuevo más tarde.</string>
<string name="token_button_unavailability_reason_buy_unavailable">Comprar %s no está disponible en este momento. Por favor revise sus actualizaciones.</string>
<string name="token_button_unavailability_reason_buy_unavailable">Comprar %s no está disponible con los proveedores actuales pero estamos trabajando para agregar más opciones.</string>
<string name="token_button_unavailability_reason_empty_balance_sell">No tiene fondos para vender. Recargue su cuenta para poder vender fondos desde ella.</string>
<string name="token_button_unavailability_reason_empty_balance_send">No tienes fondos para enviar. Recarga tu cuenta para poder enviar fondos desde ella.</string>
<string name="token_button_unavailability_reason_not_exchangeable">El servicio de intercambio %s no está disponible en este momento. Por favor consulte nuestras actualizaciones.</string>
<string name="token_button_unavailability_reason_not_exchangeable">El servicio de intercambio %s no está compatible con los proveedores actuales pero estamos trabajando para agregar más opciones.</string>
<string name="token_button_unavailability_reason_pending_transaction_sell">La venta de fondos estará disponible una vez que la(s) transacción(es) pendiente(s) en la red %s se complete</string>
<string name="token_button_unavailability_reason_pending_transaction_send">El envío de fondos estará disponible una vez que se completen las transacciones pendientes en la red %s.</string>
<string name="token_button_unavailability_reason_sell_unavailable">La venta de %s no está disponible en este momento. Por favor consulte nuestras actualizaciones.</string>
<string name="token_button_unavailability_reason_staking_unavailable">El staking %s no está disponible en este momento. Por favor consulte nuestras actualizaciones.</string>
<string name="token_button_unavailability_reason_sell_unavailable">Vender %s no es compatible con los proveedores actuales, pero estamos trabajando para agregar más opciones.</string>
<string name="token_button_unavailability_reason_staking_unavailable">El staking de %s no es compatible con los proveedores actuales, pero estamos trabajando para agregar más opciones.</string>
<string name="token_details_generate_xpub">Generar XPUB</string>
<string name="token_details_hide_alert_hide">Ocultar</string>
<string name="token_details_hide_alert_message">Está a punto de ocultar este token de la pantalla principal. Puede volver a agregarlo en cualquier momento a través de la página de gestión de tokens.</string>

View file

@ -850,14 +850,14 @@
<string name="toast_balances_shown">Soldes affichés</string>
<string name="toast_undo">Annuler</string>
<string name="token_button_unavailability_generic_description">Cette opération est actuellement indisponible. Veuillez réessayer plus tard.</string>
<string name="token_button_unavailability_reason_buy_unavailable">L\'achat de %s n\'est pas disponible pour le moment. Veuillez vérifier vos mises à jour.</string>
<string name="token_button_unavailability_reason_buy_unavailable">L\'achat de %s n\'est pas pris en charge par les fournisseurs actuels mais nous travaillons à ajouter davantage d\'options.</string>
<string name="token_button_unavailability_reason_empty_balance_sell">Vous n\'avez pas de fonds à vendre. Renflouez votre compte pour pouvoir vendre des fonds à partir de celui-ci.</string>
<string name="token_button_unavailability_reason_empty_balance_send">Vous n\'avez pas de fonds à envoyer. Renflouez votre compte pour pouvoir envoyer des fonds à partir de celui-ci.</string>
<string name="token_button_unavailability_reason_not_exchangeable">Le service d\'échange %s n\'est pas disponible pour le moment. Veuillez consulter nos mises à jour.</string>
<string name="token_button_unavailability_reason_not_exchangeable">Le service d\'échange %s n\'est pas pris en charge par les fournisseurs actuels mais nous travaillons à ajouter plus d\'options.</string>
<string name="token_button_unavailability_reason_pending_transaction_sell">La vente de fonds sera disponible une fois que la ou les transactions en attente dans le réseau %s seront terminées</string>
<string name="token_button_unavailability_reason_pending_transaction_send">L\'envoi de fonds sera disponible une fois la ou les transactions en attente dans le réseau %s terminées.</string>
<string name="token_button_unavailability_reason_sell_unavailable">La vente de %s n\'est pas disponible pour le moment. Veuillez consulter nos mises à jour.</string>
<string name="token_button_unavailability_reason_staking_unavailable">Le staking %s nest pas disponible pour le moment. Veuillez consulter nos mises à jour.</string>
<string name="token_button_unavailability_reason_sell_unavailable">L\'achat de %s n\'est pas pris en charge par les fournisseurs actuels mais nous travaillons à ajouter plus d\'options.</string>
<string name="token_button_unavailability_reason_staking_unavailable">Le staking de %s n\'est pas pris en charge par les fournisseurs actuels, mais nous travaillons à ajouter plus d\'options.</string>
<string name="token_details_generate_xpub">Générer XPUB</string>
<string name="token_details_hide_alert_hide">Masquer</string>
<string name="token_details_hide_alert_message">Vous êtes sur le point de masquer ce jeton de l\'écran principal. Vous pouvez le rajouter à tout moment via la page de gestion des jetons.</string>

View file

@ -830,7 +830,7 @@
<string name="swapping_alert_cex_description_with_slippage">金額には以下が含まれます: \n • サービス プロバイダーの手数料\n • 取引所からユーザーのアドレスに%1$sを送金するためのネットワーク手数料。 \n\nプロバイダーのスリッページは最大%2$sです</string>
<string name="swapping_alert_dex_description">この金額には、サービスプロバイダーの手数料が含まれています。</string>
<string name="swapping_alert_dex_description_with_slippage">金額にはサービスプロバイダーの手数料が含まれます。 \n\nプロバイダーのスリッページは最大%s です</string>
<string name="swapping_alert_title">手数料</string>
<string name="swapping_alert_title">情報</string>
<string name="swapping_approve_information_text">すべての分散型取引所は、スマートコントラクトがあなたの許可なくウォレットにアクセスするのを防ぐために承認を必要とします。設計上、スマートコントラクトは承認なしでトークンにアクセスできません。トークンを「ロック解除」することで、あなたは1-inchのスマートコントラクトがトークンを使うことを承認します。ネットワークのマイナーは、このアクションをブロックチェーンに記録するためのガス料金あなたが支払うを受け取ります。承認後、トークンを交換することができます。</string>
<string name="swapping_approve_information_title">承認</string>
<string name="swapping_fee_estimation_error_text">手数料見積りエラーです。サポートにフィードバックをお送りください。</string>
@ -846,14 +846,14 @@
<string name="toast_balances_shown">残高表示</string>
<string name="toast_undo">元に戻す</string>
<string name="token_button_unavailability_generic_description">この操作は現在利用できません。しばらくしてからもう一度お試しください。</string>
<string name="token_button_unavailability_reason_buy_unavailable">現在、 %sの買付はご利用いただけません。アップデート情報をご確認ください</string>
<string name="token_button_unavailability_reason_buy_unavailable">%sの買付は、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます</string>
<string name="token_button_unavailability_reason_empty_balance_sell">売却できる資金がありません。アカウントに入金して、売却できるようにしてください。</string>
<string name="token_button_unavailability_reason_empty_balance_send">送金する資金がありません。アカウントに入金して、送金できるようにしてください。</string>
<string name="token_button_unavailability_reason_not_exchangeable">現在、 %sの交換はご利用いただけません。アップデート情報を確認してください</string>
<string name="token_button_unavailability_reason_not_exchangeable">%sのスワップは、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます</string>
<string name="token_button_unavailability_reason_pending_transaction_sell">ネットワーク%s内の保留中の取引が完了すると、資金の売却が可能になります。</string>
<string name="token_button_unavailability_reason_pending_transaction_send">ネットワーク%s内の保留中の取引が完了すると、送金が可能になります。</string>
<string name="token_button_unavailability_reason_sell_unavailable">現在、 %sの売却はご利用いただけません。アップデート情報をご確認ください</string>
<string name="token_button_unavailability_reason_staking_unavailable">%s のステーキングは現在ご利用いただけません。最新情報をご確認ください</string>
<string name="token_button_unavailability_reason_sell_unavailable">%sの売却は、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます</string>
<string name="token_button_unavailability_reason_staking_unavailable">%sのステーキングは、現在のプロバイダーではサポートされていませんが、より多くのオプションを追加できるよう取り組んでいます</string>
<string name="token_details_generate_xpub">XPUBを生成する</string>
<string name="token_details_hide_alert_hide">非表示</string>
<string name="token_details_hide_alert_message">このトークンをメイン画面から非表示にします。トークンの管理ページからいつでも再度追加できます。</string>
@ -946,7 +946,7 @@
<string name="wallet_network_group_title">%sネットワーク</string>
<string name="wallet_notification_address_copied">アドレスがクリップボードにコピーされました</string>
<string name="wallet_notification_no_internet">インターネット接続がありません</string>
<string name="wallet_promo_banner_button_title">今すぐ10 %オフで購入</string>
<string name="wallet_promo_banner_button_title">今すぐ10 %%オフで購入</string>
<string name="wallet_promo_banner_description">1.3万種類以上の暗号資産にアクセス。ワンタップで買付、売却、スワップ、ステーキングが可能です。\nバックアップ用に最大3枚のカードを連携できます。</string>
<string name="wallet_promo_banner_title">Tangemウォレットを見る</string>
<string name="wallet_settings_title">ウォレット設定</string>
@ -1019,7 +1019,7 @@
<string name="warning_solana_fee_message">Solanaネットワークが混雑しています。2分以内に取引が完了しない場合は、再度取引を繰り返してください。</string>
<string name="warning_solana_fee_title">Solanaネットワークアラート</string>
<string name="warning_solana_rent_fee_message">Solana ネットワークは 2 日ごとに%1$sのレンタル料を請求します。レンタル料を支払えないアカウントはネットワークから削除されます。アカウントに%2$s以上入金すると、無料で使用できます。</string>
<string name="warning_some_networks_unreachable_message">現在、一部のネットワークにアクセスできません。しばらくしてからもう一度お試しください。</string>
<string name="warning_some_networks_unreachable_message">下にスワイプして更新するか、後でもう一度お試しください。</string>
<string name="warning_some_networks_unreachable_title">一部のネットワークにアクセスできません</string>
<string name="warning_testnet_card_message">これはテストネットカードです。取引処理はできませんので、テストおよび開発目的でのみご利用ください。</string>
<string name="warning_testnet_card_title">テスト目的のみ</string>

View file

@ -581,7 +581,7 @@
<string name="onramp_no_available_providers">Для данной валюты нет доступных провайдеров</string>
<string name="onramp_pay_with">Оплата с</string>
<string name="onramp_redirecting_to_provider_subtitle">Вы сможете завершить транзакцию через сервис стороннего провайдера, %s</string>
<string name="onramp_redirecting_to_provider_title">Переход к %s ...</string>
<string name="onramp_redirecting_to_provider_title">Переход к %s...</string>
<string name="onramp_residency_bottomsheet_country_not_supported">Наши сервисы недоступны в данной стране</string>
<string name="onramp_settings_title">Настройки</string>
<string name="onramp_via">Через</string>
@ -966,7 +966,7 @@
<string name="wallet_notification_no_internet">Нет соединения с интернетом</string>
<string name="wallet_promo_banner_button_title">Получить с 10%% скидкой</string>
<string name="wallet_promo_banner_description">Получите доступ к более чем 13 000 криптовалют. Покупайте, продавайте, обменяйте и стейкайте в один клик. Свяжите до трех карт для резервного копирования. </string>
<string name="wallet_promo_banner_title">Откройте Tangem Wallet </string>
<string name="wallet_promo_banner_title">Откройте Tangem Wallet</string>
<string name="wallet_settings_title">Настройки кошелька</string>
<string name="wallet_title">Tangem</string>
<string name="warning_access_denied_message">Используйте %s или отсканируйте карту/кольцо, чтобы разблокировать доступ к вашему кошельку</string>

View file

@ -294,6 +294,9 @@
<string name="express_provider_not_available">Unavailable for this pair</string>
<string name="express_provider_permission_needed">Permission Required</string>
<string name="express_provider_recommended">Recommended</string>
<string name="express_status_bought">Bought %s</string>
<string name="express_status_buying">Buying %s</string>
<string name="express_status_buying_active">Buying %s...</string>
<string name="express_token_list_empty_search">No tokens found. Please try another request</string>
<string name="express_transaction_id">ID: %s</string>
<string name="express_transaction_id_copied">Transaction ID copied</string>

View file

@ -5,8 +5,11 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
@ -20,15 +23,16 @@ import com.tangem.core.ui.res.TangemTheme
@Composable
fun FooterContainer(
modifier: Modifier = Modifier,
footer: String? = null,
footer: TextReference? = null,
footerTopPadding: Dp = TangemTheme.dimens.spacing8,
content: @Composable () -> Unit,
) {
Column(modifier = modifier) {
content()
AnimatedVisibility(visible = footer != null) {
val footerWrapped = remember(this) { requireNotNull(footer) }
Text(
text = footer.orEmpty(),
text = footerWrapped.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier

View file

@ -14,15 +14,11 @@ import com.tangem.blockchain.common.Token as SdkToken
class ResponseCryptoCurrenciesFactory {
fun createCurrency(
currencyId: CryptoCurrency.ID,
response: UserTokensResponse,
scanResponse: ScanResponse,
): CryptoCurrency {
fun createCurrency(currencyId: String, response: UserTokensResponse, scanResponse: ScanResponse): CryptoCurrency {
return response.tokens
.asSequence()
.mapNotNull { createCurrency(it, scanResponse) }
.first { it.id == currencyId }
.first { it.id.value == currencyId }
}
fun createCurrencies(response: UserTokensResponse, scanResponse: ScanResponse): List<CryptoCurrency> {

View file

@ -5,9 +5,9 @@ import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
internal class DefaultOnrampErrorResolver(
private val onrampErrorConverter: OnrampErrorConverter,
) : OnrampErrorResolver {
internal class DefaultOnrampErrorResolver : OnrampErrorResolver {
private val onrampErrorConverter = OnrampErrorConverter()
override fun resolve(throwable: Throwable): OnrampError {
return if (throwable is ApiResponseError.HttpException) {

View file

@ -2,6 +2,7 @@ package com.tangem.data.onramp
import com.tangem.data.onramp.converters.CountryConverter
import com.tangem.data.onramp.converters.CurrencyConverter
import com.tangem.data.onramp.converters.StatusConverter
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
@ -13,6 +14,7 @@ import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
@ -27,6 +29,7 @@ internal class DefaultOnrampRepository(
private val currencyConverter = CurrencyConverter()
private val countryConverter = CountryConverter(currencyConverter)
private val statusConverter = StatusConverter()
override suspend fun getCurrencies(): List<OnrampCurrency> = withContext(dispatchers.io) {
onrampApi.getCurrencies()
@ -46,6 +49,12 @@ internal class DefaultOnrampRepository(
.let(countryConverter::convert)
}
override suspend fun getStatus(txId: String): OnrampStatus = withContext(dispatchers.io) {
onrampApi.getStatus(txId)
.getOrThrow()
.let(statusConverter::convert)
}
override suspend fun saveDefaultCurrency(currency: OnrampCurrency) = withContext(dispatchers.io) {
appPreferencesStore.storeObject<OnrampCurrencyDTO>(
key = PreferencesKeys.ONRAMP_DEFAULT_CURRENCY,

View file

@ -0,0 +1,77 @@
package com.tangem.data.onramp
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSet
import com.tangem.datasource.local.preferences.utils.getObjectSetSync
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
internal class DefaultOnrampTransactionRepository(
private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider,
) : OnrampTransactionRepository {
override suspend fun storeTransaction(transaction: OnrampTransaction) {
withContext(dispatchers.io) {
appPreferencesStore.editData { mutablePreferences ->
val stored = mutablePreferences.getObjectSet<OnrampTransaction>(
PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
)
val updated = stored?.toMutableSet()
?.addOrReplace(transaction) { it.txId == transaction.txId }
?: mutableSetOf(transaction)
mutablePreferences.setObjectSet(
key = PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
value = updated,
)
}
}
}
override suspend fun getTransactionById(txId: String): OnrampTransaction? = withContext(dispatchers.io) {
val stored = appPreferencesStore.getObjectSetSync<OnrampTransaction>(
PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
)
stored.firstOrNull { it.txId == txId }
}
override fun getTransactions(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
): Flow<List<OnrampTransaction>> = appPreferencesStore
.getObjectSet<OnrampTransaction>(PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY)
.map { transactions ->
transactions.filter {
it.userWalletId == userWalletId && it.toCurrencyId == cryptoCurrencyId.value
}
}
override suspend fun removeTransaction(txId: String) {
withContext(dispatchers.io) {
appPreferencesStore.editData { mutablePreferences ->
runCatching {
val stored = mutablePreferences.getObjectSet<OnrampTransaction>(
PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
)?.toMutableSet()
stored?.removeIf { it.txId == txId }
mutablePreferences.setObjectSet(
key = PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY,
value = stored ?: emptySet(),
)
}
}
}
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.data.onramp.converters
import com.tangem.datasource.api.onramp.models.response.OnrampStatusResponse
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.utils.converter.Converter
internal class StatusConverter : Converter<OnrampStatusResponse, OnrampStatus> {
override fun convert(value: OnrampStatusResponse): OnrampStatus {
return OnrampStatus(
txId = value.txId,
providerId = value.providerId,
payoutAddress = value.payoutAddress,
status = OnrampStatus.Status.valueOf(value.status.name),
failReason = value.failReason,
externalTxId = value.externalTxId,
externalTxUrl = value.externalTxUrl,
payoutHash = value.payoutHash,
createdAt = value.createdAt,
fromCurrencyCode = value.fromCurrencyCode,
fromAmount = value.fromAmount,
toContractAddress = value.toContractAddress,
toNetwork = value.toNetwork,
toDecimals = value.toDecimals,
toAmount = value.toAmount,
toActualAmount = value.toActualAmount,
paymentMethod = value.paymentMethod,
countryCode = value.countryCode,
)
}
}

View file

@ -1,9 +1,13 @@
package com.tangem.data.onramp.di
import com.tangem.data.onramp.DefaultOnrampErrorResolver
import com.tangem.data.onramp.DefaultOnrampRepository
import com.tangem.data.onramp.DefaultOnrampTransactionRepository
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -28,4 +32,22 @@ internal object OnrampDataModule {
appPreferencesStore = appPreferencesStore,
)
}
@Provides
@Singleton
fun provideOnrampTransactionRepository(
appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider,
): OnrampTransactionRepository {
return DefaultOnrampTransactionRepository(
appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideOnrampErrorResolver(): OnrampErrorResolver {
return DefaultOnrampErrorResolver()
}
}

View file

@ -293,23 +293,28 @@ internal class DefaultCurrenciesRepository(
userWalletId: UserWalletId,
id: CryptoCurrency.ID,
): CryptoCurrency = withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
val response = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
responseCurrenciesFactory.createCurrency(
currencyId = id,
response = response,
scanResponse = userWallet.scanResponse,
)
getMultiCurrencyWalletCurrency(userWalletId, id.value)
}
override suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: String): CryptoCurrency =
withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true)
val response = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
},
)
responseCurrenciesFactory.createCurrency(
currencyId = id,
response = response,
scanResponse = userWallet.scanResponse,
)
}
override suspend fun getNetworkCoin(
userWalletId: UserWalletId,
networkId: Network.ID,

View file

@ -6,6 +6,8 @@ plugins {
dependencies {
api(projects.domain.onramp.models)
api(projects.domain.tokens.models)
api(projects.domain.wallets.models)
api(projects.domain.core)
implementation(deps.kotlin.serialization)

View file

@ -6,6 +6,8 @@ plugins {
dependencies {
implementation(projects.domain.core)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(deps.kotlin.serialization)
implementation(deps.jodatime)

View file

@ -0,0 +1,54 @@
package com.tangem.domain.onramp.model
import kotlinx.serialization.Serializable
@Serializable
data class OnrampStatus(
val txId: String,
val providerId: String,
val payoutAddress: String,
val status: Status,
val failReason: String?,
val externalTxId: String,
val externalTxUrl: String?,
val payoutHash: String?,
val createdAt: String,
val fromCurrencyCode: String,
val fromAmount: String,
val toContractAddress: String,
val toNetwork: String,
val toDecimals: String,
val toAmount: String?,
val toActualAmount: String?,
val paymentMethod: String,
val countryCode: String,
) {
enum class Status(val order: Int) {
Created(order = 1),
Expired(order = 2),
Paused(order = 3),
WaitingForPayment(order = 4),
PaymentProcessing(order = 5),
Verifying(order = 6),
Failed(order = 7),
Paid(order = 8),
Sending(order = 9),
Finished(order = 10),
;
fun isTerminal(): Boolean = when (this) {
Expired,
Failed,
Paused,
Finished,
-> true
Created,
WaitingForPayment,
PaymentProcessing,
Verifying,
Paid,
Sending,
-> false
}
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.onramp.model.cache
import com.tangem.domain.core.serialization.SerializedBigDecimal
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.serialization.Serializable
@Serializable
data class OnrampTransaction(
val txId: String,
val userWalletId: UserWalletId,
val fromAmount: SerializedBigDecimal,
val fromCurrency: OnrampCurrency,
val toAmount: SerializedBigDecimal,
val toCurrencyId: String,
val providerName: String,
val providerImageUrl: String,
)

View file

@ -0,0 +1,21 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
import com.tangem.domain.onramp.repositories.OnrampRepository
class GetOnrampStatusUseCase(
private val onrampRepository: OnrampRepository,
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(txId: String): Either<OnrampError, OnrampStatus> {
return Either.catch {
onrampRepository.getStatus(txId)
}.mapLeft {
errorResolver.resolve(it)
}
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
class GetOnrampTransactionUseCase(
private val onrampTransactionRepository: OnrampTransactionRepository,
) {
suspend operator fun invoke(txId: String): Either<OnrampError, OnrampTransaction> {
return Either.catch {
requireNotNull(
onrampTransactionRepository.getTransactionById(txId),
)
}.mapLeft {
OnrampError.UnknownError
}
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
class GetOnrampTransactionsUseCase(
private val onrampTransactionRepository: OnrampTransactionRepository,
) {
operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrencyId: CryptoCurrency.ID,
): Either<OnrampError, Flow<List<OnrampTransaction>>> {
return Either.catch {
onrampTransactionRepository.getTransactions(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrencyId,
)
}.mapLeft {
OnrampError.UnknownError
}
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
class OnrampRemoveTransactionUseCase(
private val onrampTransactionRepository: OnrampTransactionRepository,
) {
suspend operator fun invoke(txId: String): Either<OnrampError, Unit> {
return Either.catch {
onrampTransactionRepository.removeTransaction(txId)
}.mapLeft {
OnrampError.UnknownError
}
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.domain.onramp
import arrow.core.Either
import com.tangem.domain.onramp.model.OnrampError
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
class OnrampSaveTransactionUseCase(
private val onrampTransactionRepository: OnrampTransactionRepository,
) {
suspend operator fun invoke(transaction: OnrampTransaction): Either<OnrampError, Unit> {
return Either.catch { onrampTransactionRepository.storeTransaction(transaction) }
.mapLeft { OnrampError.UnknownError }
}
}

View file

@ -2,12 +2,17 @@ package com.tangem.domain.onramp.repositories
import com.tangem.domain.onramp.model.OnrampCountry
import com.tangem.domain.onramp.model.OnrampCurrency
import com.tangem.domain.onramp.model.OnrampStatus
import kotlinx.coroutines.flow.Flow
interface OnrampRepository {
// api
suspend fun getCurrencies(): List<OnrampCurrency>
suspend fun getCountries(): List<OnrampCountry>
suspend fun getCountryByIp(): OnrampCountry
suspend fun getStatus(txId: String): OnrampStatus
// cache
suspend fun saveDefaultCurrency(currency: OnrampCurrency)
suspend fun getDefaultCurrencySync(): OnrampCurrency?
fun getDefaultCurrency(): Flow<OnrampCurrency?>

View file

@ -0,0 +1,17 @@
package com.tangem.domain.onramp.repositories
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
interface OnrampTransactionRepository {
suspend fun storeTransaction(transaction: OnrampTransaction)
suspend fun getTransactionById(txId: String): OnrampTransaction?
fun getTransactions(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow<List<OnrampTransaction>>
suspend fun removeTransaction(txId: String)
}

View file

@ -27,6 +27,22 @@ class GetCryptoCurrencyUseCase(
return either { getCurrency(userWalletId, id) }
}
/**
* Returns specific cryptocurrency for a given user wallet.
*
* !!! Important Use only [CryptoCurrency.ID.value] as cryptoCurrencyId
*
* @param userWalletId The ID of the user's wallet.
* @param cryptoCurrencyId String representation of the [CryptoCurrency.ID.value] of the cryptocurrency.
* @return An [Either] representing success (Right) or an error (Left) in fetching the status.
*/
suspend operator fun invoke(
userWalletId: UserWalletId,
cryptoCurrencyId: String,
): Either<CurrencyStatusError, CryptoCurrency> {
return either { getCurrency(userWalletId, cryptoCurrencyId) }
}
/**
* Returns the primary cryptocurrency for a given user wallet.
*
@ -49,6 +65,18 @@ class GetCryptoCurrencyUseCase(
)
}
private suspend fun Raise<CurrencyStatusError>.getCurrency(
userWalletId: UserWalletId,
id: String,
): CryptoCurrency {
return catch(
block = {
currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id)
},
catch = { raise(CurrencyStatusError.DataError(it)) },
)
}
private suspend fun Raise<CurrencyStatusError>.getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency {
return catch(
block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) },

View file

@ -167,6 +167,17 @@ interface CurrenciesRepository {
*/
suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: CryptoCurrency.ID): CryptoCurrency
/**
* Retrieves the cryptocurrency for a specific multi-currency user wallet.
*
* @param userWalletId The unique identifier of the user wallet.
* @param id The unique identifier of the cryptocurrency to be retrieved.
* @return The cryptocurrency associated with the user wallet and ID.
* @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: String): CryptoCurrency
/**
* Get the coin for a specific network.
*

View file

@ -100,6 +100,14 @@ internal class MockCurrenciesRepository(
return token
}
override suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: String): CryptoCurrency {
val token = token.getOrElse { e -> throw e }
require(token.id.value == id)
return token
}
override suspend fun getNetworkCoin(
userWalletId: UserWalletId,
networkId: Network.ID,

View file

@ -0,0 +1,11 @@
package com.tangem.features.onramp.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface OnrampSuccessComponent : ComposableContentComponent {
data class Params(val txId: String)
interface Factory : ComponentFactory<Params, OnrampSuccessComponent>
}

View file

@ -22,6 +22,7 @@ dependencies {
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.core.featuretoggles)
implementation(projects.core.navigation)
/** Project - Common */
implementation(projects.common.routing)
@ -42,7 +43,6 @@ dependencies {
/** DI */
implementation(deps.hilt.android)
implementation(project(":common:ui"))
kapt(deps.hilt.kapt)
/** AndroidX */

View file

@ -0,0 +1,42 @@
package com.tangem.features.onramp.success
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.onramp.component.OnrampSuccessComponent
import com.tangem.features.onramp.success.model.OnrampSuccessComponentModel
import com.tangem.features.onramp.success.ui.OnrampSuccessComponentContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultOnrampSuccessComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: OnrampSuccessComponent.Params,
) : OnrampSuccessComponent, AppComponentContext by appComponentContext {
private val model: OnrampSuccessComponentModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsState()
BackHandler(onBack = router::pop)
OnrampSuccessComponentContent(
state = state,
onBackClick = router::pop,
)
}
@AssistedFactory
interface Factory : OnrampSuccessComponent.Factory {
override fun create(
context: AppComponentContext,
params: OnrampSuccessComponent.Params,
): DefaultOnrampSuccessComponent
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.onramp.success.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.onramp.success.model.OnrampSuccessComponentModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(DecomposeComponent::class)
internal interface OnrampSuccessComponentModelModule {
@Binds
@IntoMap
@ClassKey(OnrampSuccessComponentModel::class)
fun bindOnrampSuccessComponentModel(model: OnrampSuccessComponentModel): Model
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.onramp.success.di
import com.tangem.features.onramp.component.OnrampSuccessComponent
import com.tangem.features.onramp.success.DefaultOnrampSuccessComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface OnrampSuccessComponentModule {
@Binds
@Singleton
fun bindOnrampSuccessComponentFactory(
factory: DefaultOnrampSuccessComponent.Factory,
): OnrampSuccessComponent.Factory
}

View file

@ -0,0 +1,5 @@
package com.tangem.features.onramp.success.entity
interface OnrampSuccessClickIntents {
fun goToProviderClick(providerLink: String)
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.onramp.success.entity
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.extensions.TextReference
sealed class OnrampSuccessComponentUM {
data object Loading : OnrampSuccessComponentUM()
data class Content(
val txId: String,
val timestamp: Long,
val currencyImageUrl: String,
val fromAmount: TextReference,
val toAmount: TextReference,
val statusBlock: ExpressStatusUM,
val providerName: TextReference,
val providerImageUrl: String,
val notification: NotificationUM?,
) : OnrampSuccessComponentUM()
}

View file

@ -0,0 +1,194 @@
package com.tangem.features.onramp.success.entity.conterter
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.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.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.success.entity.OnrampSuccessComponentUM
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
import org.joda.time.DateTime
internal class SetOnrampSuccessContentConverter(
private val cryptoCurrency: CryptoCurrency,
private val transaction: OnrampTransaction,
private val goToProviderClick: (String) -> Unit,
) : Converter<OnrampStatus, OnrampSuccessComponentUM> {
override fun convert(value: OnrampStatus): OnrampSuccessComponentUM {
return OnrampSuccessComponentUM.Content(
txId = value.txId,
timestamp = DateTime.parse(value.createdAt).millis,
currencyImageUrl = transaction.fromCurrency.image,
fromAmount = stringReference(
transaction.fromAmount.format {
fiat(
fiatCurrencyCode = transaction.fromCurrency.name,
fiatCurrencySymbol = transaction.fromCurrency.code,
)
},
),
toAmount = stringReference(
transaction.toAmount.format {
crypto(cryptoCurrency)
},
),
providerName = stringReference(transaction.providerName),
providerImageUrl = transaction.providerImageUrl,
statusBlock = convertStatuses(value.status, value.externalTxUrl),
notification = getNotification(value.status, value.externalTxUrl),
)
}
private fun getNotification(status: OnrampStatus.Status, externalTxUrl: String?): NotificationUM? {
if (externalTxUrl == null) return null
return when (status) {
OnrampStatus.Status.Verifying -> {
ExpressNotificationsUM.NeedVerification {
goToProviderClick(externalTxUrl)
}
}
OnrampStatus.Status.Failed -> {
ExpressNotificationsUM.FailedByProvider {
goToProviderClick(externalTxUrl)
}
}
else -> null
}
}
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(cryptoCurrency.name))
}
this == OnrampStatus.Status.Paid -> {
resourceReference(R.string.express_status_buying_active, wrappedList(cryptoCurrency.name))
}
else -> {
resourceReference(R.string.express_status_bought, wrappedList(cryptoCurrency.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(cryptoCurrency.name))
}
this == OnrampStatus.Status.Sending -> {
resourceReference(
R.string.express_exchange_status_sending_active,
wrappedList(cryptoCurrency.name),
)
}
else -> {
resourceReference(R.string.express_exchange_status_sent, wrappedList(cryptoCurrency.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 = {
goToProviderClick(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

@ -0,0 +1,50 @@
package com.tangem.features.onramp.success.entity.previewdata
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.success.entity.OnrampSuccessComponentUM
import kotlinx.collections.immutable.persistentListOf
import org.joda.time.DateTime
internal data object OnrampSuccessComponentUMPreviewData {
val loadingState = OnrampSuccessComponentUM.Loading
val contentState = OnrampSuccessComponentUM.Content(
txId = "b2894851-7b63-4f56-bd75-e408f1dcba31",
timestamp = DateTime.parse("2023-09-20T13:45:10.868Z").millis,
providerName = stringReference("1Inch"),
providerImageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/1INCH1024.png",
fromAmount = stringReference("100 USDT"),
toAmount = stringReference("99.99 $"),
currencyImageUrl = "",
notification = null,
statusBlock = ExpressStatusUM(
title = resourceReference(R.string.express_exchange_status_title),
link = ExpressLinkUM.Empty,
statuses = persistentListOf(
ExpressStatusItemUM(
text = stringReference("Deposit received"),
state = ExpressStatusItemState.Done,
),
ExpressStatusItemUM(
text = stringReference("Confirmed"),
state = ExpressStatusItemState.Done,
),
ExpressStatusItemUM(
text = stringReference("Buying Bitcoin..."),
state = ExpressStatusItemState.Active,
),
ExpressStatusItemUM(
text = stringReference("Sending to you"),
state = ExpressStatusItemState.Default,
),
),
),
)
}

View file

@ -0,0 +1,97 @@
package com.tangem.features.onramp.success.model
import arrow.core.getOrElse
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.onramp.GetOnrampStatusUseCase
import com.tangem.domain.onramp.GetOnrampTransactionUseCase
import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase
import com.tangem.domain.onramp.model.OnrampStatus
import com.tangem.domain.onramp.model.cache.OnrampTransaction
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
import com.tangem.features.onramp.component.OnrampSuccessComponent
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
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
internal class OnrampSuccessComponentModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val urlOpener: UrlOpener,
private val getOnrampTransactionUseCase: GetOnrampTransactionUseCase,
private val getOnrampStatusUseCase: GetOnrampStatusUseCase,
private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase,
private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase,
paramsContainer: ParamsContainer,
) : Model(), OnrampSuccessClickIntents {
private val params: OnrampSuccessComponent.Params = paramsContainer.require()
private val _state: MutableStateFlow<OnrampSuccessComponentUM> = MutableStateFlow(
value = OnrampSuccessComponentUM.Loading,
)
val state: StateFlow<OnrampSuccessComponentUM> get() = _state.asStateFlow()
init {
loadData()
}
override fun goToProviderClick(providerLink: String) {
urlOpener.openUrl(providerLink)
}
private fun loadData() {
modelScope.launch {
getOnrampTransactionUseCase(txId = params.txId)
.fold(
ifLeft = {
Timber.e(it.toString())
},
ifRight = { transaction ->
loadTransactionStatus(transaction)
},
)
}
}
private suspend fun loadTransactionStatus(transaction: OnrampTransaction) {
val cryptoCurrencies = getCryptoCurrencyUseCase(
transaction.userWalletId,
transaction.toCurrencyId,
).getOrElse { error("Crypto currency not found") }
getOnrampStatusUseCase(txId = params.txId)
.fold(
ifLeft = {
Timber.e(it.toString())
},
ifRight = { status ->
_state.update {
SetOnrampSuccessContentConverter(
cryptoCurrency = cryptoCurrencies,
transaction = transaction,
goToProviderClick = ::goToProviderClick,
).convert(status)
}
removeTransactionIfTerminalStatus(status)
},
)
}
private fun removeTransactionIfTerminalStatus(status: OnrampStatus) {
modelScope.launch {
if (status.status.isTerminal()) {
onrampRemoveTransactionUseCase(status.txId)
}
}
}
}

View file

@ -0,0 +1,220 @@
package com.tangem.features.onramp.success.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.common.ui.expressStatus.ExpressStatusBlock
import com.tangem.common.ui.expressStatus.ExpressStatusNotificationBlock
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.features.onramp.impl.R
import com.tangem.features.onramp.success.entity.OnrampSuccessComponentUM
import com.tangem.features.onramp.success.entity.previewdata.OnrampSuccessComponentUMPreviewData
@Composable
internal fun OnrampSuccessComponentContent(
state: OnrampSuccessComponentUM,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (state !is OnrampSuccessComponentUM.Content) {
Box(modifier.background(TangemTheme.colors.background.secondary))
} else {
Content(
state = state,
onBackClick = onBackClick,
modifier = modifier,
)
}
}
@Composable
private fun Content(state: OnrampSuccessComponentUM.Content, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
Scaffold(
modifier = modifier
.background(TangemTheme.colors.background.secondary)
.systemBarsPadding(),
topBar = {
TangemTopAppBar(
startButton = TopAppBarButtonUM(
iconRes = R.drawable.ic_close_24,
onIconClicked = onBackClick,
),
)
},
bottomBar = {
PrimaryButton(
text = stringResource(R.string.common_close),
onClick = onBackClick,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
)
},
contentWindowInsets = WindowInsetsZero,
) { scaffoldPaddings ->
Column(
modifier = Modifier
.background(TangemTheme.colors.background.secondary)
.padding(scaffoldPaddings)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp),
) {
TransactionDoneTitle(
title = resourceReference(R.string.common_in_progress),
subtitle = resourceReference(
R.string.send_date_format,
wrappedList(
state.timestamp.toTimeFormat(DateTimeFormatters.dateFormatter),
state.timestamp.toTimeFormat(),
),
),
)
SpacerH24()
AmountBlock(state)
SpacerH12()
FooterContainer(
footer = resourceReference(R.string.onramp_transaction_status_footer_text),
) {
ExpressStatusBlock(state.statusBlock)
}
ExpressStatusNotificationBlock(
state.notification,
)
}
}
}
@Composable
private fun AmountBlock(state: OnrampSuccessComponentUM.Content) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.padding(16.dp),
) {
val iconModifier = Modifier.size(size = 40.dp)
SubcomposeAsyncImage(
modifier = iconModifier,
model = ImageRequest.Builder(context = LocalContext.current)
.data(state.currencyImageUrl)
.crossfade(enable = true)
.allowHardware(false)
.build(),
loading = { CircleShimmer(modifier = iconModifier) },
error = { },
contentDescription = null,
)
ResizableText(
text = state.fromAmount.resolveReference(),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
)
Text(
text = state.toAmount.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
)
SpacerH12()
AmountProviderBlock(
state.providerName,
state.providerImageUrl,
)
}
}
@Composable
private fun AmountProviderBlock(providerName: TextReference, providerImageUrl: String, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Text(
text = stringResource(R.string.common_with),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
SubcomposeAsyncImage(
modifier = Modifier.size(size = 16.dp),
model = ImageRequest.Builder(context = LocalContext.current)
.data(providerImageUrl)
.crossfade(enable = true)
.allowHardware(false)
.build(),
loading = { CircleShimmer() },
error = { },
contentDescription = null,
)
Text(
text = providerName.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360, heightDp = 720)
@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun OnrampSuccessComponentContent_Preview(
@PreviewParameter(OnrampSuccessComponentContentPreviewProvider::class)
data: OnrampSuccessComponentUM,
) {
TangemThemePreview {
OnrampSuccessComponentContent(
state = data,
onBackClick = {},
)
}
}
private class OnrampSuccessComponentContentPreviewProvider :
PreviewParameterProvider<OnrampSuccessComponentUM> {
override val values: Sequence<OnrampSuccessComponentUM>
get() = sequenceOf(
OnrampSuccessComponentUMPreviewData.contentState,
)
}
// endregion

View file

@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.inputrow.InputRowEnterAmount
import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmount
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
@ -41,7 +40,7 @@ internal fun SendCustomFee(
repeat(customValues.size) { index ->
val value = customValues[index]
FooterContainer(
footer = value.footer.resolveReference(),
footer = value.footer,
) {
if (value.label != null) {
InputRowEnterInfoAmount(

View file

@ -21,8 +21,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.components.inputrow.InputRowRecipient
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.send.impl.R
@ -32,8 +36,6 @@ import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.state.previewdata.RecipientStatePreviewData
import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import kotlinx.collections.immutable.ImmutableList
@ -107,7 +109,7 @@ private fun LazyListScope.addressItem(
) {
item(key = ADDRESS_FIELD_KEY) {
FooterContainer(
footer = stringResource(R.string.send_recipient_address_footer, network),
footer = resourceReference(R.string.send_recipient_address_footer, wrappedList(network)),
) {
InputRowRecipient(
value = address.value,
@ -137,7 +139,7 @@ private fun LazyListScope.memoField(memoField: SendTextField.RecipientMemo?, onM
value = memoField.value,
label = memoField.label,
placeholder = placeholder,
footer = stringResource(R.string.send_recipient_memo_footer),
footer = resourceReference(R.string.send_recipient_memo_footer),
onValueChange = memoField.onValueChange,
onPasteClick = onMemoChange,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing20),

View file

@ -27,7 +27,7 @@ internal fun TextFieldWithPaste(
onValueChange: (String) -> Unit,
onPasteClick: (String) -> Unit,
modifier: Modifier = Modifier,
footer: String? = null,
footer: TextReference? = null,
labelStyle: TextStyle = TangemTheme.typography.body2,
error: TextReference? = null,
isError: Boolean = false,

View file

@ -19,6 +19,7 @@ internal sealed interface ExchangeStatusNotifications {
sealed class CommonNotification(val config: NotificationConfig) : ExchangeStatusNotifications
@Deprecated("Use one in ExpressNotificationsUM")
data class NeedVerification(val onGoToProviderClick: () -> Unit) : CommonNotification(
config = NotificationConfig(
title = resourceReference(R.string.express_exchange_notification_verification_title),
@ -31,6 +32,7 @@ internal sealed interface ExchangeStatusNotifications {
),
)
@Deprecated("Use one in ExpressNotificationsUM")
data class Failed(val onGoToProviderClick: () -> Unit) : CommonNotification(
config = NotificationConfig(
title = resourceReference(R.string.express_exchange_notification_failed_title),

View file

@ -30,6 +30,7 @@ import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Deprecated("Use ExpressStatusBlock from common")
@Composable
internal fun ExchangeStatusBlock(
statuses: ImmutableList<ExchangeStatusState>,