Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-25 12:08:04 +04:00
parent d050fa2654
commit bbb3c9d6bb
16 changed files with 112 additions and 115 deletions

View file

@ -7,10 +7,10 @@ import com.tangem.screens.AlreadyUsedWalletDialogPageObject.message
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.requestSupportButton
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.thisIsMyWalletButton
import com.tangem.screens.AlreadyUsedWalletDialogPageObject.title
import com.tangem.screens.ScanWarningDialogPageObject
import com.tangem.screens.onActionIsUnavailableDialog
import com.tangem.screens.onDataNotLoadedDialog
import com.tangem.screens.onFailedTransactionDialog
import com.tangem.screens.onScanWarningDialog
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.checkFailedTransactionDialog() {
@ -31,21 +31,21 @@ fun BaseTestCase.checkFailedTransactionDialog() {
}
}
fun checkScanWarningDialog() {
fun BaseTestCase.checkScanWarningDialog() {
step("Assert 'Scan warning' dialog title is displayed") {
ScanWarningDialogPageObject { warningTitle.isDisplayed() }
onScanWarningDialog { warningTitle.assertIsDisplayed() }
}
step("Assert warning dialog message is displayed") {
ScanWarningDialogPageObject { warningMessage.isDisplayed() }
onScanWarningDialog { warningMessage.assertIsDisplayed() }
}
step("Assert 'Cancel' button is displayed") {
ScanWarningDialogPageObject { cancelButton.isDisplayed() }
onScanWarningDialog { cancelButton.assertIsDisplayed() }
}
step("Assert 'How to scan' button is displayed") {
ScanWarningDialogPageObject { howToScanButton.isDisplayed() }
onScanWarningDialog { howToScanButton.assertIsDisplayed() }
}
step("Assert 'Request support' button is displayed") {
ScanWarningDialogPageObject { requestSupportButton.isDisplayed() }
onScanWarningDialog { requestSupportButton.assertIsDisplayed() }
}
}

View file

@ -1,36 +1,42 @@
package com.tangem.screens
import com.kaspersky.kaspresso.screens.KScreen
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.tap.features.scanfails.ui.ScanFailsDialogTestTags
import com.tangem.wallet.R
import io.github.kakaocup.kakao.text.KTextView
import io.github.kakaocup.kakao.text.KButton
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
object ScanWarningDialogPageObject : KScreen<ScanWarningDialogPageObject>() {
class ScanWarningDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ScanWarningDialogPageObject>(semanticsProvider = semanticsProvider) {
override val layoutId: Int? = null
override val viewClass: Class<*>? = null
val warningTitle = KTextView {
withText(R.string.common_warning)
val warningTitle: KNode = child {
hasText(getResourceString(R.string.common_warning))
useUnmergedTree = true
}
val warningMessage = KTextView {
withText(R.string.alert_troubleshooting_scan_card_title)
val warningMessage: KNode = child {
hasText(getResourceString(R.string.alert_troubleshooting_scan_card_title))
useUnmergedTree = true
}
val tryAgainButton = KButton {
withId(R.id.try_again_button)
val howToScanButton: KNode = child {
hasTestTag(ScanFailsDialogTestTags.HOW_TO_SCAN_BUTTON)
useUnmergedTree = true
}
val howToScanButton = KButton {
withId(R.id.how_to_scan_button)
val requestSupportButton: KNode = child {
hasTestTag(ScanFailsDialogTestTags.REQUEST_SUPPORT_BUTTON)
useUnmergedTree = true
}
val requestSupportButton = KButton {
withId(R.id.request_support_button)
val cancelButton: KNode = child {
hasTestTag(ScanFailsDialogTestTags.CANCEL_BUTTON)
useUnmergedTree = true
}
}
val cancelButton = KButton {
withId(R.id.cancel_button)
}
}
internal fun BaseTestCase.onScanWarningDialog(function: ScanWarningDialogPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -7,13 +7,26 @@ import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.clickAndWaitFor
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.redux.StateDialog
import com.tangem.domain.card.ScanFailsRequester
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import com.tangem.scenarios.checkFailedTransactionDialog
import com.tangem.scenarios.checkScanWarningDialog
import com.tangem.scenarios.openMainScreen
import com.tangem.scenarios.synchronizeAddresses
import com.tangem.screens.*
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.screens.ThirdPartyAppPageObject
import com.tangem.screens.onCreateWalletStartScreen
import com.tangem.screens.onDetailsScreen
import com.tangem.screens.onDisclaimerScreen
import com.tangem.screens.onFailedTransactionDialog
import com.tangem.screens.onMainScreen
import com.tangem.screens.onScanWarningDialog
import com.tangem.screens.onSendAddressScreen
import com.tangem.screens.onSendConfirmScreen
import com.tangem.screens.onSendScreen
import com.tangem.screens.onStoriesScreen
import com.tangem.screens.onTokenDetailsScreen
import com.tangem.screens.onTopBar
import com.tangem.tap.domain.sdk.mocks.MockProvider
import com.tangem.tap.store
import dagger.hilt.android.testing.HiltAndroidTest
@ -164,8 +177,10 @@ class FeedbackTest : BaseTestCase() {
}
step("Force show 'Scan warning' dialog"){
runOnUiThread {
val scanFailsState = StateDialog.ScanFailsDialog(source = StateDialog.ScanFailsSource.MAIN)
store.dispatch(GlobalAction.ShowDialog(scanFailsState))
val requester = store.state.daggerGraphState.scanFailsRequester!!
MainScope().launch {
requester.show(ScanFailsRequester.Source.MAIN)
}
}
}
step("Check 'Scan warning' dialog") {
@ -173,7 +188,7 @@ class FeedbackTest : BaseTestCase() {
checkScanWarningDialog()
}
step("Click on 'Request support' button") {
ScanWarningDialogPageObject { requestSupportButton.click() }
onScanWarningDialog { requestSupportButton.performClick() }
}
step("Assert 'Gmail' app is open") {
ThirdPartyAppPageObject { assertElementWithTextExists(gmailText) }

View file

@ -28,6 +28,7 @@ import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanFailsRequester
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
@ -151,4 +152,6 @@ interface ApplicationEntryPoint {
fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory
fun getCustomerIoFeatureToggles(): CustomerIoFeatureToggles
fun getScanFailsRequester(): ScanFailsRequester
}

View file

@ -239,6 +239,9 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val customerIoFeatureToggles: CustomerIoFeatureToggles
get() = entryPoint.getCustomerIoFeatureToggles()
private val scanFailsRequester
get() = entryPoint.getScanFailsRequester()
// endregion
private val appScope = MainScope()
@ -368,6 +371,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
userWalletsListRepository = userWalletsListRepository,
tangemHotSdk = tangemHotSdk,
trackingContextProxy = trackingContextProxy,
scanFailsRequester = scanFailsRequester,
),
),
)

View file

@ -4,9 +4,8 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.ScanFailsRequester
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
@ -54,12 +53,14 @@ private fun handleFailureChooseBehaviour(
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
if (store.state.globalState.scanCardFailsCounter >= 2) {
val scanFailsSource = when (analyticsSource) {
is AnalyticsParam.ScreensSources.SignIn -> StateDialog.ScanFailsSource.SIGN_IN
is AnalyticsParam.ScreensSources.Settings -> StateDialog.ScanFailsSource.SETTINGS
is AnalyticsParam.ScreensSources.Intro -> StateDialog.ScanFailsSource.INTRO
else -> StateDialog.ScanFailsSource.MAIN
is AnalyticsParam.ScreensSources.SignIn -> ScanFailsRequester.Source.SIGN_IN
is AnalyticsParam.ScreensSources.Settings -> ScanFailsRequester.Source.SETTINGS
is AnalyticsParam.ScreensSources.Intro -> ScanFailsRequester.Source.INTRO
else -> ScanFailsRequester.Source.MAIN
}
scope.launch {
store.inject(DaggerGraphState::scanFailsRequester).show(scanFailsSource)
}
store.dispatchDialogShow(StateDialog.ScanFailsDialog(scanFailsSource))
}
} else {
store.dispatch(GlobalAction.ScanFailsCounter.Reset)

View file

@ -1,6 +1,5 @@
package com.tangem.tap.domain.model
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.blockchain.common.Blockchain as SdkBlockchain
import com.tangem.blockchain.common.Token as SdkToken
@ -29,20 +28,4 @@ sealed interface Currency {
) : Currency {
override val currencySymbol: CryptoCurrencyName = blockchain.currency
}
companion object {
fun fromCustomCurrency(customCurrency: CustomCurrency): Currency {
return when (customCurrency) {
is CustomCurrency.CustomBlockchain -> Blockchain(
blockchain = customCurrency.network,
derivationPath = customCurrency.derivationPath?.rawPath,
)
is CustomCurrency.CustomToken -> Token(
token = customCurrency.token,
blockchain = customCurrency.network,
derivationPath = customCurrency.derivationPath?.rawPath,
)
}
}
}
}

View file

@ -9,18 +9,19 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.card.ScanFailsRequester
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.scanCard.chains.*
import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
internal object UseCaseScanProcessor {
private val scanCardExceptionConverter = ScanCardExceptionConverter()
@ -57,7 +58,7 @@ internal object UseCaseScanProcessor {
val chains = buildList {
add(
FailedScansCounterChain(
{ showMaxUnsuccessfulScansReachedDialog(analyticsSource) },
{ showScanFailsDialog(analyticsSource) },
),
)
add(AnalyticsChain(Basic.CardWasScanned(analyticsSource)))
@ -71,14 +72,16 @@ internal object UseCaseScanProcessor {
)
}
private fun showMaxUnsuccessfulScansReachedDialog(source: AnalyticsParam.ScreensSources) {
private fun showScanFailsDialog(source: AnalyticsParam.ScreensSources) {
val scanFailsSource = when (source) {
is AnalyticsParam.ScreensSources.SignIn -> StateDialog.ScanFailsSource.SIGN_IN
is AnalyticsParam.ScreensSources.Settings -> StateDialog.ScanFailsSource.SETTINGS
is AnalyticsParam.ScreensSources.Intro -> StateDialog.ScanFailsSource.INTRO
else -> StateDialog.ScanFailsSource.MAIN
is AnalyticsParam.ScreensSources.SignIn -> ScanFailsRequester.Source.SIGN_IN
is AnalyticsParam.ScreensSources.Settings -> ScanFailsRequester.Source.SETTINGS
is AnalyticsParam.ScreensSources.Intro -> ScanFailsRequester.Source.INTRO
else -> ScanFailsRequester.Source.MAIN
}
scope.launch {
store.inject(DaggerGraphState::scanFailsRequester).show(scanFailsSource)
}
store.dispatchDialogShow(StateDialog.ScanFailsDialog(scanFailsSource))
}
private suspend fun proceedWithException(

View file

@ -13,11 +13,7 @@ import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics
import com.tangem.tap.features.scanfails.ui.ScanFailsUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject

View file

@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -61,34 +62,43 @@ internal fun ScanFailsDialogContent(state: ScanFailsUM) {
text = stringResourceSafe(R.string.alert_button_how_to_scan),
onClick = state.onHowToScan,
color = TangemTheme.colors.text.accent,
modifier = Modifier.testTag(ScanFailsDialogTestTags.HOW_TO_SCAN_BUTTON),
)
DialogTextButton(
text = stringResourceSafe(R.string.alert_button_request_support),
onClick = state.onRequestSupport,
color = TangemTheme.colors.text.accent,
modifier = Modifier.testTag(ScanFailsDialogTestTags.REQUEST_SUPPORT_BUTTON),
)
DialogTextButton(
text = stringResourceSafe(R.string.common_cancel),
onClick = state.onDismiss,
color = TangemTheme.colors.text.warning,
modifier = Modifier.testTag(ScanFailsDialogTestTags.CANCEL_BUTTON),
)
}
}
}
@Composable
private fun DialogTextButton(text: String, onClick: () -> Unit, color: Color) {
private fun DialogTextButton(text: String, onClick: () -> Unit, color: Color, modifier: Modifier = Modifier) {
Text(
text = text.uppercase(),
color = color,
style = TangemTheme.typography.button,
textAlign = TextAlign.Center,
modifier = Modifier.clickable(onClick = onClick),
modifier = modifier.clickable(onClick = onClick),
)
}
object ScanFailsDialogTestTags {
const val HOW_TO_SCAN_BUTTON = "scan_fails_how_to_scan_button"
const val REQUEST_SUPPORT_BUTTON = "scan_fails_request_support_button"
const val CANCEL_BUTTON = "scan_fails_cancel_button"
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)

View file

@ -18,6 +18,7 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.ScanFailsRequester
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
@ -73,4 +74,5 @@ data class DaggerGraphState(
val userWalletsListRepository: UserWalletsListRepository? = null,
val tangemHotSdk: TangemHotSdk? = null,
val trackingContextProxy: TrackingContextProxy? = null,
val scanFailsRequester: ScanFailsRequester? = null,
) : StateType

View file

@ -52,6 +52,7 @@ internal fun RootContent(
wcContent: @Composable (modifier: Modifier) -> Unit,
hotAccessCodeContent: @Composable (modifier: Modifier) -> Unit,
rootDetectedWarningContent: @Composable (modifier: Modifier) -> Unit,
scanFailsContent: @Composable (modifier: Modifier) -> Unit,
) {
val context = LocalContext.current
@ -89,6 +90,8 @@ internal fun RootContent(
rootDetectedWarningContent(Modifier.fillMaxSize())
scanFailsContent(Modifier.fillMaxSize())
TangemSnackbarHost(
modifier = Modifier
.align(Alignment.BottomCenter)

View file

@ -40,6 +40,8 @@ import com.tangem.hot.sdk.android.create
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.features.scanfails.ScanFailsComponent
import com.tangem.tap.features.scanfails.ScanFailsRequesterProxy
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.hot.TangemHotSDKProxy
import com.tangem.tap.features.root.RootDetectedWarningComponent
@ -75,6 +77,8 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private val cardRepository: CardRepository,
private val onboardingRepository: OnboardingRepository,
private val trackingContextProxy: TrackingContextProxy,
private val scanFailsComponentFactory: ScanFailsComponent.Factory,
private val scanFailsRequesterProxy: ScanFailsRequesterProxy,
private val analyticsEventHandler: AnalyticsEventHandler,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
private val backupServiceHolder: BackupServiceHolder,
@ -97,6 +101,11 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
.create(child("rootDetectedWarningComponent"), Unit)
}
private val scanFailsComponent: ScanFailsComponent by lazy {
scanFailsComponentFactory
.create(child("scanFailsComponent"), Unit)
}
private val navigation = navigationProvider.getOrCreateTyped<AppRoute>()
private val stack: Value<ChildStack<AppRoute, Child>> = childStack(
@ -197,6 +206,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
wcContent = { wcRoutingComponent.Content(it) },
hotAccessCodeContent = { hotAccessCodeRequestComponent.Content(it) },
rootDetectedWarningContent = { rootDetectedWarningComponent.Content(it) },
scanFailsContent = { scanFailsComponent.Content(it) },
)
}
@ -240,10 +250,12 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
onCreate = {
tangemHotSDKProxy.sdkState.value = TangemHotSdk.create(activity)
hotAccessCodeRequesterProxy.componentRequester.value = hotAccessCodeRequestComponent
scanFailsRequesterProxy.componentRequester.value = scanFailsComponent
},
onDestroy = {
tangemHotSDKProxy.sdkState.value = null
hotAccessCodeRequesterProxy.componentRequester.value = null
scanFailsRequesterProxy.componentRequester.value = null
},
)
}

View file

@ -1,27 +0,0 @@
package com.tangem.domain.features.addCustomToken
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.crypto.hdWallet.DerivationPath
/**
[REDACTED_AUTHOR]
*/
sealed class CustomCurrency(
val network: Blockchain,
val derivationPath: DerivationPath?,
) {
@Deprecated("It will be removed in next releases")
class CustomBlockchain(
network: Blockchain,
derivationPath: DerivationPath?,
) : CustomCurrency(network, derivationPath)
@Deprecated("It will be removed in next releases")
class CustomToken(
val token: Token,
network: Blockchain,
derivationPath: DerivationPath?,
) : CustomCurrency(network, derivationPath)
}

View file

@ -1,7 +0,0 @@
package com.tangem.domain.redux
import org.rekotlin.Action
sealed class OnboardingManageTokensAction : Action {
data object CurrenciesSaved : OnboardingManageTokensAction()
}

View file

@ -13,8 +13,6 @@ import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.redux.OnboardingManageTokensAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
import com.tangem.features.managetokens.component.ManageTokensMode
import com.tangem.features.managetokens.component.ManageTokensSource
@ -41,7 +39,6 @@ import javax.inject.Inject
internal class OnboardingManageTokensModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val messageSender: UiMessageSender,
private val reduxStateHolder: ReduxStateHolder,
private val analyticsEventHandler: AnalyticsEventHandler,
manageTokensListManagerFactory: ManageTokensListManager.Factory,
manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory,
@ -294,10 +291,6 @@ internal class OnboardingManageTokensModel @Inject constructor(
}
private fun returnToParentComponent() {
// old onboarding
reduxStateHolder.dispatch(OnboardingManageTokensAction.CurrenciesSaved)
// new one
modelScope.launch {
returnToParentComponentFlow.emit(Unit)
}