Updated on 2026-08-14
This commit is contained in:
parent
233d76d9f4
commit
8b49dc2f7f
17 changed files with 694 additions and 0 deletions
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.features.tangempay.cashback.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
/**
|
||||
* Public contract of the Cashback screen. Kept in an `api` sub-package (separate from `impl`) so it
|
||||
* can be lifted into a dedicated module without refactoring.
|
||||
*/
|
||||
internal interface TangemPayCashbackComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(val userWalletId: UserWalletId)
|
||||
|
||||
interface Factory : ComponentFactory<Params, TangemPayCashbackComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.features.tangempay.cashback.impl
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.tangempay.cashback.api.TangemPayCashbackComponent
|
||||
import com.tangem.features.tangempay.cashback.impl.model.TangemPayCashbackModel
|
||||
import com.tangem.features.tangempay.cashback.impl.ui.TangemPayCashbackScreen
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultTangemPayCashbackComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: TangemPayCashbackComponent.Params,
|
||||
) : TangemPayCashbackComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: TangemPayCashbackModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
TangemPayCashbackScreen(state = state, modifier = modifier)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : TangemPayCashbackComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: TangemPayCashbackComponent.Params,
|
||||
): DefaultTangemPayCashbackComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.features.tangempay.cashback.impl.model
|
||||
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import org.joda.time.DateTime
|
||||
|
||||
internal class TangemPayCashbackDateFormatter {
|
||||
|
||||
fun formatMonth(year: Int, month: Int): String =
|
||||
DateTimeFormatters.formatDate(DateTime(year, month, 1, 0, 0), DateTimeFormatters.dateMMMM)
|
||||
|
||||
fun formatMonthDay(date: DateTime): String = DateTimeFormatters.formatDate(date, DateTimeFormatters.dateMMMMd)
|
||||
|
||||
fun formatWindow(start: DateTime, end: DateTime): String {
|
||||
val isSameMonth = start.year == end.year && start.monthOfYear == end.monthOfYear
|
||||
return if (isSameMonth) {
|
||||
"${formatMonthDay(start)}–${end.dayOfMonth}"
|
||||
} else {
|
||||
"${formatMonthDay(start)} – ${formatMonthDay(end)}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.features.tangempay.cashback.impl.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.navigation.Router
|
||||
import com.tangem.domain.pay.model.TangemPayCashback
|
||||
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.joda.time.DateTime
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TangemPayCashbackModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
) : Model() {
|
||||
|
||||
private val cashbackConverter = TangemPayCashbackUmConverter(onCloseClick = router::pop)
|
||||
|
||||
val uiState: StateFlow<TangemPayCashbackUM>
|
||||
field = MutableStateFlow(cashbackConverter.convert(STUB_CASHBACK))
|
||||
|
||||
private companion object {
|
||||
// TODO([REDACTED_TASK_KEY]): replace stub with repository load
|
||||
val STUB_CASHBACK = TangemPayCashback(
|
||||
confirmedAmount = BigDecimal("22.54"),
|
||||
currency = "USD",
|
||||
period = TangemPayCashback.Period(
|
||||
year = 2026,
|
||||
month = 6,
|
||||
payoutStart = DateTime.parse("2026-07-01"),
|
||||
payoutEnd = DateTime.parse("2026-07-05"),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.features.tangempay.cashback.impl.model
|
||||
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode
|
||||
import com.tangem.core.ui.format.bigdecimal.optionalDecimals
|
||||
import com.tangem.domain.pay.model.TangemPayCashback
|
||||
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class TangemPayCashbackUmConverter(
|
||||
private val onCloseClick: () -> Unit,
|
||||
private val dateFormatter: TangemPayCashbackDateFormatter = TangemPayCashbackDateFormatter(),
|
||||
) : Converter<TangemPayCashback?, TangemPayCashbackUM> {
|
||||
|
||||
// TODO([REDACTED_TASK_KEY]): move hardcoded strings to string resources
|
||||
override fun convert(value: TangemPayCashback?): TangemPayCashbackUM {
|
||||
if (value == null || value.confirmedAmount.signum() == 0) {
|
||||
return TangemPayCashbackUM(
|
||||
title = stringReference("Start spending and earn cashback"),
|
||||
subtitle = stringReference("Collected amount will be shown here"),
|
||||
isEmpty = true,
|
||||
banner = null,
|
||||
onCloseClick = onCloseClick,
|
||||
)
|
||||
}
|
||||
val currency = getJavaCurrencyByCode(value.currency)
|
||||
val earned = value.confirmedAmount.format { fiat(currency.currencyCode, currency.symbol).optionalDecimals() }
|
||||
val month = dateFormatter.formatMonth(value.period.year, value.period.month)
|
||||
val payoutWindow = dateFormatter.formatWindow(value.period.payoutStart, value.period.payoutEnd)
|
||||
val payoutEnd = dateFormatter.formatMonthDay(value.period.payoutEnd)
|
||||
val banner = if (value.confirmedAmount.signum() < 0) {
|
||||
TangemPayCashbackUM.Banner(
|
||||
text = stringReference(
|
||||
"We received a refund for a purchase for which cashback had previously been awarded",
|
||||
),
|
||||
type = TangemPayCashbackUM.Banner.Type.Error,
|
||||
)
|
||||
} else {
|
||||
TangemPayCashbackUM.Banner(
|
||||
text = stringReference("Cashback $earned for $month will be deposited till $payoutEnd"),
|
||||
type = TangemPayCashbackUM.Banner.Type.Info,
|
||||
)
|
||||
}
|
||||
return TangemPayCashbackUM(
|
||||
title = stringReference("$earned earned in $month"),
|
||||
subtitle = stringReference("Will be deposited on $payoutWindow"),
|
||||
isEmpty = false,
|
||||
banner = banner,
|
||||
onCloseClick = onCloseClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
package com.tangem.features.tangempay.cashback.impl.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
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.BlurredEdgeTreatment
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.ds.topbar.TangemTopBar
|
||||
import com.tangem.core.ui.ds2.button.TangemButton
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.LocalIsInDarkTheme
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackUM
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.core.ui.R as CoreUiR
|
||||
|
||||
private const val GLOW_RADIUS_FACTOR = 0.585f
|
||||
private const val GLOW_BLUE_ALPHA = 0.20f
|
||||
private const val GLOW_WARM_ALPHA = 0.15f
|
||||
|
||||
@Composable
|
||||
internal fun TangemPayCashbackScreen(state: TangemPayCashbackUM, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors3.bg.primary),
|
||||
) {
|
||||
if (state.isEmpty) {
|
||||
EmptyStateGlow(modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
TangemTopBar(
|
||||
modifier = Modifier.statusBarsPadding(),
|
||||
// TODO([REDACTED_TASK_KEY]): move to string resources
|
||||
title = stringReference("Cashback"),
|
||||
endContent = {
|
||||
TangemButton(
|
||||
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_24),
|
||||
onClick = state.onCloseClick,
|
||||
size = TangemButton.Size.X11,
|
||||
variant = TangemButton.Variant.Material,
|
||||
)
|
||||
},
|
||||
)
|
||||
HeroBlock(state = state)
|
||||
state.banner?.let { banner ->
|
||||
CashbackBanner(
|
||||
banner = banner,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun EmptyStateGlow(modifier: Modifier = Modifier) {
|
||||
val isDark = LocalIsInDarkTheme.current
|
||||
val warm = if (isDark) Color(0xFF7A4A25) else Color(0xFFEA8C44)
|
||||
val blue = if (isDark) Color(0xFF0090F9) else Color(0xFF0092FC)
|
||||
Box(
|
||||
modifier = modifier
|
||||
.blur(56.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded)
|
||||
.drawBehind {
|
||||
val radius = size.width * GLOW_RADIUS_FACTOR
|
||||
val center = Offset(x = size.width / 2f, y = 0f)
|
||||
drawCircle(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
warm.copy(alpha = GLOW_WARM_ALPHA),
|
||||
blue.copy(alpha = GLOW_BLUE_ALPHA),
|
||||
),
|
||||
center = center,
|
||||
radius = radius,
|
||||
),
|
||||
radius = radius,
|
||||
center = center,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HeroBlock(state: TangemPayCashbackUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(vertical = 48.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
text = state.title.resolveReference(),
|
||||
style = TangemTheme.typography3.heading.medium,
|
||||
color = TangemTheme.colors3.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Text(
|
||||
text = state.subtitle.resolveReference(),
|
||||
style = TangemTheme.typography3.subheading.medium,
|
||||
color = TangemTheme.colors3.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CashbackBanner(banner: TangemPayCashbackUM.Banner, modifier: Modifier = Modifier) {
|
||||
val contentColor = when (banner.type) {
|
||||
TangemPayCashbackUM.Banner.Type.Info -> TangemTheme.colors3.text.status.info
|
||||
TangemPayCashbackUM.Banner.Type.Error -> TangemTheme.colors3.text.status.error
|
||||
}
|
||||
val backgroundColor = when (banner.type) {
|
||||
TangemPayCashbackUM.Banner.Type.Info -> TangemTheme.colors3.bg.status.infoSubtle
|
||||
TangemPayCashbackUM.Banner.Type.Error -> TangemTheme.colors3.bg.status.errorSubtle
|
||||
}
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(backgroundColor)
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(CoreUiR.drawable.ic_information_24),
|
||||
contentDescription = null,
|
||||
tint = contentColor,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Text(
|
||||
text = banner.text.resolveReference(),
|
||||
style = TangemTheme.typography3.subheading.medium,
|
||||
color = contentColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(device = Devices.PIXEL_7_PRO)
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_7_PRO)
|
||||
@Composable
|
||||
private fun TangemPayCashbackScreenPreview(
|
||||
@PreviewParameter(TangemPayCashbackUMProvider::class) state: TangemPayCashbackUM,
|
||||
) {
|
||||
TangemThemePreviewRedesign {
|
||||
TangemPayCashbackScreen(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
private class TangemPayCashbackUMProvider : CollectionPreviewParameterProvider<TangemPayCashbackUM>(
|
||||
collection = listOf(
|
||||
TangemPayCashbackUM(
|
||||
title = stringReference("$22.54 earned in June"),
|
||||
subtitle = stringReference("Will be deposited on July 1–5"),
|
||||
isEmpty = false,
|
||||
banner = TangemPayCashbackUM.Banner(
|
||||
text = stringReference("Cashback $22.54 for June will be deposited till July 5"),
|
||||
type = TangemPayCashbackUM.Banner.Type.Info,
|
||||
),
|
||||
onCloseClick = {},
|
||||
),
|
||||
TangemPayCashbackUM(
|
||||
title = stringReference("$22.54 earned in June"),
|
||||
subtitle = stringReference("Will be deposited on July 1–5"),
|
||||
isEmpty = false,
|
||||
banner = TangemPayCashbackUM.Banner(
|
||||
text = stringReference(
|
||||
"We received a refund for a purchase for which cashback had previously been awarded",
|
||||
),
|
||||
type = TangemPayCashbackUM.Banner.Type.Error,
|
||||
),
|
||||
onCloseClick = {},
|
||||
),
|
||||
TangemPayCashbackUM(
|
||||
title = stringReference("Start spending and earn cashback"),
|
||||
subtitle = stringReference("Collected amount will be shown here"),
|
||||
isEmpty = true,
|
||||
banner = null,
|
||||
onCloseClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.features.tangempay.cashback.impl.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
internal data class TangemPayCashbackUM(
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
val isEmpty: Boolean,
|
||||
val banner: Banner?,
|
||||
val onCloseClick: () -> Unit,
|
||||
) {
|
||||
|
||||
@Immutable
|
||||
data class Banner(
|
||||
val text: TextReference,
|
||||
val type: Type,
|
||||
) {
|
||||
enum class Type { Info, Error }
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import com.tangem.core.decompose.context.childByContext
|
|||
import com.tangem.core.decompose.navigation.inner.InnerRouter
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.promobanners.api.PromoBannersBlockComponent
|
||||
import com.tangem.features.tangempay.cashback.api.TangemPayCashbackComponent
|
||||
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
|
||||
import com.tangem.features.tangempay.tiers.current.TangemPayCurrentPlanComponent
|
||||
import com.tangem.features.tangempay.tiers.select.TangemPaySelectPlanComponent
|
||||
|
|
@ -36,6 +37,7 @@ internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constru
|
|||
private val expressTransactionsComponentFactory: ExpressTransactionsComponent.Factory,
|
||||
private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory,
|
||||
private val virtualAccountAddFundsComponentFactory: VirtualAccountAddFundsBottomSheetComponent.Factory,
|
||||
private val cashbackComponentFactory: TangemPayCashbackComponent.Factory,
|
||||
) : AppComponentContext by appComponentContext, TangemPayDetailsContainerComponent {
|
||||
|
||||
private val stackNavigation = StackNavigation<TangemPayAccountDetailsInnerRoute>()
|
||||
|
|
@ -107,6 +109,12 @@ internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constru
|
|||
TangemPayVirtualAccountDepositSuccessComponent(
|
||||
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
)
|
||||
TangemPayAccountDetailsInnerRoute.Cashback -> cashbackComponentFactory.create(
|
||||
context = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
params = TangemPayCashbackComponent.Params(
|
||||
userWalletId = params.initialStatus.userWalletId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onChildBack() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.features.tangempay.di
|
||||
|
||||
import com.tangem.features.tangempay.cashback.api.TangemPayCashbackComponent
|
||||
import com.tangem.features.tangempay.cashback.impl.DefaultTangemPayCashbackComponent
|
||||
import com.tangem.features.tangempay.components.DefaultTangemPayDetailsContainerComponent
|
||||
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
|
||||
import com.tangem.features.tangempay.components.TangemPayTransactionBottomSheetComponent
|
||||
|
|
@ -31,4 +33,10 @@ internal interface TangemPayDetailsFeatureModule {
|
|||
fun bindTangemPayTransactionBottomSheetComponentFactory(
|
||||
factory: TangemPayTxHistoryDetailsComponent.Factory,
|
||||
): TangemPayTransactionBottomSheetComponent.Factory
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayCashbackComponentFactory(
|
||||
factory: DefaultTangemPayCashbackComponent.Factory,
|
||||
): TangemPayCashbackComponent.Factory
|
||||
}
|
||||
|
|
@ -2,6 +2,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.cashback.impl.model.TangemPayCashbackModel
|
||||
import com.tangem.features.tangempay.closure.TangemPayCloseCardModel
|
||||
import com.tangem.features.tangempay.limit.setup.TangemPayCardLimitSetupModel
|
||||
import com.tangem.features.tangempay.model.*
|
||||
|
|
@ -96,4 +97,9 @@ internal interface TangemPayModelModule {
|
|||
@IntoMap
|
||||
@ClassKey(TangemPaySelectPlanModel::class)
|
||||
fun bindTangemPaySelectPlanModel(model: TangemPaySelectPlanModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(TangemPayCashbackModel::class)
|
||||
fun bindTangemPayCashbackModel(model: TangemPayCashbackModel): Model
|
||||
}
|
||||
|
|
@ -427,6 +427,10 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
router.push(TangemPayAccountDetailsInnerRoute.CurrentPlan(tariffPlan))
|
||||
}
|
||||
|
||||
override fun onClickCashback() {
|
||||
router.push(TangemPayAccountDetailsInnerRoute.Cashback)
|
||||
}
|
||||
|
||||
override fun onCardClick(cardId: String) {
|
||||
analytics.send(TangemPayAnalyticsEvents.CardIconClicked())
|
||||
router.push(TangemPayAccountDetailsInnerRoute.CardDetails(cardId = cardId))
|
||||
|
|
|
|||
|
|
@ -28,4 +28,7 @@ internal sealed class TangemPayAccountDetailsInnerRoute : Route {
|
|||
|
||||
@Serializable
|
||||
data object VirtualAccountDepositSuccess : TangemPayAccountDetailsInnerRoute()
|
||||
|
||||
@Serializable
|
||||
data object Cashback : TangemPayAccountDetailsInnerRoute()
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ internal interface TangemPayDetailIntents {
|
|||
fun onClickWithdraw()
|
||||
fun onClickTermsAndLimits()
|
||||
fun onClickVisaBenefits()
|
||||
fun onClickCashback()
|
||||
fun onClickCurrentPlan(tariffPlan: TangemPayCustomerTariffPlan)
|
||||
fun onCancelPlusTransition(orderId: String)
|
||||
fun onCardClick(cardId: String)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.features.tangempay.cashback.impl.model
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import org.joda.time.DateTime
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.util.Locale
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class TangemPayCashbackDateFormatterTest {
|
||||
|
||||
private val defaultLocale = Locale.getDefault()
|
||||
|
||||
private val formatter = TangemPayCashbackDateFormatter()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
Locale.setDefault(Locale.US)
|
||||
mockkStatic(DateFormat::class)
|
||||
every { DateFormat.getBestDateTimePattern(any(), any()) } answers { secondArg() }
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(DateFormat::class)
|
||||
Locale.setDefault(defaultLocale)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN year and month WHEN formatMonth THEN full month name`() {
|
||||
// Act
|
||||
val actual = formatter.formatMonth(year = 2026, month = 6)
|
||||
|
||||
// Assert
|
||||
assertThat(actual).isEqualTo("June")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN date WHEN formatMonthDay THEN month name then day`() {
|
||||
// Act
|
||||
val actual = formatter.formatMonthDay(DateTime.parse("2026-07-05"))
|
||||
|
||||
// Assert
|
||||
assertThat(actual).isEqualTo("July 5")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN window within one month WHEN formatWindow THEN single month with day range`() {
|
||||
// Act
|
||||
val actual = formatter.formatWindow(DateTime.parse("2026-07-01"), DateTime.parse("2026-07-05"))
|
||||
|
||||
// Assert
|
||||
assertThat(actual).isEqualTo("July 1–5")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN window spanning two months WHEN formatWindow THEN month name on both sides`() {
|
||||
// Act
|
||||
val actual = formatter.formatWindow(DateTime.parse("2026-07-30"), DateTime.parse("2026-08-02"))
|
||||
|
||||
// Assert
|
||||
assertThat(actual).isEqualTo("July 30 – August 2")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
package com.tangem.features.tangempay.cashback.impl.model
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.pay.model.TangemPayCashback
|
||||
import com.tangem.features.tangempay.cashback.impl.ui.state.TangemPayCashbackUM
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import org.joda.time.DateTime
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
import java.math.BigDecimal
|
||||
import java.util.Locale
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class TangemPayCashbackUmConverterTest {
|
||||
|
||||
private val defaultLocale = Locale.getDefault()
|
||||
|
||||
private val onCloseClick: () -> Unit = {}
|
||||
private val converter = TangemPayCashbackUmConverter(onCloseClick = onCloseClick)
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
Locale.setDefault(Locale.US)
|
||||
mockkStatic(DateFormat::class)
|
||||
every { DateFormat.getBestDateTimePattern(any(), any()) } answers { secondArg() }
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(DateFormat::class)
|
||||
Locale.setDefault(defaultLocale)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("emptyStateCashback")
|
||||
fun `GIVEN null or zero amount WHEN convert THEN empty state without banner`(cashback: TangemPayCashback?) {
|
||||
// Act
|
||||
val actual = converter.convert(cashback)
|
||||
|
||||
// Assert
|
||||
val expected = TangemPayCashbackUM(
|
||||
title = stringReference("Start spending and earn cashback"),
|
||||
subtitle = stringReference("Collected amount will be shown here"),
|
||||
isEmpty = true,
|
||||
banner = null,
|
||||
onCloseClick = onCloseClick,
|
||||
)
|
||||
assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN positive amount WHEN convert THEN earned title and info deposit banner`() {
|
||||
// Arrange
|
||||
val cashback = createCashback(confirmedAmount = BigDecimal("22.54"))
|
||||
|
||||
// Act
|
||||
val actual = converter.convert(cashback)
|
||||
|
||||
// Assert
|
||||
val expected = TangemPayCashbackUM(
|
||||
title = stringReference("$22.54 earned in June"),
|
||||
subtitle = stringReference("Will be deposited on July 1–5"),
|
||||
isEmpty = false,
|
||||
banner = TangemPayCashbackUM.Banner(
|
||||
text = stringReference("Cashback $22.54 for June will be deposited till July 5"),
|
||||
type = TangemPayCashbackUM.Banner.Type.Info,
|
||||
),
|
||||
onCloseClick = onCloseClick,
|
||||
)
|
||||
assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN negative amount WHEN convert THEN earned title and refund error banner`() {
|
||||
// Arrange
|
||||
val cashback = createCashback(confirmedAmount = BigDecimal("-22.54"))
|
||||
|
||||
// Act
|
||||
val actual = converter.convert(cashback)
|
||||
|
||||
// Assert
|
||||
val expected = TangemPayCashbackUM(
|
||||
title = stringReference("-$22.54 earned in June"),
|
||||
subtitle = stringReference("Will be deposited on July 1–5"),
|
||||
isEmpty = false,
|
||||
banner = TangemPayCashbackUM.Banner(
|
||||
text = stringReference(
|
||||
"We received a refund for a purchase for which cashback had previously been awarded",
|
||||
),
|
||||
type = TangemPayCashbackUM.Banner.Type.Error,
|
||||
),
|
||||
onCloseClick = onCloseClick,
|
||||
)
|
||||
assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN payout window spanning two months WHEN convert THEN subtitle shows month on both sides`() {
|
||||
// Arrange
|
||||
val cashback = createCashback(
|
||||
payoutStart = DateTime.parse("2026-07-30"),
|
||||
payoutEnd = DateTime.parse("2026-08-02"),
|
||||
)
|
||||
|
||||
// Act
|
||||
val actual = converter.convert(cashback)
|
||||
|
||||
// Assert
|
||||
val expected = TangemPayCashbackUM(
|
||||
title = stringReference("$22.54 earned in June"),
|
||||
subtitle = stringReference("Will be deposited on July 30 – August 2"),
|
||||
isEmpty = false,
|
||||
banner = TangemPayCashbackUM.Banner(
|
||||
text = stringReference("Cashback $22.54 for June will be deposited till August 2"),
|
||||
type = TangemPayCashbackUM.Banner.Type.Info,
|
||||
),
|
||||
onCloseClick = onCloseClick,
|
||||
)
|
||||
assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
private fun emptyStateCashback(): List<TangemPayCashback?> = listOf(
|
||||
null,
|
||||
createCashback(confirmedAmount = BigDecimal.ZERO),
|
||||
)
|
||||
|
||||
private fun createCashback(
|
||||
confirmedAmount: BigDecimal = BigDecimal("22.54"),
|
||||
currency: String = "USD",
|
||||
year: Int = 2026,
|
||||
month: Int = 6,
|
||||
payoutStart: DateTime = DateTime.parse("2026-07-01"),
|
||||
payoutEnd: DateTime = DateTime.parse("2026-07-05"),
|
||||
): TangemPayCashback = TangemPayCashback(
|
||||
confirmedAmount = confirmedAmount,
|
||||
currency = currency,
|
||||
period = TangemPayCashback.Period(
|
||||
year = year,
|
||||
month = month,
|
||||
payoutStart = payoutStart,
|
||||
payoutEnd = payoutEnd,
|
||||
),
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue