Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-30 09:22:06 +03:00
commit b9d6489d03
104 changed files with 695 additions and 532 deletions

View file

@ -14,6 +14,7 @@
</value>
</option>
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
<option name="INDENT_BEFORE_ARROW_ON_NEW_LINE" value="false" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="5" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="3" />
<option name="ALLOW_TRAILING_COMMA" value="true" />

View file

@ -9,6 +9,7 @@ plugins {
alias(deps.plugins.google.services)
alias(deps.plugins.hilt.android)
alias(deps.plugins.firebase.crashlytics)
alias(deps.plugins.firebase.perf)
id("configuration")
}
@ -213,7 +214,10 @@ dependencies {
implementation(deps.firebase.analytics)
implementation(deps.firebase.crashlytics)
implementation(deps.firebase.messaging)
implementation(deps.firebase.perf) {
exclude(group = "com.google.firebase", module = "protolite-well-known-types")
exclude(group = "com.google.protobuf", module = "protobuf-javalite")
}
/** Tangem libraries */
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")

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

@ -6,7 +6,6 @@ import coil.ImageLoaderFactory
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.tangem.Log
import com.tangem.TangemSdkLogger
import com.tangem.blockchain.common.ExceptionHandler
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.blockchainsdk.signer.TransactionSignerFactory
@ -21,6 +20,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
@ -53,7 +53,6 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.images.createCoilImageLoader
@ -208,8 +207,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val settingsManager: SettingsManager
get() = entryPoint.getSettingsManager()
private val blockchainExceptionHandler: BlockchainExceptionHandler
get() = entryPoint.getBlockchainExceptionHandler()
private val uiMessageSender: UiMessageSender
get() = entryPoint.getUiMessageSender()
// endregion
override fun onCreate() {
@ -252,7 +252,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
}
loadNativeLibraries()
ExceptionHandler.append(blockchainExceptionHandler)
// ExceptionHandler.append(blockchainExceptionHandler) // TODO [REDACTED_TASK_KEY] Send only to Firebase
if (LogConfig.network.blockchainSdkNetwork) {
BlockchainSdkRetrofitBuilder.interceptors = listOf(
createNetworkLoggingInterceptor(),
@ -308,6 +308,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
appPreferencesStore = appPreferencesStore,
clipboardManager = clipboardManager,
settingsManager = settingsManager,
uiMessageSender = uiMessageSender,
),
),
)

View file

@ -58,10 +58,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context)
is AppDialog.RussianCardholdersWarningDialog -> RussianCardholdersWarningBottomSheetDialog(
context,
state.dialog.data,
)
is OnboardingDialog.TwinningProcessNotCompleted -> TwinningProcessNotCompletedDialog.create(context)
is OnboardingDialog.InterruptOnboarding -> InterruptOnboardingDialog.create(context, state.dialog)
is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog)

View file

@ -13,7 +13,6 @@ sealed class Onboarding(
class Started : Onboarding("Onboarding", "Onboarding Started")
class Finished : Onboarding("Onboarding", "Onboarding Finished")
data object OfflineAttestationFailed : Onboarding(category = "Onboarding", event = "Offline Attestation Failed")
sealed class CreateWallet(
event: String,

View file

@ -38,7 +38,7 @@ internal class TangemPushNotificationService : FirebaseMessagingService() {
// TODO refactoring: [REDACTED_JIRA]
val intent = Intent(applicationContext, MainActivity::class.java)
intent.putExtra(OnPushClickedIntentHandler.IS_OPENED_FROM_PUSH, true)
intent.putExtra(OnPushClickedIntentHandler.OPENED_FROM_GCM_PUSH, true)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val pendingIntent = PendingIntent.getActivity(
/* context = */ this,

View file

@ -27,10 +27,6 @@ sealed class AppDialog : StateDialog {
val actionsList: List<TestAction>,
) : AppDialog()
data class RussianCardholdersWarningDialog(val data: Data?) : AppDialog() {
data class Data(val topUpUrl: String)
}
data class RemoveWalletDialog(
val currencyTitle: String,
val onOk: () -> Unit,

View file

@ -67,7 +67,7 @@ internal class AddressInfoBottomSheetDialog(
val blockchain = stateDialog.currency.blockchain
tvReceiveMessage.text = tvReceiveMessage.getString(
id = R.string.address_qr_code_message_format,
blockchain.fullName,
blockchain.getCoinName(),
blockchain.currency,
blockchain.fullName,
)

View file

@ -1,54 +0,0 @@
package com.tangem.tap.common.ui
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
import com.tangem.wallet.databinding.DialogRussiansCardholdersWarningBinding
class RussianCardholdersWarningBottomSheetDialog(
context: Context,
private val dialogData: AppDialog.RussianCardholdersWarningDialog.Data?,
) : BottomSheetDialog(context) {
private var binding: DialogRussiansCardholdersWarningBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(Token.Topup.P2PScreenOpened())
binding = DialogRussiansCardholdersWarningBinding
.inflate(LayoutInflater.from(context))
.also { setContentView(it.root) }
}
override fun show() {
super.show()
setOnDismissListener {
binding = null
store.dispatchDialogHide()
}
binding?.btnYes?.setOnClickListener {
if (dialogData != null) {
store.dispatchOpenUrl(dialogData.topUpUrl)
Analytics.send(Token.Topup.ScreenOpened())
}
dismiss()
}
binding?.btnNo?.setOnClickListener {
store.dispatchOpenUrl(INSTRUCTION_URL)
dismiss()
}
}
companion object {
private const val INSTRUCTION_URL = "https://tangem.com/howtobuy.html"
}
}

View file

@ -19,11 +19,13 @@ internal class DefaultScanCardProcessor(
override suspend fun scan(
cardId: String?,
allowsRequestAccessCodeFromRepository: Boolean,
analyticsSource: AnalyticsParam.ScreensSources,
): CompletionResult<ScanResponse> {
return if (isNewCardScanningEnabled) {
UseCaseScanProcessor.scan(cardId, allowsRequestAccessCodeFromRepository)
} else {
legacyScanProcessor.scan(
analyticsSource = analyticsSource,
cardId = cardId,
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
)

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
@ -60,13 +59,14 @@ internal class LegacyScanProcessor @Inject constructor(
suspend fun scan(
cardId: String? = null,
allowsRequestAccessCodeFromRepository: Boolean = false,
analyticsSource: AnalyticsParam.ScreensSources,
): CompletionResult<ScanResponse> {
return tangemSdkManager.scanProduct(
cardId = cardId,
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
)
.doOnFailure { error ->
onScanFailure(error = error, onFailure = {}, onCancel = {})
onScanFailure(analyticsSource = analyticsSource, error = error, onFailure = {}, onCancel = {})
}
}
@ -93,6 +93,7 @@ internal class LegacyScanProcessor @Inject constructor(
result
.doOnFailure { error ->
onScanFailure(
analyticsSource = analyticsSource,
error = error,
onFailure = onFailure,
onCancel = {
@ -163,41 +164,35 @@ internal class LegacyScanProcessor @Inject constructor(
}
private suspend inline fun onScanFailure(
analyticsSource: AnalyticsParam.ScreensSources,
error: TangemError,
crossinline onFailure: suspend (TangemError) -> Unit,
crossinline onCancel: () -> Unit,
) {
if (error is TangemSdkError.CardVerificationFailed) {
analyticsEventHandler.send(event = Onboarding.OfflineAttestationFailed)
analyticsEventHandler.send(
event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed(
analyticsSource,
),
)
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

@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import timber.log.Timber
@Suppress("LargeClass")
internal class DefaultLegacyWalletConnectRepository(
private val application: Application,
private val wcRequestDeserializer: WcJrpcRequestsDeserializer,
@ -72,19 +73,23 @@ internal class DefaultLegacyWalletConnectRepository(
}
}
WalletKit.initialize(Wallet.Params.Init(core = CoreClient)) { error ->
Timber.e("Error while initializing Web3Wallet: $error")
scope.launch {
_events.emit(
WalletConnectEvents.SessionApprovalError(
WalletConnectError.ExternalApprovalError(error.throwable.message),
),
)
}
}
val walletDelegate = defineWalletDelegate()
WalletKit.setWalletDelegate(walletDelegate)
WalletKit.initialize(
Wallet.Params.Init(core = CoreClient),
onSuccess = {
val walletDelegate = defineWalletDelegate()
WalletKit.setWalletDelegate(walletDelegate)
},
onError = { error ->
Timber.e("Error while initializing Web3Wallet: $error")
scope.launch {
_events.emit(
WalletConnectEvents.SessionApprovalError(
WalletConnectError.ExternalApprovalError(error.throwable.message),
),
)
}
},
)
}
private fun defineWalletDelegate(): WalletKit.WalletDelegate {
@ -333,7 +338,9 @@ internal class DefaultLegacyWalletConnectRepository(
optionalNamespace.key to Wallet.Model.Namespace.Session(
accounts = accountsOptional.distinct(),
methods = methods,
chains = userChains.keys.toList(),
chains = userChains.keys
.filter { it.startsWith(optionalNamespace.key) }
.toList(),
events = optionalNamespace.value.events,
)
}

View file

@ -155,8 +155,8 @@ class WalletConnectInteractor(
return@onEach
}
val networksFormatted = (wcEvent.requiredChainIds + wcEvent.optionalChainIds)
.distinct()
.mapNotNull { blockchainHelper.chainIdToFullNameOrNull(it) }
.distinct()
.toString()
handler.onProposalReceived(proposal = wcEvent, networksFormatted = networksFormatted)
}

View file

@ -66,7 +66,10 @@ internal class CardSettingsViewModel @Inject constructor(
)
private fun scanCard() = viewModelScope.launch {
scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true)
scanCardProcessor.scan(
analyticsSource = com.tangem.core.analytics.models.AnalyticsParam.ScreensSources.Settings,
allowsRequestAccessCodeFromRepository = true,
)
.doOnSuccess { scanResponse ->
val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
if (userWalletId == scannedUserWalletId || scannedUserWalletId == null) {

View file

@ -8,7 +8,7 @@ import com.tangem.tap.features.intentHandler.IntentHandler
internal class OnPushClickedIntentHandler(val analyticsEventHandler: AnalyticsEventHandler) : IntentHandler {
override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean {
val fromPush = intent?.extras?.getBoolean(IS_OPENED_FROM_PUSH) ?: false
val fromPush = intent?.extras?.containsKey(OPENED_FROM_GCM_PUSH) ?: false
return if (fromPush) {
analyticsEventHandler.send(Push.PushNotificationOpened)
@ -19,6 +19,6 @@ internal class OnPushClickedIntentHandler(val analyticsEventHandler: AnalyticsEv
}
companion object {
const val IS_OPENED_FROM_PUSH = "IS_OPENED_FROM_PUSH"
const val OPENED_FROM_GCM_PUSH = "google.sent_time" // every bundle from FCM contains this key
}
}

View file

@ -13,7 +13,6 @@ import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
@ -22,7 +21,6 @@ import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
@ -109,7 +107,7 @@ object OnboardingHelper {
}
}
fun saveWallet(
suspend fun saveWallet(
alreadyCreatedWallet: UserWallet?,
scanResponse: ScanResponse,
accessCode: String? = null,
@ -117,46 +115,42 @@ object OnboardingHelper {
hasBackupError: Boolean = false,
) {
Analytics.setContext(scanResponse)
scope.launch {
val settingsRepository = store.inject(DaggerGraphState::settingsRepository)
when {
// When should save user wallets, then save card without navigate to save wallet screen
store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> {
store.dispatchWithMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
accessCode = accessCode,
backupCardsIds = backupCardsIds?.toSet(),
),
)
val settingsRepository = store.inject(DaggerGraphState::settingsRepository)
when {
// When should save user wallets, then save card without navigate to save wallet screen
store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> {
store.dispatchWithMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
accessCode = accessCode,
backupCardsIds = backupCardsIds?.toSet(),
),
)
store.dispatchWithMain(
SaveWalletAction.SaveWalletAfterBackup(
hasBackupError = hasBackupError,
shouldNavigateToWallet = false,
),
)
}
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
// then open save wallet screen
tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen() -> {
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError, alreadyCreatedWallet)
delay(timeMillis = 1_200)
store.dispatchOnMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
accessCode = accessCode,
backupCardsIds = backupCardsIds?.toSet(),
),
)
}
// If device has no biometry and save wallet screen has been shown, then go through old scenario
else -> {
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError, alreadyCreatedWallet)
}
store.dispatchWithMain(
SaveWalletAction.SaveWalletAfterBackup(
hasBackupError = hasBackupError,
shouldNavigateToWallet = false,
),
)
}
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
// then open save wallet screen
tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen() -> {
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError, alreadyCreatedWallet)
delay(timeMillis = 1_200)
store.dispatchWithMain(
SaveWalletAction.ProvideBackupInfo(
scanResponse = scanResponse,
accessCode = accessCode,
backupCardsIds = backupCardsIds?.toSet(),
),
)
}
// If device has no biometry and save wallet screen has been shown, then go through old scenario
else -> proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError, alreadyCreatedWallet)
}
}
@ -243,18 +237,7 @@ object OnboardingHelper {
val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain)
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
scope.launch {
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
val isRussia = getUserCountryCodeUseCase.invokeSync().isRight { it is UserCountry.Russia }
val onrampFeatureToggles = store.inject(DaggerGraphState::onrampFeatureToggles)
if (isRussia && !onrampFeatureToggles.isFeatureEnabled) {
val dialogData = AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl)
store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(dialogData))
} else {
store.dispatchOpenUrl(topUpUrl)
}
}
store.dispatchOpenUrl(topUpUrl)
}
private suspend fun proceedWithScanResponse(

View file

@ -1,17 +1,26 @@
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.AnalyticsParam
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 +194,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 +216,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 +225,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 +241,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
}
}
is Result.Failure -> {
}
is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error)
}
}
}
@ -349,6 +355,31 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
}
private fun showCardVerificationFailedDialog(error: TangemError) {
if (error is TangemSdkError.CardVerificationFailed) {
Analytics.send(
event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed(AnalyticsParam.ScreensSources.Backup),
)
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

@ -4,7 +4,6 @@ import android.net.Uri
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ifNotNull
import com.tangem.common.extensions.toHexString
@ -12,9 +11,15 @@ 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.AnalyticsParam.ScreensSources
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
@ -37,7 +43,6 @@ import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -46,7 +51,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
object OnboardingWalletMiddleware {
val handler = onboardingWalletMiddleware
@ -197,7 +201,7 @@ private fun handleWalletAction(action: Action) {
}
}
private fun handleFinishBackup(scanResponse: ScanResponse, userWallet: UserWallet? = null) {
private suspend fun handleFinishBackup(scanResponse: ScanResponse, userWallet: UserWallet? = null) {
val backupState = store.state.onboardingWalletState.backupState
val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)
@ -228,31 +232,14 @@ private fun navigateToWalletScreen() {
}
}
private suspend fun readCard(onSuccess: suspend (ScanResponse) -> Unit, onFailure: (TangemError) -> Unit) {
private suspend fun readCard(): CompletionResult<ScanResponse> {
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = shouldSaveAccessCodes,
)
store.inject(DaggerGraphState::scanCardProcessor).scan(
analyticsSource = com.tangem.core.analytics.models.AnalyticsParam.ScreensSources.Intro,
onProgressStateChange = { showProgress ->
if (showProgress) {
store.dispatch(HomeAction.ScanInProgress(scanInProgress = true))
} else {
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
}
},
onFailure = {
Timber.e(it, "Unable to scan card")
delay(HIDE_PROGRESS_DELAY)
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
onFailure(it)
},
onSuccess = onSuccess,
)
return store.inject(DaggerGraphState::scanCardProcessor).scan(analyticsSource = ScreensSources.Intro)
}
private suspend fun loadArtworkForCard(cardId: String, cardPublicKey: ByteArray, defaultArtwork: Uri?): Uri {
@ -469,6 +456,29 @@ 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(
ScreensSources.Backup,
),
)
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(
@ -631,23 +641,22 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
Analytics.send(Onboarding.Finished())
scope.launch {
store.state.globalState.onboardingState.onboardingManager?.finishActivation(
cardIds = gatherCardIds(backupState, card),
)
}
if (scanResponse == null) {
scope.launch {
readCard(
onSuccess = { newScanResponse ->
handleFinishBackup(newScanResponse)
},
onFailure = {
store.dispatchNavigationAction(AppRouter::pop)
},
launch {
store.state.globalState.onboardingState.onboardingManager?.finishActivation(
cardIds = gatherCardIds(backupState, card),
)
}
} else {
handleFinishBackup(scanResponse)
with(scanResponse) {
if (this == null) {
when (val result = readCard()) {
is CompletionResult.Success -> handleFinishBackup(result.data)
is CompletionResult.Failure -> store.dispatchNavigationAction(AppRouter::pop)
}
} else {
handleFinishBackup(scanResponse = this)
}
}
}
}
is BackupAction.FinishBackup -> {
@ -663,40 +672,39 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
scanResponse = requireNotNull(value = scanResponse, lazyMessage = { "ScanResponse is null" }),
backupState = backupState,
)
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
when (backupState.startedSource) {
BackupStartedSource.Onboarding -> saveWallet(
userWalletsListManager = userWalletsListManager,
userWallet = userWallet,
scanResponse = scanResponse,
backupState = backupState,
)
BackupStartedSource.CreateBackup -> updateWallet(
userWalletsListManager = userWalletsListManager,
userWallet = userWallet,
backupState = backupState,
)
}
} else {
delay(HIDE_PROGRESS_DELAY)
readCard(
onSuccess = { newScanResponse ->
scanResponse = newScanResponse
userWallet = createUserWallet(newScanResponse, backupState)
},
onFailure = {
when (val result = readCard()) {
is CompletionResult.Failure -> {
store.dispatchNavigationAction(AppRouter::pop)
},
return@launch
}
is CompletionResult.Success -> {
scanResponse = result.data
userWallet = createUserWallet(scanResponse = result.data, backupState = backupState)
}
}
}
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
when (backupState.startedSource) {
BackupStartedSource.Onboarding -> saveWallet(
userWalletsListManager = userWalletsListManager,
userWallet = userWallet,
scanResponse = scanResponse,
backupState = backupState,
)
BackupStartedSource.CreateBackup -> updateWallet(
userWalletsListManager = userWalletsListManager,
userWallet = userWallet,
backupState = backupState,
)
}
scope.launch {
userWallet?.let {
if (it.scanResponse.cardTypesResolver.isWallet2() && it.isImported) {
store.inject(DaggerGraphState::walletsRepository).markWallet2WasCreated(it.walletId)
}
if (userWallet.scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported) {
store.inject(DaggerGraphState::walletsRepository).markWallet2WasCreated(userWallet.walletId)
}
}
@ -711,7 +719,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
// All cardIds may already be activated if the backup was skipped before.
if (notActivatedCardIds.isEmpty()) {
delay(1000)
store.dispatchWithMain(BackupAction.BackupFinished(userWallet?.walletId))
store.dispatchWithMain(BackupAction.BackupFinished(userWallet.walletId))
return@launch
}
@ -719,8 +727,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
store.state.globalState.onboardingState.onboardingManager?.finishActivation(notActivatedCardIds)
handleFinishBackup(requireNotNull(scanResponse), userWallet)
delay(1000)
store.dispatchWithMain(BackupAction.BackupFinished(userWalletId = userWallet?.walletId))
store.dispatchWithMain(BackupAction.BackupFinished(userWalletId = userWallet.walletId))
}
}

View file

@ -6,15 +6,16 @@ import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
@ -89,19 +90,6 @@ object TradeCryptoMiddleware {
)
scope.launch {
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
val onrampFeatureToggles = store.inject(DaggerGraphState::onrampFeatureToggles)
val isRussia = getUserCountryCodeUseCase.invokeSync().isRight { it is UserCountry.Russia }
if (action.checkUserLocation && isRussia && !onrampFeatureToggles.isFeatureEnabled) {
val dialogData = topUrl?.let {
AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl = it)
}
store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(data = dialogData))
return@launch
}
if (currency is CryptoCurrency.Token && currency.network.isTestnet) {
val walletManager = store.inject(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(

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

View file

@ -1,114 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/background_primary"
android:minHeight="420dp"
tools:layout_gravity="bottom">
<ImageView
android:id="@+id/iv_flag"
android:layout_width="68dp"
android:layout_height="68dp"
android:contentDescription="@null"
android:src="@drawable/img_flag_ru_24"
app:layout_constraintBottom_toTopOf="@id/tv_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<View
android:id="@+id/view_cross_outline"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="@drawable/shape_circle"
android:backgroundTint="@color/background_primary"
app:layout_constraintBottom_toBottomOf="@id/iv_cross"
app:layout_constraintEnd_toEndOf="@id/iv_cross"
app:layout_constraintStart_toStartOf="@id/iv_cross"
app:layout_constraintTop_toTopOf="@id/iv_cross" />
<ImageView
android:id="@+id/iv_cross"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginEnd="-4dp"
android:layout_marginBottom="-4dp"
android:background="@drawable/shape_circle"
android:backgroundTint="#FFEBEE"
android:contentDescription="@null"
android:scaleType="center"
android:src="@drawable/ic_cross_rounded_24"
app:layout_constraintBottom_toBottomOf="@id/iv_flag"
app:layout_constraintEnd_toEndOf="@id/iv_flag"
app:tint="#D80027" />
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="38dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="38dp"
android:text="@string/russian_bank_card_warning_title"
android:textAlignment="center"
android:textColor="@color/text_primary_1"
android:textSize="20sp"
app:layout_constraintBottom_toTopOf="@id/tv_description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_flag" />
<TextView
android:id="@+id/tv_description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="38dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="38dp"
android:layout_marginBottom="38dp"
android:text="@string/russian_bank_card_warning_subtitle"
android:textAlignment="center"
android:textColor="@color/text_primary_1"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/btn_yes"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_bias="0" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_yes"
style="@style/TapPrimaryButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="6dp"
android:layout_marginBottom="38dp"
android:backgroundTint="@color/button_primary"
android:text="@string/common_yes"
android:textColor="@color/text_primary_2"
app:cornerRadius="14dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/btn_no"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_no"
style="@style/TapPrimaryButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:layout_marginEnd="16dp"
android:backgroundTint="@color/button_secondary"
android:text="@string/common_no"
android:textColor="@color/text_primary_1"
app:cornerRadius="14dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/btn_yes"
app:layout_constraintTop_toTopOf="@id/btn_yes" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -48,7 +48,7 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) {
val network = Network(
id = Network.ID(blockchain.id),
backendId = blockchain.toNetworkId(),
name = blockchain.getNetworkName(),
name = blockchain.fullName,
isTestnet = blockchain.isTestnet(),
derivationPath = getNetworkDerivationPath(
blockchain,
@ -76,7 +76,7 @@ internal class CryptoCurrenciesMocks(private val scanResponse: ScanResponse) {
network = Network(
id = Network.ID(value = blockchain.id),
backendId = "NEVER-MIND",
name = blockchain.getNetworkName(),
name = blockchain.fullName,
currencySymbol = "NEVER-MIND",
derivationPath = Network.DerivationPath.Custom(
value = derivationBlockchain.derivationPath(

View file

@ -10,6 +10,7 @@ plugins {
alias(deps.plugins.hilt.android) apply false
alias(deps.plugins.google.services) apply false
alias(deps.plugins.firebase.crashlytics) apply false
alias(deps.plugins.firebase.perf) apply false
alias(deps.plugins.room) apply false
}

View file

@ -173,7 +173,7 @@ object NotificationsFactory {
val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) {
feeAmount
} else {
sendingAmount
sendingAmount + feeAmount
}
val diff = balance.minus(spendingAmount)
if (existentialDeposit != null && diff >= BigDecimal.ZERO && existentialDeposit > diff) {

View file

@ -79,6 +79,8 @@ sealed class AnalyticsParam {
data object Buy : ScreensSources("Buy")
data object Swap : ScreensSources("Swap")
data object Sell : ScreensSources("Sell")
data object Backup : ScreensSources("Backup")
data object Onboarding : ScreensSources("Onboarding")
}
sealed class TxSentFrom(val value: String) {

View file

@ -0,0 +1,24 @@
package com.tangem.core.analytics.models.event
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
sealed class OnboardingAnalyticsEvent(
category: String,
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category, event, params) {
sealed class Onboarding(
event: String,
params: Map<String, String> = mapOf(),
) : OnboardingAnalyticsEvent(category = "Error", event = event, params = params) {
data class OfflineAttestationFailed(
val source: AnalyticsParam.ScreensSources,
) : Onboarding(
event = "Offline Attestation Failed",
params = mapOf(AnalyticsParam.SOURCE to source.value),
)
}
}

View file

@ -11,22 +11,10 @@
"name": "NEXA/test",
"version": "undefined"
},
{
"name": "fact0rn",
"version": "undefined"
},
{
"name": "vanar-chain",
"version": "undefined"
},
{
"name": "bitrock",
"version": "undefined"
},
{
"name": "odyssey",
"version": "undefined"
},
{
"name": "sonic",
"version": "undefined"

View file

@ -40,6 +40,7 @@ internal object NetworkModule {
private const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/"
private const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L
private const val STAKE_KIT_API_TIMEOUT_SECONDS = 60L
@Provides
@Singleton
@ -89,6 +90,12 @@ internal object NetworkModule {
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
timeouts = Timeouts(
callTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS,
connectTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS,
readTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS,
writeTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS,
),
clientBuilder = {
addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore),
@ -262,6 +269,7 @@ internal object NetworkModule {
moshi: Moshi,
context: Context,
apiConfigsManager: ApiConfigsManager,
timeouts: Timeouts = Timeouts(),
clientBuilder: OkHttpClient.Builder.() -> OkHttpClient.Builder = { this },
): T {
val environmentConfig = apiConfigsManager.getEnvironmentConfig(id)
@ -273,6 +281,23 @@ internal object NetworkModule {
.client(
OkHttpClient.Builder()
.applyApiConfig(id, apiConfigsManager)
.applyTimeoutAnnotations()
.let { builder ->
var b = builder
if (timeouts.callTimeoutSeconds != null) {
b = b.callTimeout(timeouts.callTimeoutSeconds, TimeUnit.SECONDS)
}
if (timeouts.connectTimeoutSeconds != null) {
b = b.connectTimeout(timeouts.connectTimeoutSeconds, TimeUnit.SECONDS)
}
if (timeouts.readTimeoutSeconds != null) {
b = b.readTimeout(timeouts.readTimeoutSeconds, TimeUnit.SECONDS)
}
if (timeouts.writeTimeoutSeconds != null) {
b = b.writeTimeout(timeouts.writeTimeoutSeconds, TimeUnit.SECONDS)
}
b
}
.addLoggers(context)
.clientBuilder()
.build(),

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.local.datastore
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
/**
* Runtime store
@ -16,6 +17,8 @@ interface RuntimeStateStore<T> {
/** Store [value] */
suspend fun store(value: T)
suspend fun update(function: (T) -> T)
companion object {
/**
@ -32,6 +35,10 @@ interface RuntimeStateStore<T> {
override suspend fun store(value: T) {
flow.value = value
}
override suspend fun update(function: (T) -> T) {
flow.update(function)
}
}
}
}

View file

@ -658,9 +658,7 @@
<string name="reset_card_with_backup_to_factory_message">Durch das Zurücksetzen auf Werkseinstellungen wird die Wallet vollständig von der ausgewählten Karte oder Ring gelöscht. Du kannst die aktuelle Wallet nicht wiederherstellen oder die Karte oder Ring verwenden, um den Zugangscode wiederherzustellen.</string>
<string name="reset_card_without_backup_to_factory_message">Beim Zurücksetzen auf die Werkseinstellungen wird die Wallet der ausgewählten Karte oder Ring vollständig gelöscht und aus der App entfernt. Es ist nicht möglich, die aktuelle Wallet wiederherzustellen.</string>
<string name="ring_promo_text">Ringbesitzer erhalten bis zum 15.11. 3 provisionsfreie Swaps auf Changelly!</string>
<string name="ring_promo_title">Jetzt mit 0 %% Gebühren tauschen!</string>
<string name="russian_bank_card_warning_subtitle">Besitzt du eine Bankkarte aus einem anderen Land und eine Aufenthaltserlaubnis oder Registrierung außerhalb der Russischen Föderation?</string>
<string name="russian_bank_card_warning_title">Russische Bankkarten werden derzeit nicht akzeptiert</string>
<string name="ring_promo_title">Jetzt mit 0 % Gebühren tauschen!</string>
<string name="save_user_wallet_agreement_access_description">Melde dich bei der App an und überprüfe dein Guthaben, ohne die Karte oder Ring zu scannen</string>
<string name="save_user_wallet_agreement_access_title">Zugriff auf die App</string>
<string name="save_user_wallet_agreement_allow_biometrics">Nutzung biometrischer Daten zulassen</string>
@ -1000,7 +998,7 @@
<string name="wallet_network_group_title">%s Netzwerk</string>
<string name="wallet_notification_address_copied">Die Adresse wurde erfolgreich kopiert</string>
<string name="wallet_notification_no_internet">Keine Internetverbindung</string>
<string name="wallet_promo_banner_button_title">Jetzt mit 10 %% Rabatt kaufen</string>
<string name="wallet_promo_banner_button_title">Jetzt mit 10 % Rabatt kaufen</string>
<string name="wallet_promo_banner_description">Greife auf 13.000+ Kryptowährungen zu. Kaufe, verkaufe, tausche und stake Sie mit einem einzigen Fingertipp.\nVerbinde bis zu drei Karten für ein Backup.</string>
<string name="wallet_promo_banner_title">Entdecke die Tangem Wallet</string>
<string name="wallet_settings_title">Wallet-Einstellungen</string>

View file

@ -654,9 +654,7 @@
<string name="reset_card_with_backup_to_factory_message">El restablecimiento de fábrica eliminará completamente la billetera de la tarjeta seleccionada. No podrá restaurar la billetera actual ni usar la tarjeta para recuperar el código de acceso.</string>
<string name="reset_card_without_backup_to_factory_message">El reseteo a valores de fábrica eliminará completamente la billetera de la tarjeta/anillo seleccionado y lo eliminará de la app. No podrá restaurar la billetera actual.</string>
<string name="ring_promo_text">Si tienes un Anillo Tangem, ¡3 swaps sin comisión en Changelly hasta el 15/11!</string>
<string name="ring_promo_title">¡Intercambia con 0%% de comisión!</string>
<string name="russian_bank_card_warning_subtitle">¿Tiene una tarjeta bancaria de otro país y un permiso de residencia o registro fuera de la Federación Rusa?</string>
<string name="russian_bank_card_warning_title">Las tarjetas bancarias rusas no se aceptan actualmente</string>
<string name="ring_promo_title">¡Intercambia con 0% de comisión!</string>
<string name="save_user_wallet_agreement_access_description">Inicie sesión en la app y comprueba su saldo sin escanear la tarjeta o el anillo</string>
<string name="save_user_wallet_agreement_access_title">Acceder a la app</string>
<string name="save_user_wallet_agreement_allow_biometrics">Permitir el uso de biometría</string>
@ -996,7 +994,7 @@
<string name="wallet_network_group_title">%s red</string>
<string name="wallet_notification_address_copied">La dirección se copió correctamente</string>
<string name="wallet_notification_no_internet">Sin conexión a internet</string>
<string name="wallet_promo_banner_button_title">Consíguelo ahora con un 10 %% de descuento</string>
<string name="wallet_promo_banner_button_title">Consíguelo ahora con un 10 % de descuento</string>
<string name="wallet_promo_banner_description">Accede a más de 13 000 criptomonedas. Compra, vende, intercambia y realiza staking con un solo toque.\nVincula hasta tres tarjetas para hacer copias de seguridad.</string>
<string name="wallet_promo_banner_title">Descubre Tangem Wallet</string>
<string name="wallet_settings_title">Ajustes de la wallet</string>

View file

@ -654,9 +654,7 @@
<string name="reset_card_with_backup_to_factory_message">La réinitialisation aux paramètres d\'usine supprimera complètement le portefeuille de la carte sélectionnée. Vous ne pourrez pas restaurer le portefeuille actuel ni utiliser la carte pour récupérer le code d\'accès.</string>
<string name="reset_card_without_backup_to_factory_message">La réinitialisation aux paramètres d\'usine supprimera complètement le portefeuille de la carte sélectionnée et le supprimera de l\'application. Vous ne pourrez pas restaurer le portefeuille actuel.</string>
<string name="ring_promo_text">Les propriétaires du Tangem Ring ont droit à 3 swap gratis sur Changelly jusqu\'au 15/11 !</string>
<string name="ring_promo_title">Échangez avec 0 %% de frais !</string>
<string name="russian_bank_card_warning_subtitle">Avez-vous une carte bancaire d\'un autre pays et un permis de séjour ou une inscription en dehors de la Fédération de Russie ?</string>
<string name="russian_bank_card_warning_title">Les cartes bancaires russes ne sont actuellement pas acceptées</string>
<string name="ring_promo_title">Échangez avec 0 % de frais !</string>
<string name="save_user_wallet_agreement_access_description">Connectez-vous à l\'application et vérifiez votre solde sans scanner la carte</string>
<string name="save_user_wallet_agreement_access_title">Accéder à l\'application</string>
<string name="save_user_wallet_agreement_allow_biometrics">Autoriser l\'utilisation de la biométrie</string>
@ -996,7 +994,7 @@
<string name="wallet_network_group_title">%s réseau</string>
<string name="wallet_notification_address_copied">L\'adresse a été copiée avec succès</string>
<string name="wallet_notification_no_internet">Pas de connexion internet</string>
<string name="wallet_promo_banner_button_title">Obtenez-le maintenant avec 10%% de réduction</string>
<string name="wallet_promo_banner_button_title">Obtenez-le maintenant avec 10% de réduction</string>
<string name="wallet_promo_banner_description">Accédez à plus de 13 000 cryptomonnaies. Achetez, vendez, échangez et stakez en un seul clic.\nAssociez jusqu\'à trois cartes pour une sauvegarde.</string>
<string name="wallet_promo_banner_title">Découvrez le Portefeuille Tangem</string>
<string name="wallet_settings_title">Paramètres du portefeuille</string>

View file

@ -648,9 +648,7 @@
<string name="reset_card_with_backup_to_factory_message">工場出荷時設定にリセットすると、選択したカードやリングからウォレットが完全に削除されます。現在のウォレットを復元したり、カードやリングを使用してアクセスコードを復元することはできません。</string>
<string name="reset_card_without_backup_to_factory_message">工場出荷時の状態にリセットすると、選択したカードやリングからウォレットが完全に削除され、アプリから削除されます。現在のウォレットを復元することはできません。</string>
<string name="ring_promo_text">Tangem Ringユーザーは、11/15日までChangelly経由でスワップを3回手数料ゼロで行えます</string>
<string name="ring_promo_title">今すぐ手数料0%%でスワップしましょう!</string>
<string name="russian_bank_card_warning_subtitle">他の国の銀行カードと、ロシア連邦外での居住許可証または登録をお持ちですか?</string>
<string name="russian_bank_card_warning_title">ロシアの銀行カードは現在ご利用いただけません</string>
<string name="ring_promo_title">今すぐ手数料0%でスワップしましょう!</string>
<string name="save_user_wallet_agreement_access_description">アプリにログインして、カードまたはリングをスキャンせずに残高を確認できます</string>
<string name="save_user_wallet_agreement_access_title">アプリにアクセスする</string>
<string name="save_user_wallet_agreement_allow_biometrics">生体認証の使用を許可する</string>
@ -990,7 +988,7 @@
<string name="wallet_network_group_title">%sネットワーク</string>
<string name="wallet_notification_address_copied">アドレスがクリップボードにコピーされました</string>
<string name="wallet_notification_no_internet">インターネット接続がありません</string>
<string name="wallet_promo_banner_button_title">今すぐ10 %%オフで購入</string>
<string name="wallet_promo_banner_button_title">今すぐ10 %オフで購入</string>
<string name="wallet_promo_banner_description">1.3万種類以上の暗号資産にアクセス。ワンタップで買付、売却、スワップ、ステーキングが可能です。\nバックアップ用に最大3枚のカードを連携できます。</string>
<string name="wallet_promo_banner_title">Tangemウォレットを見る</string>
<string name="wallet_settings_title">ウォレット設定</string>

View file

@ -672,9 +672,7 @@
<string name="reset_card_with_backup_to_factory_message">Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты или кольца. Вы не сможете восстановить текущий кошелек или использовать данную карту или кольцо для восстановления кода доступа.</string>
<string name="reset_card_without_backup_to_factory_message">Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты или кольца. Вы не сможете восстановить текущий кошелек.</string>
<string name="ring_promo_text">Владельцам колец — 3 обмена без комиссии на Changelly до 15.11!</string>
<string name="ring_promo_title">Обмен с 0%% комиссией!</string>
<string name="russian_bank_card_warning_subtitle">У вас есть карта банка другой страны, а также вид на жительство или регистрация вне РФ?</string>
<string name="russian_bank_card_warning_title">Карты банков РФ в данный момент не принимаются</string>
<string name="ring_promo_title">Обмен с 0% комиссией!</string>
<string name="save_user_wallet_agreement_access_description">Войдите в приложение и следите за своим балансом без сканирования карты или кольца</string>
<string name="save_user_wallet_agreement_access_title">Доступ в приложение</string>
<string name="save_user_wallet_agreement_allow_biometrics">Использовать биометрию</string>
@ -1013,7 +1011,7 @@
<string name="wallet_network_group_title">Сеть %s</string>
<string name="wallet_notification_address_copied">Адрес скопирован в буфер обмена</string>
<string name="wallet_notification_no_internet">Нет соединения с интернетом</string>
<string name="wallet_promo_banner_button_title">Получить с 10%% скидкой</string>
<string name="wallet_promo_banner_button_title">Получить с 10% скидкой</string>
<string name="wallet_promo_banner_description">Получите доступ к более чем 13 000 криптовалют. Покупайте, продавайте, обменивайте и стейкайте в один клик. Свяжите до трех карт для резервного копирования. </string>
<string name="wallet_promo_banner_title">Откройте Tangem Wallet</string>
<string name="wallet_settings_title">Настройки кошелька</string>

View file

@ -51,6 +51,8 @@
<string name="balance_hidden_do_not_show_button">Більше не показувати</string>
<string name="balance_hidden_got_it_button">Зрозуміло</string>
<string name="balance_hidden_title">Баланси приховані</string>
<string name="beta_mode_warning_message">Згідно інформації від розробників мережі, токени Kaspa знаходяться у режимі бета. Слідкуйте за оновленнями!</string>
<string name="beta_mode_warning_title">Бета режим</string>
<string name="biometric_lockout_permanent_warning_description">Будь ласка, відскануйте картку або кільце</string>
<string name="biometric_lockout_warning_description">Будь ласка, спробуйте знову через 30 секунд або відскануйте картку або кільце</string>
<string name="biometric_lockout_warning_title">Забагато спроб</string>
@ -672,9 +674,7 @@
<string name="reset_card_with_backup_to_factory_message">Скидання до заводських налаштувань призведе до повного видалення гаманця з обраної картки або кільця. Ви не зможете відновити поточний гаманець або використати цю картку або кільце для відновлення коду доступу.</string>
<string name="reset_card_without_backup_to_factory_message">Скидання до заводських налаштувань призведе до повного видалення гаманця з обраної картки або кільця. Ви не зможете відновити поточний гаманець.</string>
<string name="ring_promo_text">Власникам кілець - 3 обміни без комісії на Changelly до 15.11!</string>
<string name="ring_promo_title">Обмін з 0%% комісією!</string>
<string name="russian_bank_card_warning_subtitle">У вас є банківська картка іншої країни та посвідка на проживання або реєстрація за межами Російської Федерації?</string>
<string name="russian_bank_card_warning_title">Російські банківські картки наразі не приймаються</string>
<string name="ring_promo_title">Обмін з 0% комісією!</string>
<string name="save_user_wallet_agreement_access_description">Увійдіть у додаток та слідкуйте за своїм балансом без сканування картки чи кільця</string>
<string name="save_user_wallet_agreement_access_title">Доступ до додатку</string>
<string name="save_user_wallet_agreement_allow_biometrics">Використовувати біометрію</string>
@ -1014,7 +1014,7 @@
<string name="wallet_network_group_title">Мережа %s</string>
<string name="wallet_notification_address_copied">Адреса скопійована в буфер обміну</string>
<string name="wallet_notification_no_internet">Немає підключення до інтернету</string>
<string name="wallet_promo_banner_button_title">Отримати з 10%% знижкою</string>
<string name="wallet_promo_banner_button_title">Отримати з 10% знижкою</string>
<string name="wallet_promo_banner_description">Доступ до 13,000+ криптовалют. Купуйте, продавайте, обмінюйте та стейкайте одним дотиком.\nЗєднайте до трьох карток для бекапу.</string>
<string name="wallet_promo_banner_title">Відкрийте Tangem Wallet</string>
<string name="wallet_settings_title">Налаштування гаманця</string>

View file

@ -237,7 +237,6 @@
<string name="reset_card_to_factory_condition_1">我了解執行此操作後,我將無法再訪問當前錢包</string>
<string name="reset_card_with_backup_to_factory_message">恢復原廠設置將從所選卡中完全刪除錢包。您將無法恢復當前錢包或使用卡恢復訪問密碼</string>
<string name="reset_card_without_backup_to_factory_message">恢復原廠設置將從所選卡中完全刪除錢包並將其從應用程序中刪除。您將無法恢復當前錢包</string>
<string name="russian_bank_card_warning_title">目前不接受俄羅斯銀行卡</string>
<string name="save_user_wallet_agreement_access_description">登錄應用程序並在不掃描卡片的情況下檢查您的資產</string>
<string name="save_user_wallet_agreement_access_title">訪問應用程序</string>
<string name="save_user_wallet_agreement_allow_biometrics">允許使用生物辨識</string>

View file

@ -658,9 +658,7 @@
<string name="reset_card_with_backup_to_factory_message">Factory Reset will completely delete the wallet from the selected card or ring. You will not be able to restore the current wallet or use the card or ring to recover the access code.</string>
<string name="reset_card_without_backup_to_factory_message">Factory Reset will completely delete the wallet from the selected card or ring and remove it from the app. You will not be able to restore the current wallet.</string>
<string name="ring_promo_text">Ring owners get 3 commission-free swaps on Changelly until 15.11!</string>
<string name="ring_promo_title">Swap With 0%% Fees Now!</string>
<string name="russian_bank_card_warning_subtitle">Do you have a bank card from another country and a residence permit or registration outside the Russian Federation?</string>
<string name="russian_bank_card_warning_title">Russian bank cards are not currently accepted</string>
<string name="ring_promo_title">Swap With 0% Fees Now!</string>
<string name="save_user_wallet_agreement_access_description">Log into the app and check your balance without scanning the card or ring</string>
<string name="save_user_wallet_agreement_access_title">Access the app</string>
<string name="save_user_wallet_agreement_allow_biometrics">Allow to use biometrics</string>
@ -1000,7 +998,7 @@
<string name="wallet_network_group_title">%s network</string>
<string name="wallet_notification_address_copied">Address was copied to clipboard</string>
<string name="wallet_notification_no_internet">No internet connection</string>
<string name="wallet_promo_banner_button_title">Get now with 10%% off</string>
<string name="wallet_promo_banner_button_title">Get now with 10% off</string>
<string name="wallet_promo_banner_description">Access 13,000+ cryptocurrencies. Buy, sell, swap, and stake with a single tap.\nLink up to three cards for a backup.</string>
<string name="wallet_promo_banner_title">Discover Tangem Wallet</string>
<string name="wallet_settings_title">Wallet settings</string>
@ -1084,7 +1082,7 @@
<string name="warning_solana_rent_fee_message">Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free.</string>
<string name="warning_some_networks_unreachable_message">Swipe down to refresh or try again later.</string>
<string name="warning_some_networks_unreachable_title">Some networks are unreachable</string>
<string name="warning_some_token_balances_not_updated">Some token balances could not updated</string>
<string name="warning_some_token_balances_not_updated">Some token balances could not be updated</string>
<string name="warning_testnet_card_message">This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes.</string>
<string name="warning_testnet_card_title">For testing purposes only</string>
<string name="welcome_interrupted_backup_alert_discard">Discard</string>

View file

@ -1,6 +1,5 @@
package com.tangem.core.ui.components.transactions
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -119,13 +118,12 @@ private fun PendingTxsBlock(pendingTxs: ImmutableList<TransactionState>, isBalan
}
}
@OptIn(ExperimentalFoundationApi::class)
private fun LazyListScope.nonContentItem(state: EmptyTransactionsBlockState, modifier: Modifier = Modifier) {
item(key = state::class.java, contentType = state::class.java) {
EmptyTransactionBlock(
state = state,
modifier = modifier
.animateItemPlacement()
.animateItem(fadeInSpec = null, fadeOutSpec = null)
.padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12)
.fillMaxWidth(),
)

View file

@ -0,0 +1,39 @@
package com.tangem.core.ui.message.dialog
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
/**
[REDACTED_AUTHOR]
*/
object Dialogs {
/**
* Card verification failed dialog
*
* @param errorDescription sdk error description
* @param onRequestSupport lambda be invoked when request support action is clicked
* @param onCancelClick lambda be invoked when cancel action is clicked
*/
fun cardVerificationFailed(
errorDescription: TextReference,
onRequestSupport: () -> Unit,
onCancelClick: () -> Unit = {},
): DialogMessage {
return DialogMessage(
message = errorDescription,
title = resourceReference(id = R.string.security_alert_title),
isDismissable = false,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(id = R.string.alert_button_request_support),
onClick = onRequestSupport,
)
},
secondActionBuilder = { cancelAction(onClick = onCancelClick) },
)
}
}

View file

@ -89,7 +89,7 @@ class CryptoCurrencyFactory(
return CryptoCurrency.Coin(
id = getCoinId(network, blockchain.toCoinId()),
network = network,
name = blockchain.fullName,
name = blockchain.getCoinName(),
symbol = blockchain.currency,
iconUrl = getCoinIconUrl(blockchain),
decimals = blockchain.decimals(),

View file

@ -30,7 +30,7 @@ fun getNetwork(
return Network(
id = Network.ID(blockchain.id),
backendId = blockchain.toNetworkId(),
name = blockchain.getNetworkName(),
name = blockchain.fullName,
isTestnet = blockchain.isTestnet(),
derivationPath = getNetworkDerivationPath(
blockchain = blockchain,
@ -59,7 +59,7 @@ fun getNetwork(
return Network(
id = networkId,
backendId = blockchain.toNetworkId(),
name = blockchain.getNetworkName(),
name = blockchain.fullName,
isTestnet = blockchain.isTestnet(),
derivationPath = derivationPath,
currencySymbol = blockchain.currency,

View file

@ -2,7 +2,6 @@ package com.tangem.data.common.currency
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchainsdk.compatibility.l2BlockchainsList
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toCoinId
@ -78,7 +77,7 @@ class ResponseCryptoCurrenciesFactory(
return CryptoCurrency.Coin(
id = getCoinId(network, blockchain.toCoinId()),
network = network,
name = blockchain.getNameForCoin(responseToken),
name = blockchain.getCoinName(),
symbol = blockchain.getSymbolForCoin(responseToken),
decimals = responseToken.decimals,
iconUrl = getCoinIconUrl(blockchain),
@ -86,21 +85,6 @@ class ResponseCryptoCurrenciesFactory(
)
}
private fun Blockchain.getNameForCoin(responseToken: UserTokensResponse.Token): String {
return when (this) {
// workaround: for Blockchains full name different than backend name,
// get name and symbol from enum Blockchain until backend renamed
// [REDACTED_JIRA]
Blockchain.Dischain,
Blockchain.Telos,
Blockchain.Cronos,
Blockchain.TON,
in l2BlockchainsList,
-> this.fullName
else -> responseToken.name
}
}
private fun Blockchain.getSymbolForCoin(responseToken: UserTokensResponse.Token): String {
return when (this) {
// workaround: Dischain was renamed but backend still returns the old name,

View file

@ -295,7 +295,7 @@ internal class DefaultManageTokensRepository(
)
return if (!canHandleBlockchain) {
CurrencyUnsupportedState.UnsupportedNetwork(networkName = blockchain.getNetworkName())
CurrencyUnsupportedState.UnsupportedNetwork(networkName = blockchain.fullName)
} else {
null
}
@ -314,10 +314,10 @@ internal class DefaultManageTokensRepository(
return when {
// refactor this later by moving all this logic in card config
blockchain == Blockchain.Solana && !supportedTokens.contains(Blockchain.Solana) -> {
CurrencyUnsupportedState.Token.NetworkTokensUnsupported(networkName = blockchain.getNetworkName())
CurrencyUnsupportedState.Token.NetworkTokensUnsupported(networkName = blockchain.fullName)
}
!userWallet.scanResponse.card.canHandleToken(supportedTokens, blockchain, cardTypesResolver) -> {
CurrencyUnsupportedState.Token.UnsupportedCurve(networkName = blockchain.getNetworkName())
CurrencyUnsupportedState.Token.UnsupportedCurve(networkName = blockchain.fullName)
}
else -> null
}

View file

@ -32,7 +32,7 @@ internal object CoinsResponseConverter : Converter<CoinsData, List<Token>> {
Token.Network(
networkId = network.networkId,
standardType = getNetworkStandardType(blockchain).name,
name = blockchain.getNetworkName(),
name = blockchain.fullName,
address = network.contractAddress,
iconUrl = getIconUrl(network.networkId, value.imageHost),
decimalCount = network.decimalCount?.toInt(),

View file

@ -417,16 +417,16 @@ internal class DefaultCurrenciesRepository(
}
}
override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency {
override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, network: Network): FeePaidCurrency {
return withContext(dispatchers.io) {
val blockchain = Blockchain.fromId(currency.network.id.value)
val blockchain = Blockchain.fromId(network.id.value)
when (val feePaidCurrency = blockchain.feePaidCurrency()) {
FeePaidSdkCurrency.Coin -> FeePaidCurrency.Coin
FeePaidSdkCurrency.SameCurrency -> FeePaidCurrency.SameCurrency
is FeePaidSdkCurrency.Token -> {
val balance = walletManagersFacade.tokenBalance(
userWalletId = userWalletId,
network = currency.network,
network = network,
name = feePaidCurrency.token.name,
symbol = feePaidCurrency.token.symbol,
contractAddress = feePaidCurrency.token.contractAddress,
@ -434,7 +434,7 @@ internal class DefaultCurrenciesRepository(
id = feePaidCurrency.token.id,
)
FeePaidCurrency.Token(
tokenId = getTokenId(network = currency.network, sdkToken = feePaidCurrency.token),
tokenId = getTokenId(network = network, sdkToken = feePaidCurrency.token),
name = feePaidCurrency.token.name,
symbol = feePaidCurrency.token.symbol,
contractAddress = feePaidCurrency.token.contractAddress,

View file

@ -20,13 +20,14 @@ import com.tangem.utils.coroutines.runCatching
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
internal class DefaultWalletsRepository(
private val appPreferencesStore: AppPreferencesStore,
private val tangemTechApi: TangemTechApi,
private val userWalletsStore: UserWalletsStore,
private val seedPhraseNotificationVisibilityStore: RuntimeStateStore<Boolean>,
private val seedPhraseNotificationVisibilityStore: RuntimeStateStore<Map<UserWalletId, Boolean>>,
private val dispatchers: CoroutineDispatcherProvider,
) : WalletsRepository {
@ -59,7 +60,9 @@ internal class DefaultWalletsRepository(
override fun seedPhraseNotificationStatus(userWalletId: UserWalletId): Flow<Boolean> {
return channelFlow {
launch {
seedPhraseNotificationVisibilityStore.get().collectLatest(::send)
seedPhraseNotificationVisibilityStore.get()
.map { it.getOrDefault(key = userWalletId, defaultValue = false) }
.collectLatest(::send)
}
fetchSeedPhraseNotificationStatus(userWalletId)
@ -81,7 +84,7 @@ internal class DefaultWalletsRepository(
)
}
seedPhraseNotificationVisibilityStore.store(value = status)
updateNotificationVisibility(id = userWalletId, value = status)
}
override suspend fun notifiedSeedPhraseNotification(userWalletId: UserWalletId) {
@ -101,7 +104,7 @@ internal class DefaultWalletsRepository(
).getOrThrow()
}
seedPhraseNotificationVisibilityStore.store(value = false)
updateNotificationVisibility(id = userWalletId, value = false)
}
override suspend fun declineSeedPhraseNotification(userWalletId: UserWalletId) {
@ -112,7 +115,7 @@ internal class DefaultWalletsRepository(
).getOrThrow()
}
seedPhraseNotificationVisibilityStore.store(value = false)
updateNotificationVisibility(id = userWalletId, value = false)
}
override suspend fun markWallet2WasCreated(userWalletId: UserWalletId) {
@ -122,4 +125,12 @@ internal class DefaultWalletsRepository(
)
}
}
private suspend fun updateNotificationVisibility(id: UserWalletId, value: Boolean) {
return seedPhraseNotificationVisibilityStore.update {
it.toMutableMap().apply {
this[id] = value
}
}
}
}

View file

@ -31,7 +31,7 @@ internal object WalletsDataModule {
appPreferencesStore = appPreferencesStore,
tangemTechApi = tangemTechApi,
userWalletsStore = userWalletsStore,
seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = false),
seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()),
dispatchers = dispatchers,
)
}

View file

@ -10,6 +10,7 @@ interface ScanCardProcessor {
suspend fun scan(
cardId: String? = null,
allowsRequestAccessCodeFromRepository: Boolean = false,
analyticsSource: AnalyticsParam.ScreensSources,
): CompletionResult<ScanResponse>
suspend fun scan(

View file

@ -34,7 +34,7 @@ class GetBalanceNotEnoughForFeeWarningUseCase(
coinStatus: CryptoCurrencyStatus,
): Either<Throwable, CryptoCurrencyWarning?> = Either.catch {
withContext(dispatchers.io) {
val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, tokenStatus.currency)
val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, tokenStatus.currency.network)
val coinBalance = coinStatus.value.amount ?: BigDecimal.ZERO
val isFeePaidByCoin = tokenStatus.currency is CryptoCurrency.Token

View file

@ -7,12 +7,35 @@ import arrow.core.raise.either
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
class GetCryptoCurrencyUseCase(
private val currenciesRepository: CurrenciesRepository,
) {
/**
* Returns specific cryptocurrency for a given user wallet.
*
* !!! Important Use only [CryptoCurrency.ID.value] as cryptoCurrencyId
*
* @param userWallet The user's wallet.
* @param cryptoCurrencyId The ID of the cryptocurrency.
* @return An [Either] representing success (Right) or an error (Left) in fetching the status.
*/
suspend operator fun invoke(
userWallet: UserWallet,
cryptoCurrencyId: String,
): Either<CurrencyStatusError, CryptoCurrency> {
return either {
if (userWallet.isMultiCurrency) {
getCurrency(userWallet.walletId, cryptoCurrencyId)
} else {
getPrimaryCurrency(userWallet.walletId)
}
}
}
/**
* Returns specific cryptocurrency for a given user wallet.
*

View file

@ -19,7 +19,7 @@ class GetCurrencyCheckUseCase(
currencyStatus: CryptoCurrencyStatus,
amount: BigDecimal?,
fee: BigDecimal?,
balanceAfterTransaction: BigDecimal?,
feeCurrencyBalanceAfterTransaction: BigDecimal?,
recipientAddress: String? = null,
): CryptoCurrencyCheck {
return withContext(dispatchers.io) {
@ -32,7 +32,7 @@ class GetCurrencyCheckUseCase(
val rentWarning = currencyChecksRepository.getRentExemptionError(
userWalletId = userWalletId,
currencyStatus = currencyStatus,
balanceAfterTransaction = balanceAfterTransaction ?: BigDecimal.ZERO,
balanceAfterTransaction = feeCurrencyBalanceAfterTransaction ?: BigDecimal.ZERO,
)
val isAccountFunded = recipientAddress?.let {
currencyChecksRepository.checkIfAccountFunded(

View file

@ -115,7 +115,7 @@ class GetCurrencyWarningsUseCase(
coinStatus: CryptoCurrencyStatus,
tokenStatus: CryptoCurrencyStatus,
): CryptoCurrencyWarning? {
val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, tokenStatus.currency)
val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, tokenStatus.currency.network)
val isNetworkFeeZero = currenciesRepository.isNetworkFeeZero(userWalletId, tokenStatus.currency.network)
return when {
feePaidCurrency is FeePaidCurrency.Coin &&

View file

@ -26,7 +26,8 @@ class GetFeePaidCryptoCurrencyStatusSyncUseCase(
cryptoCurrencyStatus: CryptoCurrencyStatus,
): Either<TokenListError, CryptoCurrencyStatus?> {
val cryptoCurrency = cryptoCurrencyStatus.currency
val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, cryptoCurrency)
val network = cryptoCurrency.network
val feePaidCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, network)
val operations = CurrenciesStatusesOperations(
userWalletId = userWalletId,
currenciesRepository = currenciesRepository,
@ -39,7 +40,7 @@ class GetFeePaidCryptoCurrencyStatusSyncUseCase(
when (feePaidCurrency) {
FeePaidCurrency.Coin ->
operations
.getNetworkCoinSync(cryptoCurrency.network.id, cryptoCurrency.network.derivationPath)
.getNetworkCoinSync(network.id, network.derivationPath)
.getOrNull()
FeePaidCurrency.SameCurrency,
is FeePaidCurrency.FeeResource,

View file

@ -19,7 +19,7 @@ class IsAmountSubtractAvailableUseCase(
suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either<Throwable, Boolean> =
Either.catch {
withContext(dispatchers.io) {
when (val feeCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, currency)) {
when (val feeCurrency = currenciesRepository.getFeePaidCurrency(userWalletId, currency.network)) {
is FeePaidCurrency.Coin -> currency is CryptoCurrency.Coin
is FeePaidCurrency.SameCurrency -> true
is FeePaidCurrency.Token -> currency.id == feeCurrency.tokenId

View file

@ -231,9 +231,9 @@ interface CurrenciesRepository {
): Boolean
/**
* Retrieves fee paid currency for specific [currency].
* Retrieves fee paid currency for specific [network].
*/
suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency
suspend fun getFeePaidCurrency(userWalletId: UserWalletId, network: Network): FeePaidCurrency
/**
* Creates token [cryptoCurrency] based on current token and [network] it`s will be added

View file

@ -137,7 +137,7 @@ internal class MockCurrenciesRepository(
return false
}
override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency {
override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, network: Network): FeePaidCurrency {
return FeePaidCurrency.Coin
}

View file

@ -5,11 +5,18 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
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.card.repository.CardSdkConfigRepository
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ProductType
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
@ -21,6 +28,7 @@ import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.resetB
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.state.MultiWalletBackupUM
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.sdk.extensions.localizedDescriptionRes
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@ -31,13 +39,16 @@ import javax.inject.Inject
@Stable
@ComponentScoped
@Suppress("LongParameterList")
class MultiWalletBackupModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val backupServiceHolder: BackupServiceHolder,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val tangemSdkManager: TangemSdkManager,
private val analyticsHandler: AnalyticsEventHandler,
private val analyticsEventHandler: AnalyticsEventHandler,
private val uiMessageSender: UiMessageSender,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
) : Model() {
@Suppress("UnusedPrivateMember")
@ -65,10 +76,10 @@ class MultiWalletBackupModel @Inject constructor(
init {
// for wallet 1 this event is sent in Wallet1ChooseOptionModel
if (scanResponse.productType == ProductType.Wallet2 || scanResponse.productType == ProductType.Ring) {
analyticsHandler.send(OnboardingEvent.Backup.ScreenOpened)
analyticsEventHandler.send(OnboardingEvent.Backup.ScreenOpened)
}
analyticsHandler.send(OnboardingEvent.Backup.Started)
analyticsEventHandler.send(OnboardingEvent.Backup.Started)
}
private fun getInitState(): MultiWalletBackupUM {
@ -153,7 +164,7 @@ class MultiWalletBackupModel @Inject constructor(
modelScope.launch { eventFlow.emit(MultiWalletBackupComponent.Event.Done) }
analyticsHandler.send(OnboardingEvent.Backup.Finished(cardsCount = state.value.numberOfBackupCards + 1))
analyticsEventHandler.send(OnboardingEvent.Backup.Finished(cardsCount = state.value.numberOfBackupCards + 1))
}
private fun showOnlyOneBackupWarningDialog() {
@ -203,6 +214,7 @@ class MultiWalletBackupModel @Inject constructor(
}
is CompletionResult.Failure -> {
when (val error = result.error) {
is TangemSdkError.CardVerificationFailed -> showCardVerificationFailedDialog(error)
is TangemSdkError.BackupFailedNotEmptyWallets -> {
_uiState.update { st ->
st.copy(
@ -212,7 +224,7 @@ class MultiWalletBackupModel @Inject constructor(
_uiState.update { it.copy(dialog = null) }
},
onDismissClick = {
analyticsHandler.send(OnboardingEvent.Backup.ResetCancelEvent)
analyticsEventHandler.send(OnboardingEvent.Backup.ResetCancelEvent)
},
),
)
@ -234,8 +246,29 @@ class MultiWalletBackupModel @Inject constructor(
}
}
private fun showCardVerificationFailedDialog(error: TangemSdkError.CardVerificationFailed) {
analyticsEventHandler.send(
event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed(AnalyticsParam.ScreensSources.Backup),
)
val resource = error.localizedDescriptionRes()
val resId = resource.resId ?: com.tangem.core.ui.R.string.common_unknown_error
val resArgs = resource.args.map { it.value }
uiMessageSender.send(
message = Dialogs.cardVerificationFailed(
errorDescription = resourceReference(id = resId, resArgs.toWrappedList()),
onRequestSupport = {
modelScope.launch {
sendFeedbackEmailUseCase(type = FeedbackEmailType.CardAttestationFailed)
}
},
),
)
}
private fun resetBackupCard(cardId: String) {
analyticsHandler.send(OnboardingEvent.Backup.ResetPerformEvent)
analyticsEventHandler.send(OnboardingEvent.Backup.ResetPerformEvent)
modelScope.launch {
tangemSdkManager.resetToFactorySettings(

View file

@ -95,15 +95,19 @@ internal class SendNotificationFactory(
val feeError = (feeState.feeSelectorState as? FeeSelectorState.Error)?.error
val recipientAddress = state.recipientState?.addressTextField?.value
val statusValue = cryptoCurrencyStatus.value as? CryptoCurrencyStatus.Loaded
val balanceAfterTransaction = statusValue?.let { it.amount - sendingAmount - feeValue }
val feeCurrencyBalanceAfterTransaction = getFeeCurrencyBalanceAfterTx(
feeCurrencyStatus = feeCryptoCurrencyStatusProvider(),
sendingCurrencyStatus = cryptoCurrencyStatus,
sendingAmount = sendingAmount,
feeValue = feeValue,
)
val currencyCheck = getCurrencyCheckUseCase(
userWalletId = userWalletId,
currencyStatus = cryptoCurrencyStatus,
amount = sendingAmount,
fee = feeValue,
recipientAddress = recipientAddress,
balanceAfterTransaction = balanceAfterTransaction,
feeCurrencyBalanceAfterTransaction = feeCurrencyBalanceAfterTransaction,
)
buildList {
addErrorNotifications(
@ -138,6 +142,23 @@ internal class SendNotificationFactory(
)
}
private fun getFeeCurrencyBalanceAfterTx(
feeCurrencyStatus: CryptoCurrencyStatus?,
sendingCurrencyStatus: CryptoCurrencyStatus,
sendingAmount: BigDecimal,
feeValue: BigDecimal,
): BigDecimal? {
val sendingCurrencyBalance = sendingCurrencyStatus.value as? CryptoCurrencyStatus.Loaded
val feeCurrencyBalance = feeCurrencyStatus?.value as? CryptoCurrencyStatus.Loaded
if (feeCurrencyStatus?.value !is CryptoCurrencyStatus.Loaded) return null
return when {
feeCurrencyStatus == sendingCurrencyStatus -> sendingCurrencyBalance?.let {
it.amount - sendingAmount - feeValue
}
else -> feeCurrencyBalance?.let { it.amount - feeValue }
}
}
private suspend fun MutableList<NotificationUM>.addErrorNotifications(
feeError: GetFeeError?,
sendingAmount: BigDecimal,

View file

@ -1,6 +1,5 @@
package com.tangem.features.send.impl.presentation.ui.common
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
@ -10,7 +9,6 @@ import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.res.TangemTheme
import kotlinx.collections.immutable.ImmutableList
@OptIn(ExperimentalFoundationApi::class)
internal fun LazyListScope.notifications(
notifications: ImmutableList<NotificationUM>,
modifier: Modifier = Modifier,
@ -31,7 +29,7 @@ internal fun LazyListScope.notifications(
config = item.config,
modifier = modifier
.padding(top = topPadding)
.animateItemPlacement(),
.animateItem(fadeInSpec = null, fadeOutSpec = null),
containerColor = when (item) {
is NotificationUM.Error.TokenExceedsBalance,
is NotificationUM.Warning.NetworkFeeUnreachable,

View file

@ -16,6 +16,7 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.R
import com.tangem.lib.crypto.BlockchainUtils.isTron
import com.tangem.utils.isNullOrZero
import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal
import java.math.RoundingMode
@ -41,8 +42,16 @@ internal class AmountRequirementStateTransformer(
val isIntegerOnlyError = isIntegerOnlyError(amountState, actionType)
val cryptoAmount = amountState.amountTextField.cryptoAmount
val roundedDownCrypto = cryptoAmount.value?.setScale(0, RoundingMode.DOWN)
val value = roundedDownCrypto?.parseBigDecimal(0).orEmpty()
val roundedDownCrypto = cryptoAmount.value
?.setScale(0, RoundingMode.DOWN)
?.parseBigDecimal(0)
.orEmpty()
val isAmountZeroOrNull = if (amountState.amountTextField.isFiatValue) {
amountState.amountTextField.fiatAmount.value.isNullOrZero()
} else {
amountState.amountTextField.cryptoAmount.value.isNullOrZero()
}
val errorText = when {
amountState.amountTextField.isError -> amountState.amountTextField.error
@ -50,11 +59,11 @@ internal class AmountRequirementStateTransformer(
isIntegerOnlyError -> when (actionType) {
StakingActionCommonType.Enter -> resourceReference(
R.string.staking_amount_tron_integer_error,
wrappedList(value),
wrappedList(roundedDownCrypto),
)
StakingActionCommonType.Exit -> resourceReference(
R.string.staking_amount_tron_integer_error_unstaking,
wrappedList(value),
wrappedList(roundedDownCrypto),
)
else -> null
}
@ -62,7 +71,7 @@ internal class AmountRequirementStateTransformer(
}
val isError = amountState.amountTextField.isError || requirementError != null
return amountState.copy(
isPrimaryButtonEnabled = !isError,
isPrimaryButtonEnabled = !isAmountZeroOrNull && !isError,
amountTextField = amountState.amountTextField.copy(
isError = isError,
isWarning = isIntegerOnlyError,

View file

@ -647,7 +647,7 @@ internal class StakingViewModel @Inject constructor(
currencyStatus = cryptoCurrencyStatus,
amount = amount,
fee = fee,
balanceAfterTransaction = balanceAfterTransaction,
feeCurrencyBalanceAfterTransaction = balanceAfterTransaction,
)
stateController.update(
AddStakingNotificationsTransformer(
@ -850,13 +850,11 @@ internal class StakingViewModel @Inject constructor(
)
getCurrencyStatusUpdatesUseCase(userWalletId, cryptoCurrencyId, false)
.conflate()
.distinctUntilChanged()
.distinctUntilChangedBy { it.getOrNull()?.value?.yieldBalance }
.filter { value.currentStep == StakingStep.InitialInfo }
.onEach { maybeStatus ->
maybeStatus.fold(
ifRight = { status ->
if (status.value !is CryptoCurrencyStatus.Loaded) return@fold
if (!isInitialInfoAnalyticSent) {
isInitialInfoAnalyticSent = true
val balances = status.value.yieldBalance as? YieldBalance.Data

View file

@ -409,7 +409,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
currencyStatus = fromTokenStatus,
amount = amount.value,
fee = fee,
balanceAfterTransaction = balanceAfterTransaction,
feeCurrencyBalanceAfterTransaction = balanceAfterTransaction,
)
return currencyCheck
@ -1323,13 +1323,13 @@ internal class SwapInteractorImpl @AssistedInject constructor(
amount.value > reducedBalance -> {
IncludeFeeInAmount.BalanceNotEnough
}
amountWithFee < reducedBalance -> {
amountWithFee <= reducedBalance -> {
IncludeFeeInAmount.Excluded
}
else -> {
if (feeValue < amount.value) {
IncludeFeeInAmount.Included(
SwapAmount(
amountSubtractFee = SwapAmount(
reducedBalance - feeValue,
getNativeToken(fromToken.network.backendId).decimals,
),
@ -1994,7 +1994,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
private suspend fun getFeePaidCurrency(currency: CryptoCurrency): FeePaidCurrency {
return currenciesRepository.getFeePaidCurrency(
userWalletId = userWalletId,
currency = currency,
network = currency.network,
)
}

View file

@ -40,6 +40,10 @@ dependencies {
implementation(deps.tangem.card.core)
implementation(deps.tangem.blockchain)
implementation(deps.timber)
implementation(deps.firebase.perf) {
exclude(group = "com.google.firebase", module = "protolite-well-known-types")
exclude(group = "com.google.protobuf", module = "protobuf-javalite")
}
/** DI */
implementation(deps.hilt.android)

View file

@ -68,7 +68,7 @@ internal class WalletDeepLinksHandler @Inject constructor(
}
private suspend fun onSellCurrencyDeepLink(userWallet: UserWallet, data: SellCurrencyDeepLink.Data) {
val cryptoCurrency = getCryptoCurrencyUseCase(userWallet.walletId, data.currencyId).getOrNull()
val cryptoCurrency = getCryptoCurrencyUseCase(userWallet, data.currencyId).getOrNull()
if (cryptoCurrency == null) {
Timber.e("onSellCurrencyDeepLink cryptoCurrency is null")

View file

@ -1,6 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.analytics.utils
import arrow.core.getOrElse
import com.google.firebase.perf.FirebasePerformance
import com.google.firebase.perf.metrics.Trace
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.common.extensions.isZero
@ -12,6 +14,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
@ -31,11 +34,20 @@ internal class TokenListAnalyticsSender @Inject constructor(
private val balanceWasSentMap = mutableMapOf<String, Boolean>()
private val mutex = Mutex()
private val loadingTraces = mutableMapOf<UserWalletId, Trace>()
suspend fun send(displayedUiState: WalletState?, userWallet: UserWallet, tokenList: TokenList) {
if (screenLifecycleProvider.isBackgroundState.value) return
if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return
if (tokenList.totalFiatBalance is TotalFiatBalance.Loading) return
if (tokenList.totalFiatBalance is TotalFiatBalance.Loading) {
startLoadingTraceIfNeeded(userWallet.walletId, tokenList)
return
}
if (isTerminalState(tokenList.totalFiatBalance)) {
stopLoadingTraceIfNeeded(userWallet.walletId, tokenList.totalFiatBalance)
}
val currenciesStatuses = tokenList.flattenCurrencies()
@ -45,6 +57,35 @@ internal class TokenListAnalyticsSender @Inject constructor(
sendTokenBalancesIfNeeded(currenciesStatuses)
}
private suspend fun startLoadingTraceIfNeeded(userWalletId: UserWalletId, tokenList: TokenList) {
mutex.withLock {
if (!loadingTraces.containsKey(userWalletId)) {
val trace = FirebasePerformance.getInstance().newTrace(BALANCE_LOADED_TRACE_NAME)
trace.start()
trace.putAttribute(TOKENS_COUNT, tokenList.flattenCurrencies().size.toString())
loadingTraces[userWalletId] = trace
}
}
}
private suspend fun stopLoadingTraceIfNeeded(userWalletId: UserWalletId, totalFiatBalance: TotalFiatBalance) {
mutex.withLock {
loadingTraces[userWalletId]?.apply {
when (totalFiatBalance) {
is TotalFiatBalance.Loaded -> putAttribute(HAS_ERROR, "No")
is TotalFiatBalance.Failed -> putAttribute(HAS_ERROR, "Yes")
else -> { /* Intentionally do nothing */ }
}
stop()
loadingTraces.remove(userWalletId)
}
}
}
private fun isTerminalState(balance: TotalFiatBalance): Boolean {
return balance is TotalFiatBalance.Failed || balance is TotalFiatBalance.Loaded
}
private fun sendBalanceLoadedEventIfNeeded(
fiatBalance: TotalFiatBalance,
currenciesStatuses: List<CryptoCurrencyStatus>,
@ -179,4 +220,10 @@ internal class TokenListAnalyticsSender @Inject constructor(
analyticsEventHandler.send(MainScreen.NetworksUnreachable(unreachableCurrencies))
}
}
companion object {
const val BALANCE_LOADED_TRACE_NAME = "Total_balance_loaded"
const val HAS_ERROR = "has_error"
const val TOKENS_COUNT = "tokens_count"
}
}

View file

@ -5,6 +5,7 @@ import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
@ -22,7 +23,7 @@ internal class ScanCardToUnlockWalletClickHandler @Inject constructor(
suspend operator fun invoke(walletId: UserWalletId): Either<ScanCardToUnlockWalletError, Unit> {
return either {
when (val result = scanCardProcessor.scan()) {
when (val result = scanCardProcessor.scan(analyticsSource = AnalyticsParam.ScreensSources.Main)) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.UserCancelled) {
scanFailsCounter++

View file

@ -55,6 +55,18 @@ internal enum class Wallet2CobrandImage(
batchIds = setOf("AF97"),
),
CashClubGold(
cards2ResId = R.drawable.ill_cashclubgold_card2_120_106,
cards3ResId = R.drawable.ill_cashclubgold_card3_120_106,
batchIds = setOf("BB000004"),
),
Changenow(
cards2ResId = R.drawable.ill_changenow_card2_120_106,
cards3ResId = R.drawable.ill_changenow_card3_120_106,
batchIds = setOf("BB000013"),
),
CoinMetrica(
cards2ResId = R.drawable.ill_coin_metrica_card2_120_106,
cards3ResId = R.drawable.ill_coin_metrica_card3_120_106,
@ -85,12 +97,24 @@ internal enum class Wallet2CobrandImage(
batchIds = setOf("AF32"),
),
GetsMine(
cards2ResId = R.drawable.ill_gets_mine_card2_120_106,
cards3ResId = R.drawable.ill_gets_mine_card3_120_106,
batchIds = setOf("BB000008"),
),
Grim(
cards2ResId = R.drawable.ill_grim_card2_120_106,
cards3ResId = R.drawable.ill_grim_card3_120_106,
batchIds = setOf("AF13"),
),
Hodl(
cards2ResId = R.drawable.ill_hodl_card2_120_106,
cards3ResId = R.drawable.ill_hodl_card3_120_106,
batchIds = setOf("BB000009"),
),
Jr(
cards2ResId = R.drawable.ill_jr_card2_120_106,
cards3ResId = R.drawable.ill_jr_card3_120_106,
@ -139,12 +163,24 @@ internal enum class Wallet2CobrandImage(
batchIds = setOf("AF52"),
),
Kango(
cards2ResId = R.drawable.ill_kango_card2_120_106,
cards3ResId = R.drawable.ill_kango_card3_120_106,
batchIds = setOf("BB000006"),
),
Konan(
cards2ResId = R.drawable.ill_konan_card2_120_106,
cards3ResId = R.drawable.ill_konan_card3_120_106,
batchIds = setOf("AF93"),
),
Kroak(
cards2ResId = R.drawable.ill_kroak_card2_120_106,
cards3ResId = R.drawable.ill_kroak_card3_120_106,
batchIds = setOf("BB000011"),
),
Neiro(
cards2ResId = R.drawable.ill_neiro_card2_120_106,
cards3ResId = R.drawable.ill_neiro_card3_120_106,
@ -157,6 +193,12 @@ internal enum class Wallet2CobrandImage(
batchIds = setOf("AF26"),
),
PassimPay(
cards2ResId = R.drawable.ill_passimpay_cards2_120_106,
cards3ResId = R.drawable.ill_passimpay_cards3_120_106,
batchIds = setOf("BB000007"),
),
// for multicolored cards use image of 3 cards in all cases
Pastel(
cards2ResId = R.drawable.ill_pastel_cards3_120_106,
@ -170,12 +212,24 @@ internal enum class Wallet2CobrandImage(
batchIds = setOf("AF34"),
),
Rizo(
cards2ResId = R.drawable.ill_rizo_card2_120_106,
cards3ResId = R.drawable.ill_rizo_card3_120_106,
batchIds = setOf("BB000012"),
),
SatoshiFriends(
cards2ResId = R.drawable.ill_satoshi_card2_120_106,
cards3ResId = R.drawable.ill_satoshi_card3_120_106,
batchIds = setOf("AF19"),
),
SinCity(
cards2ResId = R.drawable.ill_sincity_card2_120_106,
cards3ResId = R.drawable.ill_sincity_card3_120_106,
batchIds = setOf("BB000010"),
),
StealthCard(
cards2ResId = R.drawable.ill_stealth_cards2_120_106,
cards3ResId = R.drawable.ill_stealth_cards3_120_106,
@ -194,6 +248,12 @@ internal enum class Wallet2CobrandImage(
batchIds = setOf("AF07"),
),
USA(
cards2ResId = R.drawable.ill_usa_card2_120_106,
cards3ResId = R.drawable.ill_usa_card3_120_106,
batchIds = setOf("AF91"),
),
VeChain(
cards2ResId = R.drawable.ill_vechain_card2_120_106,
cards3ResId = R.drawable.ill_vechain_card3_120_106,
@ -207,6 +267,12 @@ internal enum class Wallet2CobrandImage(
batchIds = setOf("AF40", "AF41", "AF42", "AF75", "AF76", "AF77"),
),
Vnish(
cards2ResId = R.drawable.ill_vnish_card2_120_106,
cards3ResId = R.drawable.ill_vnish_card3_120_106,
batchIds = setOf("BB000005"),
),
VoltInu(
cards2ResId = R.drawable.ill_volt_inu_card2_120_106,
cards3ResId = R.drawable.ill_volt_inu_card3_120_106,
@ -218,4 +284,16 @@ internal enum class Wallet2CobrandImage(
cards3ResId = R.drawable.ill_white_card3_120_106,
batchIds = setOf("AF15"),
),
WildGoat(
cards2ResId = R.drawable.ill_wildgoat_card2_120_106,
cards3ResId = R.drawable.ill_wildgoat_card3_120_106,
batchIds = setOf("BB000001"),
),
Winter(
cards2ResId = R.drawable.ill_winter_card2_120_106,
cards3ResId = R.drawable.ill_winter_card3_120_106,
batchIds = setOf("AF85", "AF86", "AF87", "AF990013", "AF990012", "AF990011"),
),
}

View file

@ -35,7 +35,7 @@ internal fun LazyListScope.lazyActions(
item(key = ACTIONS_CONTENT_TYPE + selectedWalletIndex, contentType = ACTIONS_CONTENT_TYPE) {
HorizontalActionChips(
buttons = actions.map(WalletManageButton::config).toImmutableList(),
modifier = modifier.animateItem(),
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16),
)
}
@ -62,7 +62,7 @@ internal fun LazyListScope.actions(
modifier = modifier
.padding(horizontal = 16.dp)
.fillMaxWidth()
.animateItem(),
.animateItem(fadeInSpec = null, fadeOutSpec = null),
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
verticalAlignment = Alignment.CenterVertically,
) {

View file

@ -31,13 +31,13 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
is WalletNotification.NoteMigration -> {
NoteMigrationNotification(
config = it.config,
modifier = modifier.animateItem(),
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
)
}
else -> {
Notification(
config = it.config,
modifier = modifier.animateItem(),
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
iconTint = when (it) {
is WalletNotification.Critical -> TangemTheme.colors.icon.warning
is WalletNotification.Informational -> TangemTheme.colors.icon.accent

View file

@ -78,7 +78,7 @@ private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) {
) {
Column(
modifier = modifier
.animateItem()
.animateItem(fadeInSpec = null, fadeOutSpec = null)
.padding(top = TangemTheme.dimens.spacing96),
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16),
horizontalAlignment = Alignment.CenterHorizontally,

View file

@ -1,6 +1,5 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.marketprice.MarketPriceBlock
@ -14,12 +13,11 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
*
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalFoundationApi::class)
internal fun LazyListScope.marketPriceBlock(state: MarketPriceBlockState, modifier: Modifier = Modifier) {
item(
key = MarketPriceBlockState::class.java,
contentType = MarketPriceBlockState::class.java,
) {
MarketPriceBlock(state = state, modifier = modifier.animateItemPlacement())
MarketPriceBlock(state = state, modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null))
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Some files were not shown because too many files have changed in this diff Show more