Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-17 13:50:20 +05:00
parent ddc5c4836b
commit 321b764887
37 changed files with 870 additions and 141 deletions

View file

@ -634,7 +634,7 @@ internal class ChildFactory @Inject constructor(
is AppRoute.TangemPayDetails -> {
createComponentChild(
context = context,
params = TangemPayDetailsComponent.Params(config = route.config),
params = TangemPayDetailsComponent.Params(userWalletId = route.userWalletId, config = route.config),
componentFactory = tangemPayDetailsComponentFactory,
)
}

View file

@ -400,8 +400,9 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
data class TangemPayDetails(
val userWalletId: UserWalletId,
val config: TangemPayDetailsConfig,
) : AppRoute(path = "/tangem_pay_details")
) : AppRoute(path = "/tangem_pay_details/${userWalletId.stringValue}")
@Serializable
data class TangemPayOnboarding(

View file

@ -1302,6 +1302,17 @@
<string name="swapping_to_title">You receive</string>
<string name="swapping_token_list_title">Choose token</string>
<string name="swapping_token_not_available">not available</string>
<string name="tangem_pay_deposit">Deposit</string>
<string name="tangem_pay_dispute">Dispute</string>
<string name="tangem_pay_explore_transaction">Explore transaction</string>
<string name="tangem_pay_fee_subtitle">Service fees</string>
<string name="tangem_pay_fee_title">Fee</string>
<string name="tangem_pay_status_completed">Completed</string>
<string name="tangem_pay_status_declined">Declined</string>
<string name="tangem_pay_status_pending">Pending</string>
<string name="tangem_pay_transaction_declined_notification_text">The bank rejected this transaction request.</string>
<string name="tangem_pay_transaction_fee_notification_text">This fee goes to cover the cost of handling your transfer.</string>
<string name="tangem_pay_withdrawal">Withdrawal</string>
<string name="tangempay_card_details_error_text">Failed to load data. Try again later.</string>
<string name="tangempay_card_details_hide_text">Hide</string>
<string name="tangempay_card_details_receive_error_description">Technical issues detected. Please try again later or contact support.</string>

View file

@ -0,0 +1,33 @@
package com.tangem.core.ui.extensions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
/**
* Utility class for keeping themed color reference from app theme.
*
* It necessary to use [Immutable] annotation for runtime stability.
*
* @property value color provider from theme
*/
@Immutable
data class ColorReference(val value: @Composable () -> Color)
/**
* Creates a [ColorReference] using a themed color from the app theme with a lambda.
*
* @param value The color provider from theme.
* @return A [ColorReference] representing the themed color.
*/
fun themedColor(value: @Composable () -> Color): ColorReference {
return ColorReference(value)
}
/**
* Resolves [ColorReference] to [Color]
*/
@Composable
fun ColorReference.resolveReference(): Color {
return value()
}

View file

@ -1,8 +1,10 @@
package com.tangem.data.pay.repository
import com.squareup.moshi.Moshi
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.visa.utils.TangemPayTxHistoryItemConverter
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchFlow
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext
@ -26,8 +28,11 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
private val cacheRegistry: CacheRegistry,
private val txHistoryItemsStore: TangemPayTxHistoryItemsStore,
private val dispatchers: CoroutineDispatcherProvider,
@NetworkMoshi private val moshi: Moshi,
) : TangemPayTxHistoryRepository {
private val txHistoryItemConverter by lazy { TangemPayTxHistoryItemConverter(moshi) }
override fun getTxHistoryBatchFlow(
batchSize: Int,
context: TangemPayTxHistoryListBatchingContext,
@ -84,7 +89,7 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor(
val result = requestPerformer.request { authHeader ->
visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor)
}.result
val items = TangemPayTxHistoryItemConverter.convertList(result.transactions).filterNotNull()
val items = txHistoryItemConverter.convertList(result.transactions).filterNotNull()
txHistoryItemsStore.store(key = customerWalletAddress, cursor = cursor ?: INITIAL_CURSOR, value = items)
}.onLeft { error(it.toString()) }
}

View file

@ -1,14 +1,19 @@
package com.tangem.data.visa.utils
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.pay.models.response.TangemPayTxHistoryResponse
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.utils.converter.Converter
import timber.log.Timber
import java.util.Currency
internal object TangemPayTxHistoryItemConverter :
internal class TangemPayTxHistoryItemConverter(moshi: Moshi) :
Converter<TangemPayTxHistoryResponse.Transaction, TangemPayTxHistoryItem?> {
private val spendAdapter = moshi.adapter(TangemPayTxHistoryResponse.Spend::class.java)
private val paymentAdapter = moshi.adapter(TangemPayTxHistoryResponse.Payment::class.java)
private val feeAdapter = moshi.adapter(TangemPayTxHistoryResponse.Fee::class.java)
override fun convert(value: TangemPayTxHistoryResponse.Transaction): TangemPayTxHistoryItem? {
return value.spend?.let { convertSpend(id = value.id, spend = it) }
?: value.payment?.let { convertPayment(id = value.id, payment = it) }
@ -22,6 +27,7 @@ internal object TangemPayTxHistoryItemConverter :
private fun convertSpend(id: String, spend: TangemPayTxHistoryResponse.Spend): TangemPayTxHistoryItem.Spend {
return TangemPayTxHistoryItem.Spend(
id = id,
jsonRepresentation = spendAdapter.toJson(spend),
// If postedAt is null, it means transaction wasn't posted and was likely declined. Use authorizedAt
date = spend.postedAt ?: spend.authorizedAt,
amount = spend.amount,
@ -41,15 +47,18 @@ internal object TangemPayTxHistoryItemConverter :
): TangemPayTxHistoryItem.Payment {
return TangemPayTxHistoryItem.Payment(
id = id,
jsonRepresentation = paymentAdapter.toJson(payment),
date = payment.postedAt,
currency = Currency.getInstance(payment.currency),
amount = payment.amount,
transactionHash = payment.transactionHash,
)
}
private fun convertFee(id: String, fee: TangemPayTxHistoryResponse.Fee): TangemPayTxHistoryItem.Fee {
return TangemPayTxHistoryItem.Fee(
id = id,
jsonRepresentation = feeAdapter.toJson(fee),
date = fee.postedAt,
currency = Currency.getInstance(fee.currency),
amount = fee.amount,

View file

@ -1,5 +1,6 @@
package com.tangem.domain.feedback.models
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.domain.visa.model.VisaTxDetails
/**
@ -64,5 +65,10 @@ sealed interface FeedbackEmailType {
val visaTxDetails: VisaTxDetails,
override val walletMetaInfo: WalletMetaInfo,
) : Visa()
data class DisputeV2(
val item: TangemPayTxHistoryItem,
override val walletMetaInfo: WalletMetaInfo,
) : Visa()
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.domain.feedback
import com.tangem.domain.feedback.models.*
import com.tangem.domain.feedback.utils.breakLine
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.domain.visa.model.VisaTxDetails
import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses
@ -9,6 +10,10 @@ internal class FeedbackDataBuilder {
private val builder = StringBuilder()
fun addTangemPayTxInfo(item: TangemPayTxHistoryItem) {
builder.append(item.jsonRepresentation)
}
fun addVisaTxInfo(txDetails: VisaTxDetails) {
builder.appendKeyValue("Type", txDetails.type)
builder.appendKeyValue("Status", txDetails.status)

View file

@ -64,6 +64,7 @@ class SendFeedbackEmailUseCase(
is FeedbackEmailType.PreActivatedWallet,
is FeedbackEmailType.CardAttestationFailed,
is FeedbackEmailType.Visa.Dispute,
is FeedbackEmailType.Visa.DisputeV2,
-> this
is FeedbackEmailType.DirectUserRequest,
is FeedbackEmailType.RateCanBeBetter,

View file

@ -1,9 +1,10 @@
package com.tangem.domain.feedback.utils
import com.tangem.domain.feedback.FeedbackDataBuilder
import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.domain.visa.model.VisaTxDetails
/**
@ -33,11 +34,21 @@ internal class EmailMessageBodyResolver(
is FeedbackEmailType.Visa.Activation -> addUserRequestBody(type.walletMetaInfo)
is FeedbackEmailType.Visa.DirectUserRequest -> addUserRequestBody(type.walletMetaInfo)
is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody(type.walletMetaInfo, type.visaTxDetails)
is FeedbackEmailType.Visa.DisputeV2 -> addTangemPayRequestBody(type.walletMetaInfo, type.item)
}
return build()
}
private suspend fun FeedbackDataBuilder.addTangemPayRequestBody(
walletMetaInfo: WalletMetaInfo,
item: TangemPayTxHistoryItem,
) {
addUserRequestBody(walletMetaInfo)
addDelimiter()
addTangemPayTxInfo(item)
}
private suspend fun FeedbackDataBuilder.addVisaRequestBody(
walletMetaInfo: WalletMetaInfo,
visaTxDetails: VisaTxDetails,

View file

@ -23,6 +23,7 @@ internal class EmailMessageTitleResolver(private val resources: Resources) {
is FeedbackEmailType.Visa.Activation,
is FeedbackEmailType.Visa.DirectUserRequest,
is FeedbackEmailType.Visa.Dispute,
is FeedbackEmailType.Visa.DisputeV2,
-> R.string.feedback_preface_support
is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative
is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed

View file

@ -39,7 +39,9 @@ internal class EmailSubjectResolver(private val resources: Resources) {
FeedbackEmailType.CardAttestationFailed -> "Card attestation failed"
is FeedbackEmailType.Visa.Activation -> "[Visa] [Activation] {auto-filled subject}"
is FeedbackEmailType.Visa.DirectUserRequest -> "[Visa] {auto-filled subject}"
is FeedbackEmailType.Visa.Dispute -> "[Visa] [DISPUTE] {auto-filled subject}"
is FeedbackEmailType.Visa.Dispute,
is FeedbackEmailType.Visa.DisputeV2,
-> "[Visa] [DISPUTE] {auto-filled subject}"
}
}
}

View file

@ -18,6 +18,7 @@ dependencies {
implementation(deps.moshi.kotlin)
implementation(deps.moshi.adapters)
implementation(deps.kotlin.datetime)
implementation(deps.jodatime)
implementation(deps.kotlin.serialization)
ksp(deps.moshi.kotlin.codegen)
implementation(deps.arrow.core)

View file

@ -0,0 +1,24 @@
package com.tangem.domain.models.serialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.util.Currency
typealias SerializedCurrency = @Serializable(with = CurrencySerializer::class) Currency
object CurrencySerializer : KSerializer<Currency> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("java.util.Currency", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Currency) {
encoder.encodeString(value.currencyCode)
}
override fun deserialize(decoder: Decoder): Currency {
return Currency.getInstance(decoder.decodeString())
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.domain.models.serialization
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import org.joda.time.DateTime
typealias SerializedDateTime = @Serializable(with = JodaDateTimeSerializer::class) DateTime
object JodaDateTimeSerializer : KSerializer<DateTime> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("DateTime", PrimitiveKind.LONG)
override fun serialize(encoder: Encoder, value: DateTime) {
encoder.encodeLong(value.millis)
}
override fun deserialize(decoder: Decoder): DateTime {
return DateTime(decoder.decodeLong())
}
}

View file

@ -1,20 +1,25 @@
package com.tangem.domain.visa.model
import org.joda.time.DateTime
import java.math.BigDecimal
import java.util.Currency
import com.tangem.domain.models.serialization.SerializedBigDecimal
import com.tangem.domain.models.serialization.SerializedCurrency
import com.tangem.domain.models.serialization.SerializedDateTime
import kotlinx.serialization.Serializable
@Serializable
sealed class TangemPayTxHistoryItem {
abstract val id: String
abstract val date: DateTime
abstract val amount: BigDecimal
abstract val currency: Currency
abstract val date: SerializedDateTime
abstract val amount: SerializedBigDecimal
abstract val currency: SerializedCurrency
abstract val jsonRepresentation: String
@Serializable
data class Spend(
override val id: String,
override val date: DateTime,
override val amount: BigDecimal,
override val currency: Currency,
override val jsonRepresentation: String,
override val date: SerializedDateTime,
override val amount: SerializedBigDecimal,
override val currency: SerializedCurrency,
val enrichedMerchantName: String?,
val merchantName: String,
val enrichedMerchantCategory: String?,
@ -23,18 +28,23 @@ sealed class TangemPayTxHistoryItem {
val enrichedMerchantIconUrl: String?,
) : TangemPayTxHistoryItem()
@Serializable
data class Payment(
override val id: String,
override val date: DateTime,
override val amount: BigDecimal,
override val currency: Currency,
override val jsonRepresentation: String,
override val date: SerializedDateTime,
override val amount: SerializedBigDecimal,
override val currency: SerializedCurrency,
val transactionHash: String?,
) : TangemPayTxHistoryItem()
@Serializable
data class Fee(
override val id: String,
override val date: DateTime,
override val amount: BigDecimal,
override val currency: Currency,
override val jsonRepresentation: String,
override val date: SerializedDateTime,
override val amount: SerializedBigDecimal,
override val currency: SerializedCurrency,
) : TangemPayTxHistoryItem()
enum class Status {

View file

@ -2,9 +2,10 @@ package com.tangem.features.tangempay.components
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayDetailsConfig
interface TangemPayDetailsComponent : ComposableContentComponent {
data class Params(val config: TangemPayDetailsConfig)
data class Params(val userWalletId: UserWalletId, val config: TangemPayDetailsConfig)
interface Factory : ComponentFactory<Params, TangemPayDetailsComponent>
}

View file

@ -15,8 +15,9 @@ dependencies {
/** Core */
implementation(projects.core.configToggles)
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.core.error)
implementation(projects.core.navigation)
implementation(projects.core.ui)
/** Features api */
implementation(projects.features.tangempay.details.api)
@ -26,9 +27,12 @@ dependencies {
/** Domain */
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.feedback)
implementation(projects.domain.feedback.models)
implementation(projects.domain.models)
implementation(projects.domain.visa)
implementation(projects.domain.visa.models)
implementation(projects.domain.wallets)
/** Compose */
implementation(deps.compose.coil)

View file

@ -15,8 +15,9 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.tangempay.components.txHistory.DefaultTangemPayTxHistoryComponent
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryDetailsComponent
import com.tangem.features.tangempay.model.TangemPayDetailsModel
import com.tangem.features.tangempay.model.TangemPayDetailsNavigation
import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
import com.tangem.features.tangempay.ui.TangemPayDetailsScreen
import com.tangem.features.tokenreceive.TokenReceiveComponent
import dagger.assisted.Assisted
@ -39,7 +40,10 @@ internal class DefaultTangemPayDetailsComponent @AssistedInject constructor(
)
private val txHistoryComponent = DefaultTangemPayTxHistoryComponent(
appComponentContext = child("txHistoryComponent"),
params = DefaultTangemPayTxHistoryComponent.Params(customerWalletAddress = params.config.customerWalletAddress),
params = DefaultTangemPayTxHistoryComponent.Params(
customerWalletAddress = params.config.customerWalletAddress,
uiActions = model,
),
)
@Composable
@ -59,19 +63,25 @@ internal class DefaultTangemPayDetailsComponent @AssistedInject constructor(
private fun bottomSheetChild(
navigation: TangemPayDetailsNavigation,
componentContext: ComponentContext,
): ComposableBottomSheetComponent = when (navigation) {
is TangemPayDetailsNavigation.Error -> TangemPayErrorBottomSheetComponent(
appComponentContext = appComponentContext,
messageUM = navigation.messageUM,
onDismiss = model.bottomSheetNavigation::dismiss,
)
is TangemPayDetailsNavigation.Receive -> tokenReceiveComponentFactory.create(
context = childByContext(componentContext),
params = TokenReceiveComponent.Params(
config = navigation.config,
onDismiss = model.bottomSheetNavigation::dismiss,
),
)
): ComposableBottomSheetComponent {
val context = childByContext(componentContext)
return when (navigation) {
is TangemPayDetailsNavigation.Receive -> tokenReceiveComponentFactory.create(
context = context,
params = TokenReceiveComponent.Params(
config = navigation.config,
onDismiss = model.bottomSheetNavigation::dismiss,
),
)
is TangemPayDetailsNavigation.TransactionDetails -> TangemPayTxHistoryDetailsComponent(
appComponentContext = context,
params = TangemPayTxHistoryDetailsComponent.Params(
transaction = navigation.transaction,
userWalletId = params.userWalletId,
onDismiss = model.bottomSheetNavigation::dismiss,
),
)
}
}
@AssistedFactory

View file

@ -1,23 +0,0 @@
package com.tangem.features.tangempay.components
import androidx.compose.runtime.Composable
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetV2
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
internal class TangemPayErrorBottomSheetComponent(
appComponentContext: AppComponentContext,
private val messageUM: MessageBottomSheetUMV2,
private val onDismiss: () -> Unit,
) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent {
override fun dismiss() {
onDismiss()
}
@Composable
override fun BottomSheet() {
MessageBottomSheetV2(state = messageUM, onDismissRequest = ::dismiss)
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
import com.tangem.features.tangempay.model.TangemPayTxHistoryModel
import com.tangem.features.tangempay.ui.tangemPayTxHistoryItems
import com.tangem.features.tangempay.utils.TangemPayTxHistoryUiActions
import kotlinx.coroutines.flow.StateFlow
internal class DefaultTangemPayTxHistoryComponent(
@ -21,5 +22,5 @@ internal class DefaultTangemPayTxHistoryComponent(
tangemPayTxHistoryItems(listState, state)
}
data class Params(val customerWalletAddress: String)
data class Params(val customerWalletAddress: String, val uiActions: TangemPayTxHistoryUiActions)
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.tangempay.components.txHistory
import androidx.compose.runtime.Composable
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.extensions.*
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel
import com.tangem.features.tangempay.ui.TangemPayTxHistoryDetailsContent
internal class TangemPayTxHistoryDetailsComponent(
appComponentContext: AppComponentContext,
params: Params,
) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext {
private val model: TangemPayTxHistoryDetailsModel = getOrCreateModel(params = params)
override fun dismiss() {
model.dismiss()
}
@Composable
override fun BottomSheet() {
TangemPayTxHistoryDetailsContent(state = model.uiState)
}
data class Params(
val transaction: TangemPayTxHistoryItem,
val userWalletId: UserWalletId,
val onDismiss: () -> Unit,
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.tangempay.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.tangempay.model.TangemPayDetailsModel
import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel
import com.tangem.features.tangempay.model.TangemPayTxHistoryModel
import dagger.Binds
import dagger.Module
@ -23,4 +24,9 @@ internal interface TangemPayModelModule {
@IntoMap
@ClassKey(TangemPayTxHistoryModel::class)
fun bindTangemPayTxHistoryModel(model: TangemPayTxHistoryModel): Model
@Binds
@IntoMap
@ClassKey(TangemPayTxHistoryDetailsModel::class)
fun bindTangemPayTxHistoryDetailsModel(model: TangemPayTxHistoryDetailsModel): Model
}

View file

@ -0,0 +1,15 @@
package com.tangem.features.tangempay.entity
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import kotlinx.serialization.Serializable
@Serializable
internal sealed class TangemPayDetailsNavigation {
@Serializable
data class Receive(val config: TokenReceiveConfig) : TangemPayDetailsNavigation()
@Serializable
data class TransactionDetails(val transaction: TangemPayTxHistoryItem) : TangemPayDetailsNavigation()
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.tangempay.entity
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.ColorReference
import com.tangem.core.ui.extensions.ImageReference
import com.tangem.core.ui.extensions.TextReference
internal data class TangemPayTxHistoryDetailsUM(
val title: TextReference,
val iconState: ImageReference,
val transactionTitle: TextReference,
val transactionSubtitle: TextReference,
val transactionAmount: String,
val transactionAmountColor: ColorReference,
val labelState: LabelUM?,
val notification: NotificationConfig?,
val buttonState: ButtonState,
val dismiss: () -> Unit,
) {
data class ButtonState(val text: TextReference, val onClick: () -> Unit)
}

View file

@ -4,7 +4,6 @@ import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.toArgb
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -19,13 +18,16 @@ import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.models.TokenReceiveType
import com.tangem.domain.pay.DataForReceiveFactory
import com.tangem.domain.pay.repository.CardDetailsRepository
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.components.TangemPayDetailsComponent
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType
import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.features.tangempay.model.transformers.*
import com.tangem.features.tangempay.utils.TangemPayErrorMessageFactory
import com.tangem.features.tangempay.utils.TangemPayTxHistoryUiActions
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
@ -53,7 +55,7 @@ internal class TangemPayDetailsModel @Inject constructor(
private val dataForReceiveFactory: DataForReceiveFactory,
private val clipboardManager: ClipboardManager,
private val uiMessageSender: UiMessageSender,
) : Model() {
) : Model(), TangemPayTxHistoryUiActions {
private val params: TangemPayDetailsComponent.Params = paramsContainer.require()
@ -102,11 +104,8 @@ internal class TangemPayDetailsModel @Inject constructor(
bottomSheetNavigation.activate(TangemPayDetailsNavigation.Receive(config))
}
.onLeft {
val messageUM = TangemPayErrorMessageFactory.createError(
type = TangemPayDetailsErrorType.Receive,
onDismiss = bottomSheetNavigation::dismiss,
)
bottomSheetNavigation.activate(TangemPayDetailsNavigation.Error(messageUM))
val messageUM = TangemPayErrorMessageFactory.createErrorMessage(TangemPayDetailsErrorType.Receive)
uiMessageSender.send(message = messageUM)
}
}
}
@ -163,4 +162,8 @@ internal class TangemPayDetailsModel @Inject constructor(
SnackbarMessage(TextReference.Res(R.string.tangempay_card_details_error_text)),
)
}
override fun onTransactionClick(item: TangemPayTxHistoryItem) {
bottomSheetNavigation.activate(TangemPayDetailsNavigation.TransactionDetails(item))
}
}

View file

@ -1,17 +0,0 @@
package com.tangem.features.tangempay.model
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
import com.tangem.domain.models.TokenReceiveConfig
import kotlinx.serialization.Serializable
@Serializable
internal sealed class TangemPayDetailsNavigation {
data class Receive(
val config: TokenReceiveConfig,
) : TangemPayDetailsNavigation()
data class Error(
val messageUM: MessageBottomSheetUMV2,
) : TangemPayDetailsNavigation()
}

View file

@ -0,0 +1,66 @@
package com.tangem.features.tangempay.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
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.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryDetailsComponent
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM
import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryDetailsConverter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
internal class TangemPayTxHistoryDetailsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getUserWalletsUseCase: GetWalletsUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val urlOpener: UrlOpener,
paramsContainer: ParamsContainer,
) : Model() {
private val params = paramsContainer.require<TangemPayTxHistoryDetailsComponent.Params>()
val uiState: TangemPayTxHistoryDetailsUM = TangemPayTxHistoryDetailsConverter.convert(
TangemPayTxHistoryDetailsConverter.Input(
item = params.transaction,
onExplorerClick = ::openExplorer,
onDisputeClick = ::dispute,
onDismiss = ::dismiss,
),
)
fun dismiss() {
params.onDismiss()
}
fun openExplorer(txHash: String?) {
txHash?.let(urlOpener::openUrlExternalBrowser)
}
fun dispute() {
modelScope.launch {
val userWalletId = params.userWalletId
val userWallet = getUserWalletsUseCase.invokeSync()
.firstOrNull { it.walletId == userWalletId } ?: return@launch
val walletMetaInfo = getWalletMetaInfoUseCase.invoke(
userWallet.requireColdWallet().scanResponse,
).getOrNull() ?: return@launch
sendFeedbackEmailUseCase.invoke(
FeedbackEmailType.Visa.DisputeV2(
item = params.transaction,
walletMetaInfo = walletMetaInfo,
),
)
}
}
}

View file

@ -6,17 +6,14 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.components.txHistory.DefaultTangemPayTxHistoryComponent
import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM
import com.tangem.features.tangempay.utils.TangemPayTxHistoryListManager
import com.tangem.features.tangempay.utils.TangemPayTxHistoryUiActions
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Stable
@ -26,14 +23,14 @@ internal class TangemPayTxHistoryModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
tangemPayTxHistoryRepository: TangemPayTxHistoryRepository,
paramsContainer: ParamsContainer,
) : Model(), TangemPayTxHistoryUiActions {
) : Model() {
private val params: DefaultTangemPayTxHistoryComponent.Params = paramsContainer.require()
private val listManager = TangemPayTxHistoryListManager(
repository = tangemPayTxHistoryRepository,
dispatchers = dispatchers,
customerWalletAddress = params.customerWalletAddress,
txHistoryUiActions = this,
txHistoryUiActions = params.uiActions,
)
val uiState: StateFlow<TangemPayTxHistoryUM>
@ -115,10 +112,6 @@ internal class TangemPayTxHistoryModel @Inject constructor(
.launchIn(modelScope)
}
override fun onTransactionClick(item: TangemPayTxHistoryItem) {
Timber.d("onTransactionClick: $item")
}
private fun getEmptyState(isBalanceHidden: Boolean): TangemPayTxHistoryUM.Empty {
return TangemPayTxHistoryUM.Empty(isBalanceHidden = isBalanceHidden)
}

View file

@ -0,0 +1,202 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM
import com.tangem.utils.StringsSigns
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isPositive
internal object TangemPayTxHistoryDetailsConverter :
Converter<TangemPayTxHistoryDetailsConverter.Input, TangemPayTxHistoryDetailsUM> {
private val dateFormatter = DateTimeFormatters.getBestFormatterBySkeleton("dd MMMM")
override fun convert(value: Input): TangemPayTxHistoryDetailsUM {
val transaction = value.item
return TangemPayTxHistoryDetailsUM(
title = transaction.extractDate(),
iconState = transaction.extractIcon(),
transactionTitle = transaction.extractTransactionTitle(),
transactionSubtitle = transaction.extractTransactionSubtitle(),
transactionAmount = transaction.extractAmount(),
transactionAmountColor = value.item.extractAmountColor(),
labelState = value.item.extractLabel(),
notification = value.item.extractNotification(),
buttonState = value.extractButtonState(),
dismiss = value.onDismiss,
)
}
private fun TangemPayTxHistoryItem.extractDate(): TextReference {
val date = DateTimeFormatters.formatDate(this.date, dateFormatter)
val time = DateTimeFormatters.formatDate(this.date, DateTimeFormatters.timeFormatter)
return stringReference("$date ${StringsSigns.DOT} $time")
}
private fun TangemPayTxHistoryItem.extractIcon(): ImageReference {
return when (this) {
is TangemPayTxHistoryItem.Fee -> ImageReference.Res(R.drawable.ic_percent_24)
is TangemPayTxHistoryItem.Payment -> {
if (this.amount.isPositive()) {
ImageReference.Res(R.drawable.ic_arrow_down_24)
} else {
ImageReference.Res(R.drawable.ic_arrow_up_24)
}
}
is TangemPayTxHistoryItem.Spend -> {
val merchantIcon = this.enrichedMerchantIconUrl
if (merchantIcon != null) {
ImageReference.Url(merchantIcon)
} else {
ImageReference.Res(R.drawable.ic_category_24)
}
}
}
}
private fun TangemPayTxHistoryItem.extractTransactionTitle(): TextReference {
return when (this) {
is TangemPayTxHistoryItem.Fee -> resourceReference(R.string.tangem_pay_fee_title)
is TangemPayTxHistoryItem.Spend -> stringReference(this.enrichedMerchantName ?: this.merchantName)
is TangemPayTxHistoryItem.Payment -> if (this.amount.isPositive()) {
resourceReference(R.string.tangem_pay_deposit)
} else {
resourceReference(R.string.tangem_pay_withdrawal)
}
}
}
private fun TangemPayTxHistoryItem.extractTransactionSubtitle(): TextReference {
return when (this) {
is TangemPayTxHistoryItem.Fee -> resourceReference(R.string.tangem_pay_fee_subtitle)
is TangemPayTxHistoryItem.Payment -> resourceReference(R.string.common_transfer)
is TangemPayTxHistoryItem.Spend -> stringReference(this.enrichedMerchantCategory ?: this.merchantCategory)
}
}
private fun TangemPayTxHistoryItem.extractAmount(): String {
return when (this) {
is TangemPayTxHistoryItem.Fee,
is TangemPayTxHistoryItem.Spend,
-> {
val amount = this.amount.format {
fiat(
fiatCurrencyCode = this@extractAmount.currency.currencyCode,
fiatCurrencySymbol = this@extractAmount.currency.symbol,
)
}
StringsSigns.MINUS + amount
}
is TangemPayTxHistoryItem.Payment -> {
val amount = this.amount.format {
fiat(
fiatCurrencyCode = this@extractAmount.currency.currencyCode,
fiatCurrencySymbol = this@extractAmount.currency.symbol,
)
}
if (this.amount.isPositive()) {
StringsSigns.PLUS + amount
} else {
StringsSigns.MINUS + amount
}
}
}
}
private fun TangemPayTxHistoryItem.extractAmountColor(): ColorReference {
return when (this) {
is TangemPayTxHistoryItem.Fee,
is TangemPayTxHistoryItem.Spend,
-> themedColor { TangemTheme.colors.text.primary1 }
is TangemPayTxHistoryItem.Payment -> themedColor {
if (this.amount.isPositive()) {
TangemTheme.colors.text.accent
} else {
TangemTheme.colors.text.primary1
}
}
}
}
private fun TangemPayTxHistoryItem.extractLabel(): LabelUM? {
return when (this) {
is TangemPayTxHistoryItem.Fee,
is TangemPayTxHistoryItem.Payment,
-> null
is TangemPayTxHistoryItem.Spend -> when (this.status) {
TangemPayTxHistoryItem.Status.COMPLETED -> LabelUM(
text = resourceReference(R.string.tangem_pay_status_completed),
style = LabelStyle.ACCENT,
)
TangemPayTxHistoryItem.Status.PENDING -> LabelUM(
text = resourceReference(R.string.tangem_pay_status_pending),
style = LabelStyle.REGULAR,
icon = com.tangem.core.ui.R.drawable.ic_clock_24,
)
TangemPayTxHistoryItem.Status.DECLINED -> LabelUM(
text = resourceReference(R.string.tangem_pay_status_declined),
style = LabelStyle.WARNING,
)
TangemPayTxHistoryItem.Status.RESERVED,
TangemPayTxHistoryItem.Status.UNKNOWN,
-> null
}
}
}
private fun TangemPayTxHistoryItem.extractNotification(): NotificationConfig? {
return when (this) {
is TangemPayTxHistoryItem.Payment -> null
is TangemPayTxHistoryItem.Fee -> NotificationConfig(
title = resourceReference(R.string.tangem_pay_transaction_fee_notification_text),
subtitle = TextReference.EMPTY,
iconResId = R.drawable.ic_token_info_24,
)
is TangemPayTxHistoryItem.Spend -> when (this.status) {
TangemPayTxHistoryItem.Status.DECLINED -> NotificationConfig(
title = resourceReference(R.string.tangem_pay_transaction_declined_notification_text),
subtitle = TextReference.EMPTY,
iconResId = R.drawable.ic_token_info_24,
)
TangemPayTxHistoryItem.Status.PENDING,
TangemPayTxHistoryItem.Status.COMPLETED,
TangemPayTxHistoryItem.Status.RESERVED,
TangemPayTxHistoryItem.Status.UNKNOWN,
-> null
}
}
}
private fun Input.extractButtonState(): TangemPayTxHistoryDetailsUM.ButtonState {
return when (this.item) {
is TangemPayTxHistoryItem.Fee -> TangemPayTxHistoryDetailsUM.ButtonState(
text = resourceReference(R.string.tangem_pay_dispute),
onClick = this.onDisputeClick,
)
is TangemPayTxHistoryItem.Spend -> TangemPayTxHistoryDetailsUM.ButtonState(
text = resourceReference(R.string.tangem_pay_dispute),
onClick = this.onDisputeClick,
)
is TangemPayTxHistoryItem.Payment -> TangemPayTxHistoryDetailsUM.ButtonState(
text = resourceReference(R.string.tangem_pay_explore_transaction),
onClick = { this.onExplorerClick(this.item.transactionHash) },
)
}
}
data class Input(
val item: TangemPayTxHistoryItem,
val onExplorerClick: (String?) -> Unit,
val onDisputeClick: () -> Unit,
val onDismiss: () -> Unit,
)
}

View file

@ -0,0 +1,46 @@
package com.tangem.features.tangempay.ui
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
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.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import coil.compose.rememberAsyncImagePainter
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun RemoteIcon(url: String, modifier: Modifier = Modifier) {
Icon(
modifier = modifier.clip(CircleShape),
painter = rememberAsyncImagePainter(url),
contentDescription = null,
tint = Color.Unspecified,
)
}
@Composable
internal fun LocalStaticIcon(@DrawableRes id: Int, iconSize: Dp, modifier: Modifier = Modifier) {
Box(
modifier = modifier.background(
color = TangemTheme.colors.icon.secondary.copy(alpha = 0.1F),
shape = CircleShape,
),
) {
Icon(
painter = painterResource(id),
contentDescription = null,
modifier = Modifier
.size(iconSize)
.align(Alignment.Center),
tint = TangemTheme.colors.icon.informative,
)
}
}

View file

@ -1,13 +1,11 @@
package com.tangem.features.tangempay.ui
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@ -15,7 +13,6 @@ 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.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
@ -24,7 +21,6 @@ import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ChainStyle
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import coil.compose.rememberAsyncImagePainter
import com.tangem.core.ui.R
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.RectangleShimmer
@ -264,47 +260,24 @@ private fun Icon(state: TangemPayTransactionState, modifier: Modifier = Modifier
if (state.iconUrl != null) {
RemoteIcon(modifier = modifier, url = state.iconUrl)
} else {
LocalStaticIcon(modifier = modifier, id = R.drawable.ic_category_24)
LocalStaticIcon(
modifier = modifier,
id = R.drawable.ic_category_24,
iconSize = TangemTheme.dimens.size20,
)
}
}
is TangemPayTransactionState.Content.Fee -> LocalStaticIcon(modifier = modifier, id = R.drawable.ic_percent_24)
is TangemPayTransactionState.Content.Fee -> LocalStaticIcon(
modifier = modifier,
id = R.drawable.ic_percent_24,
iconSize = TangemTheme.dimens.size20,
)
is TangemPayTransactionState.Content.Payment -> LocalStaticIcon(
modifier = modifier,
id = if (state.isIncome) R.drawable.ic_arrow_down_24 else R.drawable.ic_arrow_up_24,
iconSize = TangemTheme.dimens.size20,
)
is TangemPayTransactionState.Loading -> {
CircleShimmer(modifier = modifier.size(TangemTheme.dimens.size40))
}
}
}
@Composable
private fun RemoteIcon(url: String, modifier: Modifier = Modifier) {
Icon(
modifier = modifier
.size(TangemTheme.dimens.size40)
.clip(CircleShape),
painter = rememberAsyncImagePainter(url),
contentDescription = null,
tint = Color.Unspecified,
)
}
@Composable
private fun LocalStaticIcon(@DrawableRes id: Int, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(TangemTheme.dimens.size40)
.background(color = TangemTheme.colors.icon.secondary.copy(alpha = 0.1F), shape = CircleShape),
) {
Icon(
painter = painterResource(id),
contentDescription = null,
modifier = Modifier
.size(TangemTheme.dimens.size20)
.align(Alignment.Center),
tint = TangemTheme.colors.icon.informative,
)
is TangemPayTransactionState.Loading -> CircleShimmer(modifier = modifier)
}
}

View file

@ -0,0 +1,241 @@
package com.tangem.features.tangempay.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.ImageReference
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.stringReference
import com.tangem.core.ui.extensions.themedColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM
@Composable
internal fun TangemPayTxHistoryDetailsContent(state: TangemPayTxHistoryDetailsUM) {
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = state.dismiss,
content = TangemBottomSheetConfigContent.Empty,
),
onBack = state.dismiss,
containerColor = TangemTheme.colors.background.tertiary,
title = {
TangemModalBottomSheetTitle(
title = state.title,
endIconRes = R.drawable.ic_close_24,
onEndClick = state.dismiss,
)
},
) {
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.padding(top = 24.dp)
.size(88.dp),
iconState = state.iconState,
)
Text(
modifier = Modifier.padding(top = 32.dp),
text = state.transactionTitle.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
Text(
modifier = Modifier.padding(top = 2.dp),
text = state.transactionSubtitle.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
Text(
modifier = Modifier.padding(top = 8.dp),
text = state.transactionAmount,
style = TangemTheme.typography.head,
color = state.transactionAmountColor.resolveReference(),
)
state.labelState?.let { Label(state = state.labelState, modifier = Modifier.padding(top = 12.dp)) }
SpacerH32()
state.notification?.let {
Notification(
config = state.notification,
titleColor = TangemTheme.colors.text.tertiary,
iconTint = TangemTheme.colors.icon.secondary,
)
}
ButtonContainer(
modifier = Modifier
.padding(vertical = 16.dp)
.fillMaxWidth(),
buttonState = state.buttonState,
)
}
}
}
@Composable
private fun ButtonContainer(buttonState: TangemPayTxHistoryDetailsUM.ButtonState, modifier: Modifier = Modifier) {
SecondaryButton(modifier = modifier, text = buttonState.text.resolveReference(), onClick = buttonState.onClick)
}
@Composable
private fun Icon(iconState: ImageReference, modifier: Modifier = Modifier) {
when (iconState) {
is ImageReference.Res -> LocalStaticIcon(modifier = modifier, id = iconState.resId, iconSize = 40.dp)
is ImageReference.Url -> RemoteIcon(modifier = modifier, url = iconState.url)
}
}
@Preview(device = Devices.PIXEL_7_PRO)
@Preview(device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemPayTxHistoryDetailsContentPreview(
@PreviewParameter(TangemPayTxHistoryDetailsUMProvider::class) state: TangemPayTxHistoryDetailsUM,
) {
TangemThemePreview {
TangemPayTxHistoryDetailsContent(state = state)
}
}
private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterProvider<TangemPayTxHistoryDetailsUM>(
listOf(
TangemPayTxHistoryDetailsUM(
title = stringReference("12 June • 12:40"),
iconState = ImageReference.Res(R.drawable.ic_category_24),
transactionTitle = stringReference("Starbucks"),
transactionSubtitle = stringReference("Food and drinks"),
transactionAmount = "-$5.86",
transactionAmountColor = themedColor { TangemTheme.colors.text.primary1 },
labelState = LabelUM(
text = resourceReference(R.string.tangem_pay_status_pending),
style = LabelStyle.REGULAR,
icon = R.drawable.ic_clock_24,
),
notification = null,
buttonState = TangemPayTxHistoryDetailsUM.ButtonState(
text = stringReference("Dispute"),
onClick = {},
),
dismiss = {},
),
TangemPayTxHistoryDetailsUM(
title = stringReference("12 June • 12:40"),
iconState = ImageReference.Res(R.drawable.ic_category_24),
transactionTitle = stringReference("Starbucks"),
transactionSubtitle = stringReference("Food and drinks"),
transactionAmount = "-$5.86",
transactionAmountColor = themedColor { TangemTheme.colors.text.warning },
labelState = LabelUM(
text = resourceReference(R.string.tangem_pay_status_declined),
style = LabelStyle.WARNING,
),
notification = NotificationConfig(
title = stringReference("The bank rejected this transaction request."),
subtitle = TextReference.EMPTY,
iconResId = R.drawable.ic_token_info_24,
),
buttonState = TangemPayTxHistoryDetailsUM.ButtonState(
text = stringReference("Dispute"),
onClick = {},
),
dismiss = {},
),
TangemPayTxHistoryDetailsUM(
title = stringReference("12 June • 12:40"),
iconState = ImageReference.Res(R.drawable.ic_category_24),
transactionTitle = stringReference("Starbucks"),
transactionSubtitle = stringReference("Food and drinks"),
transactionAmount = "-$5.86",
transactionAmountColor = themedColor { TangemTheme.colors.text.primary1 },
labelState = LabelUM(
text = resourceReference(R.string.tangem_pay_status_completed),
style = LabelStyle.ACCENT,
),
notification = null,
buttonState = TangemPayTxHistoryDetailsUM.ButtonState(
text = stringReference("Dispute"),
onClick = {},
),
dismiss = {},
),
TangemPayTxHistoryDetailsUM(
title = stringReference("12 June • 12:40"),
iconState = ImageReference.Res(R.drawable.ic_percent_24),
transactionTitle = stringReference("Fee"),
transactionSubtitle = stringReference("Service fee"),
transactionAmount = "-$5.86",
transactionAmountColor = themedColor { TangemTheme.colors.text.primary1 },
labelState = null,
notification = NotificationConfig(
title = stringReference("This fee goes to cover the cost of handling your transfer."),
subtitle = TextReference.EMPTY,
iconResId = R.drawable.ic_token_info_24,
),
buttonState = TangemPayTxHistoryDetailsUM.ButtonState(
text = stringReference("Dispute"),
onClick = {},
),
dismiss = {},
),
TangemPayTxHistoryDetailsUM(
title = stringReference("12 June • 12:40"),
iconState = ImageReference.Res(R.drawable.ic_arrow_down_24),
transactionTitle = stringReference("Deposit"),
transactionSubtitle = stringReference("Transfers"),
transactionAmount = "+$20",
transactionAmountColor = themedColor { TangemTheme.colors.text.accent },
labelState = null,
notification = null,
buttonState = TangemPayTxHistoryDetailsUM.ButtonState(
text = stringReference("Explore transaction"),
onClick = {},
),
dismiss = {},
),
TangemPayTxHistoryDetailsUM(
title = stringReference("12 June • 12:40"),
iconState = ImageReference.Res(R.drawable.ic_arrow_up_24),
transactionTitle = stringReference("Withdrawal"),
transactionSubtitle = stringReference("Transfers"),
transactionAmount = "-$5.86",
transactionAmountColor = themedColor { TangemTheme.colors.text.primary1 },
labelState = null,
notification = null,
buttonState = TangemPayTxHistoryDetailsUM.ButtonState(
text = stringReference("Explore transaction"),
onClick = {},
),
dismiss = {},
),
),
)

View file

@ -4,13 +4,15 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.message.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.BottomSheetMessageV2
import com.tangem.core.ui.message.bottomSheetMessage
import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType
internal object TangemPayErrorMessageFactory {
fun createError(type: TangemPayDetailsErrorType, onDismiss: () -> Unit): MessageBottomSheetUMV2 {
fun createErrorMessage(type: TangemPayDetailsErrorType): BottomSheetMessageV2 {
return when (type) {
TangemPayDetailsErrorType.Receive -> messageBottomSheetUM {
TangemPayDetailsErrorType.Receive -> bottomSheetMessage {
infoBlock {
icon(R.drawable.img_attention_20) {
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.Attention
@ -20,7 +22,7 @@ internal object TangemPayErrorMessageFactory {
}
primaryButton {
text = resourceReference(R.string.common_got_it)
onClick { onDismiss() }
onClick { closeBs() }
}
}
}

View file

@ -365,7 +365,12 @@ internal class WalletModel @Inject constructor(
value = info,
onClickIssue = ::issueOrder,
onClickKyc = innerWalletRouter::openTangemPayOnboarding,
openDetails = innerWalletRouter::openTangemPayDetails,
openDetails = { config ->
innerWalletRouter.openTangemPayDetails(
userWalletId = stateHolder.getSelectedWalletId(),
config = config,
)
},
),
)
}

View file

@ -109,8 +109,8 @@ internal class DefaultWalletRouter @Inject constructor(
router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding))
}
override fun openTangemPayDetails(config: TangemPayDetailsConfig) {
router.push(AppRoute.TangemPayDetails(config))
override fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig) {
router.push(AppRoute.TangemPayDetails(userWalletId = userWalletId, config = config))
}
override fun openYieldSupplyBottomSheet(

View file

@ -60,7 +60,7 @@ internal interface InnerWalletRouter {
fun openTangemPayOnboarding()
fun openTangemPayDetails(config: TangemPayDetailsConfig)
fun openTangemPayDetails(userWalletId: UserWalletId, config: TangemPayDetailsConfig)
/** Open BS abput yield supply active and all money deposited in AAVE */
fun openYieldSupplyBottomSheet(