Updated on 2026-08-14
This commit is contained in:
commit
6de53d32c1
129 changed files with 1419 additions and 514 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit b55ca9ec9bc7ff88a8764f00656ee7a884df9125
|
||||
Subproject commit a5d1a89425a95bc9c90c7a6fed3c578b0d324994
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.common.analytics.appsflyer
|
|||
import com.appsflyer.deeplink.DeepLink
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
|
|
@ -18,6 +19,7 @@ import kotlin.contracts.contract
|
|||
@Singleton
|
||||
class AppsFlyerReferralParamsHandler @Inject constructor(
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
private val setShouldShowMobileWalletPromoUseCase: SetShouldShowMobileWalletPromoUseCase,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
|
|
@ -69,6 +71,8 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
|
|||
private fun storeConversionData(refcode: String, campaign: String?) {
|
||||
coroutineScope.launch {
|
||||
mutex.withLock {
|
||||
setShouldShowMobileWalletPromoUseCase()
|
||||
.onLeft { Timber.e(it) }
|
||||
appsFlyerStore.storeIfAbsent(
|
||||
value = AppsFlyerConversionData(refcode = refcode, campaign = campaign),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
package com.tangem.tap.di
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.finisher.AppFinisher
|
||||
import com.tangem.domain.card.BuildConfig
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
|
||||
|
|
@ -10,6 +15,7 @@ import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
|
|||
import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask
|
||||
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -30,6 +36,11 @@ internal class TangemSdkManagerModule {
|
|||
visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
||||
tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
||||
onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||
@GlobalUiMessageSender uiMessageSender: UiMessageSender,
|
||||
appFinisher: AppFinisher,
|
||||
sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): TangemSdkManager {
|
||||
return if (BuildConfig.MOCK_DATA_SOURCE) {
|
||||
MockTangemSdkManager(resources = context.resources)
|
||||
|
|
@ -41,6 +52,11 @@ internal class TangemSdkManagerModule {
|
|||
visaCardActivationTaskFactory = visaCardActivationTaskFactory,
|
||||
tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory,
|
||||
onboardingV2FeatureToggles = onboardingV2FeatureToggles,
|
||||
uiMessageSender = uiMessageSender,
|
||||
appFinisher = appFinisher,
|
||||
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
|
||||
analyticsExceptionHandler = analyticsExceptionHandler,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,21 @@ import com.tangem.common.extensions.hexToBytes
|
|||
import com.tangem.common.services.secure.SecureStorage
|
||||
import com.tangem.common.usersCode.UserCodeRepository
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.finisher.AppFinisher
|
||||
import com.tangem.core.res.getStringSafe
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.crypto.bip39.DefaultMnemonic
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
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.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -57,13 +66,14 @@ import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
|
|||
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
|
||||
import com.tangem.tap.domain.twins.FinalizeTwinTask
|
||||
import com.tangem.tap.domain.visa.VisaCardScanHandler
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@Suppress("TooManyFunctions", "LargeClass")
|
||||
@Suppress("TooManyFunctions", "LargeClass", "LongParameterList")
|
||||
internal class DefaultTangemSdkManager(
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val resources: Resources,
|
||||
|
|
@ -71,6 +81,11 @@ internal class DefaultTangemSdkManager(
|
|||
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
|
||||
private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory,
|
||||
private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val appFinisher: AppFinisher,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
) : TangemSdkManager {
|
||||
|
||||
private val awaitInitializationMutex = Mutex()
|
||||
|
|
@ -99,6 +114,8 @@ internal class DefaultTangemSdkManager(
|
|||
override val userCodeRequestPolicy: UserCodeRequestPolicy
|
||||
get() = tangemSdk.config.userCodeRequestPolicy
|
||||
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.io)
|
||||
|
||||
override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean {
|
||||
return try {
|
||||
needEnrollBiometrics
|
||||
|
|
@ -413,9 +430,16 @@ internal class DefaultTangemSdkManager(
|
|||
break
|
||||
} else {
|
||||
if (attemps++ >= MAX_INITIALIZE_ATTEMPTS) {
|
||||
error("Can't initialize authentication manager after $MAX_INITIALIZE_ATTEMPTS attempts")
|
||||
analyticsExceptionHandler.sendException(
|
||||
ExceptionAnalyticsEvent(
|
||||
exception = IllegalStateException(
|
||||
"Can't initialize authentication manager after $MAX_INITIALIZE_ATTEMPTS attempts",
|
||||
),
|
||||
),
|
||||
)
|
||||
showAlert()
|
||||
} else {
|
||||
delay(timeMillis = 200)
|
||||
delay(timeMillis = 400)
|
||||
}
|
||||
}
|
||||
} while (true)
|
||||
|
|
@ -424,6 +448,28 @@ internal class DefaultTangemSdkManager(
|
|||
}
|
||||
}
|
||||
|
||||
private fun showAlert() {
|
||||
uiMessageSender.send(
|
||||
message = DialogMessage(
|
||||
message = resourceReference(id = R.string.alert_authentication_error_message),
|
||||
title = resourceReference(id = R.string.alert_authentication_error_title),
|
||||
isDismissable = false,
|
||||
dismissOnFirstAction = false,
|
||||
firstActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = resourceReference(R.string.alert_button_request_support),
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
sendFeedbackEmailUseCase(FeedbackEmailType.BiometricsAuthenticationFailed)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
secondActionBuilder = { cancelAction(onClick = appFinisher::finish) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// region Twin-specific
|
||||
|
||||
override suspend fun createFirstTwinWallet(
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ internal class ChildFactory @Inject constructor(
|
|||
val source = when (route.source) {
|
||||
AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS
|
||||
AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES
|
||||
AppRoute.ManageTokens.Source.ACCOUNT -> ManageTokensSource.ACCOUNT
|
||||
}
|
||||
|
||||
val mode = when (val portfolio = route.portfolioId) {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ package com.tangem.tap.common.analytics.appsflyer
|
|||
import com.appsflyer.deeplink.DeepLink
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import arrow.core.right
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
|
@ -22,9 +25,13 @@ import org.junit.jupiter.params.ParameterizedTest
|
|||
class AppsFlyerReferralParamsHandlerTest {
|
||||
|
||||
private val appsFlyerStore: AppsFlyerStore = mockk(relaxUnitFun = true)
|
||||
private val setShouldShowMobileWalletPromoUseCase: SetShouldShowMobileWalletPromoUseCase = mockk {
|
||||
coEvery { this@mockk.invoke() } returns Unit.right()
|
||||
}
|
||||
private val handler = AppsFlyerReferralParamsHandler(
|
||||
appsFlyerStore = appsFlyerStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
setShouldShowMobileWalletPromoUseCase = setShouldShowMobileWalletPromoUseCase,
|
||||
)
|
||||
|
||||
@AfterEach
|
||||
|
|
@ -43,6 +50,7 @@ class AppsFlyerReferralParamsHandlerTest {
|
|||
|
||||
if (model.shouldStore) {
|
||||
val value = AppsFlyerConversionData(refcode = SUCCESS_REFCODE, campaign = SUCCESS_CAMPAIGN)
|
||||
|
||||
coVerify { appsFlyerStore.storeIfAbsent(value = value) }
|
||||
} else {
|
||||
coVerify(inverse = true) { appsFlyerStore.storeIfAbsent(value = any()) }
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ sealed class AppRoute(val path: String) : Route {
|
|||
enum class Source {
|
||||
STORIES,
|
||||
SETTINGS,
|
||||
ACCOUNT,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ private fun RowScope.TokenRatingPlace(ratingPosition: String?) {
|
|||
.alignByBaseline()
|
||||
.heightIn(min = TangemTheme.dimens.size16)
|
||||
.background(
|
||||
color = TangemTheme.colors.field.primary,
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
shape = TangemTheme.shapes.roundedCornersSmall2,
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing5),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package com.tangem.core.analytics.models
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.REFERRAL
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.REFERRAL_ID
|
||||
|
||||
const val IS_NOT_HTTP_ERROR = "Is not http error"
|
||||
|
||||
sealed class AnalyticsParam {
|
||||
|
|
@ -284,6 +287,15 @@ sealed class AnalyticsParam {
|
|||
const val ENS = "ENS"
|
||||
const val ENS_ADDRESS = "ENS Address"
|
||||
const val ACCOUNT_DERIVATION_FROM = "Account Derivation (from)"
|
||||
const val ACCOUNT_DERIVATION_TO = "Account Derivation (to)"
|
||||
const val FEE_TOKEN = "Fee Token"
|
||||
const val ACCOUNT_DERIVATION = "Account Derivation"
|
||||
const val REFERRAL = "Referral"
|
||||
const val REFERRAL_ID = "Referral_ID"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getReferralParams(referralId: String?): List<Pair<String, String>> = listOf(
|
||||
REFERRAL to (!referralId.isNullOrBlank()).toString().replaceFirstChar(Char::titlecase),
|
||||
REFERRAL_ID to (referralId ?: "Empty"),
|
||||
)
|
||||
|
|
@ -1,22 +1,32 @@
|
|||
package com.tangem.core.analytics.models
|
||||
|
||||
/**
|
||||
* Marker interface for analytics events that should be sent only once per session.
|
||||
* Marker interface for analytics events that can be throttled by session and/or time.
|
||||
*
|
||||
* Events implementing this interface will be tracked by their [oneTimeEventId] to ensure
|
||||
* they are not sent multiple times during the same application session. Once an event
|
||||
* with a specific [oneTimeEventId] has been sent, subsequent attempts to send an event
|
||||
* with the same ID will be ignored.
|
||||
* Events implementing this interface will be tracked by their [oneTimeEventId].
|
||||
*
|
||||
* Behavior depends on [throttleSeconds]:
|
||||
* - **null** (default): One time per session — event is sent only once during the application session.
|
||||
* - **non-null**: Time-based throttling — event is not sent if less than [throttleSeconds] seconds
|
||||
* have passed since the last send for this [oneTimeEventId].
|
||||
*
|
||||
* @see Analytics.send
|
||||
*/
|
||||
interface OneTimePerSessionEvent {
|
||||
/**
|
||||
* Unique identifier for the one-time event.
|
||||
* Unique identifier for the throttled event.
|
||||
*
|
||||
* This ID is used to track whether the event has already been sent in the current session.
|
||||
* Events with the same [oneTimeEventId] will only be sent once, even if they are
|
||||
* different instances of the same event class.
|
||||
* This ID is used to track whether and when the event was last sent.
|
||||
* Events with the same [oneTimeEventId] share the same throttling state.
|
||||
*/
|
||||
val oneTimeEventId: String
|
||||
|
||||
/**
|
||||
* Minimum interval in seconds between sends for this event.
|
||||
*
|
||||
* - **null**: One time per session only. Event is sent at most once per session.
|
||||
* - **non-null**: Don't send if less than this many seconds have passed since the last send.
|
||||
*/
|
||||
val throttleSeconds: Long?
|
||||
get() = null
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.core.analytics.models.event
|
|||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
|
||||
import com.tangem.core.analytics.models.getReferralParams
|
||||
|
||||
sealed class OnboardingAnalyticsEvent(
|
||||
category: String,
|
||||
|
|
@ -55,6 +56,7 @@ sealed class OnboardingAnalyticsEvent(
|
|||
creationType: WalletCreationType = WalletCreationType.NewSeed,
|
||||
seedPhraseLength: Int? = null,
|
||||
passPhraseState: AnalyticsParam.EmptyFull,
|
||||
referralId: String?,
|
||||
) : CreateWallet(
|
||||
event = "Wallet Created Successfully",
|
||||
params = buildMap {
|
||||
|
|
@ -64,6 +66,7 @@ sealed class OnboardingAnalyticsEvent(
|
|||
if (seedPhraseLength != null) {
|
||||
put("Seed Phrase Length", seedPhraseLength.toString())
|
||||
}
|
||||
putAll(getReferralParams(referralId))
|
||||
},
|
||||
), AppsFlyerIncludedEvent
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import kotlinx.coroutines.sync.Mutex
|
|||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -32,7 +33,7 @@ object Analytics : GlobalAnalyticsEventHandler {
|
|||
|
||||
private val handlers = mutableMapOf<String, AnalyticsHandler>()
|
||||
private val paramsInterceptors = ConcurrentHashMap<String, ParamsInterceptor>()
|
||||
private val oneEventsPerSession = ConcurrentHashMap<String, Boolean>()
|
||||
private val throttledEventsState = ConcurrentHashMap<String, Long>()
|
||||
private val analyticsFilters = mutableSetOf<AnalyticsEventFilter>()
|
||||
private val analyticsMutex = Mutex()
|
||||
|
||||
|
|
@ -87,9 +88,7 @@ object Analytics : GlobalAnalyticsEventHandler {
|
|||
|
||||
override fun send(event: AnalyticsEvent) {
|
||||
analyticsScope.launch {
|
||||
if (event is OneTimePerSessionEvent &&
|
||||
oneEventsPerSession.putIfAbsent(event.oneTimeEventId, true) != null
|
||||
) {
|
||||
if (event is OneTimePerSessionEvent && !shouldSendThrottledEvent(event)) {
|
||||
return@launch
|
||||
}
|
||||
event.params = applyParamsInterceptors(event)
|
||||
|
|
@ -137,6 +136,21 @@ object Analytics : GlobalAnalyticsEventHandler {
|
|||
return interceptedParams
|
||||
}
|
||||
|
||||
private fun shouldSendThrottledEvent(event: OneTimePerSessionEvent): Boolean {
|
||||
val now = System.currentTimeMillis()
|
||||
val id = event.oneTimeEventId
|
||||
return when (val throttleMs = event.throttleSeconds?.let(TimeUnit.SECONDS::toMillis)) {
|
||||
null -> throttledEventsState.putIfAbsent(id, now) == null
|
||||
else -> throttledEventsState.compute(id) { _, lastSendTime ->
|
||||
when {
|
||||
lastSendTime == null -> now
|
||||
now - lastSendTime >= throttleMs -> now
|
||||
else -> lastSendTime
|
||||
}
|
||||
} == now
|
||||
}
|
||||
}
|
||||
|
||||
private fun createScope(): CoroutineScope {
|
||||
val name = "Analytics"
|
||||
val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ import com.squareup.moshi.JsonClass
|
|||
data class PromocodeActivationBody(
|
||||
@Json(name = "promoCode") val promoCode: String,
|
||||
@Json(name = "address") val address: String,
|
||||
@Json(name = "walletId") val walletId: String,
|
||||
)
|
||||
|
|
@ -86,6 +86,8 @@
|
|||
<string name="address_qr_code_message_format">Sende nur %1$s ( %2$s ) vom %3$s -Netzwerk an diese Adresse. Die Verwendung anderer Token und Netzwerke kann zum Verlust von Geldern führen.</string>
|
||||
<string name="address_type_default">Standard</string>
|
||||
<string name="address_type_legacy">Altbestand</string>
|
||||
<string name="alert_authentication_error_message">Bei der biometrischen Authentifizierung ist ein Fehler aufgetreten. Bitte setzen Sie die Biometrie auf Ihrem Gerät zurück oder kontaktieren Sie den Support.</string>
|
||||
<string name="alert_authentication_error_title">Authentifizierungsfehler</string>
|
||||
<string name="alert_button_how_to_scan">So scannt man</string>
|
||||
<string name="alert_button_request_support">Hilfe anfordern</string>
|
||||
<string name="alert_button_try_again">Erneut versuchen</string>
|
||||
|
|
@ -299,6 +301,7 @@
|
|||
<string name="common_go_to_token">Zum Token</string>
|
||||
<string name="common_got_it">Verstanden</string>
|
||||
<string name="common_hide">Ausblenden</string>
|
||||
<string name="common_hold_to">Halten bis %s</string>
|
||||
<string name="common_hour">Stunde</string>
|
||||
<string name="common_import">Importieren</string>
|
||||
<string name="common_in_progress">In Arbeit</string>
|
||||
|
|
@ -374,6 +377,7 @@
|
|||
<string name="common_swap">Tauschen</string>
|
||||
<string name="common_tangem">Tangem</string>
|
||||
<string name="common_tangem_wallet">Tangem Wallet</string>
|
||||
<string name="common_tap_and_hold_hint">Tippen und halten</string>
|
||||
<string name="common_terms_and_conditions">Allgemeine Geschäftsbedingungen</string>
|
||||
<string name="common_terms_of_use">Nutzungsbedingungen</string>
|
||||
<string name="common_to">An</string>
|
||||
|
|
@ -463,6 +467,14 @@
|
|||
<string name="domain_receive_assets_onboarding_description">Das Senden von Vermögenswerten in anderen Netzwerken führt zu dauerhaftem Verlust.</string>
|
||||
<string name="domain_receive_assets_onboarding_network_name">%s Netzwerk</string>
|
||||
<string name="domain_receive_assets_onboarding_title">Sende Geld nur mit</string>
|
||||
<string name="earn_best_opportunities">Beste Gelegenheiten</string>
|
||||
<string name="earn_clear_filter">Filter löschen</string>
|
||||
<string name="earn_filter_all_networks">Alle Netzwerke</string>
|
||||
<string name="earn_filter_all_types">Alle Arten</string>
|
||||
<string name="earn_filter_by">Filtern nach</string>
|
||||
<string name="earn_mostly_used">Meist verwendet</string>
|
||||
<string name="earn_no_results">Keine Ergebnisse</string>
|
||||
<string name="earn_title">Verdienen</string>
|
||||
<string name="email_preface_wc_error">Hallo Support-Team, ich habe einen Fehler mit dem Code %s festgestellt.</string>
|
||||
<string name="email_subject_wc_error">WalletConnect-Fehler</string>
|
||||
<string name="error_wrong_wallet_tapped">Du hast eine Karte oder Ring aus einer anderen Wallet verwendet. Tippe auf die Karte oder Ring, die dieser Wallet zugeordnet ist.</string>
|
||||
|
|
@ -613,6 +625,7 @@
|
|||
<string name="hw_backup_need_title">Zuerst die Sicherung abschließen</string>
|
||||
<string name="hw_backup_no_backup">Unvollständig</string>
|
||||
<string name="hw_backup_section_other_title">Andere Methoden</string>
|
||||
<string name="hw_backup_seed_code_description">Bewahre Deine Wiederherstellungsphrase an einem sicheren Ort auf und halte diese geheim, um Dein Guthaben zu schützen. Richte außerdem einen 8 stelligen Zugangscode für zusätzliche Sicherheit ein.</string>
|
||||
<string name="hw_backup_seed_description">Speicher Deinen Wiederherstellungssatz an einem sicheren Ort und halte diesen stets geheim, um Dein Geld zu schützen.</string>
|
||||
<string name="hw_backup_seed_title">Wiederherstellungs-Phrase</string>
|
||||
<string name="hw_backup_to_secure_description">Um Deine Wallet mit einem Zugangscode zu sichern, schließe den Sicherungsvorgang ab.</string>
|
||||
|
|
@ -1463,6 +1476,9 @@
|
|||
<string name="tangem_pay_freeze_card_freeze">Einfrieren</string>
|
||||
<string name="tangem_pay_freeze_card_success">Ihre Karte ist eingefroren.</string>
|
||||
<string name="tangem_pay_get_help">Hilfe erhalten</string>
|
||||
<string name="tangem_pay_history_item_spend_mc_declined_reason">Grund: %s</string>
|
||||
<string name="tangem_pay_history_item_spend_mc_title_format" formatted="false">%s · %s</string>
|
||||
<string name="tangem_pay_history_item_spend_mcc">MCC %s</string>
|
||||
<string name="tangem_pay_other">Andere</string>
|
||||
<string name="tangem_pay_rooted_device_subtitle">Nicht nutzbar auf gerooteten Geräten</string>
|
||||
<string name="tangem_pay_status_completed">Abgeschlossen</string>
|
||||
|
|
@ -1834,7 +1850,18 @@
|
|||
<string name="warning_button_ok">OK, habe ich verstanden!</string>
|
||||
<string name="warning_button_really_cool">Echt toll!</string>
|
||||
<string name="warning_button_refresh">Aktualisieren</string>
|
||||
<string name="warning_clore_migration_button">Migration starten</string>
|
||||
<string name="warning_clore_migration_copy_button">Kopie</string>
|
||||
<string name="warning_clore_migration_description">Um weiterhin Zugriff auf Deine Gelder zu haben, beginne die Migration gemäß den offiziellen Clore-Richtlinien.</string>
|
||||
<string name="warning_clore_migration_error_signing_not_supported">Die Nachrichtensignatur wird für dieses Netzwerk nicht unterstützt.</string>
|
||||
<string name="warning_clore_migration_error_wallet_manager_not_found">Nachricht konnte nicht signiert werden. Bitte versuche es erneut.</string>
|
||||
<string name="warning_clore_migration_message">Laut der offiziellen Dokumentation von Clore werden alle Münzen, die vor dem 21. Dezember erhalten wurden, in Clore (ERC-20 Token) migriert; Münzen, die nach diesem Datum erhalten wurden, nicht. Eine Lösung für den Transfer ist in Arbeit — bleibt dran.</string>
|
||||
<string name="warning_clore_migration_message_label">Nachricht</string>
|
||||
<string name="warning_clore_migration_open_portal_button">Claim-Portal öffnen</string>
|
||||
<string name="warning_clore_migration_sheet_description">Um Deine Clore-Token weiterhin nutzen zu können, musst Du die Token-Migration gemäß den Informationen im Claim Portal durchführen.</string>
|
||||
<string name="warning_clore_migration_sheet_title">Clore-Netzwerkmigration</string>
|
||||
<string name="warning_clore_migration_sign_button">signieren</string>
|
||||
<string name="warning_clore_migration_signature_label">Unterschrift</string>
|
||||
<string name="warning_clore_migration_title">Migration des Clore-Netzwerks</string>
|
||||
<string name="warning_demo_mode_message">Du befindest sich derzeit im Demo-Modus</string>
|
||||
<string name="warning_demo_mode_title">Demo-Modus aktiv</string>
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@
|
|||
<string name="address_qr_code_message_format">Envíe solo %1$s (%2$s) desde redes como %3$s a esta dirección. Usar otros tokens y redes puede resultar en la pérdida de fondos.</string>
|
||||
<string name="address_type_default">Por defecto</string>
|
||||
<string name="address_type_legacy">Legacy</string>
|
||||
<string name="alert_authentication_error_message">Algo salió mal con la biometría. Por favor, intente restablecer la biometría en su dispositivo o contacte con soporte.</string>
|
||||
<string name="alert_authentication_error_title">Error de inicio de sesión</string>
|
||||
<string name="alert_button_how_to_scan">Cómo escanear</string>
|
||||
<string name="alert_button_request_support">Solicitar soporte</string>
|
||||
<string name="alert_button_try_again">Inténtelo de nuevo</string>
|
||||
|
|
@ -836,7 +838,7 @@
|
|||
<item quantity="other">Hace %d minutos</item>
|
||||
</plurals>
|
||||
<string name="news_quick_recap">Resumen rápido</string>
|
||||
<string name="news_related_news">Noticias relacionadas</string>
|
||||
<string name="news_related_news">Noticias</string>
|
||||
<string name="news_related_tokens">Tokens relacionados</string>
|
||||
<string name="news_sources">Fuentes</string>
|
||||
<string name="news_stay_in_the_loop">Manténgase informado</string>
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@
|
|||
<string name="address_qr_code_message_format">Envoyez uniquement %1$s (%2$s) depuis les réseaux %3$s à cette adresse. L\'utilisation d\'autres jetons et réseaux peut entraîner une perte de fonds.</string>
|
||||
<string name="address_type_default">Défaut</string>
|
||||
<string name="address_type_legacy">Héritage</string>
|
||||
<string name="alert_authentication_error_message">Quelque chose s’est mal passé avec la biométrie. Veuillez réinitialiser la biométrie sur votre appareil ou contacter le support.</string>
|
||||
<string name="alert_authentication_error_title">Erreur d’authentification</string>
|
||||
<string name="alert_button_how_to_scan">Comment scanner</string>
|
||||
<string name="alert_button_request_support">Demander de l\'aide</string>
|
||||
<string name="alert_button_try_again">Réessayez</string>
|
||||
|
|
@ -466,9 +468,14 @@
|
|||
<string name="domain_receive_assets_onboarding_network_name">%s réseau</string>
|
||||
<string name="domain_receive_assets_onboarding_title">Envoyez des fonds en utilisant uniquement</string>
|
||||
<string name="earn_best_opportunities">Meilleures opportunités</string>
|
||||
<string name="earn_clear_filter">Effacer le filtre</string>
|
||||
<string name="earn_filter_all_networks">Tous les réseaux</string>
|
||||
<string name="earn_filter_all_types">Tous les types</string>
|
||||
<string name="earn_filter_by">Filtrer par</string>
|
||||
<string name="earn_filter_my_networks">Mes réseaux</string>
|
||||
<string name="earn_filter_networks">Réseaux</string>
|
||||
<string name="earn_mostly_used">Principalement utilisé</string>
|
||||
<string name="earn_no_results">Pas de résultat</string>
|
||||
<string name="earn_title">Gagner</string>
|
||||
<string name="email_preface_wc_error">Bonjour équipe de support, j’ai rencontré une erreur avec le code : %s</string>
|
||||
<string name="email_subject_wc_error">Erreur WalletConnect</string>
|
||||
|
|
@ -821,6 +828,7 @@
|
|||
<string name="markets_token_details_volume">Volume</string>
|
||||
<string name="markets_tooltip_message">Tirez vers le haut ou appuyez sur la barre de recherche pour ajouter des jetons directement depuis le marché</string>
|
||||
<string name="markets_tooltip_title">Ajouter des jetons</string>
|
||||
<string name="markets_tooltip_v2_title">Ajouter plus de tokens</string>
|
||||
<string name="markets_yield_supply_banner_description">Optimisez vos actifs tout en leur fournissant un accès instantané. %s</string>
|
||||
<string name="markets_yield_supply_banner_title">Activer le mode rendement</string>
|
||||
<string name="mobile_wallet_requires_min_os_warning_body">Vous devez effectuer la mise à jour %1$s afin de créer un portefeuille mobile.</string>
|
||||
|
|
@ -1845,7 +1853,18 @@
|
|||
<string name="warning_button_ok">Ok, compris!</string>
|
||||
<string name="warning_button_really_cool">Vraiment cool !</string>
|
||||
<string name="warning_button_refresh">Rafraîchir</string>
|
||||
<string name="warning_clore_migration_button">Lancer la migration</string>
|
||||
<string name="warning_clore_migration_copy_button">Copier</string>
|
||||
<string name="warning_clore_migration_description">Pour conserver l\'accès à vos fonds, lancez la migration conformément aux directives officielles de Clore.</string>
|
||||
<string name="warning_clore_migration_error_signing_not_supported">La signature des messages n\'est pas prise en charge pour ce réseau.</string>
|
||||
<string name="warning_clore_migration_error_wallet_manager_not_found">Impossible de signer le message. Veuillez réessayer.</string>
|
||||
<string name="warning_clore_migration_message">Selon la documentation officielle de Clore, toutes les pièces reçues avant le 21 décembre seront migrées vers Clore (token ERC-20) ; les pièces reçues après cette date ne le seront pas. Une solution de transfert arrive — restez à l\'écoute.</string>
|
||||
<string name="warning_clore_migration_message_label">Message</string>
|
||||
<string name="warning_clore_migration_open_portal_button">Ouvrir le portail des réclamations</string>
|
||||
<string name="warning_clore_migration_sheet_description">Pour continuer à utiliser vos jetons Clore, vous devez effectuer la migration des jetons conformément aux informations fournies sur le portail de réclamation.</string>
|
||||
<string name="warning_clore_migration_sheet_title">Migration du réseau Clore</string>
|
||||
<string name="warning_clore_migration_sign_button">Signer</string>
|
||||
<string name="warning_clore_migration_signature_label">Signature</string>
|
||||
<string name="warning_clore_migration_title">Migration du réseau Clore</string>
|
||||
<string name="warning_demo_mode_message">Vous êtes actuellement en mode démo</string>
|
||||
<string name="warning_demo_mode_title">Mode démo actif</string>
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@
|
|||
<string name="address_qr_code_message_format">このアドレスには%3$s ネットワークから%1$s (%2$s) のみを送信してください。他のトークンやネットワークを使用すると、資金を失う可能性があります。</string>
|
||||
<string name="address_type_default">デフォルト</string>
|
||||
<string name="address_type_legacy">レガシー</string>
|
||||
<string name="alert_authentication_error_message">デバイスの生体認証をリセットするか、サポートにお問い合わせください</string>
|
||||
<string name="alert_authentication_error_title">認証エラー</string>
|
||||
<string name="alert_button_how_to_scan">スキャン方法</string>
|
||||
<string name="alert_button_request_support">サポートをリクエストする</string>
|
||||
<string name="alert_button_try_again">もう一度やり直してください</string>
|
||||
|
|
@ -523,7 +525,7 @@
|
|||
<string name="express_provider">プロバイダー</string>
|
||||
<string name="express_provider_best_rate">ベストレート</string>
|
||||
<string name="express_provider_fca_warning_list">FCA警告リスト</string>
|
||||
<string name="express_provider_great_rate">最適な選択</string>
|
||||
<string name="express_provider_great_rate">お得なレート</string>
|
||||
<string name="express_provider_in_fca_warning_list">FCA警告リストに掲載されたプロバイダー</string>
|
||||
<string name="express_provider_max_amount">最大 %s まで使用可能</string>
|
||||
<string name="express_provider_min_amount">%s 以上で利用可能</string>
|
||||
|
|
@ -724,7 +726,7 @@
|
|||
<string name="markets_loading_no_data_title">データなし</string>
|
||||
<string name="markets_pulse_common_title">マーケット動向</string>
|
||||
<string name="markets_quick_actions">クイックアクション</string>
|
||||
<string name="markets_search_header_title">マーケットから探す</string>
|
||||
<string name="markets_search_header_title">トークンを探す</string>
|
||||
<string name="markets_search_result_title">結果</string>
|
||||
<string name="markets_search_see_tokens_under_100k">時価総額10万ドル以下のトークンを見る</string>
|
||||
<string name="markets_search_show_tokens">トークンを表示</string>
|
||||
|
|
@ -741,8 +743,8 @@
|
|||
<string name="markets_sort_by_experienced_buyers_title">経験豊富な買い手</string>
|
||||
<string name="markets_sort_by_rating_title">時価総額</string>
|
||||
<string name="markets_sort_by_title">並べ替え</string>
|
||||
<string name="markets_sort_by_top_gainers_title">上昇率上位</string>
|
||||
<string name="markets_sort_by_top_losers_title">下落率上位</string>
|
||||
<string name="markets_sort_by_top_gainers_title">値上がり</string>
|
||||
<string name="markets_sort_by_top_losers_title">値下がり</string>
|
||||
<string name="markets_sort_by_trending_title">トレンド</string>
|
||||
<string name="markets_sort_by_yield_mode_title">利息モード</string>
|
||||
<string name="markets_staking_banner_description_placeholder">ステーキングは暗号資産で報酬を受け取る最も簡単な方法です。 %s</string>
|
||||
|
|
@ -770,7 +772,7 @@
|
|||
<string name="markets_token_details_exchanges_title">取引所</string>
|
||||
<string name="markets_token_details_experienced_buyers">経験豊富な買い手</string>
|
||||
<string name="markets_token_details_experienced_buyers_description">少なくとも100の発信取引を持つネット・バイヤー</string>
|
||||
<string name="markets_token_details_experienced_buyers_full">経験豊富な買い手</string>
|
||||
<string name="markets_token_details_experienced_buyers_full">アクティブ投資家数</string>
|
||||
<string name="markets_token_details_fully_diluted_valuation">完全希薄化後評価額</string>
|
||||
<string name="markets_token_details_fully_diluted_valuation_description">現在流通していないものも含め、存在する可能性のあるすべてのコインが流通している場合の暗号資産の理論上の合計価値</string>
|
||||
<string name="markets_token_details_fully_diluted_valuation_full">完全希薄化後評価額</string>
|
||||
|
|
@ -895,7 +897,7 @@
|
|||
<string name="notification_visa_waitlist_promo_text">事前申し込み受付中。他にない特別なカードを、いち早く体験しよう。</string>
|
||||
<string name="notification_visa_waitlist_promo_title">Tangem Visaカード</string>
|
||||
<string name="notification_yield_promo_button">利用規約</string>
|
||||
<string name="notification_yield_promo_text">100ドル以上を入金し、30日間保持すると、10ドルを受け取れます。</string>
|
||||
<string name="notification_yield_promo_text">$100以上を入金して30日間保有すると、$10を受け取れます</string>
|
||||
<string name="notification_yield_promo_title">Yield Mode キャンペーンに参加しよう!</string>
|
||||
<string name="onboarding_access_code_feature_1_description">すべてのデバイスを保護するには、単一のアクセスコードを設定してください。</string>
|
||||
<string name="onboarding_access_code_feature_1_title">保護する</string>
|
||||
|
|
@ -1011,7 +1013,7 @@
|
|||
<string name="onramp_max_amount_restriction">買付金額は%s以下にしてください</string>
|
||||
<string name="onramp_min_amount_restriction">買付金額は少なくとも%sである必要があります</string>
|
||||
<string name="onramp_no_available_providers">この通貨で利用可能なプロバイダーはありません</string>
|
||||
<string name="onramp_offer_type_fastet">最速</string>
|
||||
<string name="onramp_offer_type_fastet">最短処理</string>
|
||||
<string name="onramp_pay_with">支払う</string>
|
||||
<string name="onramp_payment_method_subtitle">支払方法</string>
|
||||
<string name="onramp_provider_max_amount">最大 %s まで使用可能</string>
|
||||
|
|
@ -1598,7 +1600,7 @@
|
|||
<string name="token_swap_changelly_promotion_message">このトークンは、2月%2$s-%3$s の間、%1$s のサービス手数料で別のトークンと交換できます。</string>
|
||||
<string name="token_swap_changelly_promotion_title">Changellyでスワップ、手数料%s</string>
|
||||
<string name="token_swap_promotion_button">今すぐスワップ</string>
|
||||
<string name="tokens_list_hot_crypto_header">ホットな暗号資産🔥</string>
|
||||
<string name="tokens_list_hot_crypto_header">市場トレンド🔥</string>
|
||||
<string name="tokens_list_unavailable_to_purchase_header">買付できません</string>
|
||||
<string name="tokens_list_unavailable_to_sell_header">売却できません</string>
|
||||
<string name="tokens_list_unavailable_to_swap_header">%sからのスワップは利用できません</string>
|
||||
|
|
@ -1825,7 +1827,18 @@
|
|||
<string name="warning_button_ok">はい、わかりました!</string>
|
||||
<string name="warning_button_really_cool">すごくクールだ!</string>
|
||||
<string name="warning_button_refresh">リフレッシュ</string>
|
||||
<string name="warning_clore_migration_button">移行を開始</string>
|
||||
<string name="warning_clore_migration_copy_button">コピー</string>
|
||||
<string name="warning_clore_migration_description">資金へのアクセスを維持するため、Cloreの公式ガイドラインに従って移行を開始してください。</string>
|
||||
<string name="warning_clore_migration_error_signing_not_supported">このネットワークではメッセージ署名はサポートされていません</string>
|
||||
<string name="warning_clore_migration_error_wallet_manager_not_found">メッセージに署名できません。もう一度お試しください。</string>
|
||||
<string name="warning_clore_migration_message">Cloreの公式ドキュメントによると、12月21日以前に受け取ったすべてのコインは Clore(ERC-20トークン)へ移行されますが、同日以降に受け取ったコインは移行されません。送金(移行)ソリューションは現在準備中です。続報をお待ちください。</string>
|
||||
<string name="warning_clore_migration_message_label">メッセージ</string>
|
||||
<string name="warning_clore_migration_open_portal_button">Claim Portalを開く</string>
|
||||
<string name="warning_clore_migration_sheet_description">Cloreトークンを引き続き使用するには、Claim Portalの案内に従ってトークン移行を完了する必要があります。</string>
|
||||
<string name="warning_clore_migration_sheet_title">Cloreネットワーク移行</string>
|
||||
<string name="warning_clore_migration_sign_button">署名する</string>
|
||||
<string name="warning_clore_migration_signature_label">署名</string>
|
||||
<string name="warning_clore_migration_title">Cloreネットワークの移行</string>
|
||||
<string name="warning_demo_mode_message">現在デモモードです</string>
|
||||
<string name="warning_demo_mode_title">デモモードが有効になっています</string>
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@
|
|||
<string name="address_qr_code_message_format">Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств.</string>
|
||||
<string name="address_type_default">По умолчанию</string>
|
||||
<string name="address_type_legacy">Устаревший</string>
|
||||
<string name="alert_authentication_error_message">Произошла ошибка при работе с биометрией. Пожалуйста, попробуйте обновить биометрию на вашем устройстве или обратитесь в службу поддержки.</string>
|
||||
<string name="alert_authentication_error_title">Ошибка аутентификации</string>
|
||||
<string name="alert_button_how_to_scan">Как сканировать</string>
|
||||
<string name="alert_button_request_support">Обратиться в поддержку</string>
|
||||
<string name="alert_button_try_again">Попробовать снова</string>
|
||||
|
|
@ -308,6 +310,7 @@
|
|||
<string name="common_go_to_token">Перейти в токен</string>
|
||||
<string name="common_got_it">Понятно</string>
|
||||
<string name="common_hide">Скрыть</string>
|
||||
<string name="common_hold_to">Удерживайте, чтобы %s</string>
|
||||
<string name="common_hour">час</string>
|
||||
<string name="common_import">Импортировать</string>
|
||||
<string name="common_in_progress">В процессе</string>
|
||||
|
|
@ -385,6 +388,7 @@
|
|||
<string name="common_swap">Обменять</string>
|
||||
<string name="common_tangem">Tangem</string>
|
||||
<string name="common_tangem_wallet">Tangem Wallet</string>
|
||||
<string name="common_tap_and_hold_hint">Нажмите и удерживайте</string>
|
||||
<string name="common_terms_and_conditions">условия участия</string>
|
||||
<string name="common_terms_of_use">Условиями использования</string>
|
||||
<string name="common_to">На</string>
|
||||
|
|
@ -476,6 +480,14 @@
|
|||
<string name="domain_receive_assets_onboarding_description">Отправка средств в другой сети может повлечь потерю средств.</string>
|
||||
<string name="domain_receive_assets_onboarding_network_name">%s сеть</string>
|
||||
<string name="domain_receive_assets_onboarding_title">Отправляйте средства, используя только</string>
|
||||
<string name="earn_best_opportunities">Лучшие возможности</string>
|
||||
<string name="earn_clear_filter">Очистить фильтр</string>
|
||||
<string name="earn_filter_all_networks">Все сети</string>
|
||||
<string name="earn_filter_all_types">Все типы</string>
|
||||
<string name="earn_filter_by">Фильтровать по</string>
|
||||
<string name="earn_filter_networks">Сети</string>
|
||||
<string name="earn_mostly_used">Часто используемые</string>
|
||||
<string name="earn_no_results">Нет результата</string>
|
||||
<string name="email_preface_wc_error">Привет, команда поддержки, у меня возникла ошибка с кодом: %s</string>
|
||||
<string name="email_subject_wc_error">Ошибка WalletConnect</string>
|
||||
<string name="error_wrong_wallet_tapped">Вы использовали карту или кольцо от другого кошелька. Приложите карту или кольцо, связанную с этим кошельком.</string>
|
||||
|
|
@ -850,7 +862,7 @@
|
|||
<item quantity="other">%dмин назад</item>
|
||||
</plurals>
|
||||
<string name="news_quick_recap">Резюме</string>
|
||||
<string name="news_related_news">Связанные новости</string>
|
||||
<string name="news_related_news">Новости</string>
|
||||
<string name="news_related_tokens">Связанные токены</string>
|
||||
<string name="news_sources">Связанные новости</string>
|
||||
<string name="news_stay_in_the_loop">Будьте в курсе</string>
|
||||
|
|
@ -1805,6 +1817,7 @@
|
|||
<string name="warning_button_refresh">Обновить</string>
|
||||
<string name="warning_clore_migration_button">Начать миграцию</string>
|
||||
<string name="warning_clore_migration_copy_button">Копировать</string>
|
||||
<string name="warning_clore_migration_description">Чтобы сохранить доступ к своим средствам, начните миграцию в соответствии с официальными рекомендациями Clore.</string>
|
||||
<string name="warning_clore_migration_error_signing_not_supported">Подписание сообщений не поддерживается в этой сети</string>
|
||||
<string name="warning_clore_migration_error_wallet_manager_not_found">Невозможно подписать сообщение. Пожалуйста, попробуй позже.</string>
|
||||
<string name="warning_clore_migration_message">Согласно официальной документации Clore, все монеты, полученные до 21 декабря, будут мигрированы в токен Clore (ERC-20); монеты, полученные после этой даты, — нет. Решение для перевода находится в разработке — следите за обновлениями.</string>
|
||||
|
|
|
|||
|
|
@ -86,6 +86,8 @@
|
|||
<string name="address_qr_code_message_format">Надсилайте тільки %1$s (%2$s) в мережі %3$s на цю адресу. Використання іншої мережі може призвести до втрати коштів.</string>
|
||||
<string name="address_type_default">За замовчуванням</string>
|
||||
<string name="address_type_legacy">Застарілий</string>
|
||||
<string name="alert_authentication_error_message">Сталася помилка під час роботи з біометрією. Будь ласка, спробуйте оновити біометрію на вашому пристрої або зверніться до служби підтримки.</string>
|
||||
<string name="alert_authentication_error_title">Помилка автентифікації</string>
|
||||
<string name="alert_button_how_to_scan">Як сканувати</string>
|
||||
<string name="alert_button_request_support">Звернутися в підтримку</string>
|
||||
<string name="alert_button_try_again">Спробуйте ще раз</string>
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@
|
|||
<string name="address_qr_code_message_format">Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.</string>
|
||||
<string name="address_type_default">Default</string>
|
||||
<string name="address_type_legacy">Legacy</string>
|
||||
<string name="alert_authentication_error_message">Try to reset biometrics on your device or contact support</string>
|
||||
<string name="alert_authentication_error_message">Something went wrong with biometrics. Please try resetting biometrics on your device or contact support.</string>
|
||||
<string name="alert_authentication_error_title">Authentication error</string>
|
||||
<string name="alert_button_how_to_scan">How to scan</string>
|
||||
<string name="alert_button_request_support">Request support</string>
|
||||
|
|
@ -468,15 +468,15 @@
|
|||
<string name="domain_receive_assets_onboarding_network_name">%s network</string>
|
||||
<string name="domain_receive_assets_onboarding_title">Send funds using only</string>
|
||||
<string name="earn_best_opportunities">Best opportunities</string>
|
||||
<string name="earn_filter_all_networks">All networks</string>
|
||||
<string name="earn_filter_my_networks">My networks</string>
|
||||
<string name="earn_filter_all_types">All types</string>
|
||||
<string name="earn_filter_networks">Networks</string>
|
||||
<string name="earn_clear_filter">Clear filter</string>
|
||||
<string name="earn_filter_by">Filter by</string>
|
||||
<string name="earn_no_results">No results</string>
|
||||
<string name="earn_empty">The list is temporarily empty as it’s being refreshed. Check back in a moment.</string>
|
||||
<string name="earn_filter_all_networks">All networks</string>
|
||||
<string name="earn_filter_all_types">All types</string>
|
||||
<string name="earn_filter_by">Filter by</string>
|
||||
<string name="earn_filter_my_networks">My networks</string>
|
||||
<string name="earn_filter_networks">Networks</string>
|
||||
<string name="earn_mostly_used">Mostly used</string>
|
||||
<string name="earn_no_results">No results</string>
|
||||
<string name="earn_title">Earn</string>
|
||||
<string name="email_preface_wc_error">Hi support team, I\'ve encountered an error with code: %s</string>
|
||||
<string name="email_subject_wc_error">WalletConnect error</string>
|
||||
|
|
@ -830,6 +830,7 @@
|
|||
<string name="markets_token_details_volume">Volume</string>
|
||||
<string name="markets_tooltip_message">Pull this up or tap the search bar to add tokens directly from the market</string>
|
||||
<string name="markets_tooltip_title">Add tokens</string>
|
||||
<string name="markets_tooltip_v2_title">Add more tokens</string>
|
||||
<string name="markets_yield_supply_banner_description">Power up your assets while supplying them with instant access. %s</string>
|
||||
<string name="markets_yield_supply_banner_title">Activate Yield Mode</string>
|
||||
<string name="mobile_wallet_requires_min_os_warning_body">You must update to %1$s before creating a mobile wallet</string>
|
||||
|
|
@ -845,7 +846,7 @@
|
|||
<item quantity="other">%d minutes ago</item>
|
||||
</plurals>
|
||||
<string name="news_quick_recap">Quick recap</string>
|
||||
<string name="news_related_news">Related News</string>
|
||||
<string name="news_related_news">News</string>
|
||||
<string name="news_related_tokens">Related tokens</string>
|
||||
<string name="news_sources">Related news</string>
|
||||
<string name="news_stay_in_the_loop">Stay in the loop</string>
|
||||
|
|
|
|||
|
|
@ -1,201 +1,284 @@
|
|||
package com.tangem.core.ui.components.pager
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
// six - cause the central indicator has width multiplied twice
|
||||
private const val TOTAL_MAX_INDICATORS = 6
|
||||
private const val SPACER_COUNT_BETWEEN_INDICATORS = 4
|
||||
private const val ANIMATION_DURATION = 300
|
||||
private const val MAX_VISIBLE_DOTS = 5
|
||||
private const val MIN_HIDDEN_FOR_SMALL_DOT = 2
|
||||
private const val MIN_DISTANCE_FOR_SMALL_DOT = 3
|
||||
private const val MIN_DISTANCE_FOR_HINT_DOT = 2
|
||||
|
||||
/**
|
||||
* Horizontal pager indicator
|
||||
*
|
||||
* @param pagerState state of pager
|
||||
* @param indicatorCount counter of visible indicator items
|
||||
*/
|
||||
private val SPACING = 4.dp
|
||||
private val BACKGROUND_SIZE = DpSize(92.dp, 32.dp)
|
||||
|
||||
private val CURRENT_DOT_SIZE = DpSize(16.dp, 8.dp)
|
||||
private val NORMAL_DOT_SIZE = DpSize(8.dp, 8.dp)
|
||||
private val HINT_DOT_SIZE = DpSize(6.dp, 6.dp)
|
||||
private val SMALL_DOT_SIZE = DpSize(4.dp, 4.dp)
|
||||
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
@Composable
|
||||
fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier, indicatorCount: Int = 5) {
|
||||
if (pagerState.pageCount == 0) return
|
||||
fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier) {
|
||||
val totalPages = pagerState.pageCount
|
||||
val currentIndex = pagerState.currentPage
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
if (totalPages == 0) return
|
||||
|
||||
val indicatorColor = TangemTheme.colors.control.key
|
||||
val overlayColor = TangemTheme.colors.overlay.secondary
|
||||
val inactiveIndicatorColor = TangemTheme.colors.text.tertiary
|
||||
|
||||
val inactiveIndicatorColor = remember(indicatorColor) {
|
||||
indicatorColor.copy(alpha = 0.5f)
|
||||
}
|
||||
val density = LocalDensity.current
|
||||
|
||||
val baseIndicatorSize = 8.dp
|
||||
val spacing = 4.dp
|
||||
val (targetLower, targetUpper) = getWindowBounds(totalPages, currentIndex)
|
||||
|
||||
val indicatorState by remember(pagerState, indicatorCount) {
|
||||
derivedStateOf {
|
||||
val count = pagerState.pageCount
|
||||
val current = pagerState.currentPage
|
||||
var displayLower by remember { mutableIntStateOf(targetLower) }
|
||||
var displayUpper by remember { mutableIntStateOf(targetUpper) }
|
||||
var prevTargetLower by remember { mutableIntStateOf(targetLower) }
|
||||
|
||||
val winSize = min(indicatorCount, count)
|
||||
val centerPosition = winSize / 2
|
||||
val slideOffset = remember { Animatable(0f) }
|
||||
var isSliding by remember { mutableStateOf(false) }
|
||||
var slideDirection by remember { mutableIntStateOf(0) }
|
||||
val fadeProgress = remember { Animatable(0f) }
|
||||
var fadeJob by remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
val start = when {
|
||||
count <= winSize -> 0
|
||||
current <= centerPosition -> 0
|
||||
current >= count - centerPosition - 1 -> count - winSize
|
||||
else -> current - centerPosition
|
||||
LaunchedEffect(targetLower) {
|
||||
if (targetLower != prevTargetLower && totalPages > MAX_VISIBLE_DOTS) {
|
||||
fadeJob?.cancel()
|
||||
slideOffset.stop()
|
||||
fadeProgress.stop()
|
||||
|
||||
val dir = if (targetLower > prevTargetLower) 1 else -1
|
||||
val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() }
|
||||
val halfEdge = edgeDotSize / 2
|
||||
|
||||
isSliding = true
|
||||
slideDirection = dir
|
||||
fadeProgress.snapTo(0f)
|
||||
|
||||
if (dir > 0) {
|
||||
displayLower = prevTargetLower
|
||||
displayUpper = targetUpper
|
||||
slideOffset.snapTo(halfEdge)
|
||||
} else {
|
||||
displayLower = targetLower
|
||||
displayUpper = prevTargetLower + MAX_VISIBLE_DOTS
|
||||
slideOffset.snapTo(-halfEdge)
|
||||
}
|
||||
Triple(count, winSize, start)
|
||||
|
||||
prevTargetLower = targetLower
|
||||
|
||||
fadeJob = launch {
|
||||
fadeProgress.animateTo(1f, tween(ANIMATION_DURATION))
|
||||
}
|
||||
slideOffset.animateTo(
|
||||
if (dir > 0) -halfEdge else halfEdge,
|
||||
tween(ANIMATION_DURATION),
|
||||
)
|
||||
|
||||
displayLower = targetLower
|
||||
displayUpper = targetUpper
|
||||
slideOffset.snapTo(0f)
|
||||
isSliding = false
|
||||
slideDirection = 0
|
||||
}
|
||||
}
|
||||
|
||||
val (itemCount, windowSize, windowStart) = indicatorState
|
||||
val currentItem by remember { derivedStateOf { pagerState.currentPage } }
|
||||
|
||||
LaunchedEffect(currentItem, windowStart) {
|
||||
if (itemCount > windowSize) {
|
||||
listState.animateScrollToItem(windowStart.coerceIn(0, itemCount - 1))
|
||||
}
|
||||
}
|
||||
|
||||
val maxContainerWidth = remember(baseIndicatorSize, spacing) {
|
||||
baseIndicatorSize * TOTAL_MAX_INDICATORS + spacing * SPACER_COUNT_BETWEEN_INDICATORS
|
||||
}
|
||||
val visibleIndices = (displayLower until displayUpper).toList()
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(32.dp)
|
||||
.width(maxContainerWidth + 32.dp)
|
||||
.width(BACKGROUND_SIZE.width)
|
||||
.height(BACKGROUND_SIZE.height)
|
||||
.background(
|
||||
color = overlayColor,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
.clip(CircleShape),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
LazyRow(
|
||||
modifier = Modifier.wrapContentWidth(),
|
||||
state = listState,
|
||||
Row(
|
||||
modifier = Modifier.offset {
|
||||
IntOffset(slideOffset.value.roundToInt(), 0)
|
||||
},
|
||||
horizontalArrangement = Arrangement.spacedBy(SPACING),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing),
|
||||
userScrollEnabled = false,
|
||||
) {
|
||||
indicatorItems(
|
||||
itemCount = itemCount,
|
||||
currentItem = currentItem,
|
||||
activeColor = indicatorColor,
|
||||
inActiveColor = inactiveIndicatorColor,
|
||||
baseSize = baseIndicatorSize,
|
||||
windowSize = windowSize,
|
||||
windowStart = windowStart,
|
||||
)
|
||||
visibleIndices.forEach { index ->
|
||||
val dotAlpha = when {
|
||||
!isSliding -> 1f
|
||||
slideDirection > 0 && index == displayLower -> 1f - fadeProgress.value
|
||||
slideDirection > 0 && index == displayUpper - 1 -> fadeProgress.value
|
||||
slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress.value
|
||||
slideDirection < 0 && index == displayLower -> fadeProgress.value
|
||||
else -> 1f
|
||||
}
|
||||
|
||||
key(index) {
|
||||
Dot(
|
||||
index = index,
|
||||
currentIndex = currentIndex,
|
||||
totalPages = totalPages,
|
||||
activeColor = indicatorColor,
|
||||
inactiveColor = inactiveIndicatorColor,
|
||||
modifier = Modifier.graphicsLayer { alpha = dotAlpha },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber", "CyclomaticComplexMethod")
|
||||
private fun calculateIndicatorHeight(position: Int, currentPosition: Int, baseSize: Dp, windowSize: Int): Dp {
|
||||
val distance = abs(position - currentPosition)
|
||||
val mediumSize = 6.dp
|
||||
val smallSize = 4.dp
|
||||
|
||||
if (windowSize < 5) {
|
||||
return when {
|
||||
distance <= 1 -> baseSize
|
||||
distance == 2 -> mediumSize
|
||||
else -> smallSize
|
||||
}
|
||||
private fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair<Int, Int> {
|
||||
if (totalPages <= MAX_VISIBLE_DOTS) {
|
||||
return 0 to totalPages
|
||||
}
|
||||
|
||||
val isEdgeFocus = currentPosition == 0 || currentPosition == windowSize - 1
|
||||
val isNearEdgeFocus = currentPosition == 1 || currentPosition == windowSize - 2
|
||||
return when {
|
||||
isEdgeFocus -> when {
|
||||
distance <= 2 -> baseSize
|
||||
distance == 3 -> mediumSize
|
||||
else -> smallSize
|
||||
}
|
||||
isNearEdgeFocus -> when {
|
||||
distance <= 1 -> baseSize
|
||||
distance == 2 -> mediumSize
|
||||
else -> smallSize
|
||||
}
|
||||
else -> when {
|
||||
distance <= 1 -> baseSize
|
||||
else -> mediumSize
|
||||
}
|
||||
val lowerBound = when {
|
||||
currentIndex <= 1 -> 0
|
||||
currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS
|
||||
else -> currentIndex - 2
|
||||
}
|
||||
val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages)
|
||||
return lowerBound to upperBound
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun LazyListScope.indicatorItems(
|
||||
itemCount: Int,
|
||||
currentItem: Int,
|
||||
activeColor: Color,
|
||||
inActiveColor: Color,
|
||||
baseSize: Dp,
|
||||
windowSize: Int,
|
||||
windowStart: Int,
|
||||
private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize {
|
||||
if (index == currentIndex) {
|
||||
return CURRENT_DOT_SIZE
|
||||
}
|
||||
if (totalPages <= MAX_VISIBLE_DOTS) {
|
||||
return NORMAL_DOT_SIZE
|
||||
}
|
||||
val params = DotSizeParams.create(index, currentIndex, totalPages)
|
||||
return params.calculateSize()
|
||||
}
|
||||
|
||||
private class DotSizeParams private constructor(
|
||||
val posInWindow: Int,
|
||||
val currentPosInWindow: Int,
|
||||
val hiddenLeft: Int,
|
||||
val hiddenRight: Int,
|
||||
val distanceFromCurrent: Int,
|
||||
) {
|
||||
val safeWindowSize = min(windowSize, itemCount)
|
||||
if (safeWindowSize <= 0) return
|
||||
private val lastPos = MAX_VISIBLE_DOTS - 1
|
||||
private val isCentered = currentPosInWindow == 2 && hiddenLeft >= 1 && hiddenRight >= 1
|
||||
|
||||
val windowEnd = windowStart + safeWindowSize
|
||||
val currentPosInWindow = (currentItem - windowStart).coerceIn(0, safeWindowSize - 1)
|
||||
|
||||
items(itemCount) { pageIndex ->
|
||||
val isInWindow = pageIndex in windowStart until windowEnd
|
||||
val positionInWindow = (pageIndex - windowStart).coerceIn(0, safeWindowSize - 1)
|
||||
|
||||
val isSelected = pageIndex == currentItem
|
||||
|
||||
val refinedHeight = if (isInWindow) {
|
||||
calculateIndicatorHeight(
|
||||
position = positionInWindow,
|
||||
currentPosition = currentPosInWindow,
|
||||
baseSize = baseSize,
|
||||
windowSize = safeWindowSize,
|
||||
)
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
val targetWidth = if (isSelected) refinedHeight * 2 else refinedHeight
|
||||
val targetShape = if (isSelected) RoundedCornerShape(16.dp) else CircleShape
|
||||
val animatedWidth by animateDpAsState(targetValue = targetWidth, label = "width")
|
||||
val animatedHeight by animateDpAsState(targetValue = refinedHeight, label = "height")
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = (baseSize - animatedHeight) / 2)
|
||||
.clip(targetShape)
|
||||
.width(animatedWidth)
|
||||
.height(animatedHeight)
|
||||
.background(
|
||||
if (isSelected) activeColor else inActiveColor,
|
||||
targetShape,
|
||||
),
|
||||
)
|
||||
fun calculateSize(): DpSize = when {
|
||||
isCentered -> getCenteredSize()
|
||||
hiddenRight >= 1 -> getRightEdgeSize()
|
||||
hiddenLeft >= 1 -> getLeftEdgeSize()
|
||||
else -> NORMAL_DOT_SIZE
|
||||
}
|
||||
|
||||
private fun getCenteredSize(): DpSize = when (posInWindow) {
|
||||
0, lastPos -> HINT_DOT_SIZE
|
||||
else -> NORMAL_DOT_SIZE
|
||||
}
|
||||
|
||||
private fun getRightEdgeSize(): DpSize {
|
||||
val isLastPos = posInWindow == lastPos
|
||||
val isSecondToLast = posInWindow == lastPos - 1
|
||||
val hasExtraHidden = hiddenRight >= MIN_HIDDEN_FOR_SMALL_DOT
|
||||
val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT
|
||||
val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT
|
||||
|
||||
return when {
|
||||
isLastPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE
|
||||
isLastPos && isModerateDistance -> HINT_DOT_SIZE
|
||||
isSecondToLast && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE
|
||||
else -> NORMAL_DOT_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLeftEdgeSize(): DpSize {
|
||||
val isFirstPos = posInWindow == 0
|
||||
val isSecondPos = posInWindow == 1
|
||||
val hasExtraHidden = hiddenLeft >= MIN_HIDDEN_FOR_SMALL_DOT
|
||||
val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT
|
||||
val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT
|
||||
|
||||
return when {
|
||||
isFirstPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE
|
||||
isFirstPos && isModerateDistance -> HINT_DOT_SIZE
|
||||
isSecondPos && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE
|
||||
else -> NORMAL_DOT_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(index: Int, currentIndex: Int, totalPages: Int): DotSizeParams {
|
||||
val (windowStart, windowEnd) = getWindowBounds(totalPages, currentIndex)
|
||||
val posInWindow = index - windowStart
|
||||
val currentPosInWindow = currentIndex - windowStart
|
||||
return DotSizeParams(
|
||||
posInWindow = posInWindow,
|
||||
currentPosInWindow = currentPosInWindow,
|
||||
hiddenLeft = windowStart,
|
||||
hiddenRight = totalPages - windowEnd,
|
||||
distanceFromCurrent = abs(posInWindow - currentPosInWindow),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Dot(
|
||||
index: Int,
|
||||
currentIndex: Int,
|
||||
totalPages: Int,
|
||||
activeColor: Color,
|
||||
inactiveColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isActive = index == currentIndex
|
||||
val size = getDotSize(index, currentIndex, totalPages)
|
||||
|
||||
val animSpec = tween<Dp>(ANIMATION_DURATION)
|
||||
val colorSpec = tween<Color>(ANIMATION_DURATION)
|
||||
|
||||
val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index")
|
||||
val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index")
|
||||
val animatedColor by animateColorAsState(
|
||||
targetValue = if (isActive) activeColor else inactiveColor,
|
||||
animationSpec = colorSpec,
|
||||
label = "c$index",
|
||||
)
|
||||
|
||||
val shape = RoundedCornerShape(animatedHeight / 2)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(animatedWidth)
|
||||
.height(animatedHeight)
|
||||
.background(animatedColor, shape),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
|
|
@ -203,28 +286,82 @@ private fun LazyListScope.indicatorItems(
|
|||
private fun PagerIndicatorPreview() {
|
||||
TangemThemePreview {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
val pagerState = rememberPagerState(
|
||||
initialPage = 2,
|
||||
pageCount = { 10 },
|
||||
)
|
||||
PagerIndicator(pagerState = pagerState)
|
||||
listOf(0, 1, 2, 3, 4).forEach { page ->
|
||||
PagerIndicator(rememberPagerState(page) { 5 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val pagerState1 = rememberPagerState(
|
||||
initialPage = 0,
|
||||
pageCount = { 3 },
|
||||
)
|
||||
PagerIndicator(pagerState = pagerState1)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicator6ItemsPreview() {
|
||||
TangemThemePreview {
|
||||
Column(
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
listOf(0, 1, 2, 3, 4, 5).forEach { page ->
|
||||
PagerIndicator(rememberPagerState(page) { 6 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val pagerState2 = rememberPagerState(
|
||||
initialPage = 0,
|
||||
pageCount = { 1 },
|
||||
)
|
||||
PagerIndicator(pagerState = pagerState2)
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicator7ItemsPreview() {
|
||||
TangemThemePreview {
|
||||
Column(
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
listOf(0, 1, 2, 3, 4, 5, 6).forEach { page ->
|
||||
PagerIndicator(rememberPagerState(page) { 7 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicator10ItemsPreview() {
|
||||
TangemThemePreview {
|
||||
Column(
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).forEach { page ->
|
||||
PagerIndicator(rememberPagerState(page) { 10 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PagerIndicatorSmallCountsPreview() {
|
||||
TangemThemePreview {
|
||||
Column(
|
||||
Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
PagerIndicator(rememberPagerState(0) { 1 })
|
||||
PagerIndicator(rememberPagerState(1) { 2 })
|
||||
PagerIndicator(rememberPagerState(1) { 3 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -57,6 +57,8 @@ internal class DefaultCustomTokensRepository(
|
|||
Blockchain.Binance,
|
||||
Blockchain.BinanceTestnet,
|
||||
Blockchain.Kaspa,
|
||||
Blockchain.TerraV1,
|
||||
Blockchain.TerraV2,
|
||||
-> true
|
||||
Blockchain.Cardano,
|
||||
Blockchain.Sui,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.data.pay
|
|||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isLocked
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
|
|
@ -36,11 +37,21 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun getPossibleWalletsIds(shouldExcludePaeraCustomers: Boolean): List<UserWalletId> {
|
||||
return getPossibleWalletsForTangemPay().addPaeraCustomersData().mapNotNull {
|
||||
if (!it.isPaeraCustomer || !shouldExcludePaeraCustomers) it.userWallet.walletId else null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getTangemPayAvailability(): Boolean {
|
||||
return onboardingRepository.checkCustomerEligibility()
|
||||
.also { isEligible -> if (!isEligible) reset() }
|
||||
}
|
||||
|
||||
override suspend fun isPaeraCustomerForAnyWallet(): Boolean {
|
||||
return getUserWalletsData().any { it.isPaeraCustomer }
|
||||
}
|
||||
|
||||
private suspend fun getUserWalletsData(): List<UserWalletData> {
|
||||
cachedEligibleWallets?.let { return it }
|
||||
|
||||
|
|
@ -50,9 +61,13 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor(
|
|||
|
||||
coroutineScope {
|
||||
val deferred = async {
|
||||
getPossibleWalletsForTangemPay()
|
||||
.addPaeraCustomersData()
|
||||
.also { cachedEligibleWallets = it }
|
||||
if (!checkTangemPayEligibility()) {
|
||||
emptyList()
|
||||
} else {
|
||||
getPossibleWalletsForTangemPay()
|
||||
.addPaeraCustomersData()
|
||||
.also { cachedEligibleWallets = it }
|
||||
}
|
||||
}
|
||||
eligibleWalletsDeferred = deferred
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory
|
|||
import com.tangem.data.pay.DefaultTangemPayEligibilityManager
|
||||
import com.tangem.data.pay.repository.*
|
||||
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.data.pay.usecase.DefaultGetTangemPayCustomerIdUseCase
|
||||
import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase
|
||||
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
|
|
@ -11,6 +12,7 @@ import com.tangem.domain.pay.repository.*
|
|||
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
|
||||
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
|
||||
import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase
|
||||
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
|
||||
import com.tangem.domain.tangempay.TangemPayWithdrawUseCase
|
||||
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
|
||||
import com.tangem.security.DeviceSecurityInfoProvider
|
||||
|
|
@ -65,6 +67,10 @@ internal interface TangemPayDataModule {
|
|||
@Singleton
|
||||
fun bindTangemPayWithdrawUseCase(impl: DefaultTangemPayWithdrawUseCase): TangemPayWithdrawUseCase
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindGetTangemPayCustomerIdUseCase(impl: DefaultGetTangemPayCustomerIdUseCase): GetTangemPayCustomerIdUseCase
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemPayEligibilityManager(impl: DefaultTangemPayEligibilityManager): TangemPayEligibilityManager
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.data.pay.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultGetTangemPayCustomerIdUseCase @Inject constructor(
|
||||
private val tangemPayOnboardingRepository: OnboardingRepository,
|
||||
) : GetTangemPayCustomerIdUseCase {
|
||||
|
||||
override fun invoke(userWalletId: UserWalletId): Either<UniversalError, String> {
|
||||
val customerId = tangemPayOnboardingRepository.getSavedCustomerInfo(userWalletId)?.customerId
|
||||
return if (customerId.isNullOrEmpty()) {
|
||||
VisaApiError.CustomerIdUnavailable.left()
|
||||
} else {
|
||||
customerId.right()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -69,7 +69,7 @@ internal class WcEthAddNetworkUseCase @AssistedInject constructor(
|
|||
?: return illegalState()
|
||||
// find and add all derivation
|
||||
val networkToAddCAIP10 = networksConverter
|
||||
.allAddressForChain(networkToAddCAIP2.raw, wallet)
|
||||
.allAddressForChain(networkToAddCAIP2.raw, wallet, session.account)
|
||||
.map { address -> CAIP10(networkToAddCAIP2, address).raw }
|
||||
val newNamespaces = namespaces.copy(
|
||||
chains = namespaces.chains.plus(networkToAddCAIP2.raw),
|
||||
|
|
@ -134,7 +134,11 @@ internal class WcEthAddSwitchCommonDelegate @AssistedInject constructor(
|
|||
if (generalNetwork == null) {
|
||||
return HandleMethodError.TangemUnsupportedNetwork(caip2.raw).left()
|
||||
}
|
||||
val addedNetwork = networksConverter.mainOrAnyWalletNetworkForRequest(caip2.raw, wallet)
|
||||
val addedNetwork = networksConverter.mainOrAnyWalletNetworkForRequest(
|
||||
rawChainId = caip2.raw,
|
||||
wallet = wallet,
|
||||
account = context.session.account,
|
||||
)
|
||||
if (addedNetwork == null) {
|
||||
return HandleMethodError.NotAddedNetwork(generalNetwork.name).left()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,12 +39,17 @@ internal class WcEthNetwork(
|
|||
val name = toWcMethodName(request) ?: return error("Unknown method name")
|
||||
val session = sessionsManager.findSessionByTopic(request.topic)
|
||||
?: return HandleMethodError.UnknownSession.left()
|
||||
val account = session.account
|
||||
val wallet = session.wallet
|
||||
val chainId = request.chainId.orEmpty()
|
||||
val method: WcEthMethod = name.toMethod(request)
|
||||
.getOrElse { return error(it.message.orEmpty()) }
|
||||
?: return error("Failed to parse $name")
|
||||
suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet)
|
||||
suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(
|
||||
rawChainId = chainId,
|
||||
wallet = wallet,
|
||||
account = account,
|
||||
)
|
||||
|
||||
val accountAddress = when (method) {
|
||||
is WcEthMethod.MessageSign -> method.account
|
||||
|
|
@ -69,12 +74,13 @@ internal class WcEthNetwork(
|
|||
-> anyExistNetwork()
|
||||
} ?: return error("Failed to find walletNetwork for accountAddress $accountAddress")
|
||||
|
||||
val networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, wallet, account).size
|
||||
val context = WcMethodUseCaseContext(
|
||||
session = session,
|
||||
rawSdkRequest = request,
|
||||
network = walletNetwork,
|
||||
accountAddress = accountAddress,
|
||||
networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, wallet).size,
|
||||
networkDerivationsCount = networkDerivationsCount,
|
||||
)
|
||||
return when (method) {
|
||||
is WcEthMethod.MessageSign -> factories.messageSign.create(context, method)
|
||||
|
|
|
|||
|
|
@ -47,8 +47,9 @@ internal class WcSolanaNetwork(
|
|||
val session = sessionsManager.findSessionByTopic(request.topic)
|
||||
?: return HandleMethodError.UnknownSession.left()
|
||||
val wallet = session.wallet
|
||||
val account = session.account
|
||||
val chainId = request.chainId.orEmpty()
|
||||
suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet)
|
||||
suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet, account)
|
||||
suspend fun anyAddress() = anyExistNetwork()
|
||||
?.let { network -> networksConverter.getAddressForWC(wallet.walletId, network).orEmpty() }
|
||||
.orEmpty()
|
||||
|
|
@ -63,12 +64,13 @@ internal class WcSolanaNetwork(
|
|||
?: anyExistNetwork()
|
||||
?: return error("Failed to find walletNetwork for accountAddress $accountAddress")
|
||||
|
||||
val networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, wallet, account).size
|
||||
val context = WcMethodUseCaseContext(
|
||||
session = session,
|
||||
rawSdkRequest = request,
|
||||
network = walletNetwork,
|
||||
accountAddress = accountAddress,
|
||||
networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, wallet).size,
|
||||
networkDerivationsCount = networkDerivationsCount,
|
||||
)
|
||||
return when (method) {
|
||||
is WcSolanaMethod.SignMessage -> factories.messageSign.create(context, method)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
import com.tangem.data.walletconnect.sign.SignStateConverter.toPreSign
|
||||
import com.tangem.data.walletconnect.sign.SignStateConverter.toResult
|
||||
import com.tangem.data.walletconnect.sign.SignStateConverter.toSigning
|
||||
import com.tangem.domain.models.account.derivationIndex
|
||||
import com.tangem.domain.walletconnect.WcAnalyticEvents
|
||||
import com.tangem.domain.walletconnect.model.WcRequestError
|
||||
import com.tangem.domain.walletconnect.model.WcRequestError.Companion.code
|
||||
|
|
@ -71,6 +72,7 @@ internal class WcSignUseCaseDelegate<MiddleAction, SignModel>(
|
|||
network = context.network,
|
||||
errorCode = error.code(),
|
||||
errorMessage = errorMessage,
|
||||
accountDerivation = context.session.account?.derivationIndex?.value,
|
||||
)
|
||||
analytics.send(event)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,11 @@ internal class WcNetworksConverter @Inject constructor(
|
|||
requestAddress: String,
|
||||
): Network? {
|
||||
val wallet = session.wallet
|
||||
val allCoinNetwork = filterWalletNetworkForRequest(request.chainId.orEmpty(), session.wallet)
|
||||
val allCoinNetwork = filterWalletNetworkForRequest(
|
||||
rawChainId = request.chainId.orEmpty(),
|
||||
wallet = session.wallet,
|
||||
account = session.account,
|
||||
)
|
||||
|
||||
val requestNetwork = allCoinNetwork.find { network ->
|
||||
val address = getAddressForWC(wallet.walletId, network)
|
||||
|
|
@ -56,13 +60,13 @@ internal class WcNetworksConverter @Inject constructor(
|
|||
/**
|
||||
* return network with not custom derivationPath or first custom or any
|
||||
*/
|
||||
suspend fun mainOrAnyWalletNetworkForRequest(rawChainId: String, wallet: UserWallet): Network? {
|
||||
val networks = filterWalletNetworkForRequest(rawChainId, wallet)
|
||||
suspend fun mainOrAnyWalletNetworkForRequest(rawChainId: String, wallet: UserWallet, account: Account?): Network? {
|
||||
val networks = filterWalletNetworkForRequest(rawChainId, wallet, account)
|
||||
return networks.firstOrNull { !isCustomCoin(it) } ?: networks.firstOrNull()
|
||||
}
|
||||
|
||||
suspend fun allAddressForChain(rawChainId: String, wallet: UserWallet): List<String> {
|
||||
return filterWalletNetworkForRequest(rawChainId, wallet)
|
||||
suspend fun allAddressForChain(rawChainId: String, wallet: UserWallet, account: Account?): List<String> {
|
||||
return filterWalletNetworkForRequest(rawChainId, wallet, account)
|
||||
.mapNotNull { getAddressForWC(wallet.walletId, it)?.lowercase() }
|
||||
}
|
||||
|
||||
|
|
@ -80,13 +84,18 @@ internal class WcNetworksConverter @Inject constructor(
|
|||
/**
|
||||
* return all exist derivation networks
|
||||
*/
|
||||
suspend fun filterWalletNetworkForRequest(rawChainId: String, wallet: UserWallet): List<Network> {
|
||||
val walletNetworks = getWalletNetworks(wallet.walletId)
|
||||
suspend fun filterWalletNetworkForRequest(
|
||||
rawChainId: String,
|
||||
wallet: UserWallet,
|
||||
account: Account?,
|
||||
): List<Network> {
|
||||
val portfolioNetworks = account?.let { getAccountNetworks(it.accountId) }
|
||||
?: getWalletNetworks(wallet.walletId)
|
||||
|
||||
val blockchain = namespaceConverters
|
||||
.firstNotNullOfOrNull { it.toBlockchain(rawChainId) } ?: return listOf()
|
||||
|
||||
val allCoinNetwork = walletNetworks.filter { it.rawId == blockchain.id }
|
||||
val allCoinNetwork = portfolioNetworks.filter { it.rawId == blockchain.id }
|
||||
return allCoinNetwork
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -423,6 +423,7 @@ internal class DefaultWalletsRepository(
|
|||
}
|
||||
|
||||
override suspend fun activatePromoCode(
|
||||
userWalletId: UserWalletId,
|
||||
promoCode: String,
|
||||
bitcoinAddress: String,
|
||||
): Either<ActivatePromoCodeError, String> = withContext(dispatchers.io) {
|
||||
|
|
@ -430,6 +431,7 @@ internal class DefaultWalletsRepository(
|
|||
body = PromocodeActivationBody(
|
||||
promoCode = promoCode,
|
||||
address = bitcoinAddress,
|
||||
walletId = userWalletId.stringValue,
|
||||
),
|
||||
).fold(
|
||||
onSuccess = { it.status.right() },
|
||||
|
|
|
|||
|
|
@ -268,6 +268,7 @@ class DefaultWalletsRepositoryTest {
|
|||
@Test
|
||||
fun `GIVEN valid data WHEN activatePromoCode THEN returns Right with status and calls API`() = runTest {
|
||||
// GIVEN
|
||||
val walletId = UserWalletId("1234567890abcdef")
|
||||
val promoCode = "PROMO123"
|
||||
val address = "bc1qexampleaddress"
|
||||
coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Success(
|
||||
|
|
@ -275,7 +276,11 @@ class DefaultWalletsRepositoryTest {
|
|||
)
|
||||
|
||||
// WHEN
|
||||
val result = repository.activatePromoCode(promoCode = promoCode, bitcoinAddress = address)
|
||||
val result = repository.activatePromoCode(
|
||||
userWalletId = walletId,
|
||||
promoCode = promoCode,
|
||||
bitcoinAddress = address
|
||||
)
|
||||
|
||||
// THEN
|
||||
var right: String? = null
|
||||
|
|
@ -294,13 +299,14 @@ class DefaultWalletsRepositoryTest {
|
|||
@Test
|
||||
fun `GIVEN NOT_FOUND error WHEN activatePromoCode THEN returns Left InvalidPromoCode`() = runTest {
|
||||
// GIVEN
|
||||
val walletId = UserWalletId("1234567890abcdef")
|
||||
coEvery { tangemTechApi.activatePromoCode(any()) } returns
|
||||
ApiResponse.Error(
|
||||
HttpException(code = HttpException.Code.NOT_FOUND, message = null, errorBody = null),
|
||||
) as ApiResponse<PromocodeActivationResponse>
|
||||
|
||||
// WHEN
|
||||
val result = repository.activatePromoCode(promoCode = "PROMO", bitcoinAddress = "addr")
|
||||
val result = repository.activatePromoCode(userWalletId = walletId, promoCode = "PROMO", bitcoinAddress = "addr")
|
||||
|
||||
// THEN
|
||||
var error: ActivatePromoCodeError? = null
|
||||
|
|
@ -311,13 +317,14 @@ class DefaultWalletsRepositoryTest {
|
|||
@Test
|
||||
fun `GIVEN CONFLICT error WHEN activatePromoCode THEN returns Left PromocodeAlreadyUsed`() = runTest {
|
||||
// GIVEN
|
||||
val walletId = UserWalletId("1234567890abcdef")
|
||||
coEvery { tangemTechApi.activatePromoCode(any()) } returns
|
||||
ApiResponse.Error(
|
||||
HttpException(code = HttpException.Code.CONFLICT, message = null, errorBody = null),
|
||||
) as ApiResponse<PromocodeActivationResponse>
|
||||
|
||||
// WHEN
|
||||
val result = repository.activatePromoCode(promoCode = "PROMO", bitcoinAddress = "addr")
|
||||
val result = repository.activatePromoCode(userWalletId = walletId, promoCode = "PROMO", bitcoinAddress = "addr")
|
||||
|
||||
// THEN
|
||||
var error: ActivatePromoCodeError? = null
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.domain.card.analytics
|
|||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.getReferralParams
|
||||
|
||||
sealed class IntroductionProcess(
|
||||
event: String,
|
||||
|
|
@ -13,7 +14,14 @@ sealed class IntroductionProcess(
|
|||
class ButtonBuyCards : IntroductionProcess("Button - Buy Cards")
|
||||
class ButtonScanCardLegacy : IntroductionProcess("Button - Scan Card")
|
||||
|
||||
class CreateWalletIntroScreenOpened : IntroductionProcess("Create Wallet Intro Screen Opened")
|
||||
class CreateWalletIntroScreenOpened(
|
||||
referralId: String?,
|
||||
) : IntroductionProcess(
|
||||
event = "Create Wallet Intro Screen Opened",
|
||||
params = buildMap {
|
||||
putAll(getReferralParams(referralId))
|
||||
},
|
||||
)
|
||||
|
||||
class ButtonScanCard(
|
||||
val source: AnalyticsParam.ScreensSources,
|
||||
|
|
|
|||
|
|
@ -59,32 +59,55 @@ sealed interface FeedbackEmailType {
|
|||
override val walletMetaInfo: WalletMetaInfo? = null
|
||||
}
|
||||
|
||||
data object BiometricsAuthenticationFailed : FeedbackEmailType {
|
||||
override val walletMetaInfo: WalletMetaInfo? = null
|
||||
}
|
||||
|
||||
sealed class Visa : FeedbackEmailType {
|
||||
abstract val customerId: String
|
||||
|
||||
data class DirectUserRequest(override val walletMetaInfo: WalletMetaInfo) : Visa()
|
||||
data class DirectUserRequest(
|
||||
override val walletMetaInfo: WalletMetaInfo,
|
||||
override val customerId: String,
|
||||
) : Visa()
|
||||
|
||||
data class Activation(override val walletMetaInfo: WalletMetaInfo) : Visa()
|
||||
data class Activation(
|
||||
override val walletMetaInfo: WalletMetaInfo,
|
||||
override val customerId: String,
|
||||
) : Visa()
|
||||
|
||||
data class Dispute(
|
||||
val visaTxDetails: VisaTxDetails,
|
||||
override val walletMetaInfo: WalletMetaInfo,
|
||||
override val customerId: String,
|
||||
) : Visa()
|
||||
|
||||
data class FailedIssueCard(override val walletMetaInfo: WalletMetaInfo) : Visa()
|
||||
data class FailedIssueCard(
|
||||
override val walletMetaInfo: WalletMetaInfo,
|
||||
override val customerId: String,
|
||||
) : Visa()
|
||||
|
||||
data class DisputeV2(
|
||||
val item: TangemPayTxHistoryItem,
|
||||
override val walletMetaInfo: WalletMetaInfo,
|
||||
override val customerId: String,
|
||||
) : Visa()
|
||||
|
||||
data class Withdrawal(
|
||||
override val walletMetaInfo: WalletMetaInfo,
|
||||
override val customerId: String,
|
||||
val providerName: String,
|
||||
val txId: String,
|
||||
) : Visa()
|
||||
|
||||
data class FeatureIsBeta(override val walletMetaInfo: WalletMetaInfo) : Visa()
|
||||
data class FeatureIsBeta(
|
||||
override val walletMetaInfo: WalletMetaInfo,
|
||||
override val customerId: String,
|
||||
) : Visa()
|
||||
|
||||
data class KycRejected(override val walletMetaInfo: WalletMetaInfo, val customerId: String) : Visa()
|
||||
data class KycRejected(
|
||||
override val walletMetaInfo: WalletMetaInfo,
|
||||
override val customerId: String,
|
||||
) : Visa()
|
||||
}
|
||||
}
|
||||
|
|
@ -55,7 +55,7 @@ internal class FeedbackDataBuilder {
|
|||
}
|
||||
|
||||
fun addCustomerId(customerId: String) {
|
||||
builder.appendKeyValue("ID: ", customerId)
|
||||
builder.appendKeyValue("Tangem Pay Customer ID", customerId)
|
||||
}
|
||||
|
||||
fun addUserWalletMetaInfo(walletMetaInfo: WalletMetaInfo) {
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ class SendFeedbackEmailUseCase(
|
|||
is FeedbackEmailType.CurrencyDescriptionError,
|
||||
is FeedbackEmailType.PreActivatedWallet,
|
||||
is FeedbackEmailType.CardAttestationFailed,
|
||||
is FeedbackEmailType.BiometricsAuthenticationFailed,
|
||||
is FeedbackEmailType.Visa.Dispute,
|
||||
is FeedbackEmailType.Visa.DisputeV2,
|
||||
is FeedbackEmailType.Visa.FeatureIsBeta,
|
||||
|
|
|
|||
|
|
@ -31,10 +31,15 @@ class EmailMessageBodyResolver(
|
|||
is FeedbackEmailType.BackupProblem -> addUserRequestBody(type.walletMetaInfo)
|
||||
is FeedbackEmailType.ScanningProblem,
|
||||
is FeedbackEmailType.CardAttestationFailed,
|
||||
is FeedbackEmailType.BiometricsAuthenticationFailed,
|
||||
-> addPhoneInfoBody()
|
||||
is FeedbackEmailType.Visa.Activation -> addUserRequestBody(type.walletMetaInfo)
|
||||
is FeedbackEmailType.Visa.DirectUserRequest -> addUserRequestBody(type.walletMetaInfo)
|
||||
is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody(type.walletMetaInfo, type.visaTxDetails)
|
||||
is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody(
|
||||
walletMetaInfo = type.walletMetaInfo,
|
||||
customerId = type.customerId,
|
||||
visaTxDetails = type.visaTxDetails,
|
||||
)
|
||||
is FeedbackEmailType.Visa.FailedIssueCard -> addTangemPayFailedIssuingCardBody(type)
|
||||
is FeedbackEmailType.Visa.DisputeV2 -> addTangemPayDisputeRequestBody(type)
|
||||
is FeedbackEmailType.Visa.Withdrawal -> addTangemPayWithdrawalRequestBody(type)
|
||||
|
|
@ -48,6 +53,7 @@ class EmailMessageBodyResolver(
|
|||
private fun FeedbackDataBuilder.addTangemPayFailedIssuingCardBody(type: FeedbackEmailType.Visa.FailedIssueCard) {
|
||||
addTangemPayPhoneInfoBody(type)
|
||||
addDelimiter()
|
||||
addCustomerId(customerId = type.customerId)
|
||||
type.walletMetaInfo.userWalletId?.let { userWalletId ->
|
||||
addUserWalletId(userWalletId = userWalletId.stringValue)
|
||||
}
|
||||
|
|
@ -58,6 +64,7 @@ class EmailMessageBodyResolver(
|
|||
addDelimiter()
|
||||
addTangemPayTxInfo(type.item)
|
||||
addDelimiter()
|
||||
addCustomerId(customerId = type.customerId)
|
||||
type.walletMetaInfo.userWalletId?.let { userWalletId ->
|
||||
addUserWalletId(userWalletId = userWalletId.stringValue)
|
||||
}
|
||||
|
|
@ -66,6 +73,7 @@ class EmailMessageBodyResolver(
|
|||
private fun FeedbackDataBuilder.addTangemPayBetaRequestBody(type: FeedbackEmailType.Visa) {
|
||||
addTangemPayPhoneInfoBody(type)
|
||||
addDelimiter()
|
||||
addCustomerId(customerId = type.customerId)
|
||||
type.walletMetaInfo?.userWalletId?.let { userWalletId ->
|
||||
addUserWalletId(userWalletId = userWalletId.stringValue)
|
||||
}
|
||||
|
|
@ -82,6 +90,7 @@ class EmailMessageBodyResolver(
|
|||
) {
|
||||
addUserWalletMetaInfo(type.walletMetaInfo)
|
||||
addDelimiter()
|
||||
addCustomerId(customerId = type.customerId)
|
||||
|
||||
val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" }
|
||||
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
|
||||
|
|
@ -107,9 +116,11 @@ class EmailMessageBodyResolver(
|
|||
private suspend fun FeedbackDataBuilder.addVisaRequestBody(
|
||||
walletMetaInfo: WalletMetaInfo,
|
||||
visaTxDetails: VisaTxDetails,
|
||||
customerId: String,
|
||||
) {
|
||||
addUserRequestBody(walletMetaInfo)
|
||||
addDelimiter()
|
||||
addCustomerId(customerId = customerId)
|
||||
addVisaTxInfo(visaTxDetails)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ internal class EmailMessageTitleResolver(private val resources: Resources) {
|
|||
is FeedbackEmailType.TransactionSendingProblem,
|
||||
is FeedbackEmailType.StakingProblem,
|
||||
is FeedbackEmailType.SwapProblem,
|
||||
is FeedbackEmailType.BiometricsAuthenticationFailed,
|
||||
-> R.string.feedback_preface_tx_failed
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ internal class EmailSubjectResolver(private val resources: Resources) {
|
|||
resources.getStringSafe(R.string.feedback_token_description_error)
|
||||
}
|
||||
FeedbackEmailType.CardAttestationFailed -> "Card attestation failed"
|
||||
FeedbackEmailType.BiometricsAuthenticationFailed -> "Biometrics authentication failed"
|
||||
is FeedbackEmailType.Visa.Activation -> "[Visa] [Activation] {auto-filled subject}"
|
||||
is FeedbackEmailType.Visa.DirectUserRequest -> "[Visa] {auto-filled subject}"
|
||||
is FeedbackEmailType.Visa.FailedIssueCard -> "[Visa] {auto-filled subject}"
|
||||
|
|
|
|||
|
|
@ -185,4 +185,10 @@ sealed interface Account {
|
|||
error("Not yet implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Account.derivationIndex: DerivationIndex?
|
||||
get() = when (this) {
|
||||
is Account.CryptoPortfolio -> derivationIndex
|
||||
is Account.Payment -> TODO("[REDACTED_JIRA]")
|
||||
}
|
||||
|
|
@ -282,9 +282,12 @@ class CreateAndSendGaslessTransactionUseCase(
|
|||
val feeInTokenCurrency = txFee.transactionFee.normal as? Fee.Ethereum.TokenCurrency
|
||||
?: error("Fee must be in token currency")
|
||||
|
||||
val maxTokenFeeAmount = feeInTokenCurrency.amount
|
||||
val maxTokenFee = maxTokenFeeAmount.value?.movePointRight(maxTokenFeeAmount.decimals)?.toBigInteger()
|
||||
?: error("Max token fee amount is null")
|
||||
return GaslessTransactionData.Fee(
|
||||
feeToken = tokenForFee.contractAddress,
|
||||
maxTokenFee = feeInTokenCurrency.gasLimit,
|
||||
maxTokenFee = maxTokenFee,
|
||||
coinPriceInToken = feeInTokenCurrency.coinPriceInToken,
|
||||
feeTransferGasLimit = feeInTokenCurrency.feeTransferGasLimit,
|
||||
baseGas = feeInTokenCurrency.baseGas,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable
|
|||
|
||||
@Serializable
|
||||
data class TangemPayDetailsConfig(
|
||||
val customerId: String,
|
||||
val cardId: String,
|
||||
val isPinSet: Boolean,
|
||||
val cardFrozenState: TangemPayCardFrozenState,
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ sealed class VisaApiError(
|
|||
data object SignWithdrawError : VisaApiError(104004004)
|
||||
data object WithdrawError : VisaApiError(104004005)
|
||||
data object ServerUnavailable : VisaApiError(104004006)
|
||||
data object CustomerIdUnavailable : VisaApiError(104004007)
|
||||
|
||||
companion object {
|
||||
fun fromBackendError(backendErrorCode: Int): VisaApiError {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,21 @@
|
|||
package com.tangem.domain.pay
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface TangemPayEligibilityManager {
|
||||
|
||||
suspend fun getEligibleWallets(shouldExcludePaeraCustomers: Boolean): List<UserWallet>
|
||||
|
||||
/**
|
||||
* Returns all compatible user wallets without checking Tangem Pay eligibility, only used when opening deeplink
|
||||
* Remove after removing [TangemPayOnboardingComponent.Params.Deeplink]
|
||||
* */
|
||||
suspend fun getPossibleWalletsIds(shouldExcludePaeraCustomers: Boolean): List<UserWalletId>
|
||||
|
||||
suspend fun getTangemPayAvailability(): Boolean
|
||||
|
||||
suspend fun isPaeraCustomerForAnyWallet(): Boolean
|
||||
|
||||
fun reset()
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ sealed class MainCustomerInfoContentState {
|
|||
object Loading : MainCustomerInfoContentState()
|
||||
object OnboardingBanner : MainCustomerInfoContentState()
|
||||
data class Content(val info: MainScreenCustomerInfo) : MainCustomerInfoContentState()
|
||||
object Empty : MainCustomerInfoContentState()
|
||||
}
|
||||
|
||||
data class MainScreenCustomerInfo(
|
||||
|
|
|
|||
|
|
@ -68,17 +68,21 @@ class TangemPayMainScreenCustomerInfoUseCase(
|
|||
}
|
||||
|
||||
private suspend fun showOnboardingBannerIfEligible(userWalletId: UserWalletId) {
|
||||
if (eligibilityManager.isPaeraCustomerForAnyWallet()) {
|
||||
updateState(userWalletId, MainCustomerInfoContentState.Empty.right())
|
||||
return
|
||||
}
|
||||
val isEligible = eligibilityManager
|
||||
.getEligibleWallets(shouldExcludePaeraCustomers = false)
|
||||
.any { it.walletId == userWalletId }
|
||||
if (isEligible) {
|
||||
if (onboardingRepository.getHideMainOnboardingBanner(userWalletId)) {
|
||||
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
|
||||
updateState(userWalletId, MainCustomerInfoContentState.Empty.right())
|
||||
} else {
|
||||
updateState(userWalletId, MainCustomerInfoContentState.OnboardingBanner.right())
|
||||
}
|
||||
} else {
|
||||
updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left())
|
||||
updateState(userWalletId, MainCustomerInfoContentState.Empty.right())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.domain.tangempay
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.error.UniversalError
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface GetTangemPayCustomerIdUseCase {
|
||||
|
||||
operator fun invoke(userWalletId: UserWalletId): Either<UniversalError, String>
|
||||
}
|
||||
|
|
@ -34,8 +34,17 @@ sealed class WcAnalyticEvents(
|
|||
),
|
||||
)
|
||||
|
||||
class PairButtonConnect : WcAnalyticEvents(
|
||||
class PairButtonConnect(
|
||||
dAppName: String,
|
||||
accountDerivation: Int?,
|
||||
) : WcAnalyticEvents(
|
||||
event = "Button - Connect",
|
||||
params = buildMap {
|
||||
put(AnalyticsParam.DAPP_NAME, dAppName)
|
||||
accountDerivation?.let {
|
||||
put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
class PairRequested(
|
||||
|
|
@ -114,6 +123,7 @@ sealed class WcAnalyticEvents(
|
|||
rawRequest: WcSdkSessionRequest,
|
||||
network: Network,
|
||||
emulationStatus: EmulationStatus?,
|
||||
accountDerivation: Int?,
|
||||
securityStatus: CheckDAppResult,
|
||||
) : WcAnalyticEvents(
|
||||
event = "Signature Request Received",
|
||||
|
|
@ -124,6 +134,7 @@ sealed class WcAnalyticEvents(
|
|||
AnalyticsParam.BLOCKCHAIN to network.name,
|
||||
AnalyticsParam.EMULATION_STATUS to emulationStatus?.status,
|
||||
AnalyticsParam.TYPE to securityStatus.toAnalyticVerificationStatus(),
|
||||
AnalyticsParam.ACCOUNT_DERIVATION to accountDerivation?.toString(),
|
||||
).mapNotNullValues { it.value },
|
||||
) {
|
||||
enum class EmulationStatus(val status: String) {
|
||||
|
|
@ -137,15 +148,19 @@ sealed class WcAnalyticEvents(
|
|||
rawRequest: WcSdkSessionRequest,
|
||||
network: Network,
|
||||
securityStatus: CheckDAppResult,
|
||||
accountDerivation: Int?,
|
||||
) : WcAnalyticEvents(
|
||||
event = "Signature Request Handled",
|
||||
params = mapOf(
|
||||
AnalyticsParam.DAPP_NAME to rawRequest.dAppMetaData.name,
|
||||
AnalyticsParam.DAPP_URL to rawRequest.dAppMetaData.url,
|
||||
AnalyticsParam.METHOD_NAME to rawRequest.request.method,
|
||||
AnalyticsParam.BLOCKCHAIN to network.name,
|
||||
AnalyticsParam.TYPE to securityStatus.toAnalyticVerificationStatus(),
|
||||
),
|
||||
params = buildMap {
|
||||
put(AnalyticsParam.DAPP_NAME, rawRequest.dAppMetaData.name)
|
||||
put(AnalyticsParam.DAPP_URL, rawRequest.dAppMetaData.url)
|
||||
put(AnalyticsParam.METHOD_NAME, rawRequest.request.method)
|
||||
put(AnalyticsParam.BLOCKCHAIN, network.name)
|
||||
put(AnalyticsParam.TYPE, securityStatus.toAnalyticVerificationStatus())
|
||||
accountDerivation?.let {
|
||||
put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString())
|
||||
}
|
||||
},
|
||||
), AppsFlyerIncludedEvent
|
||||
|
||||
class SignatureRequestFailed(
|
||||
|
|
@ -153,16 +168,18 @@ sealed class WcAnalyticEvents(
|
|||
network: Network,
|
||||
errorCode: String,
|
||||
errorMessage: String,
|
||||
accountDerivation: Int?,
|
||||
) : WcAnalyticEvents(
|
||||
event = "Signature Request Failed",
|
||||
params = mapOf(
|
||||
AnalyticsParam.DAPP_NAME to rawRequest.dAppMetaData.name,
|
||||
AnalyticsParam.DAPP_URL to rawRequest.dAppMetaData.url,
|
||||
AnalyticsParam.METHOD_NAME to rawRequest.request.method,
|
||||
AnalyticsParam.BLOCKCHAIN to network.name,
|
||||
AnalyticsParam.ERROR_CODE to errorCode,
|
||||
AnalyticsParam.ERROR_DESCRIPTION to errorMessage,
|
||||
),
|
||||
params = buildMap {
|
||||
put(AnalyticsParam.DAPP_NAME, rawRequest.dAppMetaData.name)
|
||||
put(AnalyticsParam.DAPP_URL, rawRequest.dAppMetaData.url)
|
||||
put(AnalyticsParam.METHOD_NAME, rawRequest.request.method)
|
||||
put(AnalyticsParam.BLOCKCHAIN, network.name)
|
||||
put(AnalyticsParam.ERROR_CODE, errorCode)
|
||||
put(AnalyticsParam.ERROR_DESCRIPTION, errorMessage)
|
||||
accountDerivation?.let { put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString()) }
|
||||
},
|
||||
)
|
||||
|
||||
class SignatureRequestReceivedFailed(
|
||||
|
|
@ -279,23 +296,4 @@ fun CheckDAppResult.toAnalyticVerificationStatus(): String = when (this) {
|
|||
SAFE -> DAppVerificationStatus.Verified
|
||||
UNSAFE -> DAppVerificationStatus.Risky
|
||||
FAILED_TO_VERIFY -> DAppVerificationStatus.Unknown
|
||||
}.status
|
||||
|
||||
sealed class WcAnalyticAccountEvents(
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(category = WC_CATEGORY_ACCOUNT_NAME, event = event, params = params) {
|
||||
|
||||
data class PairButtonConnect(
|
||||
private val accountDerivation: Int,
|
||||
) : WcAnalyticAccountEvents(
|
||||
event = "Button - Connect",
|
||||
params = mapOf(
|
||||
"Account Derivation" to accountDerivation.toString(),
|
||||
),
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val WC_CATEGORY_ACCOUNT_NAME = "WalletConnect / Account"
|
||||
}
|
||||
}
|
||||
}.status
|
||||
|
|
@ -66,5 +66,9 @@ interface WalletsRepository {
|
|||
@Throws
|
||||
suspend fun associateWallets(applicationId: String, wallets: List<UserWallet>)
|
||||
|
||||
suspend fun activatePromoCode(promoCode: String, bitcoinAddress: String): Either<ActivatePromoCodeError, String>
|
||||
suspend fun activatePromoCode(
|
||||
userWalletId: UserWalletId,
|
||||
promoCode: String,
|
||||
bitcoinAddress: String,
|
||||
): Either<ActivatePromoCodeError, String>
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.domain.wallets.usecase
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import javax.inject.Inject
|
||||
|
|
@ -9,6 +10,13 @@ class ActivateBitcoinPromocodeUseCase @Inject constructor(
|
|||
private val walletsRepository: WalletsRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(address: String, promoCode: String): Either<ActivatePromoCodeError, String> =
|
||||
walletsRepository.activatePromoCode(promoCode = promoCode, bitcoinAddress = address)
|
||||
suspend operator fun invoke(
|
||||
userWalletId: UserWalletId,
|
||||
address: String,
|
||||
promoCode: String,
|
||||
): Either<ActivatePromoCodeError, String> = walletsRepository.activatePromoCode(
|
||||
userWalletId = userWalletId,
|
||||
promoCode = promoCode,
|
||||
bitcoinAddress = address,
|
||||
)
|
||||
}
|
||||
|
|
@ -2,14 +2,22 @@ package com.tangem.domain.wallets.usecase
|
|||
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetSavedWalletsCountUseCase(
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
) {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
operator fun invoke(): Flow<List<UserWallet>> {
|
||||
return userWalletsListRepository.userWallets.map { requireNotNull(it) }
|
||||
return flowOf(Unit)
|
||||
.flatMapLatest {
|
||||
userWalletsListRepository.load()
|
||||
userWalletsListRepository.userWallets.map { wallets -> requireNotNull(wallets) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.account.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.features.account.AccountCreateEditComponent
|
||||
|
|
@ -15,20 +16,48 @@ sealed class AccountSettingsAnalyticEvents(
|
|||
event = "Account Settings Screen Opened",
|
||||
)
|
||||
|
||||
class ButtonManageTokens : AccountSettingsAnalyticEvents(
|
||||
class ButtonManageTokens(
|
||||
accountDerivation: Int?,
|
||||
) : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Manage Tokens",
|
||||
params = buildMap {
|
||||
accountDerivation?.let {
|
||||
put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
class ButtonArchiveAccount : AccountSettingsAnalyticEvents(
|
||||
class ButtonArchiveAccount(
|
||||
accountDerivation: Int?,
|
||||
) : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Archive Account",
|
||||
params = buildMap {
|
||||
accountDerivation?.let {
|
||||
put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
class ButtonArchiveAccountConfirmation : AccountSettingsAnalyticEvents(
|
||||
class ButtonArchiveAccountConfirmation(
|
||||
accountDerivation: Int?,
|
||||
) : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Archive Account Confirmation",
|
||||
params = buildMap {
|
||||
accountDerivation?.let {
|
||||
put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
class ButtonCancelAccountArchivation : AccountSettingsAnalyticEvents(
|
||||
class ButtonCancelAccountArchivation(
|
||||
accountDerivation: Int?,
|
||||
) : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Cancel Account Archivation",
|
||||
params = buildMap {
|
||||
accountDerivation?.let {
|
||||
put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
class AccountArchived : AccountSettingsAnalyticEvents(
|
||||
|
|
@ -39,13 +68,21 @@ sealed class AccountSettingsAnalyticEvents(
|
|||
event = "Button - Edit",
|
||||
)
|
||||
|
||||
class AccountEditScreenOpened : AccountSettingsAnalyticEvents(
|
||||
class AccountEditScreenOpened(
|
||||
accountDerivation: Int?,
|
||||
) : AccountSettingsAnalyticEvents(
|
||||
event = "Account Edit Screen Opened",
|
||||
params = buildMap {
|
||||
accountDerivation?.let {
|
||||
put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
class ButtonSave(
|
||||
val name: AccountName,
|
||||
val icon: CryptoPortfolioIcon,
|
||||
accountDerivation: Int?,
|
||||
) : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Save",
|
||||
params = buildMap {
|
||||
|
|
@ -56,13 +93,16 @@ sealed class AccountSettingsAnalyticEvents(
|
|||
put("Name", accountName)
|
||||
put("Color", icon.color.name)
|
||||
put("Icon", icon.value.name)
|
||||
accountDerivation?.let {
|
||||
put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
class ButtonAddNewAccount(
|
||||
val name: AccountName,
|
||||
val icon: CryptoPortfolioIcon,
|
||||
val derivationIndex: Int,
|
||||
accountDerivation: Int?,
|
||||
) : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Add New Account",
|
||||
params = buildMap {
|
||||
|
|
@ -73,18 +113,24 @@ sealed class AccountSettingsAnalyticEvents(
|
|||
put("Name", accountName)
|
||||
put("Color", icon.color.name)
|
||||
put("Icon", icon.value.name)
|
||||
put("Derivation", derivationIndex.toString())
|
||||
accountDerivation?.let {
|
||||
put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
class AccountError(
|
||||
val source: Source,
|
||||
val error: String,
|
||||
accountDerivation: Int?,
|
||||
) : AccountSettingsAnalyticEvents(
|
||||
event = "Account Error",
|
||||
params = buildMap {
|
||||
put("Error", error)
|
||||
put("Source", source.value)
|
||||
accountDerivation?.let {
|
||||
put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString())
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.account.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
||||
sealed class WalletSettingsAccountAnalyticEvents(
|
||||
category: String = "Settings / Wallet Settings",
|
||||
|
|
@ -16,11 +17,23 @@ sealed class WalletSettingsAccountAnalyticEvents(
|
|||
event = "Account Recovered",
|
||||
)
|
||||
|
||||
class ArchivedAccountsScreenOpened : WalletSettingsAccountAnalyticEvents(
|
||||
class ArchivedAccountsScreenOpened(
|
||||
private val accountsCount: Int,
|
||||
) : WalletSettingsAccountAnalyticEvents(
|
||||
event = "Archived Accounts Screen Opened",
|
||||
params = buildMap {
|
||||
put("Accounts Count", accountsCount.toString())
|
||||
},
|
||||
)
|
||||
|
||||
class ButtonRecoverAccount : WalletSettingsAccountAnalyticEvents(
|
||||
class ButtonRecoverAccount(
|
||||
accountDerivation: Int?,
|
||||
) : WalletSettingsAccountAnalyticEvents(
|
||||
event = "Button - Recover Account",
|
||||
params = buildMap {
|
||||
accountDerivation?.let {
|
||||
put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -13,9 +13,9 @@ import com.tangem.core.ui.message.DialogMessage
|
|||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.core.ui.message.ToastMessage
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.models.ArchivedAccount
|
||||
import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase
|
||||
import com.tangem.domain.account.usecase.GetArchivedAccountsUseCase
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.features.account.ArchivedAccountListComponent
|
||||
import com.tangem.features.account.analytics.WalletSettingsAccountAnalyticEvents
|
||||
import com.tangem.features.account.archived.entity.AccountArchivedUM
|
||||
|
|
@ -53,7 +53,6 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
private val getArchivedAccountsJob = JobHolder()
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.ArchivedAccountsScreenOpened())
|
||||
getArchivedAccounts()
|
||||
}
|
||||
|
||||
|
|
@ -69,14 +68,16 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
umBuilder.mapContent(
|
||||
accounts = content,
|
||||
onCloseClick = onCloseClick,
|
||||
onRecoverClick = { recoverCryptoPortfolio(accountId = it.accountId) },
|
||||
onRecoverClick = { recoverCryptoPortfolio(account = it) },
|
||||
)
|
||||
},
|
||||
ifContent = { content ->
|
||||
val event = WalletSettingsAccountAnalyticEvents.ArchivedAccountsScreenOpened(content.size)
|
||||
analyticsEventHandler.send(event)
|
||||
umBuilder.mapContent(
|
||||
accounts = content,
|
||||
onCloseClick = onCloseClick,
|
||||
onRecoverClick = { recoverCryptoPortfolio(accountId = it.accountId) },
|
||||
onRecoverClick = { recoverCryptoPortfolio(account = it) },
|
||||
)
|
||||
},
|
||||
ifError = { error ->
|
||||
|
|
@ -97,13 +98,15 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
.saveIn(getArchivedAccountsJob)
|
||||
}
|
||||
|
||||
private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch {
|
||||
analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.ButtonRecoverAccount())
|
||||
uiState.update { it.toggleProgress(accountId, isLoading = true) }
|
||||
private fun recoverCryptoPortfolio(account: ArchivedAccount) = modelScope.launch {
|
||||
analyticsEventHandler.send(
|
||||
WalletSettingsAccountAnalyticEvents.ButtonRecoverAccount(account.derivationIndex.value),
|
||||
)
|
||||
uiState.update { it.toggleProgress(account.accountId, isLoading = true) }
|
||||
val result = withContext(dispatchers.default) {
|
||||
recoverCryptoPortfolioUseCase(accountId)
|
||||
recoverCryptoPortfolioUseCase(account.accountId)
|
||||
}
|
||||
uiState.update { it.toggleProgress(accountId, isLoading = false) }
|
||||
uiState.update { it.toggleProgress(account.accountId, isLoading = false) }
|
||||
result
|
||||
.onLeft(::handleRecoverError)
|
||||
.onRight {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase
|
|||
import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.account.derivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.account.AccountCreateEditComponent
|
||||
import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents
|
||||
|
|
@ -69,10 +70,13 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
field = MutableStateFlow(value = getInitialState())
|
||||
|
||||
init {
|
||||
if (params is AccountCreateEditComponent.Params.Create) {
|
||||
updateDerivationInfo(userWalletId = params.userWalletId)
|
||||
} else {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountEditScreenOpened())
|
||||
when (params) {
|
||||
is AccountCreateEditComponent.Params.Create -> updateDerivationInfo(userWalletId = params.userWalletId)
|
||||
is AccountCreateEditComponent.Params.Edit -> {
|
||||
val derivationIndex = params.account.derivationIndex?.value
|
||||
val event = AccountSettingsAnalyticEvents.AccountEditScreenOpened(derivationIndex)
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +120,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
val event = AccountSettingsAnalyticEvents.ButtonAddNewAccount(
|
||||
name = name,
|
||||
icon = icon,
|
||||
derivationIndex = derivationIndex.value,
|
||||
accountDerivation = derivationIndex.value,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
|
||||
|
|
@ -130,7 +134,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
uiState.value = uiState.value.toggleProgress(showProgress = false)
|
||||
|
||||
result
|
||||
.onLeft(::handleAddAccountError)
|
||||
.onLeft { error -> handleAddAccountError(error, derivationIndex.value) }
|
||||
.onRight {
|
||||
analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.AccountCreated())
|
||||
showMessage(R.string.account_create_success_message)
|
||||
|
|
@ -138,9 +142,10 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleAddAccountError(error: AddCryptoPortfolioUseCase.Error) {
|
||||
private fun handleAddAccountError(error: AddCryptoPortfolioUseCase.Error, derivationIndex: Int) {
|
||||
val event = AccountSettingsAnalyticEvents.AccountError(
|
||||
source = params.toAnalyticSource(),
|
||||
accountDerivation = derivationIndex,
|
||||
error = when (error) {
|
||||
is AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet -> error.cause.tag
|
||||
is AddCryptoPortfolioUseCase.Error.DataOperationFailed -> error.cause.message.orEmpty()
|
||||
|
|
@ -165,7 +170,8 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
val icon = CryptoPortfolioIconConverter.convertBack(state.account.portfolioIcon)
|
||||
val isNewName = name != params.account.accountName
|
||||
val isNewIcon = icon != params.account.portfolioIcon
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon))
|
||||
val derivationIndex = params.account.derivationIndex?.value
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon, derivationIndex))
|
||||
|
||||
uiState.value = uiState.value.toggleProgress(showProgress = true)
|
||||
val result = updateCryptoPortfolioUseCase(
|
||||
|
|
@ -176,16 +182,17 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
uiState.value = uiState.value.toggleProgress(showProgress = false)
|
||||
|
||||
result
|
||||
.onLeft(::handleEditAccountError)
|
||||
.onLeft { error -> handleEditAccountError(error, params.account.derivationIndex?.value) }
|
||||
.onRight {
|
||||
showMessage(R.string.account_edit_success_message)
|
||||
router.pop()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEditAccountError(error: UpdateCryptoPortfolioUseCase.Error) {
|
||||
private fun handleEditAccountError(error: UpdateCryptoPortfolioUseCase.Error, derivationIndex: Int?) {
|
||||
val event = AccountSettingsAnalyticEvents.AccountError(
|
||||
source = params.toAnalyticSource(),
|
||||
accountDerivation = derivationIndex,
|
||||
error = when (error) {
|
||||
is UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet -> error.cause.tag
|
||||
is UpdateCryptoPortfolioUseCase.Error.DataOperationFailed -> error.cause.message.orEmpty()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase
|
|||
import com.tangem.domain.account.supplier.SingleAccountSupplier
|
||||
import com.tangem.domain.models.PortfolioId
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.derivationIndex
|
||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.account.AccountDetailsComponent
|
||||
|
|
@ -65,15 +66,19 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
|
||||
private fun onManageTokensClick(account: Account) {
|
||||
val route = AppRoute.ManageTokens(
|
||||
source = AppRoute.ManageTokens.Source.SETTINGS,
|
||||
source = AppRoute.ManageTokens.Source.ACCOUNT,
|
||||
portfolioId = PortfolioId(account.accountId),
|
||||
)
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonManageTokens())
|
||||
analyticsEventHandler.send(
|
||||
AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex?.value),
|
||||
)
|
||||
router.push(route)
|
||||
}
|
||||
|
||||
private fun onArchiveAccountClick() {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonArchiveAccount())
|
||||
val accountDerivation = params.account.derivationIndex?.value
|
||||
val event = AccountSettingsAnalyticEvents.ButtonArchiveAccount(accountDerivation)
|
||||
analyticsEventHandler.send(event)
|
||||
confirmArchiveDialog()
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +86,9 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
val secondAction = EventMessageAction(
|
||||
title = resourceReference(R.string.common_cancel),
|
||||
onClick = {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonCancelAccountArchivation())
|
||||
val accountDerivation = params.account.derivationIndex?.value
|
||||
val event = AccountSettingsAnalyticEvents.ButtonCancelAccountArchivation(accountDerivation)
|
||||
analyticsEventHandler.send(event)
|
||||
},
|
||||
)
|
||||
val firstAction = EventMessageAction(
|
||||
|
|
@ -100,7 +107,9 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun archiveCryptoPortfolio() = modelScope.launch {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonArchiveAccountConfirmation())
|
||||
val accountDerivation = params.account.derivationIndex?.value
|
||||
val event = AccountSettingsAnalyticEvents.ButtonArchiveAccountConfirmation(accountDerivation)
|
||||
analyticsEventHandler.send(event)
|
||||
uiState.update { it.toggleProgress(true) }
|
||||
archiveCryptoPortfolioUseCase(accountId)
|
||||
.onLeft { error ->
|
||||
|
|
@ -119,6 +128,7 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
val event = AccountSettingsAnalyticEvents.AccountError(
|
||||
source = AccountSettingsAnalyticEvents.Source.ARCHIVE,
|
||||
error = error.tag,
|
||||
accountDerivation = params.account.derivationIndex?.value,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
val titleRes: Int
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ dependencies {
|
|||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.hotWallet)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.configToggles)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import com.tangem.core.ui.R
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.analytics.IntroductionProcess
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
|
|
@ -63,6 +64,7 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
private val urlOpener: UrlOpener,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<CreateWalletStartComponent.Params>()
|
||||
|
|
@ -130,9 +132,13 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
)
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(
|
||||
event = IntroductionProcess.CreateWalletIntroScreenOpened(),
|
||||
)
|
||||
modelScope.launch {
|
||||
analyticsEventHandler.send(
|
||||
event = IntroductionProcess.CreateWalletIntroScreenOpened(
|
||||
referralId = appsFlyerStore.get()?.refcode,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScanClick() {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.tangem.domain.models.wallet.UserWallet
|
|||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
|
||||
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
|
||||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
|
|
@ -60,6 +61,7 @@ internal class DetailsModel @Inject constructor(
|
|||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val appStateHolder: ReduxStateHolder,
|
||||
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
|
||||
private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -130,14 +132,20 @@ internal class DetailsModel @Inject constructor(
|
|||
?: error("Selected wallet is null")
|
||||
|
||||
val metaInfo = getWalletMetaInfoUseCase(selectedUserWallet.walletId).getOrNull() ?: return@launch
|
||||
val visaCustomerId = getTangemPayCustomerIdUseCase(selectedUserWallet.walletId).getOrNull()
|
||||
|
||||
val feedbackType = when {
|
||||
userWallets.all { it is UserWallet.Cold && it.scanResponse.card.isVisa } ->
|
||||
FeedbackEmailType.Visa.DirectUserRequest(metaInfo)
|
||||
userWallets.all {
|
||||
it is UserWallet.Cold && it.scanResponse.card.isVisa && !visaCustomerId.isNullOrEmpty()
|
||||
} ->
|
||||
FeedbackEmailType.Visa.DirectUserRequest(
|
||||
walletMetaInfo = metaInfo,
|
||||
customerId = requireNotNull(visaCustomerId),
|
||||
)
|
||||
userWallets.all { it !is UserWallet.Cold || it.scanResponse.card.isVisa.not() } ->
|
||||
FeedbackEmailType.DirectUserRequest(metaInfo)
|
||||
else -> {
|
||||
showFeedbackEmailTypeOptionBS(metaInfo)
|
||||
showFeedbackEmailTypeOptionBS(selectedWalletMetaInfo = metaInfo, visaCustomerId = visaCustomerId)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
|
@ -154,16 +162,16 @@ internal class DetailsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun showFeedbackEmailTypeOptionBS(selectedWalletMetaInfo: WalletMetaInfo) {
|
||||
state.update {
|
||||
it.copy(
|
||||
private fun showFeedbackEmailTypeOptionBS(selectedWalletMetaInfo: WalletMetaInfo, visaCustomerId: String?) {
|
||||
state.update { current ->
|
||||
current.copy(
|
||||
selectFeedbackEmailTypeBSConfig = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = {
|
||||
state.update {
|
||||
it.copy(
|
||||
current.copy(
|
||||
selectFeedbackEmailTypeBSConfig =
|
||||
it.selectFeedbackEmailTypeBSConfig.copy(isShown = false),
|
||||
current.selectFeedbackEmailTypeBSConfig.copy(isShown = false),
|
||||
)
|
||||
}
|
||||
},
|
||||
|
|
@ -172,6 +180,7 @@ internal class DetailsModel @Inject constructor(
|
|||
onEmailFeedbackTypeOptionSelected(
|
||||
selectedWalletMetaInfo = selectedWalletMetaInfo,
|
||||
option = option,
|
||||
visaCustomerId = visaCustomerId,
|
||||
)
|
||||
|
||||
state.update {
|
||||
|
|
@ -190,6 +199,7 @@ internal class DetailsModel @Inject constructor(
|
|||
private fun onEmailFeedbackTypeOptionSelected(
|
||||
selectedWalletMetaInfo: WalletMetaInfo,
|
||||
option: SelectEmailFeedbackTypeBS.Option,
|
||||
visaCustomerId: String?,
|
||||
) {
|
||||
modelScope.launch {
|
||||
val feedbackType = when (option) {
|
||||
|
|
@ -207,14 +217,15 @@ internal class DetailsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
SelectEmailFeedbackTypeBS.Option.Visa -> {
|
||||
if (selectedWalletMetaInfo.isVisa == true) {
|
||||
FeedbackEmailType.Visa.DirectUserRequest(selectedWalletMetaInfo)
|
||||
if (selectedWalletMetaInfo.isVisa == true && !visaCustomerId.isNullOrEmpty()) {
|
||||
FeedbackEmailType.Visa.DirectUserRequest(selectedWalletMetaInfo, visaCustomerId)
|
||||
} else {
|
||||
val userWallet = getWalletsUseCase.invokeSync()
|
||||
.firstOrNull { it is UserWallet.Cold && it.scanResponse.card.isVisa }
|
||||
?: return@launch
|
||||
val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch
|
||||
FeedbackEmailType.Visa.DirectUserRequest(metaInfo)
|
||||
val customerId = getTangemPayCustomerIdUseCase(userWallet.walletId).getOrNull() ?: return@launch
|
||||
FeedbackEmailType.Visa.DirectUserRequest(metaInfo, customerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,11 +96,11 @@ internal class AddTokenModel @Inject constructor(
|
|||
return@launch
|
||||
}
|
||||
|
||||
val status = getAccountCurrencyStatusUseCase.invokeSync(
|
||||
val status = getAccountCurrencyStatusUseCase(
|
||||
userWalletId = accountId.userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
network = cryptoCurrency.network,
|
||||
).getOrNull()
|
||||
).firstOrNull()
|
||||
if (status == null) {
|
||||
processError(error = null)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -108,9 +108,9 @@ internal class NewsDetailsModel @Inject constructor(
|
|||
private val stateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
NewsDetailsStateFactory(
|
||||
currentStateProvider = Provider { _state.value },
|
||||
shareManager = shareManager,
|
||||
onStateUpdate = { newState -> _state.update { newState } },
|
||||
onRetryClick = ::onRetryClicked,
|
||||
onShareClick = ::onShareClick,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +149,11 @@ internal class NewsDetailsModel @Inject constructor(
|
|||
urlOpener.openUrl(relatedArticle.url)
|
||||
}
|
||||
|
||||
private fun onShareClick(article: ArticleUM) {
|
||||
shareManager.shareText(article.newsUrl)
|
||||
analyticsEventHandler.send(NewsDetailsAnalyticsEvent.NewsShareButtonClick(article.id))
|
||||
}
|
||||
|
||||
private fun onArticleIndexChanged(newIndex: Int) {
|
||||
stateFactory.updateSelectedArticleIndex(newIndex)
|
||||
val currentArticle = when (state.value.articlesStateUM) {
|
||||
|
|
|
|||
|
|
@ -70,4 +70,20 @@ internal sealed class NewsDetailsAnalyticsEvent(
|
|||
ERROR_MESSAGE to message,
|
||||
),
|
||||
)
|
||||
|
||||
data class NewsShareButtonClick(
|
||||
private val newsId: Int,
|
||||
) : NewsDetailsAnalyticsEvent(
|
||||
event = "News Share Button Clicked",
|
||||
params = mapOf(
|
||||
"News Id" to newsId.toString(),
|
||||
),
|
||||
), OneTimePerSessionEvent {
|
||||
override val oneTimeEventId: String = event
|
||||
override val throttleSeconds: Long = THROTTLE_SECONDS
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val THROTTLE_SECONDS = 10L
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.features.feed.model.news.details.factory
|
||||
|
||||
import com.tangem.core.navigation.share.ShareManager
|
||||
import com.tangem.features.feed.ui.news.details.state.ArticleUM
|
||||
import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM
|
||||
import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM
|
||||
|
|
@ -10,7 +9,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
|
||||
internal class NewsDetailsStateFactory(
|
||||
private val currentStateProvider: Provider<NewsDetailsUM>,
|
||||
private val shareManager: ShareManager,
|
||||
private val onShareClick: (ArticleUM) -> Unit,
|
||||
private val onStateUpdate: (NewsDetailsUM) -> Unit,
|
||||
private val onRetryClick: () -> Unit,
|
||||
) {
|
||||
|
|
@ -23,11 +22,7 @@ internal class NewsDetailsStateFactory(
|
|||
articles = articles.toImmutableList(),
|
||||
articlesStateUM = ArticlesStateUM.Content,
|
||||
selectedArticleIndex = selectedIndex,
|
||||
onShareClick = {
|
||||
currentArticle?.let {
|
||||
shareManager.shareText(it.newsUrl)
|
||||
}
|
||||
},
|
||||
onShareClick = { currentArticle?.let(onShareClick) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -38,11 +33,7 @@ internal class NewsDetailsStateFactory(
|
|||
onStateUpdate(
|
||||
currentState.copy(
|
||||
selectedArticleIndex = newIndex,
|
||||
onShareClick = {
|
||||
currentArticle?.let {
|
||||
shareManager.shareText(it.newsUrl)
|
||||
}
|
||||
},
|
||||
onShareClick = { currentArticle?.let(onShareClick) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ dependencies {
|
|||
implementation(projects.common.routing)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.common)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.core)
|
||||
implementation(projects.domain.card)
|
||||
|
|
@ -37,6 +38,10 @@ dependencies {
|
|||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.feedback)
|
||||
implementation(projects.domain.feedback.models)
|
||||
implementation(projects.domain.referral)
|
||||
|
||||
/** Referral */
|
||||
implementation(projects.features.referral.domain)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
|
|
|
|||
|
|
@ -41,7 +41,9 @@ import com.tangem.features.home.api.HomeComponent
|
|||
import com.tangem.features.home.impl.ui.state.HomeUM
|
||||
import com.tangem.features.home.impl.ui.state.Stories
|
||||
import com.tangem.features.home.impl.ui.state.getRestrictedStories
|
||||
import com.tangem.feature.referral.domain.ShouldShowMobileWalletPromoUseCase
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.Debouncer
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -70,9 +72,12 @@ internal class HomeModel @Inject constructor(
|
|||
private val urlOpener: UrlOpener,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val reduxStateHolder: ReduxStateHolder,
|
||||
private val shouldShowMobileWalletPromoUseCase: ShouldShowMobileWalletPromoUseCase,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
private val debouncer = Debouncer()
|
||||
|
||||
val params = paramsContainer.require<HomeComponent.Params>()
|
||||
|
||||
private val _uiState = MutableStateFlow(
|
||||
|
|
@ -141,7 +146,16 @@ internal class HomeModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onGetStartedClick() {
|
||||
router.push(AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.ColdWallet))
|
||||
debouncer.debounce(modelScope) {
|
||||
modelScope.launch {
|
||||
val mode = if (shouldShowMobileWalletPromoUseCase()) {
|
||||
AppRoute.CreateWalletStart.Mode.HotWallet
|
||||
} else {
|
||||
AppRoute.CreateWalletStart.Mode.ColdWallet
|
||||
}
|
||||
router.push(AppRoute.CreateWalletStart(mode = mode))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scanCard() {
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@ internal class AccessCodeModel @Inject constructor(
|
|||
auth = HotAuth.Password(accessCode.toCharArray()),
|
||||
)
|
||||
|
||||
if (walletsRepository.requireAccessCode().not()) {
|
||||
if (walletsRepository.requireAccessCode().not() && canUseBiometryUseCase()) {
|
||||
updatedHotWalletId = tangemHotSdk.changeAuth(
|
||||
unlockHotWallet = unlockHotWallet,
|
||||
auth = HotAuth.Biometry,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.core.ui.message.bottomSheetMessage
|
||||
import com.tangem.crypto.bip39.Mnemonic
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.common.wallets.error.SaveWalletError
|
||||
import com.tangem.domain.wallets.builder.HotUserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
|
|
@ -41,6 +42,7 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
private val saveUserWalletUseCase: SaveWalletUseCase,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
) : Model() {
|
||||
|
||||
private val params: AddExistingWalletImportComponent.Params = paramsContainer.require()
|
||||
|
|
@ -122,6 +124,7 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
} else {
|
||||
AnalyticsParam.EmptyFull.Full
|
||||
},
|
||||
referralId = appsFlyerStore.get()?.refcode,
|
||||
),
|
||||
)
|
||||
params.callbacks.onWalletImported(userWallet.walletId)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.tangem.core.decompose.model.ParamsContainer
|
|||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.hotwallet.IsHotWalletCreationSupported
|
||||
import com.tangem.domain.wallets.builder.HotUserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
|
|
@ -44,6 +45,7 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
private val isHotWalletCreationSupported: IsHotWalletCreationSupported,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
) : Model() {
|
||||
|
||||
private val params: CreateMobileWalletComponent.Params = paramsContainer.require()
|
||||
|
|
@ -100,6 +102,7 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
creationType = OnboardingAnalyticsEvent.CreateWallet.WalletCreationType.NewSeed,
|
||||
seedPhraseLength = SEED_PHRASE_LENGTH,
|
||||
passPhraseState = AnalyticsParam.EmptyFull.Empty,
|
||||
referralId = appsFlyerStore.get()?.refcode,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
enum class ManageTokensSource(val analyticsName: String) {
|
||||
STORIES(analyticsName = "Stories"),
|
||||
ONBOARDING(analyticsName = "Onboarding"),
|
||||
SETTINGS(analyticsName = "Settings"),
|
||||
SETTINGS(analyticsName = "Wallet Settings"),
|
||||
ACCOUNT(analyticsName = "Account"),
|
||||
SEND_VIA_SWAP(analyticsName = "SendViaSwap"),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ internal sealed class CustomTokenAnalyticsEvent(
|
|||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent(
|
||||
category = "Manage Tokens / Custom",
|
||||
category = "Manage Tokens / Custom Token",
|
||||
event = event,
|
||||
params = params,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
|
||||
private val params: CustomTokenFormComponent.Params = paramsContainer.require()
|
||||
private var createdCurrency: CryptoCurrency? = null
|
||||
private var useCasesFacade: CustomTokenFormUseCasesFacade = customTokenFormUseCasesFacadeFactory.create(params.mode)
|
||||
private val useCasesFacade: CustomTokenFormUseCasesFacade =
|
||||
customTokenFormUseCasesFacadeFactory.create(params.mode.userWalletId)
|
||||
private val customCurrencyValidator = CustomCurrencyValidator(
|
||||
userWalletId = params.mode.userWalletId,
|
||||
useCasesFacade = useCasesFacade,
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ import com.tangem.domain.models.account.AccountId
|
|||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
|
||||
import com.tangem.features.managetokens.component.AddCustomTokenMode
|
||||
import com.tangem.lib.crypto.derivation.AccountNodeRecognizer
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -29,7 +29,7 @@ import timber.log.Timber
|
|||
|
||||
@Suppress("LongParameterList")
|
||||
internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
|
||||
@Assisted private val mode: AddCustomTokenMode,
|
||||
@Assisted private val userWalletId: UserWalletId,
|
||||
private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase,
|
||||
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase,
|
||||
|
|
@ -39,26 +39,24 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
|
|||
private val accountsFeatureToggles: AccountsFeatureToggles,
|
||||
) {
|
||||
|
||||
suspend fun addCryptoCurrenciesUseCase(currency: CryptoCurrency): Either<Throwable, Unit> = when (mode) {
|
||||
is AddCustomTokenMode.Account -> either {
|
||||
val accountId = getAccountId(currency)
|
||||
suspend fun addCryptoCurrenciesUseCase(currency: CryptoCurrency): Either<Throwable, Unit> {
|
||||
return if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
either {
|
||||
val accountId = getAccountId(currency)
|
||||
|
||||
manageCryptoCurrenciesUseCase(accountId = accountId, add = currency).bind()
|
||||
}
|
||||
is AddCustomTokenMode.Wallet -> {
|
||||
addCryptoCurrenciesUseCase.invoke(
|
||||
userWalletId = mode.userWalletId,
|
||||
currency = currency,
|
||||
)
|
||||
manageCryptoCurrenciesUseCase(accountId = accountId, add = currency).bind()
|
||||
}
|
||||
} else {
|
||||
addCryptoCurrenciesUseCase.invoke(userWalletId = userWalletId, currency = currency)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun derivePublicKeysUseCase(currencies: List<CryptoCurrency>): Either<Throwable, Unit> = when (mode) {
|
||||
is AddCustomTokenMode.Account -> Unit.right()
|
||||
is AddCustomTokenMode.Wallet -> derivePublicKeysUseCase.invoke(
|
||||
userWalletId = mode.userWalletId,
|
||||
currencies = currencies,
|
||||
)
|
||||
suspend fun derivePublicKeysUseCase(currencies: List<CryptoCurrency>): Either<Throwable, Unit> {
|
||||
return if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
Unit.right()
|
||||
} else {
|
||||
derivePublicKeysUseCase.invoke(userWalletId = userWalletId, currencies = currencies)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkIsCurrencyNotAddedUseCase(
|
||||
|
|
@ -67,7 +65,7 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
|
|||
contractAddress: String?,
|
||||
): Either<Throwable, Boolean> = if (accountsFeatureToggles.isFeatureEnabled) {
|
||||
getAccountCurrencyStatusUseCase.invokeSync(
|
||||
userWalletId = mode.userWalletId,
|
||||
userWalletId = userWalletId,
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
contractAddress = contractAddress,
|
||||
|
|
@ -76,7 +74,7 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
|
|||
.right()
|
||||
} else {
|
||||
checkIsCurrencyNotAddedUseCase.invoke(
|
||||
userWalletId = mode.userWalletId,
|
||||
userWalletId = userWalletId,
|
||||
networkId = networkId,
|
||||
derivationPath = derivationPath,
|
||||
contractAddress = contractAddress,
|
||||
|
|
@ -85,15 +83,15 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
|
|||
|
||||
private suspend fun Raise<Throwable>.getAccountId(currency: CryptoCurrency): AccountId {
|
||||
val accountList = singleAccountListSupplier.getSyncOrNull(
|
||||
params = SingleAccountListProducer.Params(userWalletId = mode.userWalletId),
|
||||
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
|
||||
)
|
||||
|
||||
ensureNotNull(accountList) {
|
||||
IllegalStateException("Account list not found: ${mode.userWalletId}")
|
||||
IllegalStateException("Account list not found: $userWalletId")
|
||||
}
|
||||
|
||||
if (accountList.activeAccounts == 1) {
|
||||
return AccountId.forMainCryptoPortfolio(userWalletId = mode.userWalletId)
|
||||
return AccountId.forMainCryptoPortfolio(userWalletId = userWalletId)
|
||||
}
|
||||
|
||||
val currencyAccountIndex = currency.getAccountIndex().bind()
|
||||
|
|
@ -104,7 +102,7 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
|
|||
cryptoPortfolioAccount?.derivationIndex?.value == currencyAccountIndex
|
||||
}
|
||||
|
||||
return account?.accountId ?: AccountId.forMainCryptoPortfolio(userWalletId = mode.userWalletId)
|
||||
return account?.accountId ?: AccountId.forMainCryptoPortfolio(userWalletId = userWalletId)
|
||||
}
|
||||
|
||||
private fun CryptoCurrency.getAccountIndex(): Either<Throwable, Int> = either {
|
||||
|
|
@ -140,6 +138,6 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor(
|
|||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(mode: AddCustomTokenMode): CustomTokenFormUseCasesFacade
|
||||
fun create(userWalletId: UserWalletId): CustomTokenFormUseCasesFacade
|
||||
}
|
||||
}
|
||||
|
|
@ -96,11 +96,11 @@ internal class AddTokenModel @Inject constructor(
|
|||
return@launch
|
||||
}
|
||||
|
||||
val status = getAccountCurrencyStatusUseCase.invokeSync(
|
||||
val status = getAccountCurrencyStatusUseCase(
|
||||
userWalletId = accountId.userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
network = cryptoCurrency.network,
|
||||
).getOrNull()
|
||||
).firstOrNull()
|
||||
if (status == null) {
|
||||
processError(error = null)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ 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.core.analytics.models.AppsFlyerIncludedEvent
|
||||
import com.tangem.core.analytics.models.getReferralParams
|
||||
import kotlin.collections.putAll
|
||||
|
||||
sealed class OnboardingEvent(
|
||||
category: String,
|
||||
|
|
@ -25,6 +27,7 @@ sealed class OnboardingEvent(
|
|||
creationType: WalletCreationType = WalletCreationType.PrivateKey,
|
||||
seedPhraseLength: Int? = null,
|
||||
passPhraseState: AnalyticsParam.EmptyFull,
|
||||
referralId: String?,
|
||||
) : CreateWallet(
|
||||
event = "Wallet Created Successfully",
|
||||
params = buildMap {
|
||||
|
|
@ -33,6 +36,7 @@ sealed class OnboardingEvent(
|
|||
if (seedPhraseLength != null) {
|
||||
put("Seed Phrase Length", seedPhraseLength.toString())
|
||||
}
|
||||
putAll(getReferralParams(referralId))
|
||||
},
|
||||
), AppsFlyerIncludedEvent
|
||||
|
||||
|
|
|
|||
|
|
@ -9,13 +9,14 @@ 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.ui.extensions.resourceReference
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
|
|
@ -45,6 +46,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor(
|
|||
private val analyticsHandler: AnalyticsEventHandler,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<MultiWalletChildParams>()
|
||||
|
|
@ -109,6 +111,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor(
|
|||
analyticsHandler.send(
|
||||
event = OnboardingEvent.CreateWallet.WalletCreatedSuccessfully(
|
||||
passPhraseState = AnalyticsParam.EmptyFull.Empty,
|
||||
referralId = appsFlyerStore.get()?.refcode,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.tangem.core.ui.R
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.crypto.bip39.Mnemonic
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
|
|
@ -64,6 +65,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor(
|
|||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<MultiWalletChildParams>()
|
||||
|
|
@ -248,6 +250,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor(
|
|||
} else {
|
||||
AnalyticsParam.EmptyFull.Full
|
||||
},
|
||||
referralId = appsFlyerStore.get()?.refcode,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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.ui.components.artwork.ArtworkUM
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
|
|
@ -33,6 +34,7 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor(
|
|||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<OnboardingNoteCreateWalletComponent.Params>()
|
||||
|
|
@ -68,6 +70,7 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor(
|
|||
analyticsEventHandler.send(
|
||||
event = OnboardingEvent.CreateWallet.WalletCreatedSuccessfully(
|
||||
passPhraseState = AnalyticsParam.EmptyFull.Empty,
|
||||
referralId = appsFlyerStore.get()?.refcode,
|
||||
),
|
||||
)
|
||||
createWalletAndNavigateBackWithDone(scanResponse.copy(card = result.data.card))
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.tangem.domain.card.common.TapWorkarounds.isVisa
|
|||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase
|
||||
import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent
|
||||
import com.tangem.features.onboarding.v2.stepper.impl.ui.OnboardingStepper
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -27,6 +28,7 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor(
|
|||
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val analyticsHandler: AnalyticsEventHandler,
|
||||
private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase,
|
||||
) : OnboardingStepperComponent, AppComponentContext by context {
|
||||
|
||||
override val state = instanceKeeper.getOrCreateSimple { MutableStateFlow(params.initState) }
|
||||
|
|
@ -41,11 +43,13 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor(
|
|||
|
||||
componentScope.launch {
|
||||
val cardInfo = getWalletMetaInfoUseCase(params.scanResponse).getOrNull() ?: return@launch
|
||||
val userWalletId = cardInfo.userWalletId ?: return@launch
|
||||
val visaCustomerId = getTangemPayCustomerIdUseCase(userWalletId).getOrNull()
|
||||
sendFeedbackEmailUseCase(
|
||||
if (params.scanResponse.card.isVisa) {
|
||||
FeedbackEmailType.Visa.Activation(cardInfo)
|
||||
if (params.scanResponse.card.isVisa && !visaCustomerId.isNullOrEmpty()) {
|
||||
FeedbackEmailType.Visa.Activation(walletMetaInfo = cardInfo, customerId = visaCustomerId)
|
||||
} else {
|
||||
FeedbackEmailType.DirectUserRequest(cardInfo)
|
||||
FeedbackEmailType.DirectUserRequest(walletMetaInfo = cardInfo)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ 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.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
||||
import com.tangem.domain.card.common.TwinCardNumber
|
||||
import com.tangem.domain.card.common.getTwinCardNumber
|
||||
|
|
@ -63,6 +64,7 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
private val cardRepository: CardRepository,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<OnboardingTwinComponent.Params>()
|
||||
|
|
@ -174,6 +176,7 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
analyticsEventHandler.send(
|
||||
event = OnboardingEvent.CreateWallet.WalletCreatedSuccessfully(
|
||||
passPhraseState = AnalyticsParam.EmptyFull.Empty,
|
||||
referralId = appsFlyerStore.get()?.refcode,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -75,11 +75,11 @@ internal class OnrampAddTokenModel @Inject constructor(
|
|||
return@launch
|
||||
}
|
||||
|
||||
val status = getAccountCurrencyStatusUseCase.invokeSync(
|
||||
val status = getAccountCurrencyStatusUseCase(
|
||||
userWalletId = accountId.userWalletId,
|
||||
currencyId = cryptoCurrency.id,
|
||||
network = cryptoCurrency.network,
|
||||
).getOrNull()
|
||||
).firstOrNull()
|
||||
if (status == null) {
|
||||
processError(error = null)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ dependencies {
|
|||
|
||||
/** Data modules */
|
||||
implementation(projects.data.common)
|
||||
implementation(deps.androidx.datastore)
|
||||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.common)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.domain.models)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.feature.referral.data
|
||||
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
|
||||
import com.tangem.feature.referral.domain.MobileWalletPromoRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultMobileWalletPromoRepository @Inject constructor(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : MobileWalletPromoRepository {
|
||||
|
||||
override suspend fun shouldShowMobileWalletPromo(): Boolean {
|
||||
return appPreferencesStore.getSyncOrDefault(key = SHOULD_SHOW_MOBILE_WALLET_PROMO_KEY, default = false)
|
||||
}
|
||||
|
||||
override suspend fun setShouldShowMobileWalletPromo(value: Boolean) {
|
||||
appPreferencesStore.editData { preferences ->
|
||||
preferences[SHOULD_SHOW_MOBILE_WALLET_PROMO_KEY] = value
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val SHOULD_SHOW_MOBILE_WALLET_PROMO_KEY = booleanPreferencesKey("should_show_mobile_wallet_promo")
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,13 @@ package com.tangem.feature.referral.di
|
|||
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.feature.referral.converters.ReferralConverter
|
||||
import com.tangem.feature.referral.data.DefaultMobileWalletPromoRepository
|
||||
import com.tangem.feature.referral.data.ExternalReferralRepository
|
||||
import com.tangem.feature.referral.data.ReferralRepositoryImpl
|
||||
import com.tangem.feature.referral.domain.MobileWalletPromoRepository
|
||||
import com.tangem.feature.referral.domain.ReferralRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -53,4 +56,11 @@ class ReferralRepositoryModule {
|
|||
excludedBlockchains = excludedBlockchains,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMobileWalletPromoRepository(appPreferencesStore: AppPreferencesStore): MobileWalletPromoRepository =
|
||||
DefaultMobileWalletPromoRepository(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
)
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ dependencies {
|
|||
/** Domain modules */
|
||||
implementation(projects.domain.account.status)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.common)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.feature.referral.domain
|
||||
|
||||
interface MobileWalletPromoRepository {
|
||||
|
||||
suspend fun shouldShowMobileWalletPromo(): Boolean
|
||||
|
||||
suspend fun setShouldShowMobileWalletPromo(value: Boolean)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.feature.referral.domain
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class SetShouldShowMobileWalletPromoUseCase @Inject constructor(
|
||||
private val mobileWalletPromoRepository: MobileWalletPromoRepository,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(): Either<Throwable, Unit> = Either.catch {
|
||||
val wallets = userWalletsListRepository.userWallets.value
|
||||
if (wallets.isNullOrEmpty()) {
|
||||
mobileWalletPromoRepository.setShouldShowMobileWalletPromo(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.feature.referral.domain
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
class ShouldShowMobileWalletPromoUseCase @Inject constructor(
|
||||
private val mobileWalletPromoRepository: MobileWalletPromoRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(): Boolean {
|
||||
return mobileWalletPromoRepository.shouldShowMobileWalletPromo()
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ sealed class FeeSelectorParams {
|
|||
abstract val feeDisplaySource: FeeDisplaySource
|
||||
abstract val analyticsCategoryName: String
|
||||
abstract val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource
|
||||
abstract val shouldShowOnlySpeedOption: Boolean
|
||||
|
||||
data class FeeSelectorBlockParams(
|
||||
override val state: FeeSelectorUM,
|
||||
|
|
@ -37,6 +38,7 @@ sealed class FeeSelectorParams {
|
|||
override val feeDisplaySource: FeeDisplaySource,
|
||||
override val analyticsCategoryName: String,
|
||||
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
|
||||
override val shouldShowOnlySpeedOption: Boolean = false,
|
||||
val bottomSheetShown: (Boolean) -> Unit = {},
|
||||
) : FeeSelectorParams()
|
||||
|
||||
|
|
@ -53,6 +55,7 @@ sealed class FeeSelectorParams {
|
|||
override val feeDisplaySource: FeeDisplaySource,
|
||||
override val analyticsCategoryName: String,
|
||||
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
|
||||
override val shouldShowOnlySpeedOption: Boolean = false,
|
||||
val callback: FeeSelectorModelCallback,
|
||||
) : FeeSelectorParams()
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
analyticsSendSource = params.analyticsSendSource,
|
||||
userWalletId = params.userWalletId,
|
||||
shouldShowOnlySpeedOption = model.shouldShowOnlySpeedOption,
|
||||
),
|
||||
onDismiss = {
|
||||
model.feeSelectorBottomSheet.dismiss()
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ internal class FeeSelectorBlockModel @Inject constructor(
|
|||
modelScope = modelScope,
|
||||
)
|
||||
|
||||
val shouldShowOnlySpeedOption: Boolean
|
||||
get() = feeSelectorLogic.shouldShowOnlySpeedOption.value
|
||||
val feeSelectorBottomSheet = SlotNavigation<Unit>()
|
||||
val uiState: StateFlow<FeeSelectorUM>
|
||||
field = feeSelectorLogic.uiState
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import dagger.assisted.AssistedFactory
|
|||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -68,6 +69,9 @@ internal class FeeSelectorLogic @AssistedInject constructor(
|
|||
isGaslessFeeSupportedForNetwork(params.feeCryptoCurrencyStatus.currency.network) &&
|
||||
params.cryptoCurrencyStatus.currency is CryptoCurrency.Token
|
||||
|
||||
val shouldShowOnlySpeedOption: StateFlow<Boolean>
|
||||
field = MutableStateFlow(params.shouldShowOnlySpeedOption)
|
||||
|
||||
init {
|
||||
initAppCurrency()
|
||||
subscribeOnFeeReloadTriggerUpdates()
|
||||
|
|
@ -253,12 +257,14 @@ internal class FeeSelectorLogic @AssistedInject constructor(
|
|||
is GetFeeError.GaslessError.NotEnoughFunds -> error.left()
|
||||
is GetFeeError.GaslessError -> {
|
||||
// Something wrong with gasless fee, fallback to basic fee
|
||||
shouldShowOnlySpeedOption.value = true
|
||||
params.onLoadFee().map { LoadedFeeResult.Basic(it) }
|
||||
}
|
||||
else -> error.left()
|
||||
}
|
||||
},
|
||||
ifRight = { fee ->
|
||||
shouldShowOnlySpeedOption.value = false
|
||||
populateExtendedFee(fee)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ internal class FeeSelectorModel @Inject constructor(
|
|||
|
||||
fun getInitialRoute(): FeeSelectorRoute {
|
||||
return when {
|
||||
params.shouldShowOnlySpeedOption -> FeeSelectorRoute.ChooseSpeed
|
||||
feeSelectorLogic.isGaslessEnabled -> FeeSelectorRoute.NetworkFee
|
||||
else -> FeeSelectorRoute.ChooseSpeed
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,4 +8,6 @@ interface SwapAmountUpdateTrigger {
|
|||
suspend fun triggerUpdateAmount(amountValue: String, isEnterInFiatSelected: Boolean)
|
||||
|
||||
suspend fun triggerQuoteReload()
|
||||
|
||||
suspend fun triggerAutoUpdateEnabled(isEnabled: Boolean)
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ interface SwapAmountUpdateListener {
|
|||
val updateAmountTriggerFlow: Flow<Pair<String, Boolean>>
|
||||
|
||||
val reloadQuotesTriggerFlow: Flow<Unit>
|
||||
|
||||
val autoUpdateTriggerFlow: Flow<Boolean>
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -59,6 +61,9 @@ internal class DefaultSwapAmountUpdateTrigger @Inject constructor() :
|
|||
override val reloadQuotesTriggerFlow: Flow<Unit>
|
||||
field = MutableSharedFlow<Unit>()
|
||||
|
||||
override val autoUpdateTriggerFlow: Flow<Boolean>
|
||||
field = MutableSharedFlow<Boolean>()
|
||||
|
||||
override suspend fun triggerUpdateAmount(amountValue: String, isEnterInFiatSelected: Boolean) {
|
||||
updateAmountTriggerFlow.emit(amountValue to isEnterInFiatSelected)
|
||||
}
|
||||
|
|
@ -67,6 +72,10 @@ internal class DefaultSwapAmountUpdateTrigger @Inject constructor() :
|
|||
reloadQuotesTriggerFlow.emit(Unit)
|
||||
}
|
||||
|
||||
override suspend fun triggerAutoUpdateEnabled(isEnabled: Boolean) {
|
||||
autoUpdateTriggerFlow.emit(isEnabled)
|
||||
}
|
||||
|
||||
override suspend fun triggerReduceBy(reduceBy: ReduceByData) {
|
||||
reduceByTriggerFlow.emit(reduceBy)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ import com.tangem.utils.coroutines.SingleTaskScheduler
|
|||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -110,6 +111,8 @@ internal class SwapAmountModel @Inject constructor(
|
|||
|
||||
private var isShowBestRateAnimation: Boolean = false
|
||||
|
||||
private var autoUpdateSubscriberJob: Job? = null
|
||||
|
||||
val uiState: StateFlow<SwapAmountUM>
|
||||
field = MutableStateFlow(params.amountUM)
|
||||
|
||||
|
|
@ -139,10 +142,12 @@ internal class SwapAmountModel @Inject constructor(
|
|||
scope = modelScope,
|
||||
task = loadQuotesTask(),
|
||||
)
|
||||
subscribeOnAutoupdateEnabling()
|
||||
}
|
||||
|
||||
fun onStop() {
|
||||
quoteTaskScheduler.cancelTask()
|
||||
autoUpdateSubscriberJob?.cancel()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
|
@ -482,6 +487,19 @@ internal class SwapAmountModel @Inject constructor(
|
|||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun subscribeOnAutoupdateEnabling() {
|
||||
autoUpdateSubscriberJob = swapAmountUpdateListener.autoUpdateTriggerFlow
|
||||
.distinctUntilChanged()
|
||||
.onEach { isEnabled ->
|
||||
if (isEnabled) {
|
||||
startLoadingQuotesTask(isSilentReload = true)
|
||||
} else {
|
||||
quoteTaskScheduler.cancelTask()
|
||||
}
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun observeChooseSelectToken() {
|
||||
swapChooseTokenNetworkListener.swapChooseTokenNetworkResultFlow
|
||||
.onEach { data ->
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.swap.v2.impl.sendviaswap.analytics
|
|||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FROM
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER
|
||||
import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_BLOCKCHAIN
|
||||
|
|
@ -22,16 +23,18 @@ internal sealed class SendWithSwapAnalyticEvents(
|
|||
val feeType: AnalyticsParam.FeeType,
|
||||
val fromToken: CryptoCurrency,
|
||||
val toToken: CryptoCurrency,
|
||||
val fromDerivationIndex: Int?,
|
||||
) : SendWithSwapAnalyticEvents(
|
||||
event = "Send With Swap In Progress Screen Opened",
|
||||
params = mapOf(
|
||||
PROVIDER to providerName,
|
||||
FEE_TYPE to if (feeType is AnalyticsParam.FeeType.Normal) "Market" else "Fast",
|
||||
SEND_TOKEN to fromToken.symbol,
|
||||
RECEIVE_TOKEN to toToken.symbol,
|
||||
SEND_BLOCKCHAIN to fromToken.network.name,
|
||||
RECEIVE_BLOCKCHAIN to toToken.network.name,
|
||||
),
|
||||
params = buildMap {
|
||||
put(PROVIDER, providerName)
|
||||
put(FEE_TYPE, if (feeType is AnalyticsParam.FeeType.Normal) "Market" else "Fast")
|
||||
put(SEND_TOKEN, fromToken.symbol)
|
||||
put(RECEIVE_TOKEN, toToken.symbol)
|
||||
put(SEND_BLOCKCHAIN, fromToken.network.name)
|
||||
put(RECEIVE_BLOCKCHAIN, toToken.network.name)
|
||||
if (fromDerivationIndex != null) put(ACCOUNT_DERIVATION_FROM, fromDerivationIndex.toString())
|
||||
},
|
||||
), AppsFlyerIncludedEvent
|
||||
|
||||
data class NoticeCanNotSwapToken(
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
|
|||
analyticsCategoryName = params.analyticsCategoryName,
|
||||
analyticsSendSource = params.analyticsSendSource,
|
||||
userWalletId = params.userWallet.walletId,
|
||||
bottomSheetShown = model::onFeeBottomSheetShown,
|
||||
),
|
||||
onResult = model::onFeeResult,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import com.tangem.features.send.v2.api.subcomponents.destination.entity.Destinat
|
|||
import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger
|
||||
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener
|
||||
import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger
|
||||
import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger
|
||||
import com.tangem.features.swap.v2.impl.R
|
||||
import com.tangem.features.swap.v2.impl.amount.SwapAmountReduceTrigger
|
||||
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
|
||||
|
|
@ -90,6 +91,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
private val sendNotificationsUpdateListener: SendNotificationsUpdateListener,
|
||||
private val swapNotificationsUpdateListener: SwapNotificationsUpdateListener,
|
||||
private val swapAmountReduceTrigger: SwapAmountReduceTrigger,
|
||||
private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger,
|
||||
private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger,
|
||||
private val swapAlertFactory: SwapAlertFactory,
|
||||
private val appRouter: AppRouter,
|
||||
|
|
@ -211,6 +213,12 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun onFeeBottomSheetShown(isShown: Boolean) {
|
||||
modelScope.launch {
|
||||
swapAmountUpdateTrigger.triggerAutoUpdateEnabled(isEnabled = !isShown)
|
||||
}
|
||||
}
|
||||
|
||||
fun showEditAmount() {
|
||||
analyticsEventHandler.send(
|
||||
CommonSendAnalyticEvents.ScreenReopened(
|
||||
|
|
@ -448,6 +456,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
val toCurrency = confirmData.toCryptoCurrencyStatus?.currency ?: return
|
||||
val feeSelectorUM = uiState.value.feeSelectorUM as? FeeSelectorUM.Content ?: return
|
||||
val feeType = feeSelectorUM.toAnalyticType()
|
||||
val fromDerivationIndex = confirmData.fromAccount?.derivationIndex?.value
|
||||
|
||||
analyticsEventHandler.send(
|
||||
SendWithSwapAnalyticEvents.TransactionScreenOpened(
|
||||
|
|
@ -455,6 +464,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
|
|||
feeType = feeType,
|
||||
fromToken = fromCurrency,
|
||||
toToken = toCurrency,
|
||||
fromDerivationIndex = fromDerivationIndex,
|
||||
),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
|
|
|
|||
|
|
@ -1648,7 +1648,14 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
IncludeFeeInAmount.Excluded
|
||||
val fee = txFeeSealedState.txFee.fee.amount.value ?: BigDecimal.ZERO
|
||||
getIncludeFeeInAmountForNative(
|
||||
networkId = networkId,
|
||||
amount = amount,
|
||||
reduceBalanceBy = reduceBalanceBy,
|
||||
fromToken = fromToken.currency,
|
||||
feeValue = fee,
|
||||
)
|
||||
}
|
||||
}
|
||||
is TxFeeSealedState.Legacy -> {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ dependencies {
|
|||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.ui)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue