Updated on 2026-08-14
This commit is contained in:
parent
889f47acdb
commit
489726e264
7 changed files with 307 additions and 3 deletions
|
|
@ -1804,6 +1804,8 @@
|
||||||
<string name="tangem_pay_transaction_details_mcc">MCC</string>
|
<string name="tangem_pay_transaction_details_mcc">MCC</string>
|
||||||
<string name="tangem_pay_transaction_fee_notification_text">A fee is charged due to the service tariffs</string>
|
<string name="tangem_pay_transaction_fee_notification_text">A fee is charged due to the service tariffs</string>
|
||||||
<string name="tangem_pay_transaction_reversed_notification_text">The transaction was partially or fully reversed by the merchant</string>
|
<string name="tangem_pay_transaction_reversed_notification_text">The transaction was partially or fully reversed by the merchant</string>
|
||||||
|
<string name="tangem_pay_unavailable_region_description">We are rolling it out gradually and let you know when Tangem Pay will be available here</string>
|
||||||
|
<string name="tangem_pay_unavailable_region_title">Tangem Pay is unavailable in your region</string>
|
||||||
<string name="tangem_pay_unfreeze_card_alert_body">Keep using your money. You can freeze anytime.</string>
|
<string name="tangem_pay_unfreeze_card_alert_body">Keep using your money. You can freeze anytime.</string>
|
||||||
<string name="tangem_pay_unfreeze_card_alert_title">Unfreeze your card?</string>
|
<string name="tangem_pay_unfreeze_card_alert_title">Unfreeze your card?</string>
|
||||||
<string name="tangem_pay_unfreeze_card_failed">Failed to unfreeze the card. Try again later.</string>
|
<string name="tangem_pay_unfreeze_card_failed">Failed to unfreeze the card. Try again later.</string>
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
|
||||||
private fun TangemPayEntryPoint.toEligibilityType(): TangemPayEligibilityType = when (this) {
|
private fun TangemPayEntryPoint.toEligibilityType(): TangemPayEligibilityType = when (this) {
|
||||||
TangemPayEntryPoint.BANNER -> TangemPayEligibilityType.BANNER
|
TangemPayEntryPoint.BANNER -> TangemPayEligibilityType.BANNER
|
||||||
TangemPayEntryPoint.DETAILS -> TangemPayEligibilityType.DETAILS
|
TangemPayEntryPoint.DETAILS -> TangemPayEligibilityType.DETAILS
|
||||||
|
TangemPayEntryPoint.DEEPLINK -> TangemPayEligibilityType.DEEPLINK
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class UserWalletData(
|
private data class UserWalletData(
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,5 @@ package com.tangem.domain.pay.model
|
||||||
enum class TangemPayEntryPoint {
|
enum class TangemPayEntryPoint {
|
||||||
BANNER,
|
BANNER,
|
||||||
DETAILS,
|
DETAILS,
|
||||||
|
DEEPLINK,
|
||||||
}
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ import com.tangem.features.tangempay.model.transformers.TangemPayOnboardingButto
|
||||||
import com.tangem.features.tangempay.ui.TangemPayOnboardingNavigation
|
import com.tangem.features.tangempay.ui.TangemPayOnboardingNavigation
|
||||||
import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState
|
import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import com.tangem.utils.coroutines.runSuspendCatching
|
||||||
import com.tangem.utils.logging.TangemLogger
|
import com.tangem.utils.logging.TangemLogger
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
@ -64,7 +65,7 @@ internal class TangemPayOnboardingModel @Inject constructor(
|
||||||
when (params) {
|
when (params) {
|
||||||
is TangemPayOnboardingComponent.Params.Deeplink -> {
|
is TangemPayOnboardingComponent.Params.Deeplink -> {
|
||||||
repository.validateDeeplink(params.deeplink)
|
repository.validateDeeplink(params.deeplink)
|
||||||
.onRight { isValid -> if (isValid) showOnboarding() else back() }
|
.onRight { isValid -> if (isValid) checkEligibilityAndShow() else back() }
|
||||||
.onLeft { back() }
|
.onLeft { back() }
|
||||||
}
|
}
|
||||||
is TangemPayOnboardingComponent.Params.ContinueOnboarding -> {
|
is TangemPayOnboardingComponent.Params.ContinueOnboarding -> {
|
||||||
|
|
@ -93,6 +94,21 @@ internal class TangemPayOnboardingModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun checkEligibilityAndShow() {
|
||||||
|
runSuspendCatching {
|
||||||
|
eligibilityManager.getTangemPayAvailability(TangemPayEntryPoint.DEEPLINK)
|
||||||
|
}
|
||||||
|
.onSuccess { isAvailable -> if (isAvailable) showOnboarding() else showNotAvailable() }
|
||||||
|
.onFailure { error ->
|
||||||
|
TangemLogger.e(messageString = "TangemPayOnboarding: eligibility check failed", throwable = error)
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNotAvailable() {
|
||||||
|
uiState.update { state -> TangemPayOnboardingScreenState.NotAvailable(onBack = state.onBack) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun checkCustomerInfo(userWalletId: UserWalletId) {
|
private fun checkCustomerInfo(userWalletId: UserWalletId) {
|
||||||
modelScope.launch {
|
modelScope.launch {
|
||||||
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true))
|
uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = true))
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,24 @@ package com.tangem.features.tangempay.ui
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -23,14 +28,35 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||||
|
import com.tangem.core.ui.ds2.button.TangemButton
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||||
|
import com.tangem.core.ui.res.generated.icons.Icons
|
||||||
|
import com.tangem.core.ui.res.generated.icons.ic_heart_broken_32
|
||||||
import com.tangem.core.ui.utils.WindowInsetsZero
|
import com.tangem.core.ui.utils.WindowInsetsZero
|
||||||
|
|
||||||
|
// Warm top glow of the "unavailable" screen: a translucent gold (sampled from Figma) drawn over
|
||||||
|
// bg.primary, so one value matches both light and dark themes. Fades to transparent by mid-screen.
|
||||||
|
private const val GLOW_COLOR = 0xFFFCB92A
|
||||||
|
private const val GLOW_ALPHA = 0.49f
|
||||||
|
private const val GLOW_FADE_STOP = 0.5f
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun TandemPayOnboardingScreen(state: TangemPayOnboardingScreenState, modifier: Modifier = Modifier) {
|
internal fun TandemPayOnboardingScreen(state: TangemPayOnboardingScreenState, modifier: Modifier = Modifier) {
|
||||||
|
when (state) {
|
||||||
|
is TangemPayOnboardingScreenState.NotAvailable ->
|
||||||
|
TangemPayOnboardingUnavailable(onBack = state.onBack, modifier = modifier)
|
||||||
|
is TangemPayOnboardingScreenState.Loading,
|
||||||
|
is TangemPayOnboardingScreenState.Content,
|
||||||
|
-> TangemPayOnboardingScaffold(state = state, modifier = modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TangemPayOnboardingScaffold(state: TangemPayOnboardingScreenState, modifier: Modifier = Modifier) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier.systemBarsPadding(),
|
modifier = modifier.systemBarsPadding(),
|
||||||
topBar = {
|
topBar = {
|
||||||
|
|
@ -52,11 +78,81 @@ internal fun TandemPayOnboardingScreen(state: TangemPayOnboardingScreenState, mo
|
||||||
state = state,
|
state = state,
|
||||||
modifier = contentModifier,
|
modifier = contentModifier,
|
||||||
)
|
)
|
||||||
|
is TangemPayOnboardingScreenState.NotAvailable -> Unit
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TangemPayOnboardingUnavailable(onBack: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(TangemTheme.colors3.bg.primary),
|
||||||
|
) {
|
||||||
|
UnavailableGlow(modifier = Modifier.matchParentSize())
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.systemBarsPadding(),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(top = 72.dp, start = 24.dp, end = 24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
imageVector = Icons.ic_heart_broken_32,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = TangemTheme.colors3.icon.primary,
|
||||||
|
)
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
Text(
|
||||||
|
text = stringResourceSafe(R.string.tangem_pay_unavailable_region_title),
|
||||||
|
style = TangemTheme.typography3.heading.medium,
|
||||||
|
color = TangemTheme.colors3.text.primary,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResourceSafe(R.string.tangem_pay_unavailable_region_description),
|
||||||
|
style = TangemTheme.typography3.subheading.medium,
|
||||||
|
color = TangemTheme.colors3.text.secondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
TangemButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
variant = TangemButton.Variant.Primary,
|
||||||
|
size = TangemButton.Size.X12,
|
||||||
|
text = resourceReference(R.string.common_got_it),
|
||||||
|
onClick = onBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun UnavailableGlow(modifier: Modifier = Modifier) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier.drawBehind {
|
||||||
|
drawRect(
|
||||||
|
brush = Brush.radialGradient(
|
||||||
|
colorStops = arrayOf(
|
||||||
|
0f to Color(GLOW_COLOR).copy(alpha = GLOW_ALPHA),
|
||||||
|
GLOW_FADE_STOP to Color.Transparent,
|
||||||
|
1f to Color.Transparent,
|
||||||
|
),
|
||||||
|
center = Offset(x = size.width / 2f, y = 0f),
|
||||||
|
radius = size.height,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TangemPayOnboardingLoading(modifier: Modifier = Modifier) {
|
private fun TangemPayOnboardingLoading(modifier: Modifier = Modifier) {
|
||||||
Box(modifier = modifier, contentAlignment = Alignment.Center) {
|
Box(modifier = modifier, contentAlignment = Alignment.Center) {
|
||||||
|
|
@ -155,7 +251,7 @@ private fun TandemPayOnboardingScreenPreview(
|
||||||
@PreviewParameter(TangemPayOnboardingScreenStateProvider::class)
|
@PreviewParameter(TangemPayOnboardingScreenStateProvider::class)
|
||||||
state: TangemPayOnboardingScreenState,
|
state: TangemPayOnboardingScreenState,
|
||||||
) {
|
) {
|
||||||
TangemThemePreview {
|
TangemThemePreviewRedesign {
|
||||||
TandemPayOnboardingScreen(state = state, modifier = Modifier.fillMaxSize())
|
TandemPayOnboardingScreen(state = state, modifier = Modifier.fillMaxSize())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -174,5 +270,6 @@ private class TangemPayOnboardingScreenStateProvider :
|
||||||
onTermsClick = {},
|
onTermsClick = {},
|
||||||
buttonConfig = TangemPayOnboardingScreenState.Content.ButtonConfig(isLoading = true, onClick = {}),
|
buttonConfig = TangemPayOnboardingScreenState.Content.ButtonConfig(isLoading = true, onClick = {}),
|
||||||
),
|
),
|
||||||
|
TangemPayOnboardingScreenState.NotAvailable(onBack = {}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -14,4 +14,9 @@ internal sealed class TangemPayOnboardingScreenState {
|
||||||
) : TangemPayOnboardingScreenState() {
|
) : TangemPayOnboardingScreenState() {
|
||||||
data class ButtonConfig(val isLoading: Boolean, val onClick: () -> Unit)
|
data class ButtonConfig(val isLoading: Boolean, val onClick: () -> Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shown when the user is not eligible for Tangem Pay. [onBack] also backs the "Got it" button.
|
||||||
|
*/
|
||||||
|
data class NotAvailable(override val onBack: () -> Unit) : TangemPayOnboardingScreenState()
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
|
package com.tangem.features.tangempay.model
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
|
import arrow.core.left
|
||||||
|
import arrow.core.right
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
|
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||||
|
import com.tangem.core.decompose.navigation.Router
|
||||||
|
import com.tangem.core.error.UniversalError
|
||||||
|
import com.tangem.core.navigation.url.UrlOpener
|
||||||
|
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||||
|
import com.tangem.domain.pay.model.TangemPayEntryPoint
|
||||||
|
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||||
|
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
||||||
|
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
|
||||||
|
import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState
|
||||||
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
|
import io.mockk.clearMocks
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
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
|
||||||
|
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
internal class TangemPayOnboardingModelTest {
|
||||||
|
|
||||||
|
private val router: Router = mockk(relaxed = true)
|
||||||
|
private val repository: OnboardingRepository = mockk(relaxed = true)
|
||||||
|
private val eligibilityManager: TangemPayEligibilityManager = mockk()
|
||||||
|
private val analytics: AnalyticsEventHandler = mockk(relaxed = true)
|
||||||
|
private val produceInitialDataUseCase: ProduceTangemPayInitialDataUseCase = mockk(relaxed = true)
|
||||||
|
private val urlOpener: UrlOpener = mockk(relaxed = true)
|
||||||
|
|
||||||
|
private val deeplink = "tangem://onboard-visa"
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun resetMocks() {
|
||||||
|
clearMocks(router, repository, eligibilityManager, analytics, produceInitialDataUseCase, urlOpener)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideAvailabilityCases")
|
||||||
|
fun `GIVEN valid deeplink WHEN model created THEN state reflects tangem pay availability`(
|
||||||
|
case: AvailabilityCase,
|
||||||
|
) = runTest {
|
||||||
|
// Arrange
|
||||||
|
coEvery { repository.validateDeeplink(deeplink) } returns true.right()
|
||||||
|
coEvery { eligibilityManager.getTangemPayAvailability(TangemPayEntryPoint.DEEPLINK) } returns case.isAvailable
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val model = createModel(TangemPayOnboardingComponent.Params.Deeplink(deeplink))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(model.uiState.value).isInstanceOf(case.expectedState)
|
||||||
|
coVerify(exactly = 1) { eligibilityManager.getTangemPayAvailability(TangemPayEntryPoint.DEEPLINK) }
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("provideRejectedDeeplinkCases")
|
||||||
|
fun `GIVEN deeplink rejected WHEN model created THEN screen closed AND availability not checked`(
|
||||||
|
validationResult: Either<UniversalError, Boolean>,
|
||||||
|
) = runTest {
|
||||||
|
// Arrange
|
||||||
|
coEvery { repository.validateDeeplink(deeplink) } returns validationResult
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val model = createModel(TangemPayOnboardingComponent.Params.Deeplink(deeplink))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
verify(exactly = 1) { router.pop() }
|
||||||
|
coVerify(exactly = 0) { eligibilityManager.getTangemPayAvailability(any()) }
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN availability check fails WHEN model created THEN screen closed`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
coEvery { repository.validateDeeplink(deeplink) } returns true.right()
|
||||||
|
coEvery { eligibilityManager.getTangemPayAvailability(TangemPayEntryPoint.DEEPLINK) } throws
|
||||||
|
RuntimeException("network error")
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val model = createModel(TangemPayOnboardingComponent.Params.Deeplink(deeplink))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
coVerify(exactly = 1) { eligibilityManager.getTangemPayAvailability(TangemPayEntryPoint.DEEPLINK) }
|
||||||
|
verify(exactly = 1) { router.pop() }
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN unavailable shown WHEN got it clicked THEN screen closed`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
coEvery { repository.validateDeeplink(deeplink) } returns true.right()
|
||||||
|
coEvery { eligibilityManager.getTangemPayAvailability(TangemPayEntryPoint.DEEPLINK) } returns false
|
||||||
|
val model = createModel(TangemPayOnboardingComponent.Params.Deeplink(deeplink))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Act
|
||||||
|
model.uiState.value.onBack.invoke()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
verify(exactly = 1) { router.pop() }
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN from banner entry WHEN model created THEN content shown without availability check`() = runTest {
|
||||||
|
// Act
|
||||||
|
val model = createModel(TangemPayOnboardingComponent.Params.FromBannerOnMain)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(model.uiState.value).isInstanceOf(TangemPayOnboardingScreenState.Content::class.java)
|
||||||
|
coVerify(exactly = 0) { eligibilityManager.getTangemPayAvailability(any()) }
|
||||||
|
model.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createModel(params: TangemPayOnboardingComponent.Params): TangemPayOnboardingModel {
|
||||||
|
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||||
|
return TangemPayOnboardingModel(
|
||||||
|
paramsContainer = MutableParamsContainer(params),
|
||||||
|
dispatchers = TestingCoroutineDispatcherProvider(
|
||||||
|
main = testDispatcher,
|
||||||
|
mainImmediate = testDispatcher,
|
||||||
|
io = testDispatcher,
|
||||||
|
default = testDispatcher,
|
||||||
|
single = testDispatcher,
|
||||||
|
),
|
||||||
|
analytics = analytics,
|
||||||
|
router = router,
|
||||||
|
repository = repository,
|
||||||
|
produceInitialDataUseCase = produceInitialDataUseCase,
|
||||||
|
urlOpener = urlOpener,
|
||||||
|
eligibilityManager = eligibilityManager,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class AvailabilityCase(
|
||||||
|
val isAvailable: Boolean,
|
||||||
|
val expectedState: Class<out TangemPayOnboardingScreenState>,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun provideAvailabilityCases() = listOf(
|
||||||
|
AvailabilityCase(
|
||||||
|
isAvailable = true,
|
||||||
|
expectedState = TangemPayOnboardingScreenState.Content::class.java,
|
||||||
|
),
|
||||||
|
AvailabilityCase(
|
||||||
|
isAvailable = false,
|
||||||
|
expectedState = TangemPayOnboardingScreenState.NotAvailable::class.java,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun provideRejectedDeeplinkCases(): List<Either<UniversalError, Boolean>> = listOf(
|
||||||
|
false.right(),
|
||||||
|
DEEPLINK_ERROR.left(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val DEEPLINK_ERROR = object : UniversalError {
|
||||||
|
override val errorCode: Int = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue