Updated on 2026-08-14
This commit is contained in:
parent
e1c58bed0e
commit
8d37a2bc1f
20 changed files with 470 additions and 54 deletions
|
|
@ -34,6 +34,15 @@ interface TangemPayApi {
|
|||
@Header("Authorization") authHeader: String,
|
||||
): ApiResponse<TariffPlanTransitionsResponse>
|
||||
|
||||
@POST("v1/customer/tariff-plan/pending-transition")
|
||||
suspend fun setPendingTariffPlanTransition(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Body body: SetPendingTariffPlanTransitionRequest,
|
||||
): ApiResponse<Any>
|
||||
|
||||
@POST("v1/customer/tariff-plan/pending-transition/cancel")
|
||||
suspend fun cancelPendingTariffPlanTransition(@Header("Authorization") authHeader: String): ApiResponse<Any>
|
||||
|
||||
/** Fiat bank requisites for the Virtual Account on-ramp (VA MVP0, TWI-1638). */
|
||||
@GET("v1/account/bank-credentials/{product_instance_id}")
|
||||
suspend fun getBankCredentials(
|
||||
|
|
|
|||
|
|
@ -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 SetPendingTariffPlanTransitionRequest(
|
||||
@Json(name = "pending_tariff_plan_id") val pendingTariffPlanId: String,
|
||||
)
|
||||
|
|
@ -140,6 +140,28 @@ internal interface TangemPayDataModule {
|
|||
return GetTangemPayTariffPlanTransitionsUseCase(repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideSetTariffPlanPendingTransitionUseCase(
|
||||
repository: TangemPayTariffPlanTransitionsRepository,
|
||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
): SetTariffPlanPendingTransitionUseCase {
|
||||
return SetTariffPlanPendingTransitionUseCase(
|
||||
repository = repository,
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideCancelTariffPlanPendingTransitionUseCase(
|
||||
repository: TangemPayTariffPlanTransitionsRepository,
|
||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
): CancelTariffPlanPendingTransitionUseCase {
|
||||
return CancelTariffPlanPendingTransitionUseCase(
|
||||
repository = repository,
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideGetTangemPayTariffPlanStateUseCase(
|
||||
customerOrderRepository: CustomerOrderRepository,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import arrow.core.Either
|
|||
import arrow.core.raise.either
|
||||
import com.tangem.data.pay.converter.TangemPayTariffPlanConverter
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.SetPendingTariffPlanTransitionRequest
|
||||
import com.tangem.datasource.api.pay.models.response.TariffPlanTransitionResponse
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlanTransition
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -26,6 +27,21 @@ internal class DefaultTariffPlanTransitionsRepository @Inject constructor(
|
|||
response.result.orEmpty().mapNotNull { it.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun setPendingTransition(
|
||||
userWalletId: UserWalletId,
|
||||
pendingTariffPlanId: String,
|
||||
): Either<VisaApiError, Unit> = requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.setPendingTariffPlanTransition(
|
||||
authHeader = authHeader,
|
||||
body = SetPendingTariffPlanTransitionRequest(pendingTariffPlanId = pendingTariffPlanId),
|
||||
)
|
||||
}.map {}
|
||||
|
||||
override suspend fun cancelPendingTransition(userWalletId: UserWalletId): Either<VisaApiError, Unit> =
|
||||
requestHelper.performRequest(userWalletId) { authHeader ->
|
||||
tangemPayApi.cancelPendingTariffPlanTransition(authHeader = authHeader)
|
||||
}.map {}
|
||||
|
||||
private fun TariffPlanTransitionResponse.toDomain(): TangemPayTariffPlanTransition? {
|
||||
val plan = TangemPayTariffPlanConverter.convert(tariffPlan) ?: return null
|
||||
return TangemPayTariffPlanTransition(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import com.google.common.truth.Truth.assertThat
|
|||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponseError
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.models.request.SetPendingTariffPlanTransitionRequest
|
||||
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
|
||||
import com.tangem.datasource.api.pay.models.response.TariffPlanTransitionResponse
|
||||
import com.tangem.datasource.api.pay.models.response.TariffPlanTransitionsResponse
|
||||
|
|
@ -14,7 +15,9 @@ import com.tangem.domain.models.account.TangemPayTariffPlanTransition
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
|
@ -121,6 +124,65 @@ internal class DefaultTariffPlanTransitionsRepositoryTest {
|
|||
.containsExactly(TangemPayTariffPlanTransition.Type.ACTIVATION)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN success WHEN setPendingTransition THEN sends pending plan id and returns Unit`() = runTest {
|
||||
// GIVEN
|
||||
val bodySlot = slot<SetPendingTariffPlanTransitionRequest>()
|
||||
coEvery {
|
||||
tangemPayApi.setPendingTariffPlanTransition(any(), capture(bodySlot))
|
||||
} returns ApiResponse.Success(Unit) as ApiResponse<Any>
|
||||
|
||||
// WHEN
|
||||
val result = repository.setPendingTransition(USER_WALLET_ID, PENDING_PLAN_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
assertThat(bodySlot.captured.pendingTariffPlanId).isEqualTo(PENDING_PLAN_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN backend error WHEN setPendingTransition THEN returns error`() = runTest {
|
||||
// GIVEN
|
||||
coEvery {
|
||||
tangemPayApi.setPendingTariffPlanTransition(any(), any())
|
||||
} returns ApiResponse.Error(ApiResponseError.NetworkException()) as ApiResponse<Any>
|
||||
|
||||
// WHEN
|
||||
val result = repository.setPendingTransition(USER_WALLET_ID, PENDING_PLAN_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN success WHEN cancelPendingTransition THEN returns Unit`() = runTest {
|
||||
// GIVEN
|
||||
coEvery {
|
||||
tangemPayApi.cancelPendingTariffPlanTransition(any())
|
||||
} returns ApiResponse.Success(Unit) as ApiResponse<Any>
|
||||
|
||||
// WHEN
|
||||
val result = repository.cancelPendingTransition(USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerify(exactly = 1) { tangemPayApi.cancelPendingTariffPlanTransition(AUTH_HEADER) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN backend error WHEN cancelPendingTransition THEN returns error`() = runTest {
|
||||
// GIVEN
|
||||
coEvery {
|
||||
tangemPayApi.cancelPendingTariffPlanTransition(any())
|
||||
} returns ApiResponse.Error(ApiResponseError.NetworkException()) as ApiResponse<Any>
|
||||
|
||||
// WHEN
|
||||
val result = repository.cancelPendingTransition(USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
|
||||
}
|
||||
|
||||
private fun tariffPlan(id: String? = PLAN_ID) = CustomerMeResponse.TariffPlan(
|
||||
id = id,
|
||||
type = "PLUS",
|
||||
|
|
@ -135,5 +197,6 @@ internal class DefaultTariffPlanTransitionsRepositoryTest {
|
|||
const val AUTH_HEADER = "auth-header"
|
||||
const val PLAN_ID = "plan-plus"
|
||||
const val PLAN_NAME = "Plus"
|
||||
const val PENDING_PLAN_ID = "plan-basic"
|
||||
}
|
||||
}
|
||||
|
|
@ -8,4 +8,11 @@ import com.tangem.domain.visa.error.VisaApiError
|
|||
interface TangemPayTariffPlanTransitionsRepository {
|
||||
|
||||
suspend fun getTransitions(userWalletId: UserWalletId): Either<VisaApiError, List<TangemPayTariffPlanTransition>>
|
||||
|
||||
suspend fun setPendingTransition(
|
||||
userWalletId: UserWalletId,
|
||||
pendingTariffPlanId: String,
|
||||
): Either<VisaApiError, Unit>
|
||||
|
||||
suspend fun cancelPendingTransition(userWalletId: UserWalletId): Either<VisaApiError, Unit>
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.repository.TangemPayTariffPlanTransitionsRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
|
||||
class CancelTariffPlanPendingTransitionUseCase(
|
||||
private val repository: TangemPayTariffPlanTransitionsRepository,
|
||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
) {
|
||||
suspend operator fun invoke(userWalletId: UserWalletId): Either<VisaApiError, Unit> = either {
|
||||
repository.cancelPendingTransition(userWalletId).bind()
|
||||
|
||||
paymentAccountStatusFetcher.invoke(userWalletId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.repository.TangemPayTariffPlanTransitionsRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
|
||||
class SetTariffPlanPendingTransitionUseCase(
|
||||
private val repository: TangemPayTariffPlanTransitionsRepository,
|
||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
) {
|
||||
suspend operator fun invoke(userWalletId: UserWalletId, pendingTariffPlanId: String): Either<VisaApiError, Unit> =
|
||||
either {
|
||||
repository.setPendingTransition(userWalletId, pendingTariffPlanId).bind()
|
||||
|
||||
paymentAccountStatusFetcher.invoke(userWalletId)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.repository.TangemPayTariffPlanTransitionsRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class CancelTariffPlanPendingTransitionUseCaseTest {
|
||||
|
||||
private val repository: TangemPayTariffPlanTransitionsRepository = mockk()
|
||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk()
|
||||
|
||||
private val useCase = CancelTariffPlanPendingTransitionUseCase(
|
||||
repository = repository,
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN cancelPendingTransition fails WHEN invoke THEN returns Left and skips fetch`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { repository.cancelPendingTransition(USER_WALLET_ID) } returns VisaApiError.Unspecified.left()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
|
||||
coVerify(exactly = 0) { paymentAccountStatusFetcher.invoke(any<UserWalletId>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cancelPendingTransition succeeds WHEN invoke THEN refreshes account status`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { repository.cancelPendingTransition(USER_WALLET_ID) } returns Unit.right()
|
||||
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val USER_WALLET_ID = UserWalletId("aabbcc112233")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.domain.pay.usecase
|
||||
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||
import com.tangem.domain.pay.repository.TangemPayTariffPlanTransitionsRepository
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class SetTariffPlanPendingTransitionUseCaseTest {
|
||||
|
||||
private val repository: TangemPayTariffPlanTransitionsRepository = mockk()
|
||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk()
|
||||
|
||||
private val useCase = SetTariffPlanPendingTransitionUseCase(
|
||||
repository = repository,
|
||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN setPendingTransition fails WHEN invoke THEN returns Left and skips fetch`() = runTest {
|
||||
// GIVEN
|
||||
coEvery {
|
||||
repository.setPendingTransition(USER_WALLET_ID, PENDING_PLAN_ID)
|
||||
} returns VisaApiError.Unspecified.left()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, PENDING_PLAN_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.leftOrNull()).isEqualTo(VisaApiError.Unspecified)
|
||||
coVerify(exactly = 0) { paymentAccountStatusFetcher.invoke(any<UserWalletId>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN setPendingTransition succeeds WHEN invoke THEN refreshes account status`() = runTest {
|
||||
// GIVEN
|
||||
coEvery { repository.setPendingTransition(USER_WALLET_ID, PENDING_PLAN_ID) } returns Unit.right()
|
||||
coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right()
|
||||
|
||||
// WHEN
|
||||
val result = useCase(USER_WALLET_ID, PENDING_PLAN_ID)
|
||||
|
||||
// THEN
|
||||
assertThat(result.isRight()).isTrue()
|
||||
coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val USER_WALLET_ID = UserWalletId("aabbcc112233")
|
||||
const val PENDING_PLAN_ID = "plan-basic"
|
||||
}
|
||||
}
|
||||
|
|
@ -92,13 +92,15 @@ internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constru
|
|||
is TangemPayAccountDetailsInnerRoute.CurrentPlan -> TangemPayCurrentPlanComponent(
|
||||
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
params = TangemPayCurrentPlanComponent.Params(
|
||||
userWalletId = params.initialStatus.userWalletId,
|
||||
tariffPlan = config.tariffPlan,
|
||||
),
|
||||
)
|
||||
TangemPayAccountDetailsInnerRoute.SelectPlan -> TangemPaySelectPlanComponent(
|
||||
is TangemPayAccountDetailsInnerRoute.SelectPlan -> TangemPaySelectPlanComponent(
|
||||
appComponentContext = childByContext(componentContext = componentContext, router = innerRouter),
|
||||
params = TangemPaySelectPlanComponent.Params(
|
||||
userWalletId = params.initialStatus.userWalletId,
|
||||
tariffPlan = config.tariffPlan,
|
||||
),
|
||||
)
|
||||
TangemPayAccountDetailsInnerRoute.VirtualAccountDepositSuccess ->
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ internal class TangemPayDetailsNotificationFactory(
|
|||
},
|
||||
)
|
||||
|
||||
// TODO v_rodionov: strings hardcoded for now - wait for localization
|
||||
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
|
||||
fun createAwaitingDepositConfig(tariffPlan: TangemPayTariffPlanState?): NotificationConfig? {
|
||||
if (!isTiersPlusPlanEnabled) return null
|
||||
if (tariffPlan == null) return null
|
||||
|
|
@ -81,7 +81,7 @@ internal class TangemPayDetailsNotificationFactory(
|
|||
iconResId = if (isRedesignEnabled) R.drawable.ic_alert_circle_24 else R.drawable.img_attention_20,
|
||||
)
|
||||
|
||||
// TODO v_rodionov: strings hardcoded for now - wait for localization
|
||||
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
|
||||
private fun createTariffSystemDownGradePendingConfig(tariffPlan: TangemPayTariffPlanState): NotificationConfig? {
|
||||
val date = tariffPlan.tariff.formatNextBillingDateOrNull() ?: return null
|
||||
val planName = tariffPlan.tariff.plan.name
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ internal sealed class TangemPayAccountDetailsInnerRoute : Route {
|
|||
) : TangemPayAccountDetailsInnerRoute()
|
||||
|
||||
@Serializable
|
||||
data object SelectPlan : TangemPayAccountDetailsInnerRoute()
|
||||
data class SelectPlan(
|
||||
val tariffPlan: TangemPayCustomerTariffPlan,
|
||||
) : TangemPayAccountDetailsInnerRoute()
|
||||
|
||||
@Serializable
|
||||
data object VirtualAccountDepositSuccess : TangemPayAccountDetailsInnerRoute()
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
internal class TangemPayCurrentPlanComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
|
|
@ -22,5 +23,8 @@ internal class TangemPayCurrentPlanComponent(
|
|||
TangemPayCurrentPlanScreen(state = state, modifier = modifier)
|
||||
}
|
||||
|
||||
data class Params(val tariffPlan: TangemPayCustomerTariffPlan)
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val tariffPlan: TangemPayCustomerTariffPlan,
|
||||
)
|
||||
}
|
||||
|
|
@ -7,18 +7,22 @@ import com.tangem.core.decompose.model.ParamsContainer
|
|||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlan
|
||||
import com.tangem.domain.pay.usecase.CancelTariffPlanPendingTransitionUseCase
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
|
||||
import com.tangem.features.tangempay.tiers.formatNextBillingDateOrNull
|
||||
import com.tangem.features.tangempay.tiers.formatRecurringFeeOrNull
|
||||
import com.tangem.features.tangempay.utils.TangemPayMessagesFactory
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
|
|
@ -27,10 +31,14 @@ internal class TangemPayCurrentPlanModel @Inject constructor(
|
|||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val cancelPendingTransition: CancelTariffPlanPendingTransitionUseCase,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<TangemPayCurrentPlanComponent.Params>()
|
||||
|
||||
private var isProcessing: Boolean = false
|
||||
|
||||
val state: StateFlow<TangemPayCurrentPlanUM>
|
||||
field = MutableStateFlow(createState(params.tariffPlan))
|
||||
|
||||
|
|
@ -39,10 +47,10 @@ internal class TangemPayCurrentPlanModel @Inject constructor(
|
|||
notification = createNotification(customerPlan),
|
||||
sections = buildSections(customerPlan.plan),
|
||||
onBackClick = router::pop,
|
||||
onChangePlanClick = { router.push(TangemPayAccountDetailsInnerRoute.SelectPlan) },
|
||||
onChangePlanClick = { router.push(TangemPayAccountDetailsInnerRoute.SelectPlan(params.tariffPlan)) },
|
||||
)
|
||||
|
||||
// TODO v_rodionov: strings hardcoded for now - wait for localization
|
||||
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
|
||||
private fun createNotification(customerPlan: TangemPayCustomerTariffPlan): TangemPayCurrentPlanUM.Notification? {
|
||||
val date = customerPlan.formatNextBillingDateOrNull(formatter = DateTimeFormatters.dateMMMd) ?: return null
|
||||
val feeText = customerPlan.plan.formatRecurringFeeOrNull() ?: return null
|
||||
|
|
@ -56,7 +64,8 @@ internal class TangemPayCurrentPlanModel @Inject constructor(
|
|||
),
|
||||
button = TangemPayCurrentPlanUM.Notification.Button(
|
||||
text = stringReference("Stay on ${customerPlan.plan.name}"),
|
||||
onClick = {}, // TODO v_rodionov: #[REDACTED_TASK_KEY] - Downgrade
|
||||
isProcessing = isProcessing,
|
||||
onClick = ::onStayOnPlanClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -67,6 +76,35 @@ internal class TangemPayCurrentPlanModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onStayOnPlanClick() {
|
||||
if (isProcessing) return
|
||||
val customerPlan = params.tariffPlan
|
||||
val targetPlanName = customerPlan.pendingPlan?.name ?: return
|
||||
uiMessageSender.send(
|
||||
message = TangemPayMessagesFactory.createStayOnPlanMessage(
|
||||
planName = customerPlan.plan.name,
|
||||
targetPlanName = targetPlanName,
|
||||
onStayClick = ::confirmStayOnPlan,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun confirmStayOnPlan() {
|
||||
if (isProcessing) return
|
||||
isProcessing = true
|
||||
state.value = createState(params.tariffPlan)
|
||||
modelScope.launch {
|
||||
cancelPendingTransition(params.userWalletId).fold(
|
||||
ifRight = { router.pop() },
|
||||
ifLeft = {
|
||||
isProcessing = false
|
||||
state.value = createState(params.tariffPlan)
|
||||
uiMessageSender.send(message = TangemPayMessagesFactory.createGenericError())
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildSections(plan: TangemPayTariffPlan) = persistentListOf(
|
||||
sectionOf(plan, TangemPayTariffPlan.Section.CARD_RELATED, R.string.tangempay_current_plan_section_card),
|
||||
sectionOf(plan, TangemPayTariffPlan.Section.PLAN_RELATED, R.string.tangempay_current_plan_section_plan),
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ private fun PlanNotification(notification: TangemPayCurrentPlanUM.Notification,
|
|||
variant = TangemButton.Variant.Secondary,
|
||||
size = TangemButton.Size.X11,
|
||||
text = button.text,
|
||||
isLoading = button.isProcessing,
|
||||
onClick = button.onClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -205,6 +206,7 @@ private fun previewState() = TangemPayCurrentPlanUM(
|
|||
),
|
||||
button = TangemPayCurrentPlanUM.Notification.Button(
|
||||
text = stringReference("Stay on Plus"),
|
||||
isProcessing = false,
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ internal data class TangemPayCurrentPlanUM(
|
|||
@Immutable
|
||||
data class Button(
|
||||
val text: TextReference,
|
||||
val isProcessing: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.account.TangemPayCustomerTariffPlan
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
internal class TangemPaySelectPlanComponent(
|
||||
|
|
@ -22,5 +23,8 @@ internal class TangemPaySelectPlanComponent(
|
|||
TangemPaySelectPlanScreen(state = state, modifier = modifier)
|
||||
}
|
||||
|
||||
data class Params(val userWalletId: UserWalletId)
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val tariffPlan: TangemPayCustomerTariffPlan,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.tangempay.tiers.select
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -8,12 +9,16 @@ import com.tangem.core.decompose.navigation.Router
|
|||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlan
|
||||
import com.tangem.domain.models.account.TangemPayTariffPlanTransition
|
||||
import com.tangem.domain.pay.usecase.CreateTariffPlanTransitionOrderUseCase
|
||||
import com.tangem.domain.pay.usecase.GetTangemPayTariffPlanTransitionsUseCase
|
||||
import com.tangem.domain.pay.usecase.SetTariffPlanPendingTransitionUseCase
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
|
||||
import com.tangem.features.tangempay.tiers.formatNextBillingDateOrNull
|
||||
import com.tangem.features.tangempay.tiers.formatRecurringFeeOrNull
|
||||
import com.tangem.features.tangempay.utils.TangemPayMessagesFactory
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -25,6 +30,7 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TangemPaySelectPlanModel @Inject constructor(
|
||||
|
|
@ -33,6 +39,7 @@ internal class TangemPaySelectPlanModel @Inject constructor(
|
|||
private val router: Router,
|
||||
private val getTransitions: GetTangemPayTariffPlanTransitionsUseCase,
|
||||
private val createTransitionOrder: CreateTariffPlanTransitionOrderUseCase,
|
||||
private val setPendingTransition: SetTariffPlanPendingTransitionUseCase,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -95,21 +102,34 @@ internal class TangemPaySelectPlanModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onConfirmClick() {
|
||||
val transition = allowedTransitions.getOrNull(selectedIndex) ?: return
|
||||
|
||||
// TODO v_rodionov: #[REDACTED_TASK_KEY] - Downgrade
|
||||
if (transition.type != TangemPayTariffPlanTransition.Type.UPGRADE) return
|
||||
|
||||
if (isProcessing) return
|
||||
|
||||
val transition = allowedTransitions.getOrNull(selectedIndex) ?: return
|
||||
when (transition.type) {
|
||||
TangemPayTariffPlanTransition.Type.ACTIVATION,
|
||||
TangemPayTariffPlanTransition.Type.UPGRADE,
|
||||
-> submitTransition {
|
||||
createTransitionOrder(
|
||||
userWalletId = params.userWalletId,
|
||||
targetTariffPlanId = transition.plan.id,
|
||||
transitionType = transition.type,
|
||||
)
|
||||
}
|
||||
TangemPayTariffPlanTransition.Type.DOWNGRADE -> submitTransition {
|
||||
setPendingTransition(
|
||||
userWalletId = params.userWalletId,
|
||||
pendingTariffPlanId = transition.plan.id,
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun submitTransition(action: suspend () -> Either<VisaApiError, Unit>) {
|
||||
isProcessing = true
|
||||
state.update { buildState() }
|
||||
modelScope.launch {
|
||||
createTransitionOrder(
|
||||
userWalletId = params.userWalletId,
|
||||
targetTariffPlanId = transition.plan.id,
|
||||
transitionType = transition.type,
|
||||
).fold(
|
||||
action().fold(
|
||||
ifRight = { router.popTo(TangemPayAccountDetailsInnerRoute.AccountDetails) },
|
||||
ifLeft = {
|
||||
isProcessing = false
|
||||
|
|
@ -121,11 +141,13 @@ internal class TangemPaySelectPlanModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun buildState(showPlanCompare: Boolean = false): TangemPaySelectPlanUM = TangemPaySelectPlanUM(
|
||||
topBarTitle = if (isConfirm) {
|
||||
resourceReference(R.string.tangempay_select_plan_confirm_title)
|
||||
} else {
|
||||
resourceReference(R.string.tangempay_select_plan_title)
|
||||
},
|
||||
topBarTitle = resourceReference(
|
||||
if (isConfirm) {
|
||||
R.string.tangempay_select_plan_confirm_title
|
||||
} else {
|
||||
R.string.tangempay_select_plan_title
|
||||
},
|
||||
),
|
||||
plans = allowedTransitions.map { it.plan.toPlanUM() }.toImmutableList(),
|
||||
selectedIndex = selectedIndex,
|
||||
onPlanSelected = ::onPlanSelected,
|
||||
|
|
@ -141,7 +163,7 @@ internal class TangemPaySelectPlanModel @Inject constructor(
|
|||
)
|
||||
|
||||
private fun buildCompare(): TangemPaySelectPlanUM.ComparePlans {
|
||||
val plans = transitions.map { it.plan }
|
||||
val plans = listOf(params.tariffPlan.plan) + transitions.map { it.plan }
|
||||
val orderedTitles = plans
|
||||
.flatMap { plan -> plan.descriptionItems.filter { it.section in COMPARE_SECTIONS } }
|
||||
.sortedWith(compareBy({ it.section.ordinal }, { it.order }))
|
||||
|
|
@ -164,19 +186,35 @@ internal class TangemPaySelectPlanModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
|
||||
private fun buildConfirmContent(): TangemPaySelectPlanUM.Content {
|
||||
val transition = allowedTransitions.getOrNull(selectedIndex) ?: return buildSelectContent()
|
||||
val planName = transition.plan.name
|
||||
val isUpgrade = transition.type == TangemPayTariffPlanTransition.Type.UPGRADE
|
||||
val targetProgramme = transition.plan.name
|
||||
return TangemPaySelectPlanUM.Content.Confirm(
|
||||
// TODO v_rodionov: strings hardcoded for now - wait for documentation update
|
||||
title = stringReference("We will issue Visa $planName for you"),
|
||||
points = buildConfirmPoints(transition, planName, isUpgrade),
|
||||
title = when (transition.type) {
|
||||
TangemPayTariffPlanTransition.Type.UPGRADE -> stringReference(
|
||||
"We will issue Visa $targetProgramme for you",
|
||||
)
|
||||
TangemPayTariffPlanTransition.Type.DOWNGRADE -> {
|
||||
val nextBillingDate = nextBillingDate()
|
||||
if (nextBillingDate != null) {
|
||||
val programName = "UNKNOWN" // TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
|
||||
val planName = params.tariffPlan.plan.name
|
||||
stringReference(
|
||||
"Your $planName plan and $programName cards will be active till $nextBillingDate",
|
||||
)
|
||||
} else {
|
||||
stringReference("You are switching to $targetProgramme")
|
||||
}
|
||||
}
|
||||
else -> stringReference("You are switching to $targetProgramme")
|
||||
},
|
||||
points = buildConfirmPoints(transition),
|
||||
confirmButtonText = resourceReference(
|
||||
if (isUpgrade) {
|
||||
R.string.tangempay_select_plan_btn_upgrade
|
||||
} else {
|
||||
R.string.tangempay_select_plan_btn_downgrade
|
||||
when (transition.type) {
|
||||
TangemPayTariffPlanTransition.Type.UPGRADE -> R.string.tangempay_select_plan_btn_upgrade
|
||||
TangemPayTariffPlanTransition.Type.DOWNGRADE -> R.string.tangempay_select_plan_btn_downgrade
|
||||
else -> R.string.common_continue
|
||||
},
|
||||
),
|
||||
isProcessing = isProcessing,
|
||||
|
|
@ -185,29 +223,39 @@ internal class TangemPaySelectPlanModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
// TODO v_rodionov: strings hardcoded for now - wait for documentation update
|
||||
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
|
||||
private fun buildConfirmPoints(
|
||||
transition: TangemPayTariffPlanTransition,
|
||||
planName: String,
|
||||
isUpgrade: Boolean,
|
||||
): ImmutableList<TangemPaySelectPlanUM.PointUM> = if (isUpgrade) {
|
||||
val feeText = transition.plan.formatRecurringFeeOrNull()
|
||||
listOf(
|
||||
stringReference("You will get your virtual Visa $planName in minutes"),
|
||||
if (feeText != null) {
|
||||
stringReference("$feeText monthly fee will be taken from your account")
|
||||
} else {
|
||||
stringReference("No fee applied")
|
||||
},
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
stringReference("Your current Visa cards will be closed"),
|
||||
stringReference("No fee applied"),
|
||||
)
|
||||
): ImmutableList<TangemPaySelectPlanUM.PointUM> {
|
||||
val programName = "UNKNOWN" // TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
|
||||
return when (transition.type) {
|
||||
TangemPayTariffPlanTransition.Type.UPGRADE -> {
|
||||
val feeText = transition.plan.formatRecurringFeeOrNull()
|
||||
buildList {
|
||||
add("You will get your virtual Visa $programName in minutes")
|
||||
if (feeText != null) {
|
||||
add("$feeText monthly fee will be taken from your account")
|
||||
}
|
||||
}
|
||||
}
|
||||
TangemPayTariffPlanTransition.Type.DOWNGRADE -> {
|
||||
val date = nextBillingDate()
|
||||
buildList {
|
||||
if (date != null) {
|
||||
add("On $date we will move you to ${transition.plan.name} plan")
|
||||
}
|
||||
add("Your Visa $programName cards will be closed")
|
||||
if (date != null) {
|
||||
add("You can cancel this transition till $date")
|
||||
}
|
||||
add("No fee applied")
|
||||
}
|
||||
}
|
||||
else -> listOf("No fee applied")
|
||||
}
|
||||
.map { TangemPaySelectPlanUM.PointUM(title = stringReference(it), body = null) }
|
||||
.toImmutableList()
|
||||
}
|
||||
.map { TangemPaySelectPlanUM.PointUM(title = it, body = null) }
|
||||
.toImmutableList()
|
||||
|
||||
private fun TangemPayTariffPlan.toPlanUM() = TangemPaySelectPlanUM.PlanUM(
|
||||
name = stringReference(name),
|
||||
|
|
@ -224,10 +272,15 @@ internal class TangemPaySelectPlanModel @Inject constructor(
|
|||
.toImmutableList(),
|
||||
)
|
||||
|
||||
private fun nextBillingDate(): String? {
|
||||
return params.tariffPlan.formatNextBillingDateOrNull(DateTimeFormatters.dateMMMd)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val ALLOWED_TYPES = setOf(
|
||||
TangemPayTariffPlanTransition.Type.UPGRADE,
|
||||
TangemPayTariffPlanTransition.Type.DOWNGRADE,
|
||||
TangemPayTariffPlanTransition.Type.ACTIVATION,
|
||||
)
|
||||
private val COMPARE_SECTIONS = setOf(
|
||||
TangemPayTariffPlan.Section.CARD_RELATED,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.message.BottomSheetMessage
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
|
|
@ -170,6 +171,35 @@ internal object TangemPayMessagesFactory {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO v_rodionov: #[REDACTED_TASK_KEY] fix hardcoded strings
|
||||
fun createStayOnPlanMessage(
|
||||
planName: String,
|
||||
targetPlanName: String,
|
||||
onStayClick: () -> Unit,
|
||||
): BottomSheetMessage {
|
||||
return bottomSheetMessage {
|
||||
infoBlock {
|
||||
icon(R.drawable.ic_heart_20) {
|
||||
type = MessageBottomSheetUM.Icon.Type.Informative
|
||||
backgroundType = MessageBottomSheetUM.Icon.BackgroundType.Informative
|
||||
}
|
||||
title = stringReference("Do you want to stay on $planName?")
|
||||
body = stringReference("Your transition on $targetPlanName will be canceled")
|
||||
}
|
||||
secondaryButton {
|
||||
text = resourceReference(R.string.common_cancel)
|
||||
onClick { closeBs() }
|
||||
}
|
||||
primaryButton {
|
||||
text = stringReference("Stay on $planName")
|
||||
onClick {
|
||||
onStayClick()
|
||||
closeBs()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createGenericError(): DialogMessage {
|
||||
return DialogMessage(
|
||||
title = resourceReference(R.string.common_something_went_wrong),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue