Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-23 11:16:28 +03:00
commit ca49abf2b1
892 changed files with 17979 additions and 6555 deletions

View file

@ -2,7 +2,7 @@ package com.tangem.features.onboarding.v2.common.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
sealed class OnboardingEvent(
category: String,

View file

@ -13,13 +13,13 @@ import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
import com.tangem.domain.wallets.builder.UserWalletBuilder
@ -39,7 +39,7 @@ import javax.inject.Inject
internal class OnboardingNoteTopUpModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
private val userWalletBuilderFactory: UserWalletBuilder.Factory,
private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase,
@ -137,8 +137,11 @@ internal class OnboardingNoteTopUpModel @Inject constructor(
private fun observeCryptoCurrencyStatus() {
val userWalletId = userWallet?.walletId ?: return
getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId).map { it.getOrNull() }.filterNotNull()
.onEach(::applyCryptoCurrencyStatusToState).launchIn(modelScope)
getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = userWalletId)
.map { it.getOrNull() }
.filterNotNull()
.onEach(::applyCryptoCurrencyStatusToState)
.launchIn(modelScope)
}
private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) {

View file

@ -7,18 +7,18 @@ import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
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.navigation.url.UrlOpener
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
@ -30,13 +30,13 @@ import com.tangem.domain.common.getTwinCardNumber
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
@ -77,7 +77,7 @@ internal class OnboardingTwinModel @Inject constructor(
private val tangemSdkManager: TangemSdkManager,
private val issuersConfigStorage: IssuersConfigStorage,
private val cardRepository: CardRepository,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase,
private val urlOpener: UrlOpener,
@ -337,16 +337,16 @@ internal class OnboardingTwinModel @Inject constructor(
setLoading(false)
}
val cryptoCurrencyStatus =
getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId).firstOrNull()?.getOrNull()
?: run {
setLoading(false)
Timber.e("Unable to get currency status")
return@coroutineScope
}
val cryptoCurrencyStatus = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId)
.firstOrNull()?.getOrNull()
?: run {
setLoading(false)
Timber.e("Unable to get currency status")
return@coroutineScope
}
launch {
getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId)
getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId)
.collect {
it.onRight { status ->
applyCryptoCurrencyStatusToState(status)

View file

@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.accesscode.model
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.input.TextFieldValue
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -9,10 +10,9 @@ import com.tangem.core.decompose.di.ModelScoped
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.error.UniversalError
import com.tangem.core.error.ext.universalError
import com.tangem.core.ui.utils.showErrorDialog
import com.tangem.domain.visa.error.VisaAPIError
import com.tangem.domain.visa.error.VisaAuthorizationAPIError
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.model.VisaCardId
import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest
@ -153,14 +153,11 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
loading(true)
modelScope.launch {
val challengeToSign = runCatching {
visaAuthRepository.getCardAuthChallenge(
cardId = activationInput.cardId,
cardPublicKey = activationInput.cardPublicKey,
)
}.getOrElse {
loading(false)
uiMessageSender.showErrorDialog(VisaAuthorizationAPIError)
val challengeToSign = visaAuthRepository.getCardAuthChallenge(
cardId = activationInput.cardId,
cardPublicKey = activationInput.cardPublicKey,
).getOrElse {
onError(it)
return@launch
}
@ -174,23 +171,17 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
val resultData = when (result) {
is CompletionResult.Failure -> {
loading(false)
uiMessageSender.showErrorDialog(result.error.universalError)
analyticsEventsHandler.send(
VisaAnalyticsEvent.ErrorOnboarding(result.error.universalError),
)
onError(result.error.universalError)
return@launch
}
is CompletionResult.Success -> result.data
}
runCatching {
visaActivationRepository.activateCard(resultData.signedActivationData)
}.onFailure {
loading(false)
uiMessageSender.showErrorDialog(VisaAPIError)
return@launch
}
visaActivationRepository.activateCard(resultData.signedActivationData)
.onLeft {
onError(it)
return@launch
}
val targetAddress =
result.data.signedActivationData.dataToSign.request.activationOrderInfo.customerWalletAddress
@ -213,6 +204,12 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
}
}
private fun onError(error: UniversalError) {
loading(false)
uiMessageSender.showErrorDialog(error)
analyticsEventsHandler.send(VisaAnalyticsEvent.ErrorOnboarding(error))
}
private fun loading(state: Boolean) {
_uiState.update { it.copy(buttonLoading = state) }
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.onboarding.v2.visa.impl.child.approve.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -8,9 +9,9 @@ import com.tangem.core.decompose.di.ModelScoped
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.error.UniversalError
import com.tangem.core.error.ext.universalError
import com.tangem.core.ui.utils.showErrorDialog
import com.tangem.domain.visa.error.VisaAPIError
import com.tangem.domain.visa.model.VisaCardId
import com.tangem.domain.visa.model.VisaDataForApprove
import com.tangem.domain.visa.repository.VisaActivationRepository
@ -67,13 +68,12 @@ internal class OnboardingVisaApproveModel @Inject constructor(
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ButtonApprove)
modelScope.launch {
val dataToSign = runCatching {
val dataToSign =
visaActivationRepository.getCustomerWalletAcceptanceData(params.preparationDataForApprove.request)
}.getOrElse {
loading(false)
uiMessageSender.showErrorDialog(VisaAPIError)
return@launch
}
.getOrElse {
onError(it)
return@launch
}
val result = tangemSdkManager.visaCustomerWalletApprove(
visaDataForApprove = VisaDataForApprove(
@ -85,26 +85,28 @@ internal class OnboardingVisaApproveModel @Inject constructor(
val resultData = when (result) {
is CompletionResult.Failure -> {
loading(false)
uiMessageSender.showErrorDialog(result.error.universalError)
analyticsEventHandler.send(VisaAnalyticsEvent.ErrorOnboarding(result.error.universalError))
onError(result.error.universalError)
return@launch
}
is CompletionResult.Success -> result.data
}
runCatching {
visaActivationRepository.approveByCustomerWallet(resultData)
}.onFailure {
loading(false)
uiMessageSender.showErrorDialog(VisaAPIError)
return@launch
}
visaActivationRepository.approveByCustomerWallet(resultData)
.onLeft {
onError(it)
return@launch
}
onDone.emit(Unit)
}
}
private fun onError(error: UniversalError) {
loading(false)
uiMessageSender.showErrorDialog(error)
analyticsEventHandler.send(VisaAnalyticsEvent.ErrorOnboarding(error))
}
private fun loading(state: Boolean) {
_uiState.update { it.copy(approveButtonLoading = state) }
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.onboarding.v2.visa.impl.child.inprogress.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
@ -12,8 +13,10 @@ import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.datasource.local.visa.VisaOTPStorage
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.error.VisaAuthorizationAPIError
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.model.VisaCardId
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.domain.wallets.builder.UserWalletBuilder
@ -64,9 +67,13 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
private fun runShortPolling() {
modelScope.launch {
while (true) {
val result = runCatching {
visaActivationRepository.getActivationRemoteState()
}.getOrNull() ?: continue
val result = visaActivationRepository.getActivationRemoteState()
.getOrElse {
uiMessageSender.showErrorDialog(it)
delay(timeMillis = 2000)
runShortPolling()
return@launch
}
when (result) {
is VisaActivationRemoteState.CardWalletSignatureRequired,
@ -156,12 +163,11 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
val authTokens = visaAuthTokenStorage.get(params.scanResponse.card.cardId)
?: error("Auth tokens are not found. This should not happen.")
val newTokens = runCatching {
visaAuthRepository.refreshAccessTokens(authTokens.refreshToken)
}.getOrElse {
uiMessageSender.showErrorDialog(VisaAuthorizationAPIError)
return
}
val newTokens = visaAuthRepository.refreshAccessTokens(authTokens.refreshToken)
.getOrElse {
uiMessageSender.showErrorDialog(it)
return
}
val userWallet = createUserWallet(params.scanResponse, newTokens)
userWalletsListManager.save(userWallet)

View file

@ -6,8 +6,10 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
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.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.utils.showErrorDialog
import com.tangem.domain.visa.model.VisaActivationOrderInfo
import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.model.VisaCardId
@ -15,6 +17,7 @@ import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.OnboardingVisaOtherWalletComponent
import com.tangem.features.onboarding.v2.visa.impl.child.otherwallet.ui.state.OnboardingVisaOtherWalletUM
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.VisaAnalyticsEvent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
@ -25,6 +28,7 @@ import javax.inject.Inject
@Stable
@ModelScoped
@Suppress("LongParameterList")
internal class OnboardingVisaOtherWalletModel @Inject constructor(
paramsContainer: ParamsContainer,
visaActivationRepositoryFactory: VisaActivationRepository.Factory,
@ -32,6 +36,7 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor(
private val urlOpener: UrlOpener,
private val shareManager: ShareManager,
private val analyticsEventHandler: AnalyticsEventHandler,
private val uiMessageSender: UiMessageSender,
) : Model() {
private val config = paramsContainer.require<OnboardingVisaOtherWalletComponent.Config>()
@ -50,14 +55,18 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor(
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.GoToWebsiteOpened)
modelScope.launch {
while (true) {
val result = runCatching {
visaActivationRepository.getActivationRemoteState()
}.getOrNull()
if (result is VisaActivationRemoteState.AwaitingPinCode) {
onDone.emit(result.activationOrderInfo)
break
}
visaActivationRepository.getActivationRemoteState()
.onLeft {
uiMessageSender.showErrorDialog(it)
analyticsEventHandler.send(VisaAnalyticsEvent.ErrorOnboarding(it))
delay(timeMillis = 60_000)
}
.onRight {
if (it is VisaActivationRemoteState.AwaitingPinCode) {
onDone.emit(it.activationOrderInfo)
return@launch
}
}
delay(timeMillis = 2000)
}

View file

@ -10,7 +10,6 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.showErrorDialog
import com.tangem.domain.visa.SetVisaPinCodeUseCase
import com.tangem.domain.visa.error.VisaAuthorizationAPIError
import com.tangem.domain.visa.model.VisaCardId
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.visa.impl.child.pincode.OnboardingVisaPinCodeComponent
@ -95,7 +94,7 @@ internal class OnboardingVisaPinCodeModel @Inject constructor(
activationOrderId = params.activationOrderInfo.orderId,
).onLeft {
loading(false)
uiMessageSender.showErrorDialog(VisaAuthorizationAPIError)
uiMessageSender.showErrorDialog(it)
return@launch
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.onboarding.v2.visa.impl.child.welcome.model
import arrow.core.getOrElse
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -7,9 +8,9 @@ import com.tangem.core.decompose.di.ModelScoped
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.error.UniversalError
import com.tangem.core.error.ext.universalError
import com.tangem.core.ui.utils.showErrorDialog
import com.tangem.domain.visa.error.VisaAPIError
import com.tangem.domain.visa.model.VisaCardId
import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest
import com.tangem.domain.visa.repository.VisaActivationRepository
@ -78,13 +79,11 @@ internal class OnboardingVisaWelcomeModel @Inject constructor(
loading(true)
modelScope.launch {
val dataToSignByCardWallet = runCatching {
visaActivationRepository.getCardWalletAcceptanceData(params.dataToSignRequest)
}.getOrElse {
loading(false)
uiMessageSender.showErrorDialog(VisaAPIError)
return@launch
}
val dataToSignByCardWallet = visaActivationRepository.getCardWalletAcceptanceData(params.dataToSignRequest)
.getOrElse {
onError(it)
return@launch
}
val result = tangemSdkManager.activateVisaCard(
mode = VisaCardActivationTaskMode.SignOnly(dataToSignByCardWallet = dataToSignByCardWallet),
@ -93,21 +92,17 @@ internal class OnboardingVisaWelcomeModel @Inject constructor(
val resultData = when (result) {
is CompletionResult.Failure -> {
loading(false)
uiMessageSender.showErrorDialog(result.error.universalError)
analyticsEventsHandler.send(VisaAnalyticsEvent.ErrorOnboarding(result.error.universalError))
onError(result.error.universalError)
return@launch
}
is CompletionResult.Success -> result.data
}
runCatching {
visaActivationRepository.activateCard(resultData.signedActivationData)
}.onFailure {
loading(false)
uiMessageSender.showErrorDialog(VisaAPIError)
return@launch
}
visaActivationRepository.activateCard(resultData.signedActivationData)
.onLeft {
onError(it)
return@launch
}
val request = result.data.signedActivationData.dataToSign.request
val targetAddress = request.activationOrderInfo.customerWalletAddress
@ -132,6 +127,12 @@ internal class OnboardingVisaWelcomeModel @Inject constructor(
}
}
private fun onError(error: UniversalError) {
loading(false)
uiMessageSender.showErrorDialog(error)
analyticsEventsHandler.send(VisaAnalyticsEvent.ErrorOnboarding(error))
}
private fun loading(state: Boolean) {
_uiState.update { it.copy(continueButtonLoading = state) }
}