Updated on 2026-08-14

This commit is contained in:
Tangem 2022-11-17 11:30:43 +03:00
commit ac96d5ac14
105 changed files with 1748 additions and 1200 deletions

View file

@ -14,11 +14,20 @@ import com.tangem.domain.common.LogConfig
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.AndroidAssetReader
import com.tangem.tap.common.AssetReader
import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandlerBuilder
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.filters.BasicSignInFilter
import com.tangem.tap.common.analytics.filters.BasicTopUpFilter
import com.tangem.tap.common.analytics.filters.ShopPurchasedEventFilter
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.common.moshi.BigDecimalAdapter
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.common.redux.global.GlobalAction
@ -30,6 +39,7 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.persistence.CardBalanceStateAdapter
import com.tangem.tap.persistence.PreferencesStorage
import com.tangem.wallet.BuildConfig
import com.zendesk.logger.Logger
@ -62,13 +72,14 @@ class TapApplication : Application(), ImageLoaderFactory {
foregroundActivityObserver = ForegroundActivityObserver()
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
initMoshiConverter()
DomainLayer.init()
NetworkConnectivity.createInstance(store, this)
preferencesStorage = PreferencesStorage(this)
walletConnectRepository = WalletConnectRepository(this)
assetReader = AndroidAssetReader(this)
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.defaultMoshi())
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.INSTANCE.moshi)
initConfigManager(configLoader, ::initWithConfigDependency)
initWarningMessagesManager()
@ -80,6 +91,17 @@ class TapApplication : Application(), ImageLoaderFactory {
)
}
private fun initMoshiConverter() {
fun appAdapters(): List<Any> = listOf(
BigDecimalAdapter(),
CardBalanceStateAdapter(),
)
MoshiConverter.reInitInstance(
adapters = appAdapters() + MoshiJsonConverter.getTangemSdkAdapters(),
typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters(),
)
}
override fun newImageLoader(): ImageLoader {
return createCoilImageLoader(
context = this,
@ -102,14 +124,23 @@ class TapApplication : Application(), ImageLoaderFactory {
}
private fun initAnalytics(application: Application, config: Config) {
val globalAnalyticsHandler = GlobalAnalyticsEventHandlerBuilder(
val factory = AnalyticsFactory()
factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder())
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder())
factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
factory.addFilter(ShopPurchasedEventFilter())
factory.addFilter(BasicSignInFilter())
factory.addFilter(BasicTopUpFilter(preferencesStorage.toppedUpWalletStorage))
val buildData = AnalyticsHandlerBuilder.Data(
application = application,
config = config,
isDebug = BuildConfig.DEBUG,
logConfig = LogConfig.analyticsHandlers,
jsonConverter = MoshiJsonConverter.INSTANCE,
).default()
store.dispatch(GlobalAction.SetGlobalAnalyticsHandler(globalAnalyticsHandler))
jsonConverter = MoshiConverter.INSTANCE,
)
factory.build(Analytics, buildData)
}
private fun initFeedbackManager(context: Context, preferencesStorage: PreferencesStorage, config: Config) {

View file

@ -153,7 +153,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
primaryButtonAction = state.dialog.onOk,
)
is WalletDialog.RussianCardholdersWarningDialog ->
RussianCardholdersWarningBottomSheetDialog(context, state.dialog.topUpUrl)
RussianCardholdersWarningBottomSheetDialog(context, state.dialog.data)
else -> null
}
dialog?.show()

View file

@ -0,0 +1,93 @@
package com.tangem.tap.common.analytics
import com.tangem.domain.common.FeatureCoroutineExceptionHandler
import com.tangem.tap.common.analytics.api.AnalyticsEventFilter
import com.tangem.tap.common.analytics.api.AnalyticsEventHandler
import com.tangem.tap.common.analytics.api.AnalyticsFilterHolder
import com.tangem.tap.common.analytics.api.AnalyticsHandler
import com.tangem.tap.common.analytics.api.AnalyticsHandlerHolder
import com.tangem.tap.common.analytics.api.ParamsInterceptor
import com.tangem.tap.common.analytics.api.ParamsInterceptorHolder
import com.tangem.tap.common.analytics.events.AnalyticsEvent
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.launch
import java.util.concurrent.Executors
/**
[REDACTED_AUTHOR]
*/
interface GlobalAnalyticsEventHandler : AnalyticsEventHandler,
AnalyticsHandlerHolder,
AnalyticsFilterHolder,
ParamsInterceptorHolder
object Analytics : GlobalAnalyticsEventHandler {
private val analyticsScope: CoroutineScope by lazy { createScope() }
private val handlers = mutableMapOf<String, AnalyticsHandler>()
private val paramsInterceptors = mutableMapOf<String, ParamsInterceptor>()
private val analyticsFilters = mutableSetOf<AnalyticsEventFilter>()
private val analyticsHandlers: List<AnalyticsHandler>
get() = handlers.values.toList()
override fun addHandler(name: String, handler: AnalyticsHandler) {
handlers[name] = handler
}
override fun removeHandler(name: String): AnalyticsHandler? {
return handlers.remove(name)
}
override fun addFilter(filter: AnalyticsEventFilter) {
analyticsFilters.add(filter)
}
override fun removeFilter(filter: AnalyticsEventFilter): Boolean {
return analyticsFilters.remove(filter)
}
override fun addParamsInterceptor(interceptor: ParamsInterceptor) {
paramsInterceptors[interceptor.id()] = interceptor
}
override fun removeParamsInterceptor(interceptor: ParamsInterceptor): ParamsInterceptor? {
return paramsInterceptors.remove(interceptor.id())
}
override fun send(event: AnalyticsEvent) {
analyticsScope.launch {
event.params = applyParamsInterceptors(event)
val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(event) }
when {
eventFilter == null -> analyticsHandlers.forEach { handler -> handler.send(event) }
eventFilter.canBeSent(event) -> {
analyticsHandlers
.filter { handler -> eventFilter.canBeConsumedByHandler(handler, event) }
.forEach { handler -> handler.send(event) }
}
}
}
}
private fun applyParamsInterceptors(event: AnalyticsEvent): MutableMap<String, String> {
val interceptedParams = event.params.toMutableMap()
paramsInterceptors.values
.filter { it.canBeAppliedTo(event) }
.forEach { it.intercept(interceptedParams) }
return interceptedParams
}
private fun createScope(): CoroutineScope {
val name = "Analytics"
val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
val exHandler = FeatureCoroutineExceptionHandler.create(name)
return CoroutineScope(SupervisorJob() + dispatcher + CoroutineName(name) + exHandler)
}
}

View file

@ -1,71 +0,0 @@
package com.tangem.tap.common.analytics
enum class AnalyticsEventAnOld(val event: String) {
CARD_IS_SCANNED("card_is_scanned"),
TRANSACTION_IS_SENT("transaction_is_sent"),
READY_TO_SCAN("ready_to_scan"),
DEMO_MODE_ACTIVATED("demo_mode_activated"),
APP_RATING_DISPLAYED("rate_app_warning_displayed"),
APP_RATING_DISMISS("dismiss_rate_app_warning"),
APP_RATING_NEGATIVE("negative_rate_app_feedback"),
APP_RATING_POSITIVE("positive_rate_app_feedback"),
WC_SUCCESS_RESPONSE("wallet_connect_success_response"),
WC_INVALID_REQUEST("wallet_connect_invalid_request"),
WC_NEW_SESSION("wallet_connect_new_session"),
WC_SESSION_DISCONNECTED("wallet_connect_session_disconnected"),
GET_CARD("get_card"),
}
enum class AnalyticsParamAnOld(val param: String) {
BLOCKCHAIN("blockchain"),
CARD_ID("cardId"),
BATCH_ID("batch_id"),
FIRMWARE("firmware"),
ACTION("action"),
ERROR_DESCRIPTION("error_description"),
ERROR_CODE("error_code"),
NEW_SECURITY_OPTION("new_security_option"),
ERROR_KEY("Tangem SDK error key"),
WALLET_CONNECT_ACTION("wallet_connect_action"),
WALLET_CONNECT_REQUEST("wallet_connect_request"),
WALLET_CONNECT_DAPP_URL("wallet_connect_dapp_url"),
SOURCE("source"),
}
enum class GetCardSourceParamsAnOld(val param: String) {
WELCOME("welcome"),
ONBOARDING_BUY_MORE("wallet_onboaring_buy_more_cards"),
ONBOARDING("wallet_onboarding")
}
object AnalyticsAnOld {
enum class ActionToLog(val key: String) {
Scan("tap_scan_task"),
SendTransaction("send_transaction"),
WalletConnectSign("wallet_connect_personal_sign"),
WalletConnectTransaction("wallet_connect_tx_sign"),
ReadPinSettings("read_pin_settings"),
ChangeSecOptions("change_sec_options"),
CreateWallet("create_wallet"),
PurgeWallet("purge_wallet"),
WriteIssuerData("write_issuer_data"),
}
enum class WcSessionEvent { Disconnect, Connect }
enum class WcAction { PersonalSign, SignTransaction, SendTransaction }
sealed class WcAnalyticsEvent {
data class Error(val error: Throwable, val action: WcAction?) :
WcAnalyticsEvent()
data class Session(val event: WcSessionEvent, val url: String?) : WcAnalyticsEvent()
data class Action(val action: WcAction) : WcAnalyticsEvent()
data class InvalidRequest(val json: String?) : WcAnalyticsEvent()
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.tap.common.analytics
import com.tangem.tap.common.analytics.api.AnalyticsEventFilter
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.api.ParamsInterceptor
/**
[REDACTED_AUTHOR]
*/
class AnalyticsFactory {
private val builders = mutableListOf<AnalyticsHandlerBuilder>()
private val filters = mutableListOf<AnalyticsEventFilter>()
private val interceptors = mutableListOf<ParamsInterceptor>()
fun addHandlerBuilder(builder: AnalyticsHandlerBuilder) {
builders.add(builder)
}
fun addFilter(filter: AnalyticsEventFilter) {
filters.add(filter)
}
fun addParamsInterceptor(interceptor: ParamsInterceptor) {
interceptors.add(interceptor)
}
fun build(analytics: Analytics, data: AnalyticsHandlerBuilder.Data) {
builders.mapNotNull { it.build(data) }.forEach { analytics.addHandler(it.id(), it) }
filters.forEach { analytics.addFilter(it) }
interceptors.forEach { analytics.addParamsInterceptor(it) }
builders.clear()
filters.clear()
interceptors.clear()
}
}

View file

@ -1,3 +0,0 @@
package com.tangem.tap.common.analytics

View file

@ -1,79 +0,0 @@
package com.tangem.tap.common.analytics
import android.app.Application
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.domain.common.AnalyticsHandlersLogConfig
import com.tangem.tap.common.analytics.api.AnalyticsEventHandler
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeClient
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeLogClient
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerClient
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerLogClient
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseClient
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseLogClient
import com.tangem.tap.domain.configurable.config.Config
/**
[REDACTED_AUTHOR]
*/
class GlobalAnalyticsEventHandlerBuilder(
val application: Application,
val config: Config,
val isDebug: Boolean,
val logConfig: AnalyticsHandlersLogConfig,
val jsonConverter: MoshiJsonConverter,
) {
private val analyticHandlers = mutableListOf<AnalyticsEventHandler>()
fun build(): GlobalAnalyticsEventHandler {
return GlobalAnalyticsHandler(analyticHandlers.toList())
}
fun default(): GlobalAnalyticsEventHandler {
addFirebaseAnalytics()
addAppsFlyer()
addAmplitude()
return GlobalAnalyticsHandler(analyticHandlers.toList())
}
fun addFirebaseAnalytics(): GlobalAnalyticsEventHandlerBuilder {
if (isDebug) {
if (logConfig.firebase) FirebaseLogClient(createLogger("FirebaseAnalyticsHandler")) else null
} else {
FirebaseClient()
}?.let {
analyticHandlers.add(FirebaseAnalyticsHandler(it))
}
return this
}
fun addAppsFlyer(): GlobalAnalyticsEventHandlerBuilder {
if (isDebug) {
if (logConfig.appsFlyer) AppsFlyerLogClient(createLogger("AppsFlyerAnalyticsHandler")) else null
} else {
AppsFlyerClient(application, config.appsFlyerDevKey)
}?.let {
analyticHandlers.add(AppsFlyerAnalyticsHandler(it))
}
return this
}
fun addAmplitude(): GlobalAnalyticsEventHandlerBuilder {
if (isDebug) {
if (logConfig.firebase) AmplitudeLogClient(createLogger("AmplitudeAnalyticsHandler")) else null
} else {
AmplitudeClient(application, config.amplitudeApiKey)
}?.let {
analyticHandlers.add(AmplitudeAnalyticsHandler(it))
}
return this
}
private fun createLogger(name: String): AnalyticsEventsLogger {
return AnalyticsEventsLogger(name, jsonConverter)
}
}

View file

@ -1,161 +0,0 @@
package com.tangem.tap.common.analytics
import com.shopify.buy3.Storefront
import com.tangem.blockchain.common.BlockchainError
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
import com.tangem.tap.common.analytics.api.AnalyticsEventHandler
import com.tangem.tap.common.analytics.api.BlockchainSdkErrorEventHandler
import com.tangem.tap.common.analytics.api.CardSdkErrorEventHandler
import com.tangem.tap.common.analytics.api.ErrorEventHandler
import com.tangem.tap.common.analytics.api.ErrorEventLogger
import com.tangem.tap.common.analytics.api.SdkErrorEventHandler
import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler
import com.tangem.tap.common.analytics.events.AnalyticsEvent
import com.tangem.tap.common.extensions.filterNotNull
interface GlobalAnalyticsEventHandler : AnalyticsEventHandler,
ErrorEventHandler,
SdkErrorEventHandler,
ShopifyOrderEventHandler {
fun attachToAllEvents(key: String, value: String)
companion object {
fun stub(): GlobalAnalyticsEventHandler {
return object : GlobalAnalyticsEventHandler {
override fun attachToAllEvents(key: String, value: String) {}
override fun handleEvent(event: String, params: Map<String, String>) {}
override fun handleErrorEvent(error: Throwable, params: Map<String, String>) {}
override fun handleCardSdkErrorEvent(
error: TangemSdkError,
action: AnalyticsAnOld.ActionToLog,
params: Map<AnalyticsParamAnOld, String>,
card: Card?,
) {
}
override fun handleBlockchainSdkErrorEvent(
error: BlockchainError,
action: AnalyticsAnOld.ActionToLog,
params: Map<AnalyticsParamAnOld, String>,
card: Card?,
) {
}
override fun handleShopifyOrderEvent(order: Storefront.Order) {}
}
}
}
}
class GlobalAnalyticsHandler(
private val analyticsHandlers: List<AnalyticsEventHandler>,
) : GlobalAnalyticsEventHandler {
private val attachToAllEventsParams: MutableMap<String, String> = mutableMapOf()
override fun attachToAllEvents(key: String, value: String) {
attachToAllEventsParams[key] = value
}
override fun handleEvent(event: String, params: Map<String, String>) {
analyticsHandlers.forEach { it.handleEvent(event, params) }
}
override fun handleAnalyticsEvent(
event: AnalyticsEvent,
card: Card?,
blockchain: String?,
) {
analyticsHandlers.forEach { it.handleAnalyticsEvent(event, card, blockchain) }
}
override fun handleAnalyticsEvent(
event: AnalyticsEventAnOld,
params: Map<String, String>,
card: Card?,
blockchain: String?,
) {
analyticsHandlers.forEach { it.handleAnalyticsEvent(event, params, card, blockchain) }
}
override fun handleErrorEvent(error: Throwable, params: Map<String, String>) {
analyticsHandlers.filterIsInstance<ErrorEventLogger>().forEach {
it.logErrorEvent(error, params)
}
}
override fun handleCardSdkErrorEvent(
error: TangemSdkError,
action: AnalyticsAnOld.ActionToLog,
params: Map<AnalyticsParamAnOld, String>,
card: Card?,
) {
analyticsHandlers.filterIsInstance<CardSdkErrorEventHandler>().forEach {
it.handleCardSdkErrorEvent(error, action, params, card)
}
}
override fun handleBlockchainSdkErrorEvent(
error: BlockchainError,
action: AnalyticsAnOld.ActionToLog,
params: Map<AnalyticsParamAnOld, String>,
card: Card?,
) {
analyticsHandlers.filterIsInstance<BlockchainSdkErrorEventHandler>().forEach {
it.handleBlockchainSdkErrorEvent(error, action, params, card)
}
}
override fun handleShopifyOrderEvent(order: Storefront.Order) {
analyticsHandlers.filterIsInstance<ShopifyOrderEventHandler>().forEach {
it.handleShopifyOrderEvent(order)
}
}
override fun prepareParams(card: Card?, blockchain: String?, params: Map<String, String>): Map<String, String> {
return super.prepareParams(card, blockchain, params).toMutableMap().apply {
putAll(attachToAllEventsParams)
}
}
}
fun GlobalAnalyticsEventHandler.logWcEvent(event: AnalyticsAnOld.WcAnalyticsEvent) {
when (event) {
is AnalyticsAnOld.WcAnalyticsEvent.Action -> {
handleAnalyticsEvent(
event = AnalyticsEventAnOld.WC_SUCCESS_RESPONSE,
params = mapOf(
AnalyticsParamAnOld.WALLET_CONNECT_ACTION.param to event.action.name,
),
)
}
is AnalyticsAnOld.WcAnalyticsEvent.Error -> {
val params = mapOf(
AnalyticsParamAnOld.WALLET_CONNECT_ACTION.param to event.action?.name,
AnalyticsParamAnOld.ERROR_DESCRIPTION.param to event.error.message,
).filterNotNull()
handleErrorEvent(event.error, params)
}
is AnalyticsAnOld.WcAnalyticsEvent.InvalidRequest ->
handleAnalyticsEvent(
event = AnalyticsEventAnOld.WC_INVALID_REQUEST,
params = mapOf(
AnalyticsParamAnOld.WALLET_CONNECT_REQUEST.param to event.json,
).filterNotNull(),
)
is AnalyticsAnOld.WcAnalyticsEvent.Session -> {
val analyticsEvent = when (event.event) {
AnalyticsAnOld.WcSessionEvent.Disconnect -> AnalyticsEventAnOld.WC_SESSION_DISCONNECTED
AnalyticsAnOld.WcSessionEvent.Connect -> AnalyticsEventAnOld.WC_NEW_SESSION
}
handleAnalyticsEvent(
event = analyticsEvent,
params = mapOf(
AnalyticsParamAnOld.WALLET_CONNECT_DAPP_URL.param to event.url,
).filterNotNull(),
)
}
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.tap.common.analytics.api
import com.tangem.tap.common.analytics.events.AnalyticsEvent
/**
[REDACTED_AUTHOR]
* The filter is used to separate events to send and determine which handler it can be processed with.
*/
interface AnalyticsEventFilter {
/**
* Recognizes the event and checks if the given filter can be applied to the event.
*/
fun canBeAppliedTo(event: AnalyticsEvent): Boolean
/**
* An internal filter check that, on external or internal conditions, recognizes the possibility of
* sending an event.
*/
fun canBeSent(event: AnalyticsEvent): Boolean
/**
* Performs a check to see if the event can be dispatched by a specific handler
*/
fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean
}
interface AnalyticsFilterHolder {
fun addFilter(filter: AnalyticsEventFilter)
fun removeFilter(filter: AnalyticsEventFilter): Boolean
}

View file

@ -1,88 +1,50 @@
package com.tangem.tap.common.analytics.api
import com.shopify.buy3.Storefront
import com.tangem.blockchain.common.BlockchainError
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
import com.tangem.tap.common.analytics.AnalyticsAnOld
import com.tangem.tap.common.analytics.AnalyticsEventAnOld
import com.tangem.tap.common.analytics.AnalyticsParamAnOld
import android.app.Application
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.domain.common.AnalyticsHandlersLogConfig
import com.tangem.tap.common.analytics.events.AnalyticsEvent
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.extensions.filterNotNull
import com.tangem.tap.domain.configurable.config.Config
/**
[REDACTED_AUTHOR]
*/
interface AnalyticsEventHandler {
fun handleEvent(
event: String,
params: Map<String, String> = emptyMap(),
)
fun send(event: AnalyticsEvent)
}
fun handleAnalyticsEvent(
event: AnalyticsEvent,
card: Card? = null,
blockchain: String? = null,
) {
handleEvent(
event = prepareEventString(event.category, event.event),
params = prepareParams(card, blockchain, event.params),
)
interface AnalyticsHandler : AnalyticsEventHandler {
fun id(): String
fun send(event: String, params: Map<String, String> = emptyMap())
override fun send(event: AnalyticsEvent) {
send(prepareEventString(event), event.params)
}
@Deprecated("Migrate to AnalyticsEvent")
fun handleAnalyticsEvent(
event: AnalyticsEventAnOld,
params: Map<String, String> = emptyMap(),
card: Card? = null,
blockchain: String? = null,
) {
handleEvent(event.event, prepareParams(card, blockchain, params))
}
fun prepareParams(
card: Card? = null,
blockchain: String? = null,
params: Map<String, String> = emptyMap(),
): Map<String, String> = mapOf(
AnalyticsParam.Firmware to card?.firmwareVersion?.stringValue,
AnalyticsParam.BatchId to card?.batchId,
AnalyticsParam.Blockchain to blockchain,
).filterNotNull() + params
fun prepareEventString(category: String, event: String): String {
return "[$category] $event"
}
fun prepareEventString(event: AnalyticsEvent): String = "[${event.category}] ${event.event}"
}
interface ErrorEventHandler {
fun handleErrorEvent(
fun send(
error: Throwable,
params: Map<String, String> = emptyMap(),
)
}
interface SdkErrorEventHandler : CardSdkErrorEventHandler, BlockchainSdkErrorEventHandler
interface AnalyticsHandlerHolder {
fun addHandler(name: String, handler: AnalyticsHandler)
fun removeHandler(name: String): AnalyticsHandler?
}
interface CardSdkErrorEventHandler {
fun handleCardSdkErrorEvent(
error: TangemSdkError,
action: AnalyticsAnOld.ActionToLog,
params: Map<AnalyticsParamAnOld, String> = emptyMap(),
card: Card? = null,
interface AnalyticsHandlerBuilder {
fun build(data: Data): AnalyticsHandler?
data class Data(
val application: Application,
val config: Config,
val isDebug: Boolean,
val logConfig: AnalyticsHandlersLogConfig,
val jsonConverter: MoshiJsonConverter,
)
}
interface BlockchainSdkErrorEventHandler {
fun handleBlockchainSdkErrorEvent(
error: BlockchainError,
action: AnalyticsAnOld.ActionToLog,
params: Map<AnalyticsParamAnOld, String> = mapOf(),
card: Card? = null,
)
}
interface ShopifyOrderEventHandler {
fun handleShopifyOrderEvent(order: Storefront.Order)
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.tap.common.analytics.api
import com.tangem.tap.common.analytics.events.AnalyticsEvent
/**
[REDACTED_AUTHOR]
*/
interface ParamsInterceptor {
fun id(): String
fun canBeAppliedTo(event: AnalyticsEvent): Boolean
fun intercept(params: MutableMap<String, String>)
}
interface ParamsInterceptorHolder {
fun addParamsInterceptor(interceptor: ParamsInterceptor)
fun removeParamsInterceptor(interceptor: ParamsInterceptor): ParamsInterceptor?
}

View file

@ -0,0 +1,70 @@
package com.tangem.tap.common.analytics.converters
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.common.Converter
import com.tangem.common.core.TangemSdkError
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.features.demo.DemoTransactionSender
/**
[REDACTED_AUTHOR]
*/
class AnalyticsErrorConverter : Converter<Throwable, Map<String, String>> {
fun canBeHandled(error: Throwable): Boolean {
return when (error) {
is TangemSdkError.UserCancelled -> false
is BlockchainSdkError -> !error.customMessage.contains(DemoTransactionSender.ID)
else -> true
}
}
override fun convert(value: Throwable): Map<String, String> {
return when (value) {
is BlockchainSdkError -> BlockchainSdkErrorConverter(CardSdkErrorConverter()).convert(value)
is TangemSdkError -> CardSdkErrorConverter().convert(value)
else -> ThrowableErrorConverter().convert(value)
}
}
}
private class ThrowableErrorConverter : Converter<Throwable, Map<String, String>> {
override fun convert(value: Throwable): Map<String, String> {
val unknown = "unknown"
return mapOf(
AnalyticsParam.ErrorKey to AnalyticsParam.Error.App.value,
AnalyticsParam.ErrorDescription to (value.message ?: value.cause?.message ?: unknown),
)
}
}
class CardSdkErrorConverter : Converter<TangemSdkError, Map<String, String>> {
override fun convert(value: TangemSdkError): Map<String, String> {
if (value is TangemSdkError.UserCancelled) return emptyMap()
return mapOf(
AnalyticsParam.ErrorKey to AnalyticsParam.Error.CardSdk.value,
AnalyticsParam.ErrorCode to value.code.toString(),
AnalyticsParam.ErrorDescription to value.toString(),
)
}
}
private class BlockchainSdkErrorConverter(
private val cardSdkErrorConverter: CardSdkErrorConverter,
) : Converter<BlockchainSdkError, Map<String, String>> {
override fun convert(value: BlockchainSdkError): Map<String, String> {
if (value.customMessage.contains(DemoTransactionSender.ID)) return emptyMap()
if (value is BlockchainSdkError.WrappedTangemError) {
return (value.tangemError as? TangemSdkError)?.let { cardSdkErrorConverter.convert(it) } ?: emptyMap()
}
return mapOf(
AnalyticsParam.ErrorKey to AnalyticsParam.Error.BlockchainSdk.value,
AnalyticsParam.ErrorCode to value.code.toString(),
AnalyticsParam.ErrorDescription to "${value.javaClass.simpleName}: ${value.customMessage}",
)
}
}

View file

@ -0,0 +1,76 @@
package com.tangem.tap.common.analytics.converters
import com.tangem.common.Converter
import com.tangem.common.extensions.isZero
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.filters.BasicTopUpFilter
import com.tangem.tap.domain.extensions.getUserWalletId
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.reducers.calculateTotalCryptoAmount
/**
[REDACTED_AUTHOR]
*/
class BasicSignInEventConverter(
private val scanResponse: ScanResponse,
) : Converter<WalletState, Basic.SignedIn?> {
override fun convert(value: WalletState): Basic.SignedIn? {
if (!statesIsReadyToCreateEvent(scanResponse, value)) return null
val cardCurrency = ParamCardCurrencyConverter().convert(scanResponse) ?: return null
return Basic.SignedIn(
state = AnalyticsParam.CardBalanceState.from(value.walletsData),
currency = cardCurrency,
batch = scanResponse.card.batchId,
).apply {
filterData = scanResponse.card.getUserWalletId()
}
}
}
class BasicTopUpEventConverter(
private val scanResponse: ScanResponse,
) : Converter<WalletState, Basic.ToppedUp?> {
override fun convert(value: WalletState): Basic.ToppedUp? {
if (!statesIsReadyToCreateEvent(scanResponse, value)) return null
val cardCurrency = ParamCardCurrencyConverter().convert(scanResponse) ?: return null
val data = BasicTopUpFilter.Data(
walletId = scanResponse.card.getUserWalletId(),
cardBalanceState = AnalyticsParam.CardBalanceState.from(value.walletsData),
)
return Basic.ToppedUp(cardCurrency).apply { filterData = data }
}
}
private fun AnalyticsParam.CardBalanceState.Companion.from(walletsData: List<WalletData>): AnalyticsParam.CardBalanceState {
val totalCryptoAmount = walletsData.calculateTotalCryptoAmount()
return when {
totalCryptoAmount.isZero() -> AnalyticsParam.CardBalanceState.Empty
else -> AnalyticsParam.CardBalanceState.Full
}
}
private fun statesIsReadyToCreateEvent(scanResponse: ScanResponse, state: WalletState): Boolean {
if (scanResponse.card.isMultiwalletAllowed && state.missingDerivations.isNotEmpty()) return false
if (state.walletsData.isEmpty()) return false
val totalBalanceState = state.totalBalance?.state ?: return false
if (totalBalanceState == ProgressState.Loading || totalBalanceState == ProgressState.Refreshing) return false
val balancesCount = state.walletsData.map {
if (it.currencyData.amount == null) 0 else 1
}.reduce { acc, i -> acc + i }
if (balancesCount != state.wallets.size) return false
return true
}

View file

@ -0,0 +1,31 @@
package com.tangem.tap.common.analytics.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.Converter
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
/**
[REDACTED_AUTHOR]
*/
class ParamCardCurrencyConverter : Converter<ScanResponse, AnalyticsParam.CardCurrency?> {
override fun convert(value: ScanResponse): AnalyticsParam.CardCurrency? {
if (value.card.isMultiwalletAllowed) return AnalyticsParam.CardCurrency.MultiCurrency
val type = when {
value.isTangemNote() -> {
value.card.getTangemNoteBlockchain()?.let { AnalyticsParam.CurrencyType.Blockchain(it) }
}
value.isTangemTwins() -> AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin)
value.isSaltPay() -> AnalyticsParam.CurrencyType.Token(SaltPayWorkaround.tokenFrom(Blockchain.SaltPay))
value.getPrimaryToken() != null -> AnalyticsParam.CurrencyType.Token(value.getPrimaryToken()!!)
else -> null
} ?: return null
return AnalyticsParam.CardCurrency.SingleCurrency(type)
}
}

View file

@ -1,88 +0,0 @@
package com.tangem.tap.common.analytics.converters
import com.tangem.blockchain.common.BlockchainError
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.common.Converter
import com.tangem.common.core.TangemSdkError
import com.tangem.tap.common.analytics.AnalyticsAnOld
import com.tangem.tap.common.analytics.AnalyticsParamAnOld
import com.tangem.tap.common.analytics.TangemSdk
import com.tangem.tap.features.demo.DemoTransactionSender
/**
[REDACTED_AUTHOR]
*/
internal data class ErrorEvent(
val throwable: Throwable,
val params: Map<String, String>,
)
internal class CardSdkErrorConverter : Converter<CardSdkErrorConverter.Model, ErrorEvent?> {
override fun convert(value: Model): ErrorEvent? {
if (value.error is TangemSdkError.UserCancelled) return null
val mutableParams = value.params.convertToParamValue().toMutableMap().apply {
putAll(value.cardParams)
}
mutableParams[AnalyticsParamAnOld.ACTION.param] = value.action.key
mutableParams[AnalyticsParamAnOld.ERROR_CODE.param] = value.error.code.toString()
mutableParams[AnalyticsParamAnOld.ERROR_DESCRIPTION.param] = value.error.javaClass.simpleName
mutableParams[AnalyticsParamAnOld.ERROR_KEY.param] = "TangemSdkError"
return ErrorEvent(TangemSdk.map(value.error), mutableParams)
}
internal data class Model(
val error: TangemSdkError,
val action: AnalyticsAnOld.ActionToLog,
val params: Map<AnalyticsParamAnOld, String>,
val cardParams: Map<String, String>,
)
}
internal class BlockchainSdkErrorConverter(
private val cardSdkErrorConverter: CardSdkErrorConverter,
) : Converter<BlockchainSdkErrorConverter.Model, ErrorEvent?> {
override fun convert(value: Model): ErrorEvent? {
val error = (value.error as? BlockchainSdkError) ?: return null
if (error is BlockchainSdkError.WrappedTangemError) {
val tangemSdkError = (error.tangemError as? TangemSdkError) ?: return null
val cardSdkModel = CardSdkErrorConverter.Model(
error = tangemSdkError,
action = value.action,
params = value.params,
cardParams = value.cardParams,
)
return cardSdkErrorConverter.convert(cardSdkModel)
}
if (error.customMessage.contains(DemoTransactionSender.ID)) return null
val mutableParams = value.params.convertToParamValue().toMutableMap()
mutableParams[AnalyticsParamAnOld.ACTION.param] = value.action.key
mutableParams[AnalyticsParamAnOld.ERROR_CODE.param] = error.code.toString()
mutableParams[AnalyticsParamAnOld.ERROR_DESCRIPTION.param] = "${error.javaClass.simpleName}: ${error.customMessage}"
mutableParams[AnalyticsParamAnOld.ERROR_KEY.param] = "BlockchainSdkError"
return ErrorEvent(value.error, mutableParams)
}
data class Model(
val error: BlockchainError,
val action: AnalyticsAnOld.ActionToLog,
val params: Map<AnalyticsParamAnOld, String>,
val cardParams: Map<String, String>,
)
}
internal fun Map<AnalyticsParamAnOld, String>.convertToParamValue(): Map<String, String> {
return mutableMapOf<String, String>().also { newMap ->
this.forEach {
newMap[it.key.param] = it.value
}
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.tap.common.analytics.converters
import com.shopify.buy3.Storefront
import com.tangem.common.Converter
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.shop.data.ProductType
/**
[REDACTED_AUTHOR]
*/
class ShopOrderToEventConverter : Converter<Pair<Storefront.Order, ProductType>, Shop.Purchased> {
override fun convert(value: Pair<Storefront.Order, ProductType>): Shop.Purchased {
val order = value.first
val productType = value.second
val sku = order.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: productType.sku
val count = when (productType) {
ProductType.WALLET_2_CARDS -> "2"
ProductType.WALLET_3_CARDS -> "3"
}
val amount = "${order.totalPriceV2.amount} ${order.totalPriceV2.currencyCode.name}"
val code = (order.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code
return Shop.Purchased(sku, count, amount, code)
}
}

View file

@ -6,5 +6,7 @@ package com.tangem.tap.common.analytics.events
sealed class AnalyticsEvent(
val category: String,
val event: String,
val params: Map<String, String> = mapOf(),
var params: Map<String, String> = mapOf(),
val error: Throwable? = null,
var filterData: Any? = null,
)

View file

@ -1,22 +1,27 @@
package com.tangem.tap.common.analytics.events
import com.tangem.tap.features.details.redux.SecurityOption
sealed class AnalyticsParam {
sealed class CurrencyType(val value: String) {
class Currency(currency: com.tangem.tap.features.wallet.models.Currency) : CurrencyType(currency.currencySymbol)
class Blockchain(blockchain: com.tangem.blockchain.common.Blockchain) : CurrencyType(blockchain.currency)
class Token(token: com.tangem.blockchain.common.Token) : CurrencyType(token.symbol)
class FiatCurrency(fiatCurrency: com.tangem.tap.common.entities.FiatCurrency) : CurrencyType(fiatCurrency.symbol)
class Amount(amount: com.tangem.blockchain.common.Amount) : CurrencyType(amount.currencySymbol)
}
// Multicurrency or CurrencyType
// MultiCurrency or CurrencyType
sealed class CardCurrency(val value: String) {
object MultiCurrency : CardCurrency("Multicurrency")
class SingleCurrency(type: CurrencyType) : CardCurrency(type.value)
}
sealed class CardState(val value: String) {
object Empty : CardState("Empty")
object Full : CardState("Full")
sealed class CardBalanceState(val value: String) {
object Empty : CardBalanceState("Empty")
object Full : CardBalanceState("Full")
companion object
}
sealed class RateApp(val value: String) {
@ -39,20 +44,26 @@ sealed class AnalyticsParam {
object AccessCode : SecurityMode("Access Code")
object Passcode : SecurityMode("Passcode")
object LongTap : SecurityMode("Long Tap")
companion object {
fun from(option: SecurityOption): SecurityMode = when (option) {
SecurityOption.AccessCode -> AccessCode
SecurityOption.PassCode -> Passcode
SecurityOption.LongTap -> LongTap
}
}
}
sealed class SocialNetwork(val value: String) {
object Facebook : SocialNetwork("Facebook")
object Instagram : SocialNetwork("Instagram")
object Youtube : SocialNetwork("Youtube")
object Twitter : SocialNetwork("Twitter")
object LinkedIn : SocialNetwork("LinkedIn")
object GitHub : SocialNetwork("GitHub")
sealed class Error(val value: String) {
object App : Error("App Error")
object CardSdk : Error("Card Sdk Error")
object BlockchainSdk : Error("Blockchain Sdk Error")
}
companion object Key {
const val Blockchain = "blockchain"
const val BatchId = "batch_id"
const val Firmware = "firmware"
const val BatchId = "Batch"
const val ErrorDescription = "Error Description"
const val ErrorCode = "Error Code"
const val ErrorKey = "Error Key"
}
}

View file

@ -6,10 +6,11 @@ package com.tangem.tap.common.analytics.events
sealed class Basic(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Basic", event, params) {
error: Throwable? = null,
) : AnalyticsEvent("Basic", event, params, error) {
sealed class SignedIn(
state: AnalyticsParam.CardState,
class SignedIn(
state: AnalyticsParam.CardBalanceState,
currency: AnalyticsParam.CardCurrency,
batch: String,
) : Basic(
@ -17,7 +18,7 @@ sealed class Basic(
params = mapOf(
"State" to state.value,
"Currency" to currency.value,
"Batch" to batch,
AnalyticsParam.BatchId to batch,
),
)
@ -25,4 +26,9 @@ sealed class Basic(
event = "Topped up",
params = mapOf("Currency" to currency.value),
)
class ScanError(error: Throwable) : Basic(
event = "Scan",
error = error,
)
}

View file

@ -19,7 +19,7 @@ sealed class MainScreen(
params = mapOf("Currency Type" to currencyType.value),
)
sealed class NoticeRateAppButton(result: AnalyticsParam.RateApp) : MainScreen(
class NoticeRateAppButton(result: AnalyticsParam.RateApp) : MainScreen(
event = "Notice - Rate The App Button Tapped",
params = mapOf("Result" to result.value),
)

View file

@ -1,5 +1,9 @@
package com.tangem.tap.common.analytics.events
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.extensions.filterNotNull
/**
[REDACTED_AUTHOR]
*/
@ -10,7 +14,18 @@ sealed class ManageTokens(
class ScreenOpened : ManageTokens("Manage Tokens Screen Opened")
class TokenSearched : ManageTokens("Token Searched")
class TokenSwitcherChanged : ManageTokens("Token Switcher Changed")
class TokenSwitcherChanged(
type: AnalyticsParam.CurrencyType,
state: AnalyticsParam.OnOffState,
) : ManageTokens(
"Token Switcher Changed",
params = mapOf(
"Token" to type.value,
"State" to state.value,
),
)
class ButtonSaveChanges : ManageTokens("Button - Save Changes")
class ButtonCustomToken : ManageTokens("Button - Custom Token")
@ -21,13 +36,26 @@ sealed class ManageTokens(
class ScreenOpened : ManageTokens("Custom Token Screen Opened")
class TokenWasAdded(symbol: String, network: String, address: String) : ManageTokens(
class TokenWasAdded(customCurrency: CustomCurrency) : ManageTokens(
event = "Custom Token Was Added",
params = mapOf(
"Symbol" to symbol,
"Network" to network,
"Address" to address,
),
)
params = convertToParam(customCurrency),
) {
companion object {
private fun convertToParam(customCurrency: CustomCurrency): Map<String, String> = with(customCurrency) {
return when (this) {
is CustomCurrency.CustomBlockchain -> mapOf(
"Token" to network.currency,
"Derivation Path" to derivationPath?.rawPath,
).filterNotNull()
is CustomCurrency.CustomToken -> mapOf(
"Token" to token.symbol,
"Derivation Path" to derivationPath?.rawPath,
"Network Id" to network.toNetworkId(),
"Contract Address" to token.contractAddress,
).filterNotNull()
}
}
}
}
}
}

View file

@ -34,9 +34,9 @@ sealed class Onboarding(
class AccessCodeEntered : Backup("Access Code Entered")
class AccessCodeReEntered : Backup("Access Code Re-entered")
class Finished(cardsCount: String) : Backup(
class Finished(cardsCount: Int) : Backup(
event = "Backup Finished",
params = mapOf("Cards count" to cardsCount),
params = mapOf("Cards count" to "$cardsCount"),
)
}
@ -65,12 +65,12 @@ sealed class Onboarding(
class SetupFinished : Twins("Twin Setup Finished")
}
class PinCodeSet: Onboarding("Onboarding", "PIN code set")
class ButtonConnect: Onboarding("Onboarding", "Button - Connect")
class KYCStarted: Onboarding("Onboarding", "KYC started")
class KYCInProgress: Onboarding("Onboarding", "KYC in progress")
class KYCRejected: Onboarding("Onboarding", "KYC rejected")
class ClaimScreenOpened: Onboarding("Onboarding", "Claim screen opened")
class ButtonClaim: Onboarding("Onboarding", "Button - Claim")
class ClaimWasSuccessfully: Onboarding("Onboarding", "Claim was successfully")
class PinCodeSet : Onboarding("Onboarding", "PIN code set")
class ButtonConnect : Onboarding("Onboarding", "Button - Connect")
class KYCStarted : Onboarding("Onboarding", "KYC started")
class KYCInProgress : Onboarding("Onboarding", "KYC in progress")
class KYCRejected : Onboarding("Onboarding", "KYC rejected")
class ClaimScreenOpened : Onboarding("Onboarding", "Claim screen opened")
class ButtonClaim : Onboarding("Onboarding", "Button - Claim")
class ClaimWasSuccessfully : Onboarding("Onboarding", "Claim was successfully")
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.common.analytics.events
import com.tangem.tap.features.details.ui.details.SocialNetwork
/**
[REDACTED_AUTHOR]
*/
@ -7,7 +9,8 @@ sealed class Settings(
category: String = "Settings",
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category, event, params) {
error: Throwable? = null,
) : AnalyticsEvent(category, event, params, error) {
class ScreenOpened : Settings(event = "Settings Screen Opened")
class ButtonChat : Settings(event = "Button - Chat")
@ -18,29 +21,35 @@ sealed class Settings(
class ButtonAppSettings : Settings(event = "Button - App Settings")
class ButtonCreateBackup : Settings(event = "Button - Create Backup")
sealed class ButtonSocialNetwork(network: AnalyticsParam.SocialNetwork) : Settings(
class ButtonSocialNetwork(network: SocialNetwork) : Settings(
event = "Button - Social Network",
params = mapOf("Network" to network.value),
params = mapOf("Network" to network.id),
)
sealed class CardSettings(
event: String,
params: Map<String, String> = mapOf(),
) : Settings("Settings / Card Settings", event, params) {
error: Throwable? = null,
) : Settings("Settings / Card Settings", event, params, error) {
class ButtonFactoryReset : CardSettings("Button - Factory Reset")
class FactoryResetFinished : CardSettings("Factory Reset Finished")
class FactoryResetFinished(error: Throwable? = null) : CardSettings(
event = "Factory Reset Finished",
error = error,
)
class UserCodeChanged : CardSettings("User Code Changed")
class ButtonChangeSecurityMode : CardSettings("Button - Change Security Mode")
sealed class ButtonChangeUserCode(type: AnalyticsParam.UserCode) : CardSettings(
class ButtonChangeUserCode(type: AnalyticsParam.UserCode) : CardSettings(
event = "Button - Change User Code",
params = mapOf("Type" to type.value),
)
sealed class SecurityModeChanged(mode: AnalyticsParam.SecurityMode) : CardSettings(
class SecurityModeChanged(mode: AnalyticsParam.SecurityMode, error: Throwable? = null) : CardSettings(
event = "Security Mode Changed",
params = mapOf("Mode" to mode.value),
error = error,
)
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.common.analytics.events
import com.tangem.tap.common.extensions.filterNotNull
/**
[REDACTED_AUTHOR]
*/
@ -10,9 +12,14 @@ sealed class Shop(
class ScreenOpened : IntroductionProcess("Shop Screen Opened")
class Purchased(productSku: String) : Shop(
class Purchased(sku: String, count: String, amount: String, couponCode: String?) : Shop(
event = "Purchased",
params = mapOf("SKU" to productSku),
params = mapOf(
"SKU" to sku,
"Count" to count,
"Amount" to amount,
"Coupon Code" to couponCode,
).filterNotNull(),
)
class Redirected(partnerName: String) : Shop(

View file

@ -1,5 +1,7 @@
package com.tangem.tap.common.analytics.events
import com.tangem.tap.common.analytics.events.AnalyticsParam.CurrencyType
/**
[REDACTED_AUTHOR]
*/
@ -7,15 +9,41 @@ sealed class Token(
category: String,
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category, event, params) {
error: Throwable? = null,
) : AnalyticsEvent(category, event, params, error) {
class Refreshed : Token("Token", "Refreshed")
class ButtonRemoveToken : Token("Token", "Button - Remove Token")
class ButtonExplore : Token("Token", "Button - Explore")
class ButtonBuy(token: String) : Token("Token", "Button - Buy", mapOf("Token" to token))
class ButtonSell(token: String) : Token("Token", "Button - Sell", mapOf("Token" to token))
class ButtonExchange(token: String) : Token("Token", "Button - Exchange", mapOf("Token" to token))
class ButtonSend(token: String) : Token("Token", "Button - Send", mapOf("Token" to token))
class ButtonRemoveToken(type: CurrencyType) : Token(
"Token",
"Button - Remove Token",
params = mapOf("Token" to type.value),
)
class ButtonBuy(type: CurrencyType) : Token(
category = "Token",
event = "Button - Buy",
params = mapOf("Token" to type.value),
)
class ButtonSell(type: CurrencyType) : Token(
category = "Token",
event = "Button - Sell",
params = mapOf("Token" to type.value),
)
class ButtonExchange(type: CurrencyType) : Token(
category = "Token",
event = "Button - Exchange",
params = mapOf("Token" to type.value),
)
class ButtonSend(type: CurrencyType) : Token(
category = "Token",
event = "Button - Send",
params = mapOf("Token" to type.value),
)
sealed class Recieve(
event: String,
@ -30,16 +58,18 @@ sealed class Token(
sealed class Send(
event: String,
params: Map<String, String> = mapOf(),
) : Token("Token / Send", event, params) {
error: Throwable? = null,
) : Token("Token / Send", event, params, error) {
class ScreenOpened : Send("Send Screen Opened")
class ButtonPaste : Send("Button - Paste")
class ButtonQRCode : Send("Button - QR Code")
class ButtonSwapCurrency : Send("Button - Swap Currency")
class TransactionSent(token: AnalyticsParam.CurrencyType) : Send(
class TransactionSent(type: CurrencyType, error: Throwable? = null) : Send(
event = "Transaction Sent",
params = mapOf("Token" to token.value),
params = mapOf("Token" to type.value),
error = error,
)
}

View file

@ -6,9 +6,13 @@ package com.tangem.tap.common.analytics.events
sealed class WalletConnect(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Wallet Connect", event, params) {
error: Throwable? = null,
) : AnalyticsEvent("Wallet Connect", event, params, error) {
class NewSessionEstablished : WalletConnect("New Session Established")
class SessionDisconnected : WalletConnect("Session Disconnected")
class RequestSigned : WalletConnect("Request Signed")
class SignError(error: Throwable) : WalletConnect("Sign", error = error)
class TransactionError(error: Throwable) : WalletConnect("Transaction", error = error)
}

View file

@ -0,0 +1,27 @@
package com.tangem.tap.common.analytics.filters
import com.tangem.tap.common.analytics.api.AnalyticsEventFilter
import com.tangem.tap.common.analytics.api.AnalyticsHandler
import com.tangem.tap.common.analytics.events.AnalyticsEvent
import com.tangem.tap.common.analytics.events.Basic
/**
[REDACTED_AUTHOR]
*/
class BasicSignInFilter : AnalyticsEventFilter {
private val alreadySignedIn = mutableSetOf<String>()
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = event is Basic.SignedIn
override fun canBeSent(event: AnalyticsEvent): Boolean {
val userWalletId = event.filterData as? String ?: return false
val canBeSent = !alreadySignedIn.contains(userWalletId)
alreadySignedIn.add(userWalletId)
return canBeSent
}
override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean = true
}

View file

@ -0,0 +1,50 @@
package com.tangem.tap.common.analytics.filters
import com.tangem.common.extensions.guard
import com.tangem.tap.common.analytics.api.AnalyticsEventFilter
import com.tangem.tap.common.analytics.api.AnalyticsHandler
import com.tangem.tap.common.analytics.events.AnalyticsEvent
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.persistence.ToppedUpWalletStorage
/**
[REDACTED_AUTHOR]
*/
class BasicTopUpFilter(
private val topupWalletStorage: ToppedUpWalletStorage,
) : AnalyticsEventFilter {
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = event is Basic.ToppedUp
override fun canBeSent(event: AnalyticsEvent): Boolean {
val data = event.filterData as? Data ?: return false
val walletInfo = topupWalletStorage.restore(data.walletId).guard {
val newWalletInfo = Data(
walletId = data.walletId,
cardBalanceState = data.cardBalanceState,
)
topupWalletStorage.save(newWalletInfo)
return false
}
if (walletInfo.isToppedUp) return false
return if (!walletInfo.isToppedUp && data.isToppedUp) {
topupWalletStorage.save(walletInfo.copy(cardBalanceState = AnalyticsParam.CardBalanceState.Full))
true
} else {
false
}
}
override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean = true
data class Data(
val walletId: String,
val cardBalanceState: AnalyticsParam.CardBalanceState,
) {
val isToppedUp: Boolean = cardBalanceState == AnalyticsParam.CardBalanceState.Full
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.common.analytics.filters
import com.tangem.tap.common.analytics.api.AnalyticsEventFilter
import com.tangem.tap.common.analytics.api.AnalyticsHandler
import com.tangem.tap.common.analytics.events.AnalyticsEvent
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
/**
[REDACTED_AUTHOR]
*/
class ShopPurchasedEventFilter : AnalyticsEventFilter {
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = event is Shop.Purchased
override fun canBeSent(event: AnalyticsEvent): Boolean = true
override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean {
return when (handler) {
is AppsFlyerAnalyticsHandler, is FirebaseAnalyticsHandler -> true
else -> false
}
}
}

View file

@ -1,12 +1,27 @@
package com.tangem.tap.common.analytics.handlers.amplitude
import com.tangem.tap.common.analytics.api.AnalyticsEventHandler
import com.tangem.tap.common.analytics.api.AnalyticsHandler
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
class AmplitudeAnalyticsHandler(
private val client: AmplitudeAnalyticsClient,
) : AnalyticsEventHandler {
) : AnalyticsHandler {
override fun handleEvent(event: String, params: Map<String, String>) {
override fun id(): String = ID
override fun send(event: String, params: Map<String, String>) {
client.logEvent(event, params)
}
companion object {
const val ID = "Amplitude"
}
class Builder : AnalyticsHandlerBuilder {
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when {
!data.isDebug -> AmplitudeClient(data.application, data.config.amplitudeApiKey)
data.isDebug && data.logConfig.amplitude -> AmplitudeLogClient(data.jsonConverter)
else -> null
}?.let { AmplitudeAnalyticsHandler(it) }
}
}

View file

@ -1,14 +1,17 @@
package com.tangem.tap.common.analytics.handlers.amplitude
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.tap.common.analytics.AnalyticsEventsLogger
/**
[REDACTED_AUTHOR]
*/
internal class AmplitudeLogClient(
private val logger: AnalyticsEventsLogger,
jsonConverter: MoshiJsonConverter,
) : AmplitudeAnalyticsClient {
private val logger: AnalyticsEventsLogger = AnalyticsEventsLogger(AmplitudeAnalyticsHandler.ID, jsonConverter)
override fun logEvent(event: String, params: Map<String, String>) {
logger.logEvent(event, params)
}

View file

@ -1,47 +1,27 @@
package com.tangem.tap.common.analytics.handlers.appsFlyer
import com.appsflyer.AFInAppEventParameterName
import com.appsflyer.AFInAppEventType
import com.shopify.buy3.Storefront
import com.tangem.common.Converter
import com.tangem.tap.common.analytics.api.AnalyticsEventHandler
import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler
import com.tangem.tap.common.analytics.api.AnalyticsHandler
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
class AppsFlyerAnalyticsHandler(
private val client: AppsFlyerAnalyticsClient,
) : AnalyticsEventHandler, ShopifyOrderEventHandler {
) : AnalyticsHandler {
override fun handleEvent(event: String, params: Map<String, String>) {
override fun id(): String = ID
override fun send(event: String, params: Map<String, String>) {
client.logEvent(event, params)
}
override fun handleShopifyOrderEvent(order: Storefront.Order) {
handleEvent(ORDER_EVENT, OrderToParamsConverter().convert(order))
}
companion object {
const val ORDER_EVENT = AFInAppEventType.PURCHASE
const val ID = "AppsFlyer"
}
}
private class OrderToParamsConverter : Converter<Storefront.Order, Map<String, String>> {
override fun convert(value: Storefront.Order): Map<String, String> {
val sku = value.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: "unknown"
val discountCode =
(value.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code
val discountParams = if (discountCode != null) {
mapOf(AFInAppEventParameterName.COUPON_CODE to discountCode)
} else {
mapOf()
}
return mapOf(
AFInAppEventParameterName.CONTENT_ID to sku,
AFInAppEventParameterName.REVENUE to value.totalPriceV2.amount,
AFInAppEventParameterName.CURRENCY to value.currencyCode.name,
) + discountParams
class Builder : AnalyticsHandlerBuilder {
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when {
!data.isDebug -> AppsFlyerClient(data.application, data.config.appsFlyerDevKey)
data.isDebug && data.logConfig.appsFlyer -> AppsFlyerLogClient(data.jsonConverter)
else -> null
}?.let { AppsFlyerAnalyticsHandler(it) }
}
}

View file

@ -1,14 +1,17 @@
package com.tangem.tap.common.analytics.handlers.appsFlyer
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.tap.common.analytics.AnalyticsEventsLogger
/**
[REDACTED_AUTHOR]
*/
internal class AppsFlyerLogClient(
private val logger: AnalyticsEventsLogger,
jsonConverter: MoshiJsonConverter,
) : AppsFlyerAnalyticsClient {
private val logger = AnalyticsEventsLogger(AppsFlyerAnalyticsHandler.ID, jsonConverter)
override fun logEvent(event: String, params: Map<String, String>) {
logger.logEvent(event, params)
}

View file

@ -1,81 +1,50 @@
package com.tangem.tap.common.analytics.handlers.firebase
import com.google.firebase.analytics.FirebaseAnalytics
import com.shopify.buy3.Storefront
import com.tangem.blockchain.common.BlockchainError
import com.tangem.common.Converter
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
import com.tangem.tap.common.analytics.AnalyticsAnOld
import com.tangem.tap.common.analytics.AnalyticsParamAnOld
import com.tangem.tap.common.analytics.api.AnalyticsEventHandler
import com.tangem.tap.common.analytics.api.AnalyticsHandler
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.api.ErrorEventHandler
import com.tangem.tap.common.analytics.api.SdkErrorEventHandler
import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler
import com.tangem.tap.common.analytics.converters.BlockchainSdkErrorConverter
import com.tangem.tap.common.analytics.converters.CardSdkErrorConverter
import com.tangem.tap.common.analytics.converters.AnalyticsErrorConverter
import com.tangem.tap.common.analytics.events.AnalyticsEvent
class FirebaseAnalyticsHandler(
private val client: FirebaseAnalyticsClient,
) : AnalyticsEventHandler, ErrorEventHandler, SdkErrorEventHandler, ShopifyOrderEventHandler {
) : AnalyticsHandler, ErrorEventHandler {
override fun handleEvent(event: String, params: Map<String, String>) {
override fun id(): String = ID
override fun send(event: String, params: Map<String, String>) {
client.logEvent(event, params)
}
override fun handleErrorEvent(error: Throwable, params: Map<String, String>) {
override fun send(event: AnalyticsEvent) {
when (event.error) {
null -> super.send(event)
else -> {
val errorConverter = AnalyticsErrorConverter()
if (!errorConverter.canBeHandled(event.error)) return
val errorParams = errorConverter.convert(event.error).toMutableMap()
errorParams["Category"] = event.category
errorParams["Event"] = event.event
errorParams.putAll(event.params)
send(event.error, errorParams)
}
}
}
override fun send(error: Throwable, params: Map<String, String>) {
client.logErrorEvent(error, params)
}
override fun handleCardSdkErrorEvent(
error: TangemSdkError,
action: AnalyticsAnOld.ActionToLog,
params: Map<AnalyticsParamAnOld, String>,
card: Card?,
) {
val model = CardSdkErrorConverter.Model(error, action, params, prepareParams(card))
val converter = CardSdkErrorConverter()
converter.convert(model)?.let {
handleErrorEvent(it.throwable, it.params)
}
companion object {
const val ID = "Firebase"
}
override fun handleBlockchainSdkErrorEvent(
error: BlockchainError,
action: AnalyticsAnOld.ActionToLog,
params: Map<AnalyticsParamAnOld, String>,
card: Card?,
) {
val model = BlockchainSdkErrorConverter.Model(error, action, params, prepareParams(card))
val converter = BlockchainSdkErrorConverter(CardSdkErrorConverter())
converter.convert(model)?.let {
handleErrorEvent(it.throwable, it.params)
}
}
override fun handleShopifyOrderEvent(order: Storefront.Order) {
handleEvent(FirebaseClient.ORDER_EVENT, OrderToParamsConverter().convert(order))
}
}
private class OrderToParamsConverter : Converter<Storefront.Order, Map<String, String>> {
override fun convert(value: Storefront.Order): Map<String, String> {
val sku = value.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: "unknown"
val discountCode =
(value.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code
val discountParams = if (discountCode != null) {
mapOf(FirebaseAnalytics.Param.DISCOUNT to discountCode)
} else {
mapOf()
}
return mapOf(
FirebaseAnalytics.Param.ITEM_ID to sku,
FirebaseAnalytics.Param.VALUE to value.totalPriceV2.amount,
FirebaseAnalytics.Param.CURRENCY to value.currencyCode.name,
) + discountParams
class Builder : AnalyticsHandlerBuilder {
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when {
!data.isDebug -> FirebaseClient()
data.isDebug && data.logConfig.firebase -> FirebaseLogClient(data.jsonConverter)
else -> null
}?.let { FirebaseAnalyticsHandler(it) }
}
}

View file

@ -1,14 +1,17 @@
package com.tangem.tap.common.analytics.handlers.firebase
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.tap.common.analytics.AnalyticsEventsLogger
/**
[REDACTED_AUTHOR]
*/
internal class FirebaseLogClient(
private val logger: AnalyticsEventsLogger,
jsonConverter: MoshiJsonConverter,
) : FirebaseAnalyticsClient {
private val logger: AnalyticsEventsLogger = AnalyticsEventsLogger(FirebaseAnalyticsHandler.ID, jsonConverter)
override fun logEvent(event: String, params: Map<String, String>) {
logger.logEvent(event, params)
}

View file

@ -0,0 +1,21 @@
package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.tap.common.analytics.api.ParamsInterceptor
import com.tangem.tap.common.analytics.events.AnalyticsEvent
import com.tangem.tap.common.analytics.events.AnalyticsParam
/**
[REDACTED_AUTHOR]
*/
class BatchIdParamsInterceptor(
val batchId: String,
) : ParamsInterceptor {
override fun id(): String = this::class.java.simpleName
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = true
override fun intercept(params: MutableMap<String, String>) {
params[AnalyticsParam.BatchId] = batchId
}
}

View file

@ -19,6 +19,7 @@ import android.view.inputmethod.InputMethodManager
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
@ -57,7 +58,7 @@ fun View.getString(@StringRes id: Int, vararg formatArgs: String): String {
return context.getString(id, *formatArgs)
}
fun View.getQuantityString(@StringRes id: Int, quantity: Int): String {
fun View.getQuantityString(@PluralsRes id: Int, quantity: Int): String {
return context.resources.getQuantityString(id, quantity, quantity)
}

View file

@ -55,11 +55,10 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
}
}
fun WalletManager?.getToUpUrl(): String? {
fun WalletManager.getTopUpUrl(): String? {
val globalState = store.state.globalState
val wallet = this?.wallet ?: return null
val defaultAddress = wallet.address
return globalState.exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Buy,
blockchain = wallet.blockchain,

View file

@ -0,0 +1,16 @@
package com.tangem.tap.common.moshi
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class BigDecimalAdapter {
@FromJson
fun fromJson(value: String) = BigDecimal(value)
@ToJson
fun toJson(value: BigDecimal) = value.toString()
}

View file

@ -5,7 +5,6 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackData
import com.tangem.tap.common.feedback.FeedbackManager
@ -89,7 +88,6 @@ sealed class GlobalAction : Action {
data class SetConfigManager(val configManager: ConfigManager) : GlobalAction()
data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction()
data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction()
data class SetGlobalAnalyticsHandler(val analyticsHandler: GlobalAnalyticsEventHandler) : GlobalAction()
data class SendEmail(val feedbackData: FeedbackData) : GlobalAction()
data class OpenChat(val feedbackData: FeedbackData, val zendeskConfig: ZendeskConfig? = null) : GlobalAction()

View file

@ -167,7 +167,6 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
scope.launch {
tangemSdkManager.changeDisplayedCardIdNumbersCount(null)
val result = tangemSdkManager.scanProduct(
store.state.globalState.analyticsHandler,
userTokensRepository,
action.additionalBlockchainsToDerive,
action.messageResId,

View file

@ -48,7 +48,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
globalState.copy(configManager = action.configManager)
}
is GlobalAction.SetWarningManager -> globalState.copy(warningManager = action.warningManager)
is GlobalAction.SetGlobalAnalyticsHandler -> globalState.copy(analyticsHandler = action.analyticsHandler)
is GlobalAction.UpdateWalletSignedHashes -> {
val card = globalState.scanResponse?.card ?: return globalState
val wallet = card.wallet(action.walletPublicKey) ?: return globalState

View file

@ -1,7 +1,6 @@
package com.tangem.tap.common.redux.global
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.StateDialog
@ -27,11 +26,9 @@ data class GlobalState(
val dialog: StateDialog? = null,
val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(),
val resources: AndroidResources = AndroidResources(),
val analyticsHandler: GlobalAnalyticsEventHandler = GlobalAnalyticsEventHandler.stub(),
val userCountryCode: String? = null,
) : StateType
data class AndroidResources(
val strings: RString = RString(),
) {

View file

@ -4,8 +4,8 @@ import android.app.Application
import android.content.Intent
import com.google.android.gms.wallet.PaymentData
import com.shopify.buy3.Storefront
import com.tangem.common.extensions.ifNotNull
import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.converters.ShopOrderToEventConverter
import com.tangem.tap.common.extensions.filterNotNull
import com.tangem.tap.common.shop.data.ProductType
import com.tangem.tap.common.shop.data.TangemProduct
@ -23,7 +23,6 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
private val shopifyService = ShopifyService(application, shopifyShop)
private val checkouts = mutableMapOf<ProductType, Storefront.Checkout>()
private val variants = mutableMapOf<ProductType, Storefront.ProductVariant>()
@ -41,24 +40,27 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
variants.putAll(availableVariants)
if (variants.size < SKUS_TO_DISPLAY.size) {
return Result.failure(Exception(
"Shopify: products are missing, " +
"\nproducts available: ${variants.keys.map { it.sku }}"))
return Result.failure(
Exception(
"Shopify: products are missing, " +
"\nproducts available: ${variants.keys.map { it.sku }}",
),
)
}
val twoCardsProduct = TangemProduct(
type = ProductType.WALLET_2_CARDS,
totalSum = TotalSum(
finalValue = variants[ProductType.WALLET_2_CARDS]?.priceV2?.format(),
beforeDiscount = variants[ProductType.WALLET_2_CARDS]?.compareAtPriceV2?.format()
)
beforeDiscount = variants[ProductType.WALLET_2_CARDS]?.compareAtPriceV2?.format(),
),
)
val threeCardsProduct = TangemProduct(
type = ProductType.WALLET_3_CARDS,
totalSum = TotalSum(
finalValue = variants[ProductType.WALLET_3_CARDS]?.priceV2?.format(),
beforeDiscount = variants[ProductType.WALLET_3_CARDS]?.compareAtPriceV2?.format()
)
beforeDiscount = variants[ProductType.WALLET_3_CARDS]?.compareAtPriceV2?.format(),
),
)
createCheckouts()
return Result.success(listOf(twoCardsProduct, threeCardsProduct))
@ -86,7 +88,7 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
val totalPrice = checkouts[productType]!!.totalPriceV2.amount
googlePayService.payWithGooglePay(
totalPriceCents = totalPrice, currencyCode = checkouts[productType]!!.currencyCode.name,
merchantID = shopifyService.shop.merchantID
merchantID = shopifyService.shop.merchantID,
)
}
@ -105,7 +107,7 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
suspend fun handleGooglePayResult(
resultCode: Int,
data: Intent?,
productType: ProductType
productType: ProductType,
): Result<Unit> {
val result = googlePayService.handleResponseFromGooglePay(resultCode, data)
result.onSuccess {
@ -120,7 +122,7 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
private suspend fun completeTokenizedPayment(
paymentData: PaymentData,
productType: ProductType
productType: ProductType,
): Result<Storefront.Checkout> {
val checkout = checkouts[productType]!!
val googlePayResponse =
@ -141,19 +143,18 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
phone = addressGPay.phoneNumber
}
val payment = Storefront.TokenizedPaymentInputV3(
amount,
idempotencyKey,
address,
paymentData.toJson(),
Storefront.PaymentTokenType.GOOGLE_PAY
Storefront.PaymentTokenType.GOOGLE_PAY,
)
.setTest(true)
return shopifyService.completeWithTokenizedPayment(
payment = payment,
checkoutID = checkout.id
checkoutID = checkout.id,
)
}
@ -182,12 +183,12 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
productType,
TotalSum(
finalValue = it.totalPriceV2.format(),
beforeDiscount = variants[productType]!!.compareAtPriceV2.format()
beforeDiscount = variants[productType]!!.compareAtPriceV2.format(),
),
appliedDiscount = it.getAppliedDiscount()
)
appliedDiscount = it.getAppliedDiscount(),
),
)
)
}
return Result.failure(result.exceptionOrNull()!!)
}
@ -196,10 +197,13 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
return checkouts[productType]!!.webUrl
}
suspend fun waitForCheckout(productType: ProductType, analyticsHandler: GlobalAnalyticsEventHandler?) {
suspend fun waitForCheckout(productType: ProductType) {
val result = shopifyService.checkout(true, checkouts[productType]!!.id)
result.onSuccess {
ifNotNull(analyticsHandler, it.order) { handler, order -> handler.handleShopifyOrderEvent(order) }
result.onSuccess { checkout ->
checkout.order?.let {
val event = ShopOrderToEventConverter().convert(it to productType)
Analytics.send(event)
}
}
}

View file

@ -26,15 +26,12 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.pins.CheckUserCodesCommand
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.tap.common.analytics.AnalyticsAnOld
import com.tangem.tap.common.analytics.AnalyticsEventAnOld
import com.tangem.tap.common.analytics.AnalyticsParamAnOld
import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
@ -45,18 +42,15 @@ import kotlin.coroutines.suspendCoroutine
class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) {
suspend fun scanProduct(
analyticsHandler: GlobalAnalyticsEventHandler?,
userTokensRepository: UserTokensRepository,
additionalBlockchainsToDerive: Collection<Blockchain>? = null,
messageRes: Int? = null,
): CompletionResult<ScanResponse> {
analyticsHandler?.handleAnalyticsEvent(AnalyticsEventAnOld.READY_TO_SCAN, card = null)
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(
runnable = ScanProductTask(null, userTokensRepository, additionalBlockchainsToDerive),
cardId = null, initialMessage = message,
).also { sendScanResultsToAnalytics(analyticsHandler, it) }
).also { sendScanResultsToAnalytics(it) }
}
suspend fun createProductWallet(
@ -70,32 +64,12 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
}
private fun sendScanResultsToAnalytics(
analyticsHandler: GlobalAnalyticsEventHandler?,
result: CompletionResult<ScanResponse>,
) {
when (result) {
is CompletionResult.Success -> {
analyticsHandler?.handleAnalyticsEvent(
event = AnalyticsEventAnOld.CARD_IS_SCANNED,
card = result.data.card,
blockchain = result.data.walletData?.blockchain,
)
if (DemoHelper.isDemoCard(result.data)) {
analyticsHandler?.handleAnalyticsEvent(
event = AnalyticsEventAnOld.DEMO_MODE_ACTIVATED,
params = mapOf(AnalyticsParamAnOld.CARD_ID.param to result.data.card.cardId),
card = result.data.card,
blockchain = result.data.walletData?.blockchain,
)
}
if (result is CompletionResult.Failure) {
(result.error as? TangemSdkError)?.let { error ->
Analytics.send(Basic.ScanError(error))
}
is CompletionResult.Failure ->
(result.error as? TangemSdkError)?.let { error ->
analyticsHandler?.handleCardSdkErrorEvent(
error = error,
action = AnalyticsAnOld.ActionToLog.Scan,
)
}
}
}

View file

@ -73,7 +73,7 @@ class ScanProductTask(
// it need because processorResult.data.card doesn't contains attestation result
// and CardWallet.derivedKeys
val processorScanResponseWithNewCard = processorResult.data.copy(
card = scanTaskResult.data
card = scanTaskResult.data,
)
callback(CompletionResult.Success(processorScanResponseWithNewCard))
}
@ -96,7 +96,7 @@ private class ScanNoteProcessor : ProductCommandProcessor<ScanResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
callback(
CompletionResult.Success(
@ -145,7 +145,7 @@ private class ScanWalletProcessor(
is CompletionResult.Success -> {
PreflightReadTask(
readMode = PreflightReadMode.FullCardRead,
cardId = card.cardId
cardId = card.cardId,
).run(session) { readResult ->
when (readResult) {
is CompletionResult.Success -> startLinkingForBackupIfNeeded(card, session, callback)
@ -157,14 +157,17 @@ private class ScanWalletProcessor(
}
}
}
private fun startLinkingForBackupIfNeeded(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val activationIsFinished = preferencesStorage.usedCardsPrefStorage.isActivationFinished(card.cardId)
val activationInProgress = preferencesStorage.usedCardsPrefStorage.isActivationInProgress(card.cardId)
if (card.backupStatus == Card.BackupStatus.NoBackup && !activationIsFinished && card.wallets.isNotEmpty()) {
if ((card.backupStatus == Card.BackupStatus.NoBackup && card.wallets.isNotEmpty())
&& (activationInProgress || card.isSaltPay)
) {
StartPrimaryCardLinkingTask().run(session) { linkingResult ->
when (linkingResult) {
is CompletionResult.Success -> {
@ -180,6 +183,7 @@ private class ScanWalletProcessor(
deriveKeysIfNeeded(card, session, callback)
}
}
private fun deriveKeysIfNeeded(
card: Card,
session: CardSession,
@ -251,7 +255,7 @@ private class ScanWalletProcessor(
Blockchain.RSK,
Blockchain.Fantom, Blockchain.FantomTestnet,
Blockchain.Avalanche, Blockchain.AvalancheTestnet,
).map { BlockchainNetwork(it, card) }
).map { BlockchainNetwork(it, card) },
)
}
return blockchainsToDerive.distinct()

View file

@ -7,21 +7,17 @@ import com.tangem.blockchain.extensions.Result
import com.tangem.common.CompletionResult
import com.tangem.common.KeyPair
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.domain.common.ScanResponse
import com.tangem.network.common.MoshiConverter
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.common.AssetReader
import com.tangem.tap.common.analytics.AnalyticsAnOld
import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.tangemSdkManager
class TwinCardsManager(
private val card: Card,
card: Card,
assetReader: AssetReader,
val analyticsHandler: GlobalAnalyticsEventHandler?
) {
private val currentCardId: String = card.cardId
@ -35,15 +31,7 @@ class TwinCardsManager(
val response = tangemSdkManager.runTaskAsync(CreateFirstTwinWalletTask(), currentCardId, message)
when (response) {
is CompletionResult.Success -> currentCardPublicKey = response.data.wallet.publicKey.toHexString()
is CompletionResult.Failure -> {
(response.error as? TangemSdkError)?.let { error ->
analyticsHandler?.handleCardSdkErrorEvent(
error,
AnalyticsAnOld.ActionToLog.CreateWallet,
card = card
)
}
}
is CompletionResult.Failure -> {}
}
return response
}
@ -58,22 +46,14 @@ class TwinCardsManager(
firstCardId = currentCardId,
issuerKeys = issuerKeyPair,
preparingMessage = preparingMessage,
creatingWalletMessage = creatingWalletMessage
creatingWalletMessage = creatingWalletMessage,
)
val response = tangemSdkManager.runTaskAsync(task, null, initialMessage)
when (response) {
is CompletionResult.Success -> {
secondCardPublicKey = response.data.wallet.publicKey.toHexString()
}
is CompletionResult.Failure -> {
(response.error as? TangemSdkError)?.let { error ->
analyticsHandler?.handleCardSdkErrorEvent(
error,
AnalyticsAnOld.ActionToLog.CreateWallet,
card = card
)
}
}
is CompletionResult.Failure -> {}
}
return response
}
@ -81,20 +61,11 @@ class TwinCardsManager(
suspend fun complete(message: Message): Result<ScanResponse> {
val response = tangemSdkManager.runTaskAsync(
FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeyPair),
currentCardId, message
currentCardId, message,
)
return when (response) {
is CompletionResult.Success -> Result.Success(response.data)
is CompletionResult.Failure -> {
(response.error as? TangemSdkError)?.let { error ->
analyticsHandler?.handleCardSdkErrorEvent(
error,
AnalyticsAnOld.ActionToLog.WriteIssuerData,
card = card
)
}
Result.fromTangemSdkError(response.error)
}
is CompletionResult.Failure -> Result.fromTangemSdkError(response.error)
}
}
@ -103,13 +74,13 @@ class TwinCardsManager(
val issuer = getIssuers(reader).first { it.publicKey == publicKey }
return KeyPair(
publicKey = issuer.publicKey.hexToBytes(),
privateKey = issuer.privateKey.hexToBytes()
privateKey = issuer.privateKey.hexToBytes(),
)
}
private fun getAdapter(): JsonAdapter<List<Issuer>> {
return MoshiConverter.defaultMoshi().adapter(
Types.newParameterizedType(List::class.java, Issuer::class.java)
Types.newParameterizedType(List::class.java, Issuer::class.java),
)
}

View file

@ -4,8 +4,8 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.guard
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.AnalyticsAnOld
import com.tangem.tap.common.analytics.logWcEvent
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
@ -226,11 +226,7 @@ class WalletConnectManager {
}
private fun onSessionClosed(session: WCSession) {
store.state.globalState.analyticsHandler.logWcEvent(
AnalyticsAnOld.WcAnalyticsEvent.Session(
AnalyticsAnOld.WcSessionEvent.Disconnect, sessions[session]?.peerMeta?.url,
),
)
Analytics.send(WalletConnect.SessionDisconnected())
sessions.remove(session)
walletConnectRepository.removeSession(session)
store.dispatchOnMain(WalletConnectAction.RemoveSession(session))
@ -375,11 +371,7 @@ class WalletConnectManager {
),
)
}
store.state.globalState.analyticsHandler.logWcEvent(
AnalyticsAnOld.WcAnalyticsEvent.Session(
AnalyticsAnOld.WcSessionEvent.Connect, peer.url,
),
)
Analytics.send(WalletConnect.NewSessionEstablished())
}
}
}
@ -390,11 +382,11 @@ class WalletConnectManager {
}
client.onEthSendTransaction = { id: Long, transaction: WCEthereumTransaction ->
Timber.d("onEthSendTransaction: $transaction")
store.state.globalState.analyticsHandler.logWcEvent(
AnalyticsAnOld.WcAnalyticsEvent.Action(
AnalyticsAnOld.WcAction.SendTransaction,
),
)
// Analytics.logWcEvent(
// AnalyticsAnOld.WcAnalyticsEvent.Action(
// AnalyticsAnOld.WcAction.SendTransaction
// )
// )
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(
WalletConnectAction.HandleTransactionRequest(
@ -408,11 +400,11 @@ class WalletConnectManager {
}
client.onEthSignTransaction = { id: Long, transaction: WCEthereumTransaction ->
Timber.d("onEthSignTransaction: $transaction")
store.state.globalState.analyticsHandler.logWcEvent(
AnalyticsAnOld.WcAnalyticsEvent.Action(
AnalyticsAnOld.WcAction.SignTransaction,
),
)
// Analytics.logWcEvent(
// AnalyticsAnOld.WcAnalyticsEvent.Action(
// AnalyticsAnOld.WcAction.SignTransaction
// )
// )
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(
WalletConnectAction.HandleTransactionRequest(
@ -426,11 +418,11 @@ class WalletConnectManager {
}
client.onEthSign = { id: Long, message: WCEthereumSignMessage ->
Timber.d("onEthSign: $message")
store.state.globalState.analyticsHandler.logWcEvent(
AnalyticsAnOld.WcAnalyticsEvent.Action(
AnalyticsAnOld.WcAction.PersonalSign,
),
)
// Analytics.logWcEvent(
// AnalyticsAnOld.WcAnalyticsEvent.Action(
// AnalyticsAnOld.WcAction.PersonalSign
// )
// )
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(
WalletConnectAction.HandlePersonalSignRequest(
@ -502,11 +494,11 @@ class WalletConnectManager {
val message = EthSignHelper.tryToParseEthTypedMessage(request)
if (message != null) {
Timber.d("onEthSign_v4: $message")
store.state.globalState.analyticsHandler.logWcEvent(
AnalyticsAnOld.WcAnalyticsEvent.Action(
AnalyticsAnOld.WcAction.PersonalSign,
),
)
// Analytics.logWcEvent(
// AnalyticsAnOld.WcAnalyticsEvent.Action(
// AnalyticsAnOld.WcAction.PersonalSign
// )
// )
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(
WalletConnectAction.HandlePersonalSignRequest(

View file

@ -24,7 +24,8 @@ import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.crypto.CryptoUtils
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.analytics.AnalyticsAnOld
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
@ -87,8 +88,8 @@ class WalletConnectSdkHelper {
extras = EthereumTransactionExtras(
data = transaction.data.removePrefix(HEX_PREFIX).hexToBytes(),
gasLimit = gas.toBigInteger(),
nonce = transaction.nonce?.hexToBigDecimal()?.toBigInteger()
)
nonce = transaction.nonce?.hexToBigDecimal()?.toBigInteger(),
),
)
val dialogData = TransactionRequestDialogData(
dAppName = session.peerMeta.name,
@ -100,7 +101,7 @@ class WalletConnectSdkHelper {
isEnoughFundsToSend = (balance - total) >= BigDecimal.ZERO,
session = session.session,
id = id,
type = type
type = type,
)
return WcTransactionData(
type = type,
@ -108,7 +109,7 @@ class WalletConnectSdkHelper {
session = session,
id = id,
walletManager = walletManager,
dialogData = dialogData
dialogData = dialogData,
)
}
@ -122,7 +123,7 @@ class WalletConnectSdkHelper {
val blockchain = session.wallet.getBlockchainForSession()
return factory.makeWalletManager(
blockchain = blockchain,
publicKey = publicKey
publicKey = publicKey,
)
}
@ -143,10 +144,7 @@ class WalletConnectSdkHelper {
HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash
}
is SimpleResult.Failure -> {
store.state.globalState.analyticsHandler.handleBlockchainSdkErrorEvent(
result.error,
AnalyticsAnOld.ActionToLog.WalletConnectTransaction,
)
(result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.TransactionError(it)) }
Timber.e(result.error as BlockchainSdkError)
null
}
@ -172,12 +170,7 @@ class WalletConnectSdkHelper {
HEX_PREFIX + result.data
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandler.handleCardSdkErrorEvent(
error,
AnalyticsAnOld.ActionToLog.WalletConnectSign,
)
}
(result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.SignError(it)) }
Timber.e(result.error.customMessage)
null
}
@ -197,16 +190,11 @@ class WalletConnectSdkHelper {
?: session.wallet.walletPublicKey.toDecompressedPublicKey()
getBnbResultString(
key.toHexString(),
result.data.signature.toHexString()
result.data.signature.toHexString(),
)
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandler.handleCardSdkErrorEvent(
error,
AnalyticsAnOld.ActionToLog.WalletConnectTransaction,
)
}
(result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.TransactionError(it)) }
Timber.e(result.error.customMessage)
null
}
@ -231,18 +219,17 @@ class WalletConnectSdkHelper {
val prefixData = (ETH_MESSAGE_PREFIX + messageData.size.toString()).toByteArray()
val hashToSign = (prefixData + messageData).toKeccak()
val dialogData = PersonalSignDialogData(
dAppName = session.peerMeta.name,
message = messageString,
session = session.session,
id = id
id = id,
)
return WcPersonalSignData(
hash = hashToSign,
session = session,
id = id,
dialogData = dialogData
dialogData = dialogData,
)
}
@ -277,12 +264,7 @@ class WalletConnectSdkHelper {
)
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandler.handleCardSdkErrorEvent(
error,
AnalyticsAnOld.ActionToLog.WalletConnectSign,
)
}
(result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.SignError(it)) }
Timber.e(result.error.customMessage)
null
}

View file

@ -3,8 +3,9 @@ package com.tangem.tap.features.details.redux
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.tap.common.analytics.AnalyticsAnOld
import com.tangem.tap.common.analytics.AnalyticsParamAnOld
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.extensions.dispatchOnMain
@ -119,19 +120,16 @@ class DetailsMiddleware {
val card = store.state.detailsState.cardSettingsState?.card ?: return
scope.launch {
val result = tangemSdkManager.resetToFactorySettings(card)
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandler.handleCardSdkErrorEvent(
error,
AnalyticsAnOld.ActionToLog.PurgeWallet,
card = store.state.detailsState.scanResponse?.card,
)
}
when (result) {
is CompletionResult.Success -> {
Analytics.send(Settings.CardSettings.FactoryResetFinished())
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
Analytics.send(Settings.CardSettings.FactoryResetFinished(error))
}
}
}
}
}
@ -156,28 +154,20 @@ class DetailsMiddleware {
SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId)
SecurityOption.PassCode -> tangemSdkManager.setPasscode(cardId)
SecurityOption.AccessCode -> tangemSdkManager.setAccessCode(cardId)
else -> null
else -> return@launch
}
withContext(Dispatchers.Main) {
val paramValue = AnalyticsParam.SecurityMode.from(selectedOption)
when (result) {
is CompletionResult.Success -> {
selectedOption?.let {
store.dispatch(GlobalAction.UpdateSecurityOptions(it))
}
Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue))
store.dispatch(GlobalAction.UpdateSecurityOptions(selectedOption))
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Success)
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandler.handleCardSdkErrorEvent(
error = error,
action = AnalyticsAnOld.ActionToLog.ChangeSecOptions,
params = mapOf(
AnalyticsParamAnOld.NEW_SECURITY_OPTION to
(selectedOption?.name ?: ""),
),
card = store.state.detailsState.scanResponse?.card,
)
Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue, error))
}
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure)
}
@ -190,7 +180,10 @@ class DetailsMiddleware {
is DetailsAction.ManageSecurity.ChangeAccessCode -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
scope.launch {
tangemSdkManager.setAccessCode(card.cardId)
when (tangemSdkManager.setAccessCode(card.cardId)) {
is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged())
is CompletionResult.Failure -> {}
}
}
}
else -> { /* no-op */

View file

@ -2,6 +2,9 @@ package com.tangem.tap.features.details.ui.cardsettings
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.getTwinCardIdForUser
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.CardSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
@ -59,12 +62,15 @@ class CardSettingsViewModel(private val store: Store<AppState>) {
private fun handleClickingItem(item: CardInfo) {
when (item) {
is CardInfo.ChangeAccessCode -> {
Analytics.send(Settings.CardSettings.ButtonChangeUserCode(AnalyticsParam.UserCode.AccessCode))
store.dispatch(DetailsAction.ManageSecurity.ChangeAccessCode)
}
is CardInfo.ResetToFactorySettings -> {
Analytics.send(Settings.CardSettings.ButtonFactoryReset())
store.dispatch(DetailsAction.ResetToFactory.Start)
}
is CardInfo.SecurityMode -> {
Analytics.send(Settings.CardSettings.ButtonChangeSecurityMode())
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)
}
else -> {}

View file

@ -10,6 +10,8 @@ import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
@ -24,7 +26,7 @@ class DetailsFragment : Fragment(), StoreSubscriber<DetailsState> {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(Settings.ScreenOpened())
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(android.R.transition.fade)
exitTransition = inflater.inflateTransition(android.R.transition.fade)

View file

@ -162,8 +162,8 @@ fun DetailsItem(
@Composable
fun TangemSocialAccounts(
links: List<TangemLink>,
onSocialNetworkClick: (String) -> Unit,
links: List<SocialNetworkLink>,
onSocialNetworkClick: (SocialNetworkLink) -> Unit,
modifier: Modifier = Modifier,
) {
LazyRow(
@ -171,11 +171,11 @@ fun TangemSocialAccounts(
) {
items(links) {
Icon(
painter = painterResource(id = it.iconRes),
painter = painterResource(id = it.network.iconRes),
contentDescription = "",
modifier = modifier
.padding(8.dp)
.clickable { onSocialNetworkClick(it.url) },
.clickable { onSocialNetworkClick(it) },
tint = colorResource(id = R.color.icon_informative),
)
}

View file

@ -4,11 +4,11 @@ import com.tangem.wallet.R
data class DetailsScreenState(
val elements: List<SettingsElement>,
val tangemLinks: List<TangemLink>,
val tangemLinks: List<SocialNetworkLink>,
val tangemVersion: String,
val appCurrency: String,
val onItemsClick: (SettingsElement) -> Unit,
val onSocialNetworkClick: (String) -> Unit,
val onSocialNetworkClick: (SocialNetworkLink) -> Unit,
) {
val appNameRes: Int = R.string.app_name
}
@ -30,28 +30,38 @@ enum class SettingsElement(
;
}
data class TangemLink(
val iconRes: Int,
data class SocialNetworkLink(
val network: SocialNetwork,
val url: String,
)
sealed class SocialNetwork(val id: String, val iconRes: Int) {
object Telegram : SocialNetwork("Telegram", R.drawable.ic_telegram)
object Twitter : SocialNetwork("Twitter", R.drawable.ic_twitter)
object Facebook : SocialNetwork("Facebook", R.drawable.ic_facebook)
object Instagram : SocialNetwork("Instagram", R.drawable.ic_instagram)
object GitHub : SocialNetwork("GitHub", R.drawable.ic_github)
object Youtube : SocialNetwork("Youtube", R.drawable.ic_youtube)
object LinkedIn : SocialNetwork("LinkedIn", R.drawable.ic_linkedin)
}
object TangemSocialAccounts {
val accountsEn: List<TangemLink> = listOf(
TangemLink(R.drawable.ic_telegram, "https://t.me/TangemCards"),
TangemLink(R.drawable.ic_twitter, "https://twitter.com/tangem"),
TangemLink(R.drawable.ic_facebook, "https://m.facebook.com/TangemCards/"),
TangemLink(R.drawable.ic_instagram, "https://instagram.com/tangemcards"),
TangemLink(R.drawable.ic_github, "https://github.com/tangem"),
TangemLink(R.drawable.ic_youtube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
TangemLink(R.drawable.ic_linkedin, "https://www.linkedin.com/company/tangem"),
val accountsEn: List<SocialNetworkLink> = listOf(
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/TangemCards"),
SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"),
SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"),
SocialNetworkLink(SocialNetwork.Instagram, "https://instagram.com/tangemcards"),
SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"),
SocialNetworkLink(SocialNetwork.Youtube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"),
)
val accountsRu: List<TangemLink> = listOf(
TangemLink(R.drawable.ic_telegram, "https://t.me/tangem_ru"),
TangemLink(R.drawable.ic_twitter, "https://twitter.com/tangem"),
TangemLink(R.drawable.ic_facebook, "https://m.facebook.com/TangemCards/"),
TangemLink(R.drawable.ic_instagram, "https://instagram.com/tangemcards"),
TangemLink(R.drawable.ic_github, "https://github.com/tangem"),
TangemLink(R.drawable.ic_youtube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
TangemLink(R.drawable.ic_linkedin, "https://www.linkedin.com/company/tangem"),
val accountsRu: List<SocialNetworkLink> = listOf(
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_ru"),
SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"),
SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"),
SocialNetworkLink(SocialNetwork.Instagram, "https://instagram.com/tangemcards"),
SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"),
SocialNetworkLink(SocialNetwork.Youtube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"),
)
}

View file

@ -2,6 +2,8 @@ package com.tangem.tap.features.details.ui.details
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.AppState
@ -51,8 +53,9 @@ class DetailsViewModel(private val store: Store<AppState>) {
)
}
private fun handleSocialNetworkClick(url: String) {
store.dispatch(NavigationAction.OpenUrl(url))
private fun handleSocialNetworkClick(link: SocialNetworkLink) {
Analytics.send(Settings.ButtonSocialNetwork(link.network))
store.dispatch(NavigationAction.OpenUrl(link.url))
}
private fun handleClickingSettingsItem(item: SettingsElement) {
@ -61,21 +64,26 @@ class DetailsViewModel(private val store: Store<AppState>) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
}
SettingsElement.Chat -> {
Analytics.send(Settings.ButtonChat())
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
}
SettingsElement.SendFeedback -> {
Analytics.send(Settings.ButtonSendFeedback())
store.dispatch(GlobalAction.SendEmail(FeedbackEmail()))
}
SettingsElement.CardSettings -> {
Analytics.send(Settings.ButtonCardSettings())
store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings))
}
SettingsElement.AppCurrency -> {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
SettingsElement.AppSettings -> {
Analytics.send(Settings.ButtonAppSettings())
store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings)) //TODO: To be available later
}
SettingsElement.LinkMoreCards -> {
Analytics.send(Settings.ButtonCreateBackup())
store.dispatch(DetailsAction.CreateBackup)
}
SettingsElement.TermsOfService -> {
@ -90,7 +98,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
}
}
private fun getSocialLinks(): List<TangemLink> {
private fun getSocialLinks(): List<SocialNetworkLink> {
val locale = LocaleRegionProvider().getRegion()
return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) {
TangemSocialAccounts.accountsRu

View file

@ -30,6 +30,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
@ -53,7 +55,12 @@ fun WalletConnectScreen(
},
fab = {
if (!state.isLoading) {
AddSessionFab(onAddSession = { state.onAddSession(context.getFromClipboard()?.toString()) })
AddSessionFab(
onAddSession = {
Analytics.send(Settings.ButtonStartWalletConnectSession())
state.onAddSession(context.getFromClipboard()?.toString())
},
)
}
},
titleRes = R.string.wallet_connect_title,
@ -147,7 +154,10 @@ private fun WalletConnectSessions(
modifier = modifier.weight(1f),
)
IconButton(
onClick = { state.onRemoveSession(session.sessionId) },
onClick = {
Analytics.send(Settings.ButtonStopWalletConnectSession())
state.onRemoveSession(session.sessionId)
},
) {
Icon(
painter = painterResource(id = R.drawable.ic_cross_rounded_24),

View file

@ -11,6 +11,8 @@ import androidx.compose.ui.platform.ComposeView
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.home.compose.StoriesScreen
@ -28,6 +30,7 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(IntroductionProcess.ScreenOpened())
store.dispatch(HomeAction.Init)
}
@ -81,9 +84,16 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
private fun ScreenContent() {
StoriesScreen(
homeState,
onScanButtonClick = { store.dispatch(HomeAction.ReadCard) },
onShopButtonClick = { store.dispatch(HomeAction.GoToShop(store.state.globalState.userCountryCode)) },
onScanButtonClick = {
Analytics.send(IntroductionProcess.ButtonScanCard())
store.dispatch(HomeAction.ReadCard)
},
onShopButtonClick = {
Analytics.send(IntroductionProcess.ButtonBuyCards())
store.dispatch(HomeAction.GoToShop(store.state.globalState.userCountryCode))
},
onSearchTokensClick = {
Analytics.send(IntroductionProcess.ButtonTokensList())
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
store.dispatch(TokensAction.AllowToAddTokens(false))
store.dispatch(TokensAction.LoadCurrencies())

View file

@ -9,10 +9,9 @@ import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.backup.BackupService
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.backupService
import com.tangem.tap.common.analytics.AnalyticsEventAnOld
import com.tangem.tap.common.analytics.AnalyticsParamAnOld
import com.tangem.tap.common.analytics.GetCardSourceParamsAnOld
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.paramsInterceptor.BatchIdParamsInterceptor
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOpenUrl
@ -105,10 +104,6 @@ private fun handleHomeAction(appState: () -> AppState?, action: Action, dispatch
RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE -> store.dispatchOpenUrl(BUY_WALLET_URL)
else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
}
store.state.globalState.analyticsHandler.handleAnalyticsEvent(
event = AnalyticsEventAnOld.GET_CARD,
params = mapOf(AnalyticsParamAnOld.SOURCE.param to GetCardSourceParamsAnOld.WELCOME.param),
)
}
}
}
@ -158,11 +153,12 @@ private fun showDisclaimerIfNeed(scanResponse: ScanResponse, nextHandler: (ScanR
}
private fun onScanSuccess(scanResponse: ScanResponse) {
Analytics.send(IntroductionProcess.CardWasScanned())
val globalState = store.state.globalState
val tapWalletManager = globalState.tapWalletManager
tapWalletManager.updateConfigManager(scanResponse)
globalState.analyticsHandler.attachToAllEvents(AnalyticsParam.BatchId, scanResponse.card.batchId)
Analytics.addParamsInterceptor(BatchIdParamsInterceptor(scanResponse.card.batchId))
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
@ -178,7 +174,7 @@ private fun onScanSuccess(scanResponse: ScanResponse) {
val isOnboardingCase = result.data
if (isOnboardingCase) {
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
store.dispatch(OnboardingSaltPayAction.Init.SetDependencies(manager, config))
store.dispatch(OnboardingSaltPayAction.SetDependencies(manager, config))
store.dispatch(OnboardingSaltPayAction.Update)
navigateTo(AppScreen.OnboardingWallet)
} else {

View file

@ -5,6 +5,8 @@ import android.os.Bundle
import android.view.LayoutInflater
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchShare
@ -21,7 +23,7 @@ import com.tangem.wallet.databinding.DialogOnboardingAddressInfoBinding
*/
class AddressInfoBottomSheetDialog(
private val stateDialog: AppDialog.AddressInfoDialog,
context: Context
context: Context,
) : BottomSheetDialog(context) {
var binding: DialogOnboardingAddressInfoBinding? = null
@ -40,6 +42,7 @@ class AddressInfoBottomSheetDialog(
override fun show() {
super.show()
Analytics.send(Token.Recieve.ScreenOpened())
showData(data = stateDialog.addressData)
}
@ -51,10 +54,12 @@ class AddressInfoBottomSheetDialog(
imvQrCode.setImageBitmap(data.qrCode)
tvAddress.text = data.address
btnFlCopyAddress.setOnClickListener {
Analytics.send(Token.Recieve.ButtonCopyAddress())
context.copyToClipboard(data.address)
store.dispatchToastNotification(R.string.copy_toast_msg)
}
btnFlShare.setOnClickListener {
Analytics.send(Token.Recieve.ButtonShareAddress())
store.dispatchShare(data.shareUrl)
}
tvReceiveMessage.text = getQRReceiveMessage(tvReceiveMessage.context, stateDialog.currency)
@ -67,7 +72,7 @@ fun getQRReceiveMessage(context: Context, currency: Currency): String {
context.getString(
R.string.address_qr_code_message_format,
currency.blockchain.fullName,
currency.currencySymbol
currency.currencySymbol,
)
}
is Currency.Token -> {
@ -75,7 +80,7 @@ fun getQRReceiveMessage(context: Context, currency: Currency): String {
R.string.address_qr_code_message_token_format,
currency.token.name,
currency.currencySymbol,
currency.blockchain.fullName
currency.blockchain.fullName,
)
}
}

View file

@ -14,15 +14,16 @@ class OnboardingHelper {
fun isOnboardingCase(response: ScanResponse): Boolean {
val cardInfoStorage = preferencesStorage.usedCardsPrefStorage
val cardId = response.card.cardId
return when {
response.isTangemTwins() -> {
if (!response.twinsIsTwinned()) {
true
} else {
cardInfoStorage.activationIsStarted(response.card.cardId)
cardInfoStorage.isActivationInProgress(cardId)
}
}
response.card.hasWallets() -> cardInfoStorage.activationIsStarted(response.card.cardId)
response.card.hasWallets() -> cardInfoStorage.isActivationInProgress(cardId)
else -> true
}
}

View file

@ -67,8 +67,10 @@ class OnboardingManager(
}
}
return balance.copy(currency = Currency.Blockchain(
walletManager.wallet.blockchain, walletManager.wallet.publicKey.derivationPath?.rawPath)
return balance.copy(
currency = Currency.Blockchain(
walletManager.wallet.blockchain, walletManager.wallet.publicKey.derivationPath?.rawPath,
),
)
}
@ -79,6 +81,14 @@ class OnboardingManager(
fun activationFinished(cardId: String) {
usedCardsPrefStorage.activationFinished(cardId)
}
fun isActivationFinished(cardId: String): Boolean {
return usedCardsPrefStorage.isActivationFinished(cardId)
}
fun isActivationStarted(cardId: String): Boolean {
return usedCardsPrefStorage.isActivationStarted(cardId)
}
}
data class OnboardingWalletBalance(
@ -98,23 +108,23 @@ data class OnboardingWalletBalance(
companion object {
fun error(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(
state = ProgressState.Error,
error = error
error = error,
)
fun criticalError(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(
state = ProgressState.Error,
criticalError = error
criticalError = error,
)
fun loading(value: BigDecimal = BigDecimal.ZERO): OnboardingWalletBalance = OnboardingWalletBalance(
value,
state = ProgressState.Loading
state = ProgressState.Loading,
)
fun done(value: BigDecimal, hasTransactions: Boolean): OnboardingWalletBalance = OnboardingWalletBalance(
value,
hasIncomingTransaction = hasTransactions,
state = ProgressState.Done
state = ProgressState.Done,
)
}
}

View file

@ -9,6 +9,8 @@ import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import coil.load
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.navigation.ShareElement
@ -41,6 +43,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
binding.toolbar.setTitle(R.string.onboarding_title)
btnRefreshBalanceWidget = RefreshBalanceWidget(mainBinding.onboardingTopContainer.onboardingWalletContainer)
store.dispatch(OnboardingNoteAction.Init)
store.dispatch(OnboardingNoteAction.LoadCardArtwork)
store.dispatch(OnboardingNoteAction.DetermineStepOfScreen)
}
@ -108,7 +111,10 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
private fun setupCreateWalletState(state: OnboardingNoteState) =
with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
btnMainAction.setOnClickListener { store.dispatch(OnboardingNoteAction.CreateWallet) }
btnMainAction.setOnClickListener {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(OnboardingNoteAction.CreateWallet)
}
btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean)
btnAlternativeAction.setOnClickListener { }

View file

@ -10,6 +10,7 @@ import org.rekotlin.Action
*/
sealed class OnboardingNoteAction : Action {
// from user, ui
object Init : OnboardingNoteAction()
object LoadCardArtwork : OnboardingNoteAction()
object CreateWallet : OnboardingNoteAction()
object TopUp : OnboardingNoteAction()

View file

@ -4,12 +4,16 @@ import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.getAddressData
import com.tangem.tap.common.extensions.getToUpUrl
import com.tangem.tap.common.extensions.getTopUpUrl
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppDialog
@ -25,6 +29,7 @@ import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -61,6 +66,11 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
val noteState = store.state.onboardingNoteState
when (action) {
is OnboardingNoteAction.Init -> {
if (!onboardingManager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
is OnboardingNoteAction.LoadCardArtwork -> {
scope.launch {
val artworkUrl = onboardingManager.loadArtworkUrl()
@ -77,10 +87,15 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
}
is OnboardingNoteAction.SetStepOfScreen -> {
when (action.step) {
OnboardingNoteStep.CreateWallet -> {
Analytics.send(Onboarding.CreateWallet.ScreenOpened())
}
OnboardingNoteStep.TopUpWallet -> {
Analytics.send(Onboarding.Topup.ScreenOpened())
store.dispatch(OnboardingNoteAction.Balance.Update)
}
OnboardingNoteStep.Done -> {
Analytics.send(Onboarding.Finished())
onboardingManager.activationFinished(card.cardId)
postUi(DELAY_SDK_DIALOG_CLOSE) { store.dispatch(OnboardingNoteAction.Confetti.Show) }
}
@ -92,6 +107,7 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
withMainContext {
when (result) {
is CompletionResult.Success -> {
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
val updatedResponse = scanResponse.copy(card = result.data.card)
onboardingManager.scanResponse = updatedResponse
onboardingManager.activationStarted(updatedResponse.card.cardId)
@ -150,15 +166,27 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
is OnboardingNoteAction.ShowAddressInfoDialog -> {
val addressData = noteState.walletManager?.getAddressData() ?: return
Analytics.send(Onboarding.Topup.ButtonShowWalletAddress())
val appDialog = AppDialog.AddressInfoDialog(noteState.walletBalance.currency, addressData)
store.dispatchDialogShow(appDialog)
}
is OnboardingNoteAction.TopUp -> {
val topUpUrl = noteState.walletManager?.getToUpUrl() ?: return
if (globalState.userCountryCode == RUSSIA_COUNTRY_CODE) {
store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog(topUpUrl))
val walletManager = noteState.walletManager.guard {
store.dispatchDebugErrorNotification("NPE: WalletManager")
return
}
val topUpUrl = walletManager.getTopUpUrl() ?: return
val blockchain = walletManager.wallet.blockchain
if (globalState.userCountryCode == RUSSIA_COUNTRY_CODE) {
val dialogData = WalletDialog.RussianCardholdersWarningDialog.Data(topUpUrl, blockchain)
store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog(dialogData))
return
}
val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain)
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
store.dispatchOpenUrl(topUpUrl)
}
OnboardingNoteAction.Done -> {

View file

@ -8,6 +8,8 @@ import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import coil.load
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.redux.navigation.ShareElement
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
@ -33,6 +35,7 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
mainBinding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard
binding.toolbar.setTitle(R.string.onboarding_title)
store.dispatch(OnboardingOtherCardsAction.Init)
store.dispatch(OnboardingOtherCardsAction.LoadCardArtwork)
store.dispatch(OnboardingOtherCardsAction.DetermineStepOfScreen)
}
@ -68,7 +71,10 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
private fun setupCreateWalletState() = with(mainBinding.onboardingActionContainer) {
btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
btnMainAction.setOnClickListener { store.dispatch(OnboardingOtherCardsAction.CreateWallet) }
btnMainAction.setOnClickListener {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(OnboardingOtherCardsAction.CreateWallet)
}
btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean)
btnAlternativeAction.setOnClickListener { }

View file

@ -7,6 +7,7 @@ import org.rekotlin.Action
*/
sealed class OnboardingOtherCardsAction : Action {
// from user, ui
object Init : OnboardingOtherCardsAction()
object LoadCardArtwork : OnboardingOtherCardsAction()
object CreateWallet : OnboardingOtherCardsAction()

View file

@ -4,6 +4,8 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppState
@ -45,6 +47,11 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) {
val card = onboardingManager.scanResponse.card
when (action) {
is OnboardingOtherCardsAction.Init -> {
if (!onboardingManager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
is OnboardingOtherCardsAction.LoadCardArtwork -> {
scope.launch {
val artworkUrl = onboardingManager.loadArtworkUrl()
@ -59,9 +66,15 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) {
store.dispatch((OnboardingOtherCardsAction.SetStepOfScreen(step)))
}
is OnboardingOtherCardsAction.SetStepOfScreen -> {
if (action.step == OnboardingOtherCardsStep.Done) {
onboardingManager.activationFinished(card.cardId)
postUi(200) { store.dispatch(OnboardingOtherCardsAction.Confetti.Show) }
when (action.step) {
OnboardingOtherCardsStep.CreateWallet -> {
Analytics.send(Onboarding.CreateWallet.ScreenOpened())
}
OnboardingOtherCardsStep.Done -> {
Analytics.send(Onboarding.Finished())
onboardingManager.activationFinished(card.cardId)
postUi(200) { store.dispatch(OnboardingOtherCardsAction.Confetti.Show) }
}
}
}
is OnboardingOtherCardsAction.CreateWallet -> {
@ -70,8 +83,9 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) {
withMainContext {
when (result) {
is CompletionResult.Success -> {
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
val updatedResponse = onboardingManager.scanResponse.copy(
card = result.data.card
card = result.data.card,
)
onboardingManager.scanResponse = updatedResponse
onboardingManager.activationStarted(updatedResponse.card.cardId)

View file

@ -1,16 +1,20 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.extensions.Result
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.getAddressData
import com.tangem.tap.common.extensions.getToUpUrl
import com.tangem.tap.common.extensions.getTopUpUrl
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppDialog
@ -76,6 +80,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
fun finishCardActivation() {
if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) {
Analytics.send(Onboarding.Finished())
onboardingManager?.activationFinished(getScanResponse().card.cardId)
twinCardsState.pairCardId?.let { onboardingManager?.activationFinished(it) }
}
@ -85,11 +90,21 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
is TwinCardsAction.Init -> {
if (twinCardsState.currentStep == TwinCardsStep.WelcomeOnly) return
val scanResponse = getScanResponse()
onboardingManager?.apply {
if (!isActivationStarted(scanResponse.card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> {
if (preferencesStorage.wasTwinsOnboardingShown()) {
val step = when {
!getScanResponse().twinsIsTwinned() -> TwinCardsStep.CreateFirstWallet
!scanResponse.twinsIsTwinned() -> {
Analytics.send(Onboarding.CreateWallet.ScreenOpened())
TwinCardsStep.CreateFirstWallet
}
twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done
else -> TwinCardsStep.TopUpWallet
}
@ -105,8 +120,8 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
is TwinCardsAction.Wallet.HandleOnBackPressed -> {
val shouldReturnCardBack = twinCardsState.mode == CreateTwinWalletMode.CreateWallet
&& twinCardsState.currentStep != TwinCardsStep.TopUpWallet
&& twinCardsState.currentStep != TwinCardsStep.Done
&& twinCardsState.currentStep != TwinCardsStep.TopUpWallet
&& twinCardsState.currentStep != TwinCardsStep.Done
if (twinCardsState.showAlert) {
val onInterruptPrompt = {
@ -126,8 +141,10 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
is TwinCardsAction.SetStepOfScreen -> {
when (action.step) {
TwinCardsStep.WelcomeOnly -> preferencesStorage.saveTwinsOnboardingShown()
TwinCardsStep.Welcome -> preferencesStorage.saveTwinsOnboardingShown()
TwinCardsStep.WelcomeOnly, TwinCardsStep.Welcome -> {
Analytics.send(Onboarding.Twins.ScreenOpened())
preferencesStorage.saveTwinsOnboardingShown()
}
TwinCardsStep.TopUpWallet -> store.dispatch(TwinCardsAction.Balance.Update)
TwinCardsStep.Done -> {
finishCardActivation()
@ -136,16 +153,17 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
}
is TwinCardsAction.Wallet.LaunchFirstStep -> {
Analytics.send(Onboarding.Twins.SetupStarted())
val manager = TwinCardsManager(
card = getScanResponse().card,
assetReader = action.reader,
analyticsHandler = store.state.globalState.analyticsHandler
)
store.dispatch(TwinCardsAction.CardsManager.Set(manager))
scope.launch {
when (val result = manager.createFirstWallet(action.initialMessage)) {
is CompletionResult.Success -> {
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
startCardActivation(result.data.cardId)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext {
@ -164,7 +182,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
val result = manager.createSecondWallet(
action.initialMessage,
action.preparingMessage,
action.creatingWalletMessage
action.creatingWalletMessage,
)
when (result) {
is CompletionResult.Success -> {
@ -186,6 +204,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
scope.launch {
when (val result = manager.complete(action.message)) {
is Result.Success -> {
Analytics.send(Onboarding.Twins.SetupFinished())
updateScanResponse(result.data)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext {
@ -222,11 +241,11 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
val balanceIsLoading = twinCardsState.walletBalance.copy(
currency = Currency.Blockchain(
walletManager.wallet.blockchain,
walletManager.wallet.publicKey.derivationPath?.rawPath
walletManager.wallet.publicKey.derivationPath?.rawPath,
),
state = ProgressState.Loading,
error = null,
criticalError = null
criticalError = null,
)
store.dispatch(TwinCardsAction.Balance.Set(balanceIsLoading))
@ -240,7 +259,6 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
store.dispatch(TwinCardsAction.Balance.SetNonCriticalError(loadedBalance.error))
}
}
}
is TwinCardsAction.Balance.Set -> {
if (action.balance.balanceIsToppedUp()) {
@ -256,7 +274,11 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
store.dispatchDialogShow(appDialog)
}
is TwinCardsAction.TopUp -> {
val topUpUrl = twinCardsState.walletManager?.getToUpUrl() ?: return
val topUpUrl = twinCardsState.walletManager?.getTopUpUrl() ?: return
val currencyType = AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin)
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
store.dispatchOpenUrl(topUpUrl)
}
TwinCardsAction.Done -> {

View file

@ -16,6 +16,8 @@ import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tap.common.AndroidAssetReader
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.hide
@ -238,6 +240,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
btnMainAction.setOnClickListener {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(
TwinCardsAction.Wallet.LaunchFirstStep(
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),

View file

@ -5,7 +5,6 @@ import org.rekotlin.Action
sealed class OnboardingWalletAction : Action {
object Init : OnboardingWalletAction()
object StartSaltPayCardActivation : OnboardingWalletAction()
object GetToCreateWalletStep : OnboardingWalletAction()
object GetToSaltPayStep : OnboardingWalletAction()
object CreateWallet : OnboardingWalletAction()

View file

@ -3,10 +3,14 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.extensions.ifNotNull
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.backup.BackupService
import com.tangem.tap.backupService
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa
@ -61,6 +65,12 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
when (action) {
OnboardingWalletAction.Init -> {
ifNotNull(onboardingManager, card) { manager, card ->
if (!card.isSaltPay && !manager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
}
}
when {
card == null -> {
// it's possible when found unfinished backup for standard Wallet cards
@ -84,16 +94,10 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
}
else -> {
store.dispatch(OnboardingWalletAction.GetToCreateWalletStep)
Analytics.send(Onboarding.CreateWallet.ScreenOpened())
}
}
}
is OnboardingWalletAction.StartSaltPayCardActivation -> {
// we need to do this because the standard activation happens in the OnboardingWalletAction.CreateWallet
// step which is omitted for SaltPay cards
if (onboardingWalletState.isSaltPay && scanResponse != null) {
startCardActivation(scanResponse)
}
}
is OnboardingWalletAction.LoadArtwork -> {
scope.launch {
val artwork = onboardingManager?.loadArtworkUrl()
@ -112,6 +116,7 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
withMainContext {
when (result) {
is CompletionResult.Success -> {
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
//here we must use updated scanResponse after createWallet & derivation
val updatedResponse = globalState.onboardingState.onboardingManager.scanResponse.copy(
card = result.data.card,
@ -148,9 +153,6 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
} else {
val backupState = store.state.onboardingWalletState.backupState
val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)
if (onboardingWalletState.isSaltPay) {
finishCardActivation(backupState, scanResponse.card)
}
scope.launch { globalState.tapWalletManager.onCardScanned(updatedScanResponse) }
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
@ -166,6 +168,7 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
if (onboardingWalletState.isSaltPay) {
BackupAction.StartBackup
} else {
Analytics.send(Onboarding.Backup.ScreenOpened())
BackupAction.IntroduceBackup
}
} else {
@ -221,6 +224,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
when (action) {
is BackupAction.StartBackup -> {
Analytics.send(Onboarding.Backup.Started())
backupService.discardSavedBackup()
val primaryCard = scanResponse?.primaryCard
if (primaryCard != null) {
@ -266,7 +270,11 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.AddMoreBackupCards))
}
}
is BackupAction.ShowEnterAccessCodeScreen -> {
Analytics.send(Onboarding.Backup.SettingAccessCodeStarted())
}
is BackupAction.CheckAccessCode -> {
Analytics.send(Onboarding.Backup.AccessCodeEntered())
if (action.accessCode.length < 4) {
store.dispatch(BackupAction.SetAccessCodeError(AccessCodeError.CodeTooShort))
} else {
@ -275,6 +283,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
}
is BackupAction.SaveAccessCodeConfirmation -> {
Analytics.send(Onboarding.Backup.AccessCodeReEntered())
if (action.accessCodeConfirmation == backupState.accessCode) {
store.dispatch(BackupAction.SetAccessCodeError(null))
backupService.setAccessCode(action.accessCodeConfirmation)
@ -312,7 +321,9 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
}
is BackupAction.FinishBackup -> {
Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
if (!onboardingWalletState.isSaltPay) {
Analytics.send(Onboarding.Finished())
finishCardActivation(backupState, card)
}
}
@ -320,7 +331,10 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
backupService.discardSavedBackup()
}
is BackupAction.DiscardSavedBackup -> {
backupService.primaryCardId?.let { finishCardsActivationForDiscardedUnfinishedBackup(it) }
backupService.primaryCardId?.let {
Analytics.send(Onboarding.Finished())
finishCardsActivationForDiscardedUnfinishedBackup(it)
}
backupService.discardSavedBackup()
}
is BackupAction.CheckForUnfinishedBackup -> {
@ -333,6 +347,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}
is BackupAction.DismissBackup -> {
Analytics.send(Onboarding.Backup.Skipped())
if (onboardingWalletState.isSaltPay) throw UnsupportedOperationException()
store.dispatch(BackupAction.FinishBackup)
}
@ -351,7 +366,7 @@ private fun startCardActivation(scanResponse: ScanResponse) {
* Standard Wallet cards finish activation at BackupAction.FinishBackup
* SaltPay cards finish activation at OnboardingWalletAction.FinishOnboarding
*/
private fun finishCardActivation(backupState: BackupState, card: Card?) {
internal fun finishCardActivation(backupState: BackupState, card: Card?) {
(listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds)
.distinct().filterNotNull()
.forEach { cardId ->
@ -377,7 +392,7 @@ private fun initSaltPayOnBackupFinishedIfNeeded(
if (scanResponse == null) throw IllegalArgumentException()
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
store.dispatchOnMain(OnboardingSaltPayAction.Init.SetDependencies(manager, config))
store.dispatchOnMain(OnboardingSaltPayAction.SetDependencies(manager, config))
store.dispatchOnMain(OnboardingSaltPayAction.Update)
}
}

View file

@ -10,12 +10,12 @@ import java.math.BigDecimal
[REDACTED_AUTHOR]
*/
sealed class OnboardingSaltPayAction : Action {
sealed class Init : OnboardingSaltPayAction() {
data class SetDependencies(
val registrationManager: SaltPayActivationManager,
val saltPayConfig: SaltPayConfig,
) : Init()
}
object Init : OnboardingSaltPayAction()
data class SetDependencies(
val registrationManager: SaltPayActivationManager,
val saltPayConfig: SaltPayConfig,
) : OnboardingSaltPayAction()
data class SetInProgress(val isInProgress: Boolean) : OnboardingSaltPayAction()
data class SetClaimRefreshInProgress(val isInProgress: Boolean) : OnboardingSaltPayAction()

View file

@ -10,15 +10,18 @@ import com.tangem.domain.common.extensions.successOr
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.network.api.paymentology.KYCStatus
import com.tangem.network.api.paymentology.RegistrationResponse
import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.isPositive
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.redux.finishCardActivation
import com.tangem.tap.features.onboarding.products.wallet.saltPay.AllSymbolsTheSameFilter
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
@ -58,10 +61,23 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
fun getAppState(): AppState = appState()!!
fun getOnboardingWalletState(): OnboardingWalletState = getAppState().onboardingWalletState
fun getOnboardingManager(): OnboardingManager? = getAppState().globalState.onboardingState.onboardingManager
fun getState(): OnboardingSaltPayState = getOnboardingWalletState().onboardingSaltPayState!!
val analyticsHandler = getAppState().globalState.analyticsHandler
when (action) {
is OnboardingSaltPayAction.Init -> {
if (!getOnboardingWalletState().isSaltPay) return
val onboardingManager = getOnboardingManager().guard {
// onboardingManager may be null if it's started from standard Wallet unfinished backup
return
}
val card = onboardingManager.scanResponse.card
if (!onboardingManager.isActivationStarted(card.cardId)) {
Analytics.send(Onboarding.Started())
onboardingManager.activationStarted(card.cardId)
}
}
is OnboardingSaltPayAction.Update -> {
handleInProgress = true
@ -77,8 +93,6 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
}
}
is OnboardingSaltPayAction.RegisterCard -> {
analyticsHandler.handleAnalyticsEvent(Onboarding.ButtonConnect())
handleInProgress = true
val state = getState()
@ -151,7 +165,7 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
is OnboardingSaltPayAction.TrySetPin -> {
try {
assertPinValid(action.pin, getState().pinLength)
analyticsHandler.handleAnalyticsEvent(Onboarding.PinCodeSet())
Analytics.send(Onboarding.PinCodeSet())
store.dispatch(OnboardingSaltPayAction.SetPin(action.pin))
store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.CardRegistration))
} catch (error: SaltPayActivationError) {
@ -159,7 +173,6 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
}
}
is OnboardingSaltPayAction.Claim -> {
analyticsHandler.handleAnalyticsEvent(Onboarding.ButtonClaim())
val state = getState()
handleInProgress = true
@ -192,7 +205,7 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
}
handleInProgress = false
analyticsHandler.handleAnalyticsEvent(Onboarding.ClaimWasSuccessfully())
Analytics.send(Onboarding.ClaimWasSuccessfully())
dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimInProgress))
dispatchOnMain(OnboardingSaltPayAction.RefreshClaim)
}
@ -233,23 +246,31 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App
}
}
}
is OnboardingSaltPayAction.SetStep -> handleAnalytics(analyticsHandler, action.newStep)
is OnboardingSaltPayAction.SetStep -> {
when (action.newStep) {
SaltPayActivationStep.KycStart -> Analytics.send(Onboarding.KYCStarted())
SaltPayActivationStep.KycWaiting -> Analytics.send(Onboarding.KYCInProgress())
SaltPayActivationStep.KycReject -> Analytics.send(Onboarding.KYCRejected())
SaltPayActivationStep.Claim -> Analytics.send(Onboarding.ClaimScreenOpened())
SaltPayActivationStep.ClaimSuccess, SaltPayActivationStep.Success -> {
Analytics.send(Onboarding.Finished())
val onboardingManager = getOnboardingManager().guard {
// Null is possible if it is started from a standard pending backup and in this case
// it is impossible to get here
store.dispatchDebugErrorNotification("OnboardingManager can't be NULL")
return
}
finishCardActivation(getOnboardingWalletState().backupState, onboardingManager.scanResponse.card)
}
else -> {}
}
}
else -> {
/* do nothing, only reduce */
}
}
}
private fun handleAnalytics(analyticsHandler: GlobalAnalyticsEventHandler, step: SaltPayActivationStep) {
when (step) {
SaltPayActivationStep.KycStart -> analyticsHandler.handleAnalyticsEvent(Onboarding.KYCStarted())
SaltPayActivationStep.KycWaiting -> analyticsHandler.handleAnalyticsEvent(Onboarding.KYCInProgress())
SaltPayActivationStep.KycReject -> analyticsHandler.handleAnalyticsEvent(Onboarding.KYCRejected())
SaltPayActivationStep.Claim -> analyticsHandler.handleAnalyticsEvent(Onboarding.ClaimScreenOpened())
else -> {}
}
}
suspend fun SaltPayActivationManager.update(
currentStep: SaltPayActivationStep,
currentAmountToClaim: Amount?,
@ -265,7 +286,7 @@ suspend fun SaltPayActivationManager.update(
return Result.Failure(ex)
}
checkGasIfNeeded(this, newStep).successOr { return it }
// checkGasIfNeeded(this, newStep).successOr { return it }
Timber.d("update: success: %s", newStep)
return Result.Success(newStep)

View file

@ -17,7 +17,7 @@ class OnboardingSaltPayReducer {
private fun internalReduce(anyAction: Action, onboardingWalletState: OnboardingWalletState): OnboardingWalletState {
val action = anyAction as? OnboardingSaltPayAction ?: return onboardingWalletState
if (onboardingWalletState.onboardingSaltPayState == null && action is OnboardingSaltPayAction.Init.SetDependencies) {
if (onboardingWalletState.onboardingSaltPayState == null && action is OnboardingSaltPayAction.SetDependencies) {
return onboardingWalletState.copy(
onboardingSaltPayState = OnboardingSaltPayState(
saltPayManager = action.registrationManager,

View file

@ -14,6 +14,8 @@ import coil.size.Scale
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.button.MaterialButton
import com.tangem.common.extensions.guard
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.compose.PinCodeWidget
import com.tangem.tap.common.extensions.configureSettings
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
@ -178,6 +180,7 @@ internal class OnboardingSaltPayView(
showOnlyView(connectCard.root) {
progressButton = SaltPayProgressButton(connectCard.root)
connectCard.btnConnect.setOnClickListener {
Analytics.send(Onboarding.ButtonConnect())
store.dispatch(OnboardingSaltPayAction.RegisterCard)
}
}
@ -286,6 +289,7 @@ internal class OnboardingSaltPayView(
btnMain.text = getText(R.string.onboarding_button_claim)
btnMain.isEnabled
btnMain.setOnClickListener {
Analytics.send(Onboarding.ButtonClaim())
store.dispatch(OnboardingSaltPayAction.Claim)
}
progressButton = SaltPayProgressButton(actionContainer.root)

View file

@ -68,7 +68,7 @@ class WalletBackupAnimator(
when (backupCardsCounts) {
0 -> {
when (currentStep) {
STEP_CREATE_WALLET -> {
UNDEFINED, STEP_CREATE_WALLET -> {
cardsWidget.toFolded(false) { cardsWidget.toFan() }
FIRST_BACKUP.animateAlpha(0.6f, 0)

View file

@ -19,6 +19,8 @@ import com.tangem.common.CardIdFormatter
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.PropertyCalculator
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.inflate
import com.tangem.tap.common.extensions.show
@ -32,6 +34,7 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.ui.OnboardingSaltPayView
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog
import com.tangem.tap.store
@ -81,7 +84,7 @@ class OnboardingWalletFragment : BaseFragment(R.layout.fragment_onboarding_walle
addBackPressHandler(this)
store.dispatch(OnboardingWalletAction.Init)
store.dispatch(OnboardingWalletAction.StartSaltPayCardActivation)
store.dispatch(OnboardingSaltPayAction.Init)
store.dispatch(OnboardingWalletAction.LoadArtwork)
}
@ -170,7 +173,10 @@ class OnboardingWalletFragment : BaseFragment(R.layout.fragment_onboarding_walle
private fun setupCreateWalletState() = with(binding) {
layoutButtonsCommon.btnWalletMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(OnboardingWalletAction.CreateWallet) }
layoutButtonsCommon.btnWalletMainAction.setOnClickListener {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(OnboardingWalletAction.CreateWallet)
}
layoutButtonsCommon.btnWalletAlternativeAction.hide()
toolbar.title = getText(R.string.onboarding_getting_started)

View file

@ -17,9 +17,9 @@ import com.tangem.common.services.Result
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.analytics.AnalyticsAnOld
import com.tangem.tap.common.analytics.AnalyticsEventAnOld
import com.tangem.tap.common.analytics.AnalyticsParamAnOld
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
@ -219,13 +219,10 @@ private fun sendTransaction(
dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED))
tangemSdk.config.linkedTerminal = isLinkedTerminal
val currencyType = AnalyticsParam.CurrencyType.Amount(amountToSend)
when (sendResult) {
is SimpleResult.Success -> {
store.state.globalState.analyticsHandler?.handleAnalyticsEvent(
event = AnalyticsEventAnOld.TRANSACTION_IS_SENT,
card = card,
blockchain = walletManager.wallet.blockchain.currency,
)
Analytics.send(Token.Send.TransactionSent(currencyType))
dispatch(SendAction.SendSuccess)
if (externalTransactionData != null) {
@ -250,15 +247,10 @@ private fun sendTransaction(
feeAmount = feeAmount,
destinationAddress = destinationAddress,
)
store.state.globalState.analyticsHandler?.handleBlockchainSdkErrorEvent(
error = sendResult.error,
action = AnalyticsAnOld.ActionToLog.SendTransaction,
params = mapOf(AnalyticsParamAnOld.BLOCKCHAIN to walletManager.wallet.blockchain.currency),
card = card,
)
val error = (sendResult.error as? BlockchainSdkError) ?: return@withMainContext
Analytics.send(Token.Send.TransactionSent(currencyType, error))
when (error) {
is BlockchainSdkError.WrappedTangemError -> {
val tangemSdkError = (error.tangemError as? TangemSdkError) ?: return@withMainContext

View file

@ -16,6 +16,8 @@ import com.google.android.material.textfield.TextInputEditText
import com.tangem.Message
import com.tangem.tangem_sdk_new.extensions.hideSoftKeyboard
import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.common.extensions.setOnImeActionListener
@ -28,10 +30,25 @@ import com.tangem.tap.common.toggleWidget.IndeterminateProgressButtonWidget
import com.tangem.tap.common.toggleWidget.ViewStateWidget
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.*
import com.tangem.tap.features.send.redux.AmountActionUi.*
import com.tangem.tap.features.send.redux.FeeActionUi.*
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.CheckClipboard
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.PasteAddressPayId
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.SetTruncateHandler
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.TruncateOrRestore
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.AmountActionUi.CheckAmountToSend
import com.tangem.tap.features.send.redux.AmountActionUi.SetMainCurrency
import com.tangem.tap.features.send.redux.AmountActionUi.SetMaxAmount
import com.tangem.tap.features.send.redux.AmountActionUi.ToggleMainCurrency
import com.tangem.tap.features.send.redux.FeeActionUi.ChangeIncludeFee
import com.tangem.tap.features.send.redux.FeeActionUi.ChangeSelectedFee
import com.tangem.tap.features.send.redux.FeeActionUi.ToggleControlsVisibility
import com.tangem.tap.features.send.redux.ReceiptAction
import com.tangem.tap.features.send.redux.ReleaseSendState
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.SendActionUi
import com.tangem.tap.features.send.redux.TransactionExtrasAction
import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
@ -43,7 +60,12 @@ import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentSendBinding
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import java.text.DecimalFormatSymbols
/**
@ -61,6 +83,11 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
val binding: FragmentSendBinding by viewBinding(FragmentSendBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(Token.Send.ScreenOpened())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(this)
@ -101,10 +128,12 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
.launchIn(mainScope)
imvPaste.setOnClickListener {
Analytics.send(Token.Send.ButtonPaste())
store.dispatch(PasteAddressPayId(requireContext().getFromClipboard()?.toString() ?: ""))
store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused))
}
imvQrCode.setOnClickListener {
Analytics.send(Token.Send.ButtonQRCode())
startActivityForResult(
Intent(requireContext(), ScanQrCodeActivity::class.java),
ScanQrCodeActivity.SCAN_QR_REQUEST_CODE
@ -165,6 +194,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
store.dispatch(AmountAction.SetDecimalSeparator(decimalSeparator))
binding.lSendAmount.tvAmountCurrency.setOnClickListener {
Analytics.send(Token.Send.ButtonSwapCurrency())
store.dispatch(ToggleMainCurrency)
store.dispatch(ReceiptAction.RefreshReceipt)
store.dispatch(SendAction.ChangeSendButtonState(store.state.sendState.getButtonState()))

View file

@ -119,13 +119,8 @@ private fun handle(action: Action) {
is ShopAction.FinishSuccessfulOrder -> {
scope.launch {
shopService.waitForCheckout(
productType = shopState.selectedProduct,
analyticsHandler = store.state.globalState.analyticsHandler
)
shopService.waitForCheckout(shopState.selectedProduct)
}
}
}
}

View file

@ -12,6 +12,8 @@ import androidx.activity.OnBackPressedCallback
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tap.common.GlobalLayoutStateHandler
import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.shop.data.ProductType
@ -41,6 +43,8 @@ class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(Shop.ScreenOpened())
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())

View file

@ -13,6 +13,8 @@ import androidx.compose.ui.platform.ComposeView
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.redux.domainStore
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.compose.ClosePopupTrigger
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.FragmentOnBackPressedHandler
@ -28,6 +30,11 @@ class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment)
private var state: MutableState<AddCustomTokenState> = mutableStateOf(domainStore.state.addCustomTokensState)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(ManageTokens.CustomToken.ScreenOpened())
}
override fun subscribeToStore() {
domainStore.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
@ -45,30 +52,29 @@ class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
view.findViewById<Toolbar>(R.id.toolbar)?.let {
it.setTitle(R.string.add_custom_token_title)
}
requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
view.findViewById<Toolbar>(R.id.toolbar)?.setTitle(R.string.add_custom_token_title)
val closePopupTrigger = initClosingPopupTriggerEvent()
view.findViewById<ComposeView>(R.id.view_compose)?.setContent {
AppCompatTheme(requireContext()) {
Box(modifier = Modifier
.fillMaxSize()
Box(
modifier = Modifier
.fillMaxSize(),
) {
AddCustomTokenScreen(state, closePopupTrigger)
}
}
}
}
private fun initClosingPopupTriggerEvent(): ClosePopupTrigger = ClosePopupTrigger().apply {
onCloseComplete = ::handleOnBackPressed
addBackPressHandler(object : FragmentOnBackPressedHandler {
override fun handleOnBackPressed() = close()
})
addBackPressHandler(
object : FragmentOnBackPressedHandler {
override fun handleOnBackPressed() = close()
},
)
}
}

View file

@ -20,6 +20,8 @@ import com.tangem.domain.redux.domainStore
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.assetReader
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
@ -51,6 +53,7 @@ class TokensMiddleware {
handleAddingCustomToken(action)
}
is TokensAction.SetSearchInput -> {
Analytics.send(ManageTokens.TokenSearched())
handleLoadCurrencies(
scanResponse = store.state.globalState.scanResponse,
newSearchInput = action.searchInput
@ -335,6 +338,7 @@ class TokensMiddleware {
store.dispatchOnMain(NavigationAction.PopBackTo())
}
Analytics.send(ManageTokens.CustomToken.TokenWasAdded(customCurrency))
val currency = Currency.fromCustomCurrency(customCurrency)
val isNeedToDerive = isNeedToDerive(scanResponse, currency)
val currencyList = listOf(currency)

View file

@ -15,6 +15,8 @@ import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.extensions.dispatchOnMain
@ -48,12 +50,17 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(TokensAction.ResetState)
}
})
Analytics.send(ManageTokens.ScreenOpened())
activity?.onBackPressedDispatcher?.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(TokensAction.ResetState)
}
},
)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
@ -80,6 +87,7 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
val onSaveChanges = { tokens: List<TokenWithBlockchain>, blockchains: List<Blockchain> ->
Analytics.send(ManageTokens.ButtonSaveChanges())
store.dispatch(TokensAction.SaveChanges(tokens, blockchains))
}
val onNetworkItemClicked = { contractAddress: ContractAddress ->
@ -88,11 +96,11 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
}
val onLoadMore = {
store.dispatch(
TokensAction.LoadMore(
scanResponse = store.state.globalState.scanResponse
)
)
store.dispatch(
TokensAction.LoadMore(
scanResponse = store.state.globalState.scanResponse,
),
)
}
cvCurrencies.setContent {
@ -101,7 +109,7 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
tokensState = tokensState,
onSaveChanges = onSaveChanges,
onNetworkItemClicked = onNetworkItemClicked,
onLoadMore = onLoadMore
onLoadMore = onLoadMore,
)
}
}
@ -119,6 +127,7 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
return when (item.itemId) {
R.id.menu_search -> true
R.id.menu_navigate_add_custom_token -> {
Analytics.send(ManageTokens.ButtonCustomToken())
store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken)
true
}
@ -154,15 +163,18 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
}
fun SearchView.inputtedTextAsFlow(): Flow<String> = callbackFlow {
val watcher = setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
val watcher =
setOnQueryTextListener(
object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
trySend(newText ?: "")
return false
}
})
override fun onQueryTextChange(newText: String?): Boolean {
trySend(newText ?: "")
return false
}
},
)
awaitClose { (watcher) }
}

View file

@ -8,8 +8,16 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.ExtendedFloatingActionButton
import androidx.compose.material.FabPosition
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -20,6 +28,9 @@ import androidx.compose.ui.unit.dp
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.compose.Keyboard
import com.tangem.tap.common.compose.extensions.addAndNotify
import com.tangem.tap.common.compose.extensions.removeAndNotify
@ -41,7 +52,7 @@ fun CurrenciesScreen(
tokensState: MutableState<TokensState> = mutableStateOf(store.state.tokensState),
onSaveChanges: (List<TokenWithBlockchain>, List<Blockchain>) -> Unit,
onNetworkItemClicked: (ContractAddress) -> Unit,
onLoadMore: () -> Unit
onLoadMore: () -> Unit,
) {
val tokensAddedOnMainScreen = remember { tokensState.value.addedTokens }
val blockchainsAddedOnMainScreen = remember { tokensState.value.addedBlockchains }
@ -82,21 +93,21 @@ fun CurrenciesScreen(
AnimatedVisibility(
visible = tokensState.value.loadCoinsState == LoadCoinsState.LOADING,
enter = fadeIn(),
exit = fadeOut()
exit = fadeOut(),
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize(),
) {
CircularProgressIndicator(
color = Color(0xFF1ACE80)
color = Color(0xFF1ACE80),
)
}
}
AnimatedVisibility(
visible = tokensState.value.loadCoinsState == LoadCoinsState.LOADED,
enter = fadeIn(animationSpec = tween(1000)),
exit = fadeOut(animationSpec = tween(1000))
exit = fadeOut(animationSpec = tween(1000)),
) {
Column {
val showHeader = tokensState.value.scanResponse?.card?.useOldStyleDerivation == true
@ -110,7 +121,7 @@ fun CurrenciesScreen(
onAddCurrencyToggleClick(currency, token)
},
onNetworkItemClicked = onNetworkItemClicked,
onLoadMore = onLoadMore
onLoadMore = onLoadMore,
)
}
}
@ -122,29 +133,39 @@ private fun toggleBlockchain(
blockchain: Blockchain,
blockchainsAddedOnMainScreen: List<Blockchain>,
addedBlockchainsState: MutableState<List<Blockchain>>,
addedTokens: List<TokenWithBlockchain>
addedTokens: List<TokenWithBlockchain>,
) {
val isTryingToRemove = addedBlockchainsState.value.contains(blockchain)
val isAddedOnMainScreen = blockchainsAddedOnMainScreen.contains(blockchain)
val isTokenWithSameBlockchainFound = addedTokens.any { it.blockchain == blockchain }
val analyticsCurrencyTypeParam = AnalyticsParam.CurrencyType.Blockchain(blockchain)
if (isTryingToRemove) {
if (isTokenWithSameBlockchainFound) {
store.dispatchDialogShow(WalletDialog.TokensAreLinkedDialog(
currencyTitle = blockchain.name,
currencySymbol = blockchain.currency
))
store.dispatchDialogShow(
WalletDialog.TokensAreLinkedDialog(
currencyTitle = blockchain.name,
currencySymbol = blockchain.currency,
),
)
} else {
if (isAddedOnMainScreen) {
store.dispatchDialogShow(WalletDialog.RemoveWalletDialog(
currencyTitle = blockchain.name,
onOk = { addedBlockchainsState.removeAndNotify(blockchain) }
))
store.dispatchDialogShow(
WalletDialog.RemoveWalletDialog(
currencyTitle = blockchain.name,
onOk = {
analyticsCurrencyTypeParam.sendOn()
addedBlockchainsState.removeAndNotify(blockchain)
},
),
)
} else {
analyticsCurrencyTypeParam.sendOff()
addedBlockchainsState.removeAndNotify(blockchain)
}
}
} else {
analyticsCurrencyTypeParam.sendOn()
addedBlockchainsState.addAndNotify(blockchain)
}
}
@ -158,28 +179,46 @@ private fun toggleToken(
val isTryingToRemove = addedTokensState.value.contains(token)
val isAddedOnMainScreen = tokensAddedOnMainScreen.contains(token)
val isUnsupportedToken = !tokensState.canHandleToken(token)
val analyticsCurrencyTypeParam = AnalyticsParam.CurrencyType.Token(token.token)
if (isTryingToRemove) {
if (isAddedOnMainScreen) {
store.dispatchDialogShow(WalletDialog.RemoveWalletDialog(
currencyTitle = token.token.name,
onOk = { addedTokensState.removeAndNotify(token) }
))
store.dispatchDialogShow(
WalletDialog.RemoveWalletDialog(
currencyTitle = token.token.name,
onOk = {
analyticsCurrencyTypeParam.sendOff()
addedTokensState.removeAndNotify(token)
},
),
)
} else {
analyticsCurrencyTypeParam.sendOff()
addedTokensState.removeAndNotify(token)
}
} else {
if (isUnsupportedToken) {
store.dispatchDialogShow(AppDialog.SimpleOkDialogRes(
headerId = R.string.common_warning,
messageId = R.string.alert_manage_tokens_unsupported_message,
))
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
headerId = R.string.common_warning,
messageId = R.string.alert_manage_tokens_unsupported_message,
),
)
} else {
analyticsCurrencyTypeParam.sendOn()
addedTokensState.addAndNotify(token)
}
}
}
private fun AnalyticsParam.CurrencyType.sendOn() {
Analytics.send(ManageTokens.TokenSwitcherChanged(this, AnalyticsParam.OnOffState.On))
}
private fun AnalyticsParam.CurrencyType.sendOff() {
Analytics.send(ManageTokens.TokenSwitcherChanged(this, AnalyticsParam.OnOffState.Off))
}
@Composable
fun SaveChangesButton(keyboardState: Keyboard, onSaveChanges: () -> Unit) {
@ -198,6 +237,6 @@ fun SaveChangesButton(keyboardState: Keyboard, onSaveChanges: () -> Unit) {
onClick = onSaveChanges,
backgroundColor = colorResource(id = R.color.accent),
contentColor = Color.White,
modifier = Modifier.padding(bottom = padding.dp)
modifier = Modifier.padding(bottom = padding.dp),
)
}

View file

@ -15,6 +15,7 @@ import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
@ -154,7 +155,9 @@ sealed class WalletAction : Action {
object SignedHashesMultiWalletDialog : DialogAction()
object ChooseTradeActionDialog : DialogAction()
data class ChooseCurrency(val amounts: List<Amount>?) : DialogAction()
data class RussianCardholdersWarningDialog(val topUpUrl: String? = null) : DialogAction()
data class RussianCardholdersWarningDialog(
val dialogData: WalletDialog.RussianCardholdersWarningDialog.Data? = null
) : DialogAction()
object Hide : DialogAction()
}

View file

@ -3,6 +3,9 @@ package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.common.services.Result
import com.tangem.network.api.tangemTech.CurrenciesResponse
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.redux.global.GlobalAction
@ -61,6 +64,7 @@ class AppCurrencyMiddleware(
}
private fun selectCurrency(action: WalletAction.AppCurrencyAction.SelectAppCurrency) {
Analytics.send(MainScreen.MainCurrencyChanged(AnalyticsParam.CurrencyType.FiatCurrency(action.fiatCurrency)))
tapWalletManager.rates.clear()
fiatCurrenciesPrefStorage.saveAppCurrency(action.fiatCurrency)
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency))

View file

@ -5,6 +5,9 @@ import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.guard
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.safeUpdate
@ -109,6 +112,7 @@ class MultiWalletMiddleware {
WalletDialog.RemoveWalletDialog(
currencyTitle = currency.currencyName,
onOk = {
Analytics.send(ButtonRemoveToken(AnalyticsParam.CurrencyType.Currency(currency)))
store.dispatch(WalletAction.MultiWallet.RemoveWallet(currency))
store.dispatch(NavigationAction.PopBackTo())
},

View file

@ -2,8 +2,12 @@ package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.AmountType
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -19,7 +23,6 @@ import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
class TradeCryptoMiddleware {
fun handle(state: () -> AppState?, action: WalletAction.TradeCryptoAction) {
if (DemoHelper.tryHandle(state, action)) return
@ -50,6 +53,7 @@ class TradeCryptoMiddleware {
val exchangeManager = store.state.globalState.exchangeManager
val appCurrency = store.state.globalState.appCurrency
val currency = selectedWalletData.currency
Analytics.send(Token.ButtonBuy(AnalyticsParam.CurrencyType.Currency(currency)))
if (currency is Currency.Token && currency.blockchain.isTestnet()) {
val walletManager = store.state.walletState.getWalletManager(currency)
@ -62,7 +66,7 @@ class TradeCryptoMiddleware {
exchangeManager.buyErc20TestnetTokens(
card = card,
walletManager = walletManager,
token = currency.token
token = currency.token,
)
}
return
@ -73,8 +77,11 @@ class TradeCryptoMiddleware {
blockchain = currency.blockchain,
cryptoCurrencyName = currency.currencySymbol,
fiatCurrencyName = appCurrency.code,
walletAddress = addresses[0].address
)?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) }
walletAddress = addresses[0].address,
)?.let {
store.dispatchOpenUrl(it)
Analytics.send(Token.Topup.ScreenOpened())
}
}
private fun proceedSellAction() {
@ -85,29 +92,40 @@ class TradeCryptoMiddleware {
if (addresses.isEmpty()) return
val currency = selectedWalletData.currency
Analytics.send(Token.ButtonSell(AnalyticsParam.CurrencyType.Currency(currency)))
store.state.globalState.exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Sell,
blockchain = currency.blockchain,
cryptoCurrencyName = currency.currencySymbol,
fiatCurrencyName = appCurrency.code,
walletAddress = addresses[0].address
)?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) }
walletAddress = addresses[0].address,
)?.let {
store.dispatchOpenUrl(it)
Analytics.send(Token.Withdraw.ScreenOpened())
}
}
private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) {
val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return
Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(selectedWalletData.currency)))
val walletManager = store.state.walletState.getWalletManager(selectedWalletData.currency)
store.dispatchOnMain(PrepareSendScreen(
coinAmount = walletManager?.wallet?.amounts?.get(AmountType.Coin),
coinRate = selectedWalletData.fiatRate,
walletManager = walletManager
))
store.dispatchOnMain(SendAction.SendSpecificTransaction(
sendAmount = action.amount,
destinationAddress = action.destinationAddress,
transactionId = action.transactionId
))
store.dispatchOnMain(
PrepareSendScreen(
coinAmount = walletManager?.wallet?.amounts?.get(AmountType.Coin),
coinRate = selectedWalletData.fiatRate,
walletManager = walletManager,
),
)
store.dispatchOnMain(
SendAction.SendSpecificTransaction(
sendAmount = action.amount,
destinationAddress = action.destinationAddress,
transactionId = action.transactionId,
),
)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send))
}
@ -116,6 +134,6 @@ class TradeCryptoMiddleware {
store.state.globalState.exchangeManager.getSellCryptoReceiptUrl(
action = CurrencyExchangeManager.Action.Sell,
transactionId = transactionId,
)?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) }
)?.let { store.dispatchOpenUrl(it) }
}
}

View file

@ -1,5 +1,8 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.redux.AppDialog
@ -14,6 +17,9 @@ class WalletDialogsMiddleware {
store.dispatchDialogShow(WalletDialog.SignedHashesMultiWalletDialog)
}
is WalletAction.DialogAction.ChooseTradeActionDialog -> {
store.state.walletState.getSelectedWalletData()?.let {
Analytics.send(Token.ButtonExchange(AnalyticsParam.CurrencyType.Currency(it.currency)))
}
store.dispatchDialogShow(WalletDialog.ChooseTradeActionDialog)
}
is WalletAction.DialogAction.QrCode -> {
@ -21,18 +27,18 @@ class WalletDialogsMiddleware {
AppDialog.AddressInfoDialog(
currency = action.currency,
addressData = action.selectedAddress,
)
),
)
}
is WalletAction.DialogAction.ChooseCurrency -> {
store.dispatchDialogShow(
WalletDialog.SelectAmountToSendDialog(
amounts = action.amounts
)
amounts = action.amounts,
),
)
}
is WalletAction.DialogAction.RussianCardholdersWarningDialog -> {
store.dispatchDialogShow(WalletDialog.RussianCardholdersWarningDialog(action.topUpUrl))
store.dispatchDialogShow(WalletDialog.RussianCardholdersWarningDialog(action.dialogData))
}
is WalletAction.DialogAction.Hide -> {
store.dispatchDialogHide()

View file

@ -6,13 +6,13 @@ import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.analytics.AnalyticsAnOld
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchErrorNotification
@ -189,15 +189,7 @@ class WalletMiddleware {
globalState.scanResponse?.copy(card = result.data)
scanNoteResponse?.let { store.onCardScanned(scanNoteResponse) }
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandler.handleCardSdkErrorEvent(
error,
AnalyticsAnOld.ActionToLog.CreateWallet,
card = store.state.detailsState.scanResponse?.card,
)
}
}
is CompletionResult.Failure -> {}
}
}
}
@ -251,6 +243,7 @@ class WalletMiddleware {
action.context.shareText(action.address)
}
is WalletAction.ExploreAddress -> {
Analytics.send(Token.ButtonExplore())
store.dispatchOpenUrl(action.exploreUrl)
}
is WalletAction.Send -> {

View file

@ -8,7 +8,6 @@ import com.tangem.common.card.Card
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.common.analytics.AnalyticsEventAnOld
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.isGreaterThan
import com.tangem.tap.common.redux.global.GlobalState
@ -54,14 +53,10 @@ class WarningsMiddleware {
if (action.remainingSignatures != null &&
action.remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING
) {
store.state.globalState.warningManager
?.removeWarnings(
messageRes = R.string.warning_low_signatures_format
)
store.state.globalState.warningManager?.removeWarnings(R.string.warning_low_signatures_format)
addWarningMessage(
warning =
WarningMessagesManager.remainingSignaturesNotEnough(action.remainingSignatures),
autoUpdate = true
warning = WarningMessagesManager.remainingSignaturesNotEnough(action.remainingSignatures),
autoUpdate = true,
)
}
}
@ -69,7 +64,8 @@ class WarningsMiddleware {
is WalletAction.Warnings.CheckHashesCount,
is WalletAction.Warnings.CheckHashesCount.ConfirmHashesCount,
is WalletAction.Warnings.CheckHashesCount.NeedToCheckHashesCountOnline,
is WalletAction.Warnings.Set -> Unit
is WalletAction.Warnings.Set,
-> Unit
}
}
@ -81,7 +77,6 @@ class WarningsMiddleware {
preferencesStorage.appRatingLaunchObserver.foundWalletWithFunds()
}
if (preferencesStorage.appRatingLaunchObserver.isReadyToShow()) {
store.state.globalState.analyticsHandler?.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_DISPLAYED)
addWarningMessage(WarningMessagesManager.appRatingWarning(), true)
}
}
@ -118,11 +113,7 @@ class WarningsMiddleware {
if (remainingSignatures != null &&
remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING
) {
addWarningMessage(
WarningMessagesManager.remainingSignaturesNotEnough(
remainingSignatures
)
)
addWarningMessage(WarningMessagesManager.remainingSignaturesNotEnough(remainingSignatures))
}
}
@ -178,15 +169,9 @@ class WarningsMiddleware {
}
is SimpleResult.Failure ->
if (result.error is BlockchainSdkError.SignatureCountNotMatched) {
addWarningMessage(
WarningMessagesManager.alreadySignedHashesWarning(),
true
)
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true)
} else if (signedHashes > 0) {
addWarningMessage(
WarningMessagesManager.alreadySignedHashesWarning(),
true
)
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true)
}
null -> Unit
}
@ -207,7 +192,7 @@ class WarningsMiddleware {
val warningManager = store.state.globalState.warningManager ?: return emptyList()
return warningManager.getWarnings(
WarningMessage.Location.MainScreen,
store.state.walletState.blockchains
store.state.walletState.blockchains,
)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.wallet.redux.models
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.StateDialog
import com.tangem.wallet.R
@ -31,5 +32,7 @@ sealed interface WalletDialog : StateDialog {
val titleRes: Int = R.string.token_details_unable_hide_alert_title
}
data class RussianCardholdersWarningDialog(val topUpUrl: String?) : WalletDialog
data class RussianCardholdersWarningDialog(val data: Data?) : WalletDialog {
data class Data(val topUpUrl: String, val blockchain: Blockchain)
}
}

View file

@ -17,6 +17,12 @@ fun List<WalletData>.calculateTotalFiatAmount(): BigDecimal {
.reduce(BigDecimal::plus)
}
fun List<WalletData>.calculateTotalCryptoAmount(): BigDecimal {
return this
.map { it.currencyData.amount ?: BigDecimal.ZERO }
.reduce(BigDecimal::plus)
}
private fun List<WalletData>.mapToProgressState(): List<ProgressState> {
return this.map { wallet ->
if (wallet.fiatRate == null) ProgressState.Error
@ -25,12 +31,15 @@ private fun List<WalletData>.mapToProgressState(): List<ProgressState> {
BalanceStatus.VerifiedOnline,
BalanceStatus.SameCurrencyTransactionInProgress,
BalanceStatus.TransactionInProgress,
BalanceStatus.NoAccount -> ProgressState.Done
BalanceStatus.NoAccount,
-> ProgressState.Done
BalanceStatus.Unreachable,
BalanceStatus.EmptyCard,
BalanceStatus.UnknownBlockchain -> ProgressState.Error
BalanceStatus.UnknownBlockchain,
-> ProgressState.Error
BalanceStatus.Loading,
null -> ProgressState.Loading
null,
-> ProgressState.Loading
}
}
}
@ -41,20 +50,24 @@ private infix fun ProgressState.or(newState: ProgressState): ProgressState {
ProgressState.Loading,
ProgressState.Refreshing,
ProgressState.Error,
ProgressState.Done -> this
ProgressState.Done,
-> this
}
ProgressState.Done,
ProgressState.Error -> when (newState) {
ProgressState.Error,
-> when (newState) {
ProgressState.Loading,
ProgressState.Refreshing,
ProgressState.Error -> newState
ProgressState.Error,
-> newState
ProgressState.Done -> this
}
ProgressState.Refreshing -> when (newState) {
ProgressState.Loading -> this
ProgressState.Refreshing,
ProgressState.Error,
ProgressState.Done -> newState
ProgressState.Done,
-> newState
}
}
}

View file

@ -19,6 +19,9 @@ import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.DetailsScreen
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.appendIfNotNull
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.fitChipsByGroupWidth
@ -59,12 +62,17 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(WalletAction.MultiWallet.SelectWallet(null))
store.dispatch(NavigationAction.PopBackTo())
}
})
Analytics.send(DetailsScreen.ScreenOpened())
activity?.onBackPressedDispatcher?.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(WalletAction.MultiWallet.SelectWallet(null))
store.dispatch(NavigationAction.PopBackTo())
}
},
)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
@ -125,7 +133,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
view?.findViewById<View>(R.id.l_balance)?.let { view ->
TestActions.initFor(
view = view,
actions = TestWallet.solanaRentExemptWarning()
actions = TestWallet.solanaRentExemptWarning(),
)
}
}
@ -149,13 +157,15 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
binding.srlWalletDetails.setOnRefreshListener {
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
store.dispatch(WalletAction.LoadWallet(
blockchain = BlockchainNetwork(
selectedWallet.currency.blockchain,
selectedWallet.currency.derivationPath,
emptyList()
)
)
Analytics.send(Token.Refreshed())
store.dispatch(
WalletAction.LoadWallet(
blockchain = BlockchainNetwork(
selectedWallet.currency.blockchain,
selectedWallet.currency.derivationPath,
emptyList(),
),
),
)
}
}
@ -183,7 +193,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
tvCurrencyTitle.text = currencyData.currency
tvCurrencySubtitle.text = tvCurrencySubtitle.getString(
R.string.wallet_currency_subtitle,
currency.blockchain.fullName
currency.blockchain.fullName,
)
}
@ -225,7 +235,6 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
)
}
private fun showPendingTransactionsIfPresent(pendingTransactions: List<PendingTransaction>) {
pendingTransactionAdapter.submitList(pendingTransactions)
binding.rvPendingTransaction.show(pendingTransactions.isNotEmpty())
@ -256,8 +265,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
store.dispatch(
WalletAction.ExploreAddress(
state.walletAddresses.selectedAddress.exploreUrl,
requireContext()
)
requireContext(),
),
)
}
ivQrCode.setImageBitmap(state.walletAddresses.selectedAddress.shareUrl.toQrCode())
@ -272,7 +281,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
binding.srlWalletDetails.isRefreshing = false
(activity as? SnackbarHandler)?.showSnackbar(
text = R.string.wallet_notification_no_internet,
buttonTitle = R.string.common_retry
buttonTitle = R.string.common_retry,
) { store.dispatch(WalletAction.LoadData) }
}
} else {
@ -292,7 +301,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
lBalance.tvStatus.setLoadingStatus(R.string.wallet_balance_loading)
}
BalanceStatus.VerifiedOnline, BalanceStatus.SameCurrencyTransactionInProgress,
BalanceStatus.TransactionInProgress -> {
BalanceStatus.TransactionInProgress,
-> {
lBalanceError.root.hide()
lBalance.root.show()
lBalance.groupBalance.show()
@ -315,7 +325,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
lBalance.tvError.show()
lBalance.tvError.setWarningStatus(
R.string.wallet_balance_blockchain_unreachable,
data.errorMessage
data.errorMessage,
)
}
BalanceStatus.NoAccount -> {
@ -325,7 +335,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
lBalanceError.tvErrorDescriptions.text =
getString(
R.string.wallet_error_no_account_subtitle_format,
data.amountToCreateAccount, data.currencySymbol
data.amountToCreateAccount, data.currencySymbol,
)
}
}
@ -364,7 +374,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
private fun TextView.setStatus(
text: String,
@ColorRes color: Int,
@DrawableRes drawable: Int?
@DrawableRes drawable: Int?,
) {
this.text = text
setTextColor(getColor(color))

View file

@ -16,6 +16,11 @@ import coil.load
import coil.size.Scale
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.tap.MainActivity
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.converters.BasicSignInEventConverter
import com.tangem.tap.common.analytics.converters.BasicTopUpEventConverter
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Portfolio
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.redux.global.GlobalAction
@ -52,6 +57,8 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
Analytics.send(MainScreen.ScreenOpened())
activity?.onBackPressedDispatcher?.addCallback(
this,
object : OnBackPressedCallback(true) {
@ -71,8 +78,6 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
state.select { it.walletState }
}
walletView.setFragment(this, binding)
// store.dispatch(WalletAction.UpdateWallet(force = false))
// store.dispatch(WalletAction.LoadWallet())
}
override fun onStop() {
@ -86,6 +91,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
binding.toolbar.setNavigationOnClickListener {
Analytics.send(MainScreen.ButtonScanCard())
store.dispatch(WalletAction.Scan)
}
setupWarningsRecyclerView()
@ -115,6 +121,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun newState(state: WalletState) {
if (activity == null || view == null) return
handleBasicAnalyticsEvent(state)
val isSaltPay = store.state.globalState.scanResponse?.card?.isSaltPay == true
when {
@ -152,6 +159,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
if (state.state != ProgressState.Loading ||
state.state != ProgressState.Refreshing
) {
Analytics.send(Portfolio.Refreshed())
store.dispatch(WalletAction.LoadData.Refresh)
}
}
@ -221,4 +229,11 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
if (store.state.walletState.shouldShowDetails) inflater.inflate(R.menu.menu_wallet, menu)
}
private fun handleBasicAnalyticsEvent(state: WalletState) {
val scanResponse = store.state.globalState.scanResponse ?: return
BasicSignInEventConverter(scanResponse).convert(state)?.let { Analytics.send(it) }
BasicTopUpEventConverter(scanResponse).convert(state)?.let { Analytics.send(it) }
}
}

View file

@ -9,6 +9,8 @@ import androidx.recyclerview.widget.RecyclerView
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Portfolio
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
@ -20,8 +22,7 @@ import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ItemCurrencyWalletBinding
class WalletAdapter
: ListAdapter<WalletData, WalletAdapter.WalletsViewHolder>(DiffUtilCallback) {
class WalletAdapter : ListAdapter<WalletData, WalletAdapter.WalletsViewHolder>(DiffUtilCallback) {
override fun getItemId(position: Int): Long {
return currentList[position].currencyData.currencySymbol?.hashCode()?.toLong() ?: 0
@ -30,7 +31,7 @@ class WalletAdapter
fun submitList(
list: List<WalletData>,
primaryBlockchain: Blockchain?,
primaryToken: Token? = null
primaryToken: Token? = null,
) {
// We used this method to sort the list of currencies. Sorting is disabled for now.
super.submitList(list)
@ -38,7 +39,7 @@ class WalletAdapter
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WalletsViewHolder {
val layout = ItemCurrencyWalletBinding.inflate(
LayoutInflater.from(parent.context), parent, false
LayoutInflater.from(parent.context), parent, false,
)
return WalletsViewHolder(layout)
}
@ -49,11 +50,11 @@ class WalletAdapter
object DiffUtilCallback : DiffUtil.ItemCallback<WalletData>() {
override fun areContentsTheSame(
oldItem: WalletData, newItem: WalletData
oldItem: WalletData, newItem: WalletData,
) = oldItem == newItem
override fun areItemsTheSame(
oldItem: WalletData, newItem: WalletData
oldItem: WalletData, newItem: WalletData,
) = oldItem == newItem
}
@ -112,6 +113,7 @@ class WalletAdapter
if (wallet.walletAddresses != null) {
cardWallet.setOnClickListener {
Analytics.send(Portfolio.TokenTapped())
store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet))
}
} else {

View file

@ -7,7 +7,9 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.play.core.review.ReviewManagerFactory
import com.tangem.tap.common.analytics.AnalyticsEventAnOld
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.extensions.getActivity
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.common.extensions.getString
@ -26,7 +28,7 @@ class WarningMessagesAdapter : ListAdapter<WarningMessage, WarningMessageVH>(Dif
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WarningMessageVH {
val binding = LayoutWarningCardActionBinding.inflate(
LayoutInflater.from(parent.context), parent, false
LayoutInflater.from(parent.context), parent, false,
)
return WarningMessageVH(binding)
}
@ -60,7 +62,7 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
tvMessage.text = getString(
resId = warning.messageResId,
default = warning.message,
formatArgs = warning.messageFormatArg
formatArgs = warning.messageFormatArg,
)
}
@ -98,7 +100,7 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
}
}
val buttonTitle = binding.root.getString(
warning.buttonTextId ?: R.string.how_to_got_it_button
warning.buttonTextId ?: R.string.how_to_got_it_button,
)
binding.btnGotIt.setOnClickListener(buttonAction)
binding.btnGotIt.text = buttonTitle
@ -107,14 +109,13 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
binding.groupControlsTemporary.hide()
binding.groupControlsRating.show()
binding.btnClose.show()
val analyticsHandler = store.state.globalState.analyticsHandler
binding.btnClose.setOnClickListener {
analyticsHandler?.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_DISMISS)
Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Closed))
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(WalletAction.Warnings.AppRating.RemindLater)
}
binding.btnCanBeBetter.setOnClickListener {
analyticsHandler?.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_NEGATIVE)
Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Disliked))
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(GlobalAction.SendEmail(RateCanBeBetterEmail()))
@ -122,7 +123,7 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
binding.btnReallyCool.setOnClickListener {
val activity = binding.root.context.getActivity() ?: return@setOnClickListener
analyticsHandler?.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_POSITIVE)
Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Liked))
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
val reviewManager = ReviewManagerFactory.create(activity)
val task = reviewManager.requestReviewFlow()

View file

@ -4,15 +4,21 @@ import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.store
import com.tangem.wallet.databinding.DialogRussiansCardholdersWarningBinding
class RussianCardholdersWarningBottomSheetDialog(
context: Context, private val topUpUrl: String?,
context: Context,
private val dialogData: WalletDialog.RussianCardholdersWarningDialog.Data?,
) : BottomSheetDialog
(context) {
@ -20,6 +26,7 @@ class RussianCardholdersWarningBottomSheetDialog(
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(Token.Topup.P2PScreenOpened())
binding = DialogRussiansCardholdersWarningBinding
.inflate(LayoutInflater.from(context))
.also { setContentView(it.root) }
@ -33,8 +40,10 @@ class RussianCardholdersWarningBottomSheetDialog(
}
binding?.btnYes?.setOnClickListener {
if (topUpUrl != null) {
store.dispatchOpenUrl(topUpUrl)
if (dialogData != null) {
val currencyType = AnalyticsParam.CurrencyType.Blockchain(dialogData.blockchain)
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
store.dispatchOpenUrl(dialogData.topUpUrl)
} else {
store.dispatch(WalletAction.TradeCryptoAction.Buy(checkUserLocation = false))
}

View file

@ -2,6 +2,8 @@ package com.tangem.tap.features.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.feedback.ScanFailsEmail
import com.tangem.tap.common.redux.global.GlobalAction
@ -19,6 +21,7 @@ class ScanFailsDialog {
setTitle(context.getString(R.string.common_warning))
setMessage(R.string.alert_troubleshooting_scan_card_title)
setPositiveButton(R.string.alert_button_request_support) { _, _ ->
Analytics.send(IntroductionProcess.ButtonRequestSupport())
store.dispatch(GlobalAction.SendEmail(ScanFailsEmail()))
}
setNeutralButton(R.string.alert_troubleshooting_scan_card_ok) { _, _ -> }

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