Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-21 13:32:44 +03:00
parent 780686645b
commit 5b841dbac5
13 changed files with 235 additions and 54 deletions

View file

@ -8,6 +8,8 @@ import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
@ -134,4 +136,7 @@ interface ApplicationEntryPoint {
fun getSettingsManager(): SettingsManager
fun getBlockchainExceptionHandler(): BlockchainExceptionHandler
@GlobalUiMessageSender
fun getUiMessageSender(): UiMessageSender
}

View file

@ -21,6 +21,7 @@ import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.Basic.TransactionSent.WalletForm
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.datasource.api.common.MoshiConverter
@ -210,6 +211,10 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val blockchainExceptionHandler: BlockchainExceptionHandler
get() = entryPoint.getBlockchainExceptionHandler()
private val uiMessageSender: UiMessageSender
get() = entryPoint.getUiMessageSender()
// endregion
override fun onCreate() {
@ -308,6 +313,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
appPreferencesStore = appPreferencesStore,
clipboardManager = clipboardManager,
settingsManager = settingsManager,
uiMessageSender = uiMessageSender,
),
),
)

View file

@ -11,13 +11,13 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
@ -28,7 +28,6 @@ import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.sdk.extensions.localizedDescriptionRes
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
@ -168,36 +167,25 @@ internal class LegacyScanProcessor @Inject constructor(
crossinline onCancel: () -> Unit,
) {
if (error is TangemSdkError.CardVerificationFailed) {
analyticsEventHandler.send(event = Onboarding.OfflineAttestationFailed)
analyticsEventHandler.send(event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed)
val resource = error.localizedDescriptionRes()
val resId = resource.resId ?: R.string.common_unknown_error
val resArgs = resource.args.map { it.value }
uiMessageSender.send(
message = DialogMessage(
message = resourceReference(id = resId, resArgs.toWrappedList()),
title = resourceReference(id = R.string.security_alert_title),
isDismissable = false,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(id = R.string.alert_button_request_support),
onClick = {
mainScope.launch {
onCancel()
message = Dialogs.cardVerificationFailed(
errorDescription = resourceReference(id = resId, resArgs.toWrappedList()),
onRequestSupport = {
mainScope.launch {
onCancel()
store.inject(DaggerGraphState::sendFeedbackEmailUseCase).invoke(
type = FeedbackEmailType.CardAttestationFailed,
)
}
},
)
},
secondActionBuilder = {
cancelAction {
mainScope.launch { onCancel() }
store.inject(DaggerGraphState::sendFeedbackEmailUseCase).invoke(
type = FeedbackEmailType.CardAttestationFailed,
)
}
},
onCancelClick = { onCancel() },
),
)
} else {

View file

@ -4,7 +4,9 @@ import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.domain.common.TwinsHelper
import com.tangem.operations.attestation.AttestationTask
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.operations.wallet.PurgeWalletCommand
@ -13,7 +15,7 @@ class CreateFirstTwinWalletTask(private val firstCardId: String) : CardSessionRu
override val allowsRequestAccessCodeFromRepository: Boolean = false
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
override fun run(session: CardSession, callback: CompletionCallback<CreateWalletResponse>) {
val card = session.environment.card
val publicKey = card?.wallets?.firstOrNull()?.publicKey
if (publicKey != null) {
@ -29,13 +31,40 @@ class CreateFirstTwinWalletTask(private val firstCardId: String) : CardSessionRu
when (response) {
is CompletionResult.Success -> {
session.environment.card = session.environment.card?.setWallets(emptyList())
CreateWalletTask(EllipticCurve.Secp256k1).run(session) { callback(it) }
createWallet(session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(response.error))
}
}
} else {
CreateWalletTask(EllipticCurve.Secp256k1).run(session) { callback(it) }
createWallet(session, callback)
}
}
private fun createWallet(session: CardSession, callback: CompletionCallback<CreateWalletResponse>) {
CreateWalletTask(EllipticCurve.Secp256k1).run(session) { createWalletResponse ->
when (createWalletResponse) {
is CompletionResult.Success -> {
runAttestation(session) { attestationResponse ->
when (attestationResponse) {
is CompletionResult.Success -> callback(createWalletResponse)
is CompletionResult.Failure -> callback(CompletionResult.Failure(attestationResponse.error))
}
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(createWalletResponse.error))
}
}
}
companion object {
internal fun runAttestation(session: CardSession, callback: CompletionCallback<*>) {
val mode = session.environment.config.attestationMode
val secureStorage = session.environment.secureStorage
val attestationTask = AttestationTask(mode, secureStorage)
attestationTask.run(session, callback)
}
}
}

View file

@ -6,11 +6,13 @@ import com.tangem.common.KeyPair
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.extensions.hexToBytes
import com.tangem.domain.common.TwinsHelper
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.operations.wallet.PurgeWalletCommand
import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask.Companion.runAttestation
class CreateSecondTwinWalletTask(
private val firstPublicKey: String,
@ -59,12 +61,10 @@ class CreateSecondTwinWalletTask(
CreateWalletTask(EllipticCurve.Secp256k1).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
session.environment.card = session.environment.card?.updateWallet(result.data.wallet)
WriteProtectedIssuerDataTask(firstPublicKey.hexToBytes(), issuerKeys).run(session) { writeResult ->
when (writeResult) {
is CompletionResult.Success -> callback(result)
is CompletionResult.Failure -> callback(CompletionResult.Failure(writeResult.error))
runAttestation(session) { attestationResponse ->
when (attestationResponse) {
is CompletionResult.Success -> writeProtectedIssuerData(session, result, callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(attestationResponse.error))
}
}
}
@ -72,4 +72,19 @@ class CreateSecondTwinWalletTask(
}
}
}
private fun writeProtectedIssuerData(
session: CardSession,
result: CompletionResult.Success<CreateWalletResponse>,
callback: CompletionCallback<CreateWalletResponse>,
) {
session.environment.card = session.environment.card?.updateWallet(result.data.wallet)
WriteProtectedIssuerDataTask(firstPublicKey.hexToBytes(), issuerKeys).run(session) { writeResult ->
when (writeResult) {
is CompletionResult.Success -> callback(result)
is CompletionResult.Failure -> callback(CompletionResult.Failure(writeResult.error))
}
}
}
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.domain.twins
import com.tangem.Message
import com.tangem.blockchain.extensions.Result
import com.tangem.common.CompletionResult
import com.tangem.common.KeyPair
import com.tangem.common.extensions.hexToBytes
@ -57,7 +56,7 @@ class TwinCardsManager(card: CardDTO) {
return response
}
suspend fun complete(message: Message): Result<ScanResponse> {
suspend fun complete(message: Message): CompletionResult<ScanResponse> {
val response = tangemSdkManager.finalizeTwin(
secondCardPublicKey = secondCardPublicKey!!.hexToBytes(),
issuerKeyPair = getIssuerKeys(),
@ -65,10 +64,7 @@ class TwinCardsManager(card: CardDTO) {
initialMessage = message,
)
return when (response) {
is CompletionResult.Success -> Result.Success(response.data)
is CompletionResult.Failure -> Result.fromTangemSdkError(response.error)
}
return response
}
private suspend fun getIssuerKeys(): KeyPair {

View file

@ -1,17 +1,25 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.extensions.Result
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.domain.common.extensions.makePrimaryWalletManager
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.sdk.extensions.localizedDescriptionRes
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.entities.ProgressState
import com.tangem.tap.common.extensions.*
@ -185,8 +193,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateSecondWallet))
}
}
is CompletionResult.Failure -> {
}
is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error)
}
}
}
@ -208,8 +215,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateThirdWallet))
}
}
is CompletionResult.Failure -> {
}
is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error)
}
}
}
@ -218,7 +224,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
scope.launch {
when (val result = manager.complete(action.message)) {
is Result.Success -> {
is CompletionResult.Success -> {
Analytics.send(Onboarding.Twins.SetupFinished())
updateScanResponse(result.data)
@ -234,8 +240,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
}
}
is Result.Failure -> {
}
is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error)
}
}
}
@ -349,6 +354,29 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
}
private fun showCardVerificationFailedDialog(error: TangemError) {
if (error is TangemSdkError.CardVerificationFailed) {
Analytics.send(event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed)
val resource = error.localizedDescriptionRes()
val resId = resource.resId ?: R.string.common_unknown_error
val resArgs = resource.args.map { it.value }
store.inject(DaggerGraphState::uiMessageSender).send(
message = Dialogs.cardVerificationFailed(
errorDescription = resourceReference(id = resId, resArgs.toWrappedList()),
onRequestSupport = {
mainScope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase).invoke(
type = FeedbackEmailType.CardAttestationFailed,
)
}
},
),
)
}
}
private fun getPopBackScreen(): KClass<out AppRoute> {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)

View file

@ -12,9 +12,14 @@ import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.domain.common.extensions.withIOContext
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX
@ -30,6 +35,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.CreateProductWalletTaskResponse
import com.tangem.sdk.extensions.localizedDescriptionRes
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
@ -469,6 +475,25 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
val crashlytics = FirebaseCrashlytics.getInstance()
when (val error = result.error) {
is TangemSdkError.CardVerificationFailed -> {
Analytics.send(event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed)
val resource = error.localizedDescriptionRes()
val resId = resource.resId ?: com.tangem.core.ui.R.string.common_unknown_error
val resArgs = resource.args.map { it.value }
store.inject(DaggerGraphState::uiMessageSender).send(
message = Dialogs.cardVerificationFailed(
errorDescription = resourceReference(id = resId, resArgs.toWrappedList()),
onRequestSupport = {
mainScope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
.invoke(type = FeedbackEmailType.CardAttestationFailed)
}
},
),
)
}
is TangemSdkError.BackupFailedNotEmptyWallets -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(

View file

@ -4,6 +4,7 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.signer.TransactionSignerFactory
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.common.routing.AppRouter
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
@ -79,4 +80,5 @@ data class DaggerGraphState(
val appPreferencesStore: AppPreferencesStore? = null,
val clipboardManager: ClipboardManager? = null,
val settingsManager: SettingsManager? = null,
val uiMessageSender: UiMessageSender? = null,
) : StateType