Updated on 2026-08-14
This commit is contained in:
parent
952dbd4732
commit
7e375ac36b
20 changed files with 233 additions and 27 deletions
|
|
@ -593,7 +593,10 @@ internal class ChildFactory @Inject constructor(
|
|||
is AppRoute.TangemPayDetails -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = TangemPayDetailsComponent.Params(customerWalletAddress = route.customerWalletAddress),
|
||||
params = TangemPayDetailsComponent.Params(
|
||||
customerWalletAddress = route.customerWalletAddress,
|
||||
cardNumberEnd = route.cardNumberEnd,
|
||||
),
|
||||
componentFactory = tangemPayDetailsComponentFactory,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -441,7 +441,10 @@ sealed class AppRoute(val path: String) : Route {
|
|||
) : AppRoute(path = "/archived_account/${userWalletId.stringValue}")
|
||||
|
||||
@Serializable
|
||||
data class TangemPayDetails(val customerWalletAddress: String) : AppRoute(path = "/tangem_pay_details")
|
||||
data class TangemPayDetails(
|
||||
val customerWalletAddress: String,
|
||||
val cardNumberEnd: String,
|
||||
) : AppRoute(path = "/tangem_pay_details")
|
||||
|
||||
@Serializable
|
||||
data class TangemPayOnboarding(
|
||||
|
|
|
|||
|
|
@ -137,4 +137,13 @@ interface TangemPayApi {
|
|||
@Header("Authorization") authHeader: String,
|
||||
@Body body: OrderRequest,
|
||||
): ApiResponse<OrderResponse>
|
||||
|
||||
@GET("v1/customer/balance")
|
||||
suspend fun getCardBalance(@Header("Authorization") authHeader: String): ApiResponse<CardBalanceResponse>
|
||||
|
||||
@POST("v1/customer/card/details")
|
||||
suspend fun revealCardDetails(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body body: CardDetailsRequest,
|
||||
): ApiResponse<CardDetailsResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.datasource.api.pay.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CardDetailsRequest(
|
||||
@Json(name = "session_id") val sessionId: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class CardBalanceResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "currency") val currency: String,
|
||||
@Json(name = "available_balance") val availableBalance: BigDecimal,
|
||||
@Json(name = "credit_limit") val creditLimit: BigDecimal,
|
||||
@Json(name = "pending_charges") val pendingCharges: BigDecimal,
|
||||
@Json(name = "posted_charges") val postedCharges: BigDecimal,
|
||||
@Json(name = "balance_due") val balanceDue: BigDecimal,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
data class CardDetailsResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "token") val token: String,
|
||||
@Json(name = "expiration_month") val expirationMonth: String,
|
||||
@Json(name = "expiration_year") val expirationYear: String,
|
||||
@Json(name = "emboss_name") val embossName: String,
|
||||
@Json(name = "card_type") val cardType: String,
|
||||
@Json(name = "card_status") val cardStatus: String,
|
||||
@Json(name = "card_number_end") val cardNumberEnd: String,
|
||||
@Json(name = "pan") val pan: String,
|
||||
@Json(name = "cvv") val cvv: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.data.pay.di
|
||||
|
||||
import com.tangem.data.pay.repository.DefaultCardDetailsRepository
|
||||
import com.tangem.data.pay.repository.DefaultKycRepository
|
||||
import com.tangem.data.pay.repository.DefaultTangemPayTxHistoryRepository
|
||||
import com.tangem.data.pay.repository.DefaultOnboardingRepository
|
||||
import com.tangem.domain.pay.repository.CardDetailsRepository
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.pay.usecase.TangemPayIssueOrderUseCase
|
||||
|
|
@ -31,6 +33,10 @@ internal interface TangemPayDataModule {
|
|||
@Singleton
|
||||
fun bindTangemPayTxHistoryRepository(repository: DefaultTangemPayTxHistoryRepository): TangemPayTxHistoryRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindCardDetailsRepository(repository: DefaultCardDetailsRepository): CardDetailsRepository
|
||||
|
||||
companion object {
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.data.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.domain.pay.model.TangemPayCardBalance
|
||||
import com.tangem.domain.pay.model.TangemPayCardDetails
|
||||
import com.tangem.domain.pay.repository.CardDetailsRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "TangemPay: CardDetailsRepository"
|
||||
|
||||
internal class DefaultCardDetailsRepository @Inject constructor(
|
||||
private val tangemPayApi: TangemPayApi,
|
||||
private val requestHelper: TangemPayRequestPerformer,
|
||||
) : CardDetailsRepository {
|
||||
|
||||
override suspend fun getCardBalance(): Either<UniversalError, TangemPayCardBalance> {
|
||||
return requestHelper.runWithErrorLogs(TAG) {
|
||||
val result = requestHelper.request { authHeader ->
|
||||
tangemPayApi.getCardBalance(authHeader)
|
||||
}.result ?: error("Cannot get card balance")
|
||||
TangemPayCardBalance(
|
||||
balance = result.availableBalance,
|
||||
currencyCode = result.currency,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun revealCardDetails(): Either<UniversalError, TangemPayCardDetails> {
|
||||
TODO("[REDACTED_TASK_KEY] add reveal card details")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.pay.model
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class TangemPayCardBalance(
|
||||
val balance: BigDecimal,
|
||||
val currencyCode: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.domain.pay.model
|
||||
|
||||
/**
|
||||
* Returned dynamically to show on the UI. Never saved anywhere
|
||||
*/
|
||||
data class TangemPayCardDetails(
|
||||
val pan: String,
|
||||
val cvv: String,
|
||||
val expirationYear: String,
|
||||
val expirationMonth: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.domain.pay.repository
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.domain.pay.model.TangemPayCardBalance
|
||||
import com.tangem.domain.pay.model.TangemPayCardDetails
|
||||
|
||||
interface CardDetailsRepository {
|
||||
|
||||
suspend fun getCardBalance(): Either<UniversalError, TangemPayCardBalance>
|
||||
|
||||
suspend fun revealCardDetails(): Either<UniversalError, TangemPayCardDetails>
|
||||
}
|
||||
|
|
@ -4,6 +4,6 @@ import com.tangem.core.decompose.factory.ComponentFactory
|
|||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
||||
interface TangemPayDetailsComponent : ComposableContentComponent {
|
||||
data class Params(val customerWalletAddress: String)
|
||||
data class Params(val customerWalletAddress: String, val cardNumberEnd: String)
|
||||
interface Factory : ComponentFactory<Params, TangemPayDetailsComponent>
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ dependencies {
|
|||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.error)
|
||||
|
||||
/** Features api */
|
||||
implementation(projects.features.tangempay.details.api)
|
||||
|
|
|
|||
|
|
@ -3,20 +3,24 @@ 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.decompose.navigation.Router
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig.ShowRefreshState
|
||||
import com.tangem.domain.pay.repository.CardDetailsRepository
|
||||
import com.tangem.features.tangempay.components.TangemPayDetailsComponent
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarConfig
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
|
||||
import com.tangem.features.tangempay.model.transformers.TangemPayDetailsRefreshTransformer
|
||||
import com.tangem.features.tangempay.transformer.CardDetailsStateTransformer
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -25,21 +29,35 @@ import javax.inject.Inject
|
|||
@Stable
|
||||
@ModelScoped
|
||||
internal class TangemPayDetailsModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val cardDetailsRepository: CardDetailsRepository,
|
||||
) : Model() {
|
||||
|
||||
private val params: TangemPayDetailsComponent.Params = paramsContainer.require()
|
||||
|
||||
val uiState: StateFlow<TangemPayDetailsUM>
|
||||
field = MutableStateFlow(getInitialState())
|
||||
|
||||
private val refreshStateJobHolder = JobHolder()
|
||||
private val fetchBalanceJobHolder = JobHolder()
|
||||
|
||||
init {
|
||||
fetchBalance()
|
||||
}
|
||||
|
||||
private fun fetchBalance(): Job {
|
||||
return modelScope.launch {
|
||||
val result = cardDetailsRepository.getCardBalance()
|
||||
uiState.update(CardDetailsStateTransformer(result))
|
||||
}.saveIn(fetchBalanceJobHolder)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber", "UnusedPrivateMember")
|
||||
private fun onRefreshSwipe(refreshState: ShowRefreshState) {
|
||||
uiState.update(TangemPayDetailsRefreshTransformer(isRefreshing = true))
|
||||
modelScope.launch {
|
||||
// simulate update logic
|
||||
delay(2000)
|
||||
uiState.update(TangemPayDetailsRefreshTransformer(isRefreshing = refreshState.value))
|
||||
fetchBalance().join()
|
||||
uiState.update(TangemPayDetailsRefreshTransformer(isRefreshing = false))
|
||||
}.saveIn(refreshStateJobHolder)
|
||||
}
|
||||
|
|
@ -49,13 +67,19 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
topBarConfig = TangemPayDetailsTopBarConfig(onBackClick = router::pop, items = null),
|
||||
pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = ::onRefreshSwipe),
|
||||
balanceBlockState = TangemPayDetailsBalanceBlockState.Loading(actionButtons = persistentListOf()),
|
||||
cardDetailsUM = TangemPayCardDetailsUM(
|
||||
number = "•••• •••• •••• 1245",
|
||||
expiry = "••/••",
|
||||
cvv = "•••",
|
||||
onReveal = {},
|
||||
),
|
||||
cardDetailsUM = getHiddenCardDetails(),
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getHiddenCardDetails() = TangemPayCardDetailsUM(
|
||||
number = "•••• •••• •••• ${params.cardNumberEnd}",
|
||||
expiry = "••/••",
|
||||
cvv = "•••",
|
||||
onReveal = ::revealCardDetails,
|
||||
)
|
||||
|
||||
private fun revealCardDetails() {
|
||||
// TODO "[REDACTED_TASK_KEY] add reveal card details"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.features.tangempay.transformer
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.pay.model.TangemPayCardBalance
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import java.util.Currency
|
||||
|
||||
internal class CardDetailsStateTransformer(
|
||||
private val balance: Either<UniversalError, TangemPayCardBalance>,
|
||||
) : Transformer<TangemPayDetailsUM> {
|
||||
|
||||
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
|
||||
val balance = when (balance) {
|
||||
is Either.Left<UniversalError> -> {
|
||||
TangemPayDetailsBalanceBlockState.Error(persistentListOf())
|
||||
}
|
||||
is Either.Right<TangemPayCardBalance> -> {
|
||||
TangemPayDetailsBalanceBlockState.Content(
|
||||
isBalanceFlickering = false,
|
||||
fiatBalance = getBalanceText(balance.value),
|
||||
// TODO [REDACTED_TASK_KEY]: Add crypto balance when the BFF is ready
|
||||
cryptoBalance = "",
|
||||
actionButtons = persistentListOf(),
|
||||
)
|
||||
}
|
||||
}
|
||||
return prevState.copy(balanceBlockState = balance)
|
||||
}
|
||||
|
||||
private fun getBalanceText(balance: TangemPayCardBalance): String {
|
||||
val currency = Currency.getInstance(balance.currencyCode)
|
||||
return balance.balance.format {
|
||||
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -133,11 +133,12 @@ internal fun TangemPayDetailsBalanceBlock(
|
|||
state = state,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
CryptoBalance(
|
||||
modifier = Modifier.padding(start = 12.dp, top = 4.dp),
|
||||
state = state,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
// TODO [REDACTED_TASK_KEY]: Uncomment after adding crypto balance when the BFF is ready
|
||||
// CryptoBalance(
|
||||
// modifier = Modifier.padding(start = 12.dp, top = 4.dp),
|
||||
// state = state,
|
||||
// isBalanceHidden = isBalanceHidden,
|
||||
// )
|
||||
if (state.actionButtons.isNotEmpty()) {
|
||||
HorizontalActionChips(
|
||||
modifier = Modifier.padding(top = 12.dp),
|
||||
|
|
@ -179,6 +180,7 @@ private fun FiatBalance(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
@Composable
|
||||
private fun CryptoBalance(
|
||||
state: TangemPayDetailsBalanceBlockState,
|
||||
|
|
|
|||
|
|
@ -388,8 +388,8 @@ internal class WalletModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun tangemPayCardOnClick(customerWalletAddress: String) {
|
||||
innerWalletRouter.openTangemPayDetails(customerWalletAddress)
|
||||
private fun tangemPayCardOnClick(customerWalletAddress: String, cardNumberEnd: String) {
|
||||
innerWalletRouter.openTangemPayDetails(customerWalletAddress, cardNumberEnd)
|
||||
}
|
||||
|
||||
private fun needToRefreshTimer() {
|
||||
|
|
|
|||
|
|
@ -114,8 +114,8 @@ internal class DefaultWalletRouter @Inject constructor(
|
|||
router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.ContinueOnboarding))
|
||||
}
|
||||
|
||||
override fun openTangemPayDetails(custoemrWalletAddress: String) {
|
||||
router.push(AppRoute.TangemPayDetails(custoemrWalletAddress))
|
||||
override fun openTangemPayDetails(customerWalletAddress: String, cardNumberEnd: String) {
|
||||
router.push(AppRoute.TangemPayDetails(customerWalletAddress, cardNumberEnd))
|
||||
}
|
||||
|
||||
override fun openYieldSupplyBottomSheet(
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ internal interface InnerWalletRouter {
|
|||
|
||||
fun openTangemPayOnboarding()
|
||||
|
||||
fun openTangemPayDetails(custoemrWalletAddress: String)
|
||||
fun openTangemPayDetails(customerWalletAddress: String, cardNumberEnd: String)
|
||||
|
||||
/** Open BS abput yield supply active and all money deposited in AAVE */
|
||||
fun openYieldSupplyBottomSheet(
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ internal class TangemPayStateTransformer(
|
|||
private val value: MainScreenCustomerInfo? = null,
|
||||
private val onIssueOrderClick: () -> Unit = {},
|
||||
private val onContinueKycClick: () -> Unit = {},
|
||||
private val cardOnClick: (String) -> Unit = {},
|
||||
private val cardOnClick: (customerWalletAddress: String, cardNumberEnd: String) -> Unit = { _, _ -> },
|
||||
private val issueProgressState: Boolean = false,
|
||||
private val issueState: Boolean = false,
|
||||
) : Transformer<TangemPayState> {
|
||||
|
|
@ -33,8 +33,8 @@ internal class TangemPayStateTransformer(
|
|||
return when {
|
||||
value == null -> TangemPayState.Empty
|
||||
!value.info.isKycApproved() -> createKycInProgressState(onContinueKycClick)
|
||||
value.orderStatus == NOT_ISSUED || value.orderStatus == CANCELED -> createIssueState()
|
||||
cardInfo != null -> getCardInfoState(cardInfo)
|
||||
value.orderStatus == NOT_ISSUED || value.orderStatus == CANCELED -> createIssueState()
|
||||
else -> createIssueProgressState()
|
||||
}
|
||||
}
|
||||
|
|
@ -64,7 +64,7 @@ internal class TangemPayStateTransformer(
|
|||
private fun getCardInfoState(cardInfo: CardInfo): TangemPayState = TangemPayState.Card(
|
||||
lastFourDigits = TextReference.Str("*${cardInfo.lastFourDigits}"),
|
||||
balanceText = TextReference.Str(getBalanceText(cardInfo)),
|
||||
onClick = { cardOnClick(cardInfo.customerWalletAddress) },
|
||||
onClick = { cardOnClick(cardInfo.customerWalletAddress, cardInfo.lastFourDigits) },
|
||||
)
|
||||
|
||||
private fun getBalanceText(cardInfo: CardInfo): String {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue