diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index c0d6e566b4..5074f09e8d 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -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 = 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) { diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index a1739329b5..4d1c3734ea 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -153,7 +153,7 @@ class DialogManager : StoreSubscriber { primaryButtonAction = state.dialog.onOk, ) is WalletDialog.RussianCardholdersWarningDialog -> - RussianCardholdersWarningBottomSheetDialog(context, state.dialog.topUpUrl) + RussianCardholdersWarningBottomSheetDialog(context, state.dialog.data) else -> null } dialog?.show() diff --git a/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt b/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt new file mode 100644 index 0000000000..8a6ee52f27 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt @@ -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() + private val paramsInterceptors = mutableMapOf() + private val analyticsFilters = mutableSetOf() + + private val analyticsHandlers: List + 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 { + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventAnOld.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventAnOld.kt deleted file mode 100644 index 41cc0d9b96..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventAnOld.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsFactory.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsFactory.kt new file mode 100644 index 0000000000..8936adfc3c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsFactory.kt @@ -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() + private val filters = mutableListOf() + private val interceptors = mutableListOf() + + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsInterfaces.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsInterfaces.kt deleted file mode 100644 index 6fd6fc7dc8..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsInterfaces.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.tangem.tap.common.analytics - - diff --git a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandlerBuilder.kt b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandlerBuilder.kt deleted file mode 100644 index 8e2b439e08..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandlerBuilder.kt +++ /dev/null @@ -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() - - 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) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt deleted file mode 100644 index d1bef7d01b..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt +++ /dev/null @@ -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) {} - override fun handleErrorEvent(error: Throwable, params: Map) {} - override fun handleCardSdkErrorEvent( - error: TangemSdkError, - action: AnalyticsAnOld.ActionToLog, - params: Map, - card: Card?, - ) { - } - - override fun handleBlockchainSdkErrorEvent( - error: BlockchainError, - action: AnalyticsAnOld.ActionToLog, - params: Map, - card: Card?, - ) { - } - - override fun handleShopifyOrderEvent(order: Storefront.Order) {} - } - } - } -} - -class GlobalAnalyticsHandler( - private val analyticsHandlers: List, -) : GlobalAnalyticsEventHandler { - - private val attachToAllEventsParams: MutableMap = mutableMapOf() - - override fun attachToAllEvents(key: String, value: String) { - attachToAllEventsParams[key] = value - } - - override fun handleEvent(event: String, params: Map) { - 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, - card: Card?, - blockchain: String?, - ) { - analyticsHandlers.forEach { it.handleAnalyticsEvent(event, params, card, blockchain) } - } - - override fun handleErrorEvent(error: Throwable, params: Map) { - analyticsHandlers.filterIsInstance().forEach { - it.logErrorEvent(error, params) - } - } - - override fun handleCardSdkErrorEvent( - error: TangemSdkError, - action: AnalyticsAnOld.ActionToLog, - params: Map, - card: Card?, - ) { - analyticsHandlers.filterIsInstance().forEach { - it.handleCardSdkErrorEvent(error, action, params, card) - } - } - - override fun handleBlockchainSdkErrorEvent( - error: BlockchainError, - action: AnalyticsAnOld.ActionToLog, - params: Map, - card: Card?, - ) { - analyticsHandlers.filterIsInstance().forEach { - it.handleBlockchainSdkErrorEvent(error, action, params, card) - } - } - - override fun handleShopifyOrderEvent(order: Storefront.Order) { - analyticsHandlers.filterIsInstance().forEach { - it.handleShopifyOrderEvent(order) - } - } - - override fun prepareParams(card: Card?, blockchain: String?, params: Map): Map { - 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(), - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/api/EventFilterApi.kt b/app/src/main/java/com/tangem/tap/common/analytics/api/EventFilterApi.kt new file mode 100644 index 0000000000..04d6dbbb01 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/api/EventFilterApi.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/api/EventHandlerApi.kt b/app/src/main/java/com/tangem/tap/common/analytics/api/EventHandlerApi.kt index 08e1609e3c..344402f7f1 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/api/EventHandlerApi.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/api/EventHandlerApi.kt @@ -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 = 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 = emptyMap()) + + override fun send(event: AnalyticsEvent) { + send(prepareEventString(event), event.params) } - @Deprecated("Migrate to AnalyticsEvent") - fun handleAnalyticsEvent( - event: AnalyticsEventAnOld, - params: Map = emptyMap(), - card: Card? = null, - blockchain: String? = null, - ) { - handleEvent(event.event, prepareParams(card, blockchain, params)) - } - - fun prepareParams( - card: Card? = null, - blockchain: String? = null, - params: Map = emptyMap(), - ): Map = 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 = 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 = 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 = mapOf(), - card: Card? = null, - ) -} - -interface ShopifyOrderEventHandler { - fun handleShopifyOrderEvent(order: Storefront.Order) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/api/ParamsInterceptorApi.kt b/app/src/main/java/com/tangem/tap/common/analytics/api/ParamsInterceptorApi.kt new file mode 100644 index 0000000000..245ef32da5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/api/ParamsInterceptorApi.kt @@ -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) +} + +interface ParamsInterceptorHolder { + fun addParamsInterceptor(interceptor: ParamsInterceptor) + fun removeParamsInterceptor(interceptor: ParamsInterceptor): ParamsInterceptor? +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/AnalyticsErrorConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/AnalyticsErrorConverter.kt new file mode 100644 index 0000000000..b6fab47d07 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/AnalyticsErrorConverter.kt @@ -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> { + 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 { + return when (value) { + is BlockchainSdkError -> BlockchainSdkErrorConverter(CardSdkErrorConverter()).convert(value) + is TangemSdkError -> CardSdkErrorConverter().convert(value) + else -> ThrowableErrorConverter().convert(value) + } + } +} + +private class ThrowableErrorConverter : Converter> { + override fun convert(value: Throwable): Map { + val unknown = "unknown" + return mapOf( + AnalyticsParam.ErrorKey to AnalyticsParam.Error.App.value, + AnalyticsParam.ErrorDescription to (value.message ?: value.cause?.message ?: unknown), + ) + } +} + +class CardSdkErrorConverter : Converter> { + + override fun convert(value: TangemSdkError): Map { + 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> { + + override fun convert(value: BlockchainSdkError): Map { + 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}", + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt new file mode 100644 index 0000000000..3950f458bc --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt @@ -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 { + + 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 { + + 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): 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 +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt new file mode 100644 index 0000000000..e64b751f4f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt @@ -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 { + + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/SdkErrorConverters.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/SdkErrorConverters.kt deleted file mode 100644 index 5229df8174..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/converters/SdkErrorConverters.kt +++ /dev/null @@ -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, -) - -internal class CardSdkErrorConverter : Converter { - - 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, - val cardParams: Map, - ) -} - -internal class BlockchainSdkErrorConverter( - private val cardSdkErrorConverter: CardSdkErrorConverter, -) : Converter { - - 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, - val cardParams: Map, - ) -} - -internal fun Map.convertToParamValue(): Map { - return mutableMapOf().also { newMap -> - this.forEach { - newMap[it.key.param] = it.value - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt new file mode 100644 index 0000000000..ddbd1683bd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt @@ -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, Shop.Purchased> { + + override fun convert(value: Pair): 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsEvent.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsEvent.kt index 0451fdd68c..b2db44fe52 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsEvent.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsEvent.kt @@ -6,5 +6,7 @@ package com.tangem.tap.common.analytics.events sealed class AnalyticsEvent( val category: String, val event: String, - val params: Map = mapOf(), + var params: Map = mapOf(), + val error: Throwable? = null, + var filterData: Any? = null, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index 8c69239429..bad36d3f0d 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -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" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt index bb37145672..80afe6d3f8 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt @@ -6,10 +6,11 @@ package com.tangem.tap.common.analytics.events sealed class Basic( event: String, params: Map = 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, + ) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt index c6825718b7..e19202bf4f 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt @@ -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), ) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt index e5e3fdb96a..f944f58d24 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt @@ -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 = 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() + } + } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt index b65408b7d5..5634c25afa 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt @@ -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") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt index 56893d2f31..38725048a7 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt @@ -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 = 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 = 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, ) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt index c09bf89ab3..9634f2929b 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt index afe5da529a..7445e31f33 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt @@ -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 = 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 = 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, ) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt index 624145c63d..1ff587b2aa 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt @@ -6,9 +6,13 @@ package com.tangem.tap.common.analytics.events sealed class WalletConnect( event: String, params: Map = 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) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/filters/BasicSignInFilter.kt b/app/src/main/java/com/tangem/tap/common/analytics/filters/BasicSignInFilter.kt new file mode 100644 index 0000000000..a582ea34f1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/filters/BasicSignInFilter.kt @@ -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() + + 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 +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/filters/BasicTopUpFilter.kt b/app/src/main/java/com/tangem/tap/common/analytics/filters/BasicTopUpFilter.kt new file mode 100644 index 0000000000..be780ccd2d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/filters/BasicTopUpFilter.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/filters/ShopPurchasedEventFilter.kt b/app/src/main/java/com/tangem/tap/common/analytics/filters/ShopPurchasedEventFilter.kt new file mode 100644 index 0000000000..dffa123240 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/filters/ShopPurchasedEventFilter.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt index 27132a1f61..c76fe21d38 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt @@ -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) { + override fun id(): String = ID + + override fun send(event: String, params: Map) { 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) } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt index b800259b5f..f60d2edfcf 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt @@ -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) { logger.logEvent(event, params) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt index 0acfb1dc2f..c3968d9d72 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt @@ -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) { + override fun id(): String = ID + + override fun send(event: String, params: Map) { 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> { - - override fun convert(value: Storefront.Order): Map { - 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) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt index 0d3737a161..8e87061b2c 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerLogClient.kt @@ -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) { logger.logEvent(event, params) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt index 3ab6326670..109e61d0a5 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt @@ -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) { + override fun id(): String = ID + + override fun send(event: String, params: Map) { client.logEvent(event, params) } - override fun handleErrorEvent(error: Throwable, params: Map) { + 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) { client.logErrorEvent(error, params) } - override fun handleCardSdkErrorEvent( - error: TangemSdkError, - action: AnalyticsAnOld.ActionToLog, - params: Map, - 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, - 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> { - override fun convert(value: Storefront.Order): Map { - 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) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt index ccacc09f9e..e460f9dbf9 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt @@ -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) { logger.logEvent(event, params) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/BatchIdParamsInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/BatchIdParamsInterceptor.kt new file mode 100644 index 0000000000..ffe48cf198 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/BatchIdParamsInterceptor.kt @@ -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) { + params[AnalyticsParam.BatchId] = batchId + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt index 4be902ebf7..9e58542ecb 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt @@ -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) } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 15d02907bf..cda489f536 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -55,11 +55,10 @@ suspend fun WalletManager.safeUpdate(): Result = 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, diff --git a/app/src/main/java/com/tangem/tap/common/moshi/Adapters.kt b/app/src/main/java/com/tangem/tap/common/moshi/Adapters.kt new file mode 100644 index 0000000000..c0ec09ea9c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/moshi/Adapters.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index b226be3401..c1e6d85f11 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index 6f5af6bb92..0b13471f65 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index b0cefc6d9e..820506ab18 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index a02172db23..11c84ed856 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -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(), ) { diff --git a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt index c8bd0a5405..271886dea1 100644 --- a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt +++ b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt @@ -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() private val variants = mutableMapOf() @@ -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 { 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 { 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) + } } } diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 70576a26b9..7d6db12a26 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -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? = null, messageRes: Int? = null, ): CompletionResult { - 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, ) { - 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, - ) - } } } diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 252d3585e6..459191b260 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -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 { override fun proceed( card: Card, session: CardSession, - callback: (result: CompletionResult) -> Unit + callback: (result: CompletionResult) -> 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) -> 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() diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt index feb58ca6f0..c1f7001ebc 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt @@ -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 { 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> { return MoshiConverter.defaultMoshi().adapter( - Types.newParameterizedType(List::class.java, Issuer::class.java) + Types.newParameterizedType(List::class.java, Issuer::class.java), ) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt index f95b187f09..bfd63f82d0 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index eccfbaefdf..cb5fa5b527 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -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 } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 3595380cb6..b316098ff4 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -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 */ diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index b238ffe8f2..44f5cdb8d2 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -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) { 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 -> {} diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt index 6674371319..2c206736e5 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt @@ -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 { 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) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt index e0b4f1f186..47946d1e73 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt @@ -162,8 +162,8 @@ fun DetailsItem( @Composable fun TangemSocialAccounts( - links: List, - onSocialNetworkClick: (String) -> Unit, + links: List, + 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), ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt index 1962a53261..a92efc7e2b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt @@ -4,11 +4,11 @@ import com.tangem.wallet.R data class DetailsScreenState( val elements: List, - val tangemLinks: List, + val tangemLinks: List, 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 = 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 = 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 = 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 = 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"), ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 1de39fa063..c48aeab31d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -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) { ) } - 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) { 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) { } } - private fun getSocialLinks(): List { + private fun getSocialLinks(): List { val locale = LocaleRegionProvider().getRegion() return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) { TangemSocialAccounts.accountsRu diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt index 168bd4a024..499f31ddcf 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt @@ -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), diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index 1d5243d02b..c06f921c2f 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -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 { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + Analytics.send(IntroductionProcess.ScreenOpened()) store.dispatch(HomeAction.Init) } @@ -81,9 +84,16 @@ class HomeFragment : Fragment(), StoreSubscriber { 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()) diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 75ce02a22a..c3c0145ec8 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -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 { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/AddressInfoBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/AddressInfoBottomSheetDialog.kt index 50faf24376..4635716436 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/AddressInfoBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/AddressInfoBottomSheetDialog.kt @@ -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, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index f2227fcc32..2917a052fe 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -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 } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt index 8284efcd3d..853044464b 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt @@ -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, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt index a54a27bd28..82d575fafe 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt @@ -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() { 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() { 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 { } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteAction.kt index 5b377fd072..8e7046f114 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteAction.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index abe0536c76..5ecccaa431 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -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 -> { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt index 0ed23143da..f01eb6fbcd 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt @@ -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 { + 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) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index 47dd842d1d..aa6de330d6 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -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 -> { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt index d4c6826a3c..450e9ecd50 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt @@ -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() { 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)), diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt index 885f57fa99..f3ff070f97 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 8438c8b760..dfe53282ed 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -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) } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayAction.kt index 009f2350ad..a2194e9568 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayAction.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt index 51a593e33c..69f3e0a674 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayReducer.kt index 264829c157..71570fd52e 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayReducer.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/ui/OnboardingSaltPayView.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/ui/OnboardingSaltPayView.kt index 90321f426a..dc934546aa 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/ui/OnboardingSaltPayView.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/ui/OnboardingSaltPayView.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupAnimator.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupAnimator.kt index d57205ce46..66f90f159c 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupAnimator.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupAnimator.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index 47b6beb3aa..59b784620f 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 22cd5ad13a..da468c2636 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 4837aa7449..9fe57ae5bd 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -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())) diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt index ceba9a5217..f7f126c05f 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt @@ -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) } } - } - } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt index 51fe6cb54c..338561267b 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt @@ -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()) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/AddCustomTokenFragment.kt index 2192fd0477..3954882be5 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/AddCustomTokenFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/AddCustomTokenFragment.kt @@ -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 = 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(R.id.toolbar)?.let { - it.setTitle(R.string.add_custom_token_title) - } + requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + view.findViewById(R.id.toolbar)?.setTitle(R.string.add_custom_token_title) val closePopupTrigger = initClosingPopupTriggerEvent() view.findViewById(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() + }, + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index 04eb4ac5b0..6ccf1b9b58 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt index 9a9052115c..36b1f82337 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt @@ -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, blockchains: List -> + 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 = 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) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt index 49886cbf86..7cf23fb44c 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt @@ -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 = mutableStateOf(store.state.tokensState), onSaveChanges: (List, List) -> 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, addedBlockchainsState: MutableState>, - addedTokens: List + addedTokens: List, ) { 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), ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index b9451b117a..02e6073066 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -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?) : DialogAction() - data class RussianCardholdersWarningDialog(val topUpUrl: String? = null) : DialogAction() + data class RussianCardholdersWarningDialog( + val dialogData: WalletDialog.RussianCardholdersWarningDialog.Data? = null + ) : DialogAction() object Hide : DialogAction() } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt index f1fb6371cc..40613f791e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt @@ -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)) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index b457d7e622..ca702cc439 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -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()) }, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index f5040c42f4..5d6a9fc2d7 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -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) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt index 5191319318..f94e6def86 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index ea47afc58a..5e245d4038 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -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 -> { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt index 8bf72ef3c7..112c3a0afb 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt @@ -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, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/models/WalletDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/models/WalletDialog.kt index b4188f8dd8..fc182e2c4f 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/models/WalletDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/models/WalletDialog.kt @@ -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) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TotalBalanceOperations.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TotalBalanceOperations.kt index 15e44ea361..79470dae62 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TotalBalanceOperations.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TotalBalanceOperations.kt @@ -17,6 +17,12 @@ fun List.calculateTotalFiatAmount(): BigDecimal { .reduce(BigDecimal::plus) } +fun List.calculateTotalCryptoAmount(): BigDecimal { + return this + .map { it.currencyData.amount ?: BigDecimal.ZERO } + .reduce(BigDecimal::plus) +} + private fun List.mapToProgressState(): List { return this.map { wallet -> if (wallet.fiatRate == null) ProgressState.Error @@ -25,12 +31,15 @@ private fun List.mapToProgressState(): List { 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 } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index 281d4b3938..9d3dfca291 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -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(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) { 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)) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index 87a31563fa..544d94e6ca 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -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(DiffUtilCallback) { +class WalletAdapter : ListAdapter(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, 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() { 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 { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt index 57e8e81f77..dd3bcee91c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt @@ -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(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() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt index b634f76c68..b8022e2724 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt @@ -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)) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt index 91afcc3819..d3bc64fa17 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt @@ -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) { _, _ -> } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index 9d4fe68894..d4172e31ef 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -6,6 +6,9 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.tangem.common.card.Card import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.tap.common.analytics.Analytics +import com.tangem.tap.common.analytics.events.MainScreen +import com.tangem.tap.common.analytics.events.Portfolio import com.tangem.tap.common.extensions.animateVisibility import com.tangem.tap.common.extensions.formatAmountAsSpannedString import com.tangem.tap.common.extensions.getQuantityString @@ -85,6 +88,7 @@ class MultiWalletView : WalletView() { binding.btnAddToken.setOnClickListener { val card = store.state.globalState.scanResponse!!.card + Analytics.send(Portfolio.ButtonManageTokens()) store.dispatch( TokensAction.LoadCurrencies( supportedBlockchains = CurrenciesRepository.getBlockchains( @@ -112,6 +116,7 @@ class MultiWalletView : WalletView() { ) = with(binding.lWalletBackupWarning) { root.isVisible = showBackupWarning root.setOnClickListener { + Analytics.send(MainScreen.NoticeBackupYourWalletTapped()) store.dispatch(WalletAction.MultiWallet.BackupWallet) } } @@ -122,6 +127,7 @@ class MultiWalletView : WalletView() { ) = with(binding.lWalletRescanWarning) { root.isVisible = showRescanWarning root.setOnClickListener { + Analytics.send(MainScreen.NoticeScanYourCardTapped()) store.dispatch(WalletAction.MultiWallet.ScanToGetDerivations) } } diff --git a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt index fad5cd8acd..0e213fc01b 100644 --- a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt @@ -4,7 +4,7 @@ import android.app.Application import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit -import com.tangem.common.json.MoshiJsonConverter +import com.tangem.network.common.MoshiConverter import java.util.* class PreferencesStorage(applicationContext: Application) { @@ -16,15 +16,17 @@ class PreferencesStorage(applicationContext: Application) { val usedCardsPrefStorage: UsedCardsPrefStorage val fiatCurrenciesPrefStorage: FiatCurrenciesPrefStorage val disclaimerPrefStorage: DisclaimerPrefStorage + val toppedUpWalletStorage: ToppedUpWalletStorage init { incrementLaunchCounter() appRatingLaunchObserver = AppRatingLaunchObserver(preferences, getCountOfLaunches()) - usedCardsPrefStorage = UsedCardsPrefStorage(preferences, MoshiJsonConverter.INSTANCE) + usedCardsPrefStorage = UsedCardsPrefStorage(preferences, MoshiConverter.INSTANCE) usedCardsPrefStorage.migrate() - fiatCurrenciesPrefStorage = FiatCurrenciesPrefStorage(preferences, MoshiJsonConverter.INSTANCE) + fiatCurrenciesPrefStorage = FiatCurrenciesPrefStorage(preferences, MoshiConverter.INSTANCE) fiatCurrenciesPrefStorage.migrate() disclaimerPrefStorage = DisclaimerPrefStorage(preferences) + toppedUpWalletStorage = ToppedUpWalletStorage(preferences, MoshiConverter.INSTANCE) } var chatFirstLaunchTime: Long? @@ -33,11 +35,6 @@ class PreferencesStorage(applicationContext: Application) { fun getCountOfLaunches(): Int = preferences.getInt(APP_LAUNCH_COUNT_KEY, 1) - @Deprecated("Use UsedCardsPrefStorage instead") - fun wasCardScannedBefore(cardId: String): Boolean { - return usedCardsPrefStorage.wasScanned(cardId) - } - fun saveTwinsOnboardingShown() { preferences.edit { putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true) } } diff --git a/app/src/main/java/com/tangem/tap/persistence/ToppedUpWalletStorage.kt b/app/src/main/java/com/tangem/tap/persistence/ToppedUpWalletStorage.kt new file mode 100644 index 0000000000..e270b2b820 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/persistence/ToppedUpWalletStorage.kt @@ -0,0 +1,76 @@ +package com.tangem.tap.persistence + +import android.content.SharedPreferences +import androidx.core.content.edit +import com.squareup.moshi.FromJson +import com.squareup.moshi.ToJson +import com.tangem.common.json.MoshiJsonConverter +import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.filters.BasicTopUpFilter +import timber.log.Timber + +/** +[REDACTED_AUTHOR] + */ +class ToppedUpWalletStorage( + private val preferences: SharedPreferences, + private val jsonConverter: MoshiJsonConverter, +) { + + private val walletList: MutableSet = mutableSetOf() + + init { + walletList.addAll(restore()) + } + + fun save(userWalletInfo: BasicTopUpFilter.Data): Boolean { + walletList.removeAll { it.walletId == userWalletInfo.walletId } + walletList.add(userWalletInfo) + return save(walletList) + } + + fun restore(walletId: String): BasicTopUpFilter.Data? { + return walletList.firstOrNull { it.walletId == walletId } + } + + private fun save(userWallets: MutableSet): Boolean { + return try { + val json = jsonConverter.toJson(userWallets) + preferences.edit(true) { putString(KEY, json) } + true + } catch (ex: Exception) { + Timber.e(ex) + false + } + } + + private fun restore(): MutableSet { + val json = preferences.getString(KEY, null) ?: return mutableSetOf() + return try { + val typedList = jsonConverter.typedList(BasicTopUpFilter.Data::class.java) + val listData = jsonConverter.fromJson>(json, typedList)!! + listData.toMutableSet() + } catch (ex: Exception) { + preferences.edit(true) { remove(KEY) } + mutableSetOf() + } + } + + companion object { + private const val KEY = "userWalletsInfo" + } +} + +class CardBalanceStateAdapter { + @ToJson + fun toJson(src: AnalyticsParam.CardBalanceState): String = src.value + + @FromJson + fun fromJson(json: String): AnalyticsParam.CardBalanceState { + return when (json) { + AnalyticsParam.CardBalanceState.Empty.value -> AnalyticsParam.CardBalanceState.Empty + AnalyticsParam.CardBalanceState.Full.value -> AnalyticsParam.CardBalanceState.Full + else -> throw IllegalArgumentException() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/persistence/UsedCardsPrefStorage.kt b/app/src/main/java/com/tangem/tap/persistence/UsedCardsPrefStorage.kt index 5d7f241fb8..f814eb7e16 100644 --- a/app/src/main/java/com/tangem/tap/persistence/UsedCardsPrefStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/UsedCardsPrefStorage.kt @@ -11,16 +11,16 @@ import timber.log.Timber */ class UsedCardsPrefStorage( private val preferences: SharedPreferences, - private val jsonConverter: MoshiJsonConverter + private val jsonConverter: MoshiJsonConverter, ) { - internal fun migrate() { - val scannedIds = preferences.getString(SCANNED_CARDS_IDS_KEY, null) ?: return + private val migrationList = mutableListOf( + UserCardInfoToV2(this), + ) - val usedCardsInfo = scannedIds.split(",").map { UsedCardInfo(it.trim(), true) } - if (save(usedCardsInfo.toMutableList())) { - preferences.edit { this.remove(SCANNED_CARDS_IDS_KEY) } - } + internal fun migrate() { + migrationList.forEach { it.migrate() } + migrationList.clear() } fun scanned(cardId: String) { @@ -43,20 +43,28 @@ class UsedCardsPrefStorage( save(foundItem, restoredList) } - fun isActivationFinished(cardId: String): Boolean { - return !(findCardInfo(cardId)?.isActivationStarted ?: true) + fun activationFinished(cardId: String) { + val restoredList = restore() + var foundItem = findCardInfo(cardId, restoredList) ?: UsedCardInfo(cardId) + foundItem = foundItem.copy( + isActivationStarted = true, + isActivationFinished = true, + ) + + save(foundItem, restoredList) } - fun activationIsStarted(cardId: String): Boolean { + fun isActivationStarted(cardId: String): Boolean { return findCardInfo(cardId)?.isActivationStarted ?: false } - fun activationFinished(cardId: String) { - val restoredList = restore() - val foundItem = findCardInfo(cardId, restoredList)?.copy(isActivationStarted = false) - ?: UsedCardInfo(cardId, isActivationStarted = false) + fun isActivationFinished(cardId: String): Boolean { + return findCardInfo(cardId)?.isActivationFinished ?: false + } - save(foundItem, restoredList) + fun isActivationInProgress(cardId: String): Boolean { + val cardInfo = findCardInfo(cardId) ?: return false + return cardInfo.isActivationStarted && !cardInfo.isActivationFinished } private fun findCardInfo(cardId: String, list: MutableList? = null): UsedCardInfo? { @@ -74,7 +82,7 @@ class UsedCardsPrefStorage( private fun save(list: MutableList): Boolean { return try { val json = jsonConverter.toJson(list) - preferences.edit { putString(USED_CARDS_INFO, json) } + preferences.edit { putString(USED_CARDS_INFO_V2, json) } true } catch (ex: Exception) { Timber.e(ex) @@ -83,23 +91,64 @@ class UsedCardsPrefStorage( } private fun restore(): MutableList { - val json = preferences.getString(USED_CARDS_INFO, null) ?: return mutableListOf() + val json = preferences.getString(USED_CARDS_INFO_V2, null) ?: return mutableListOf() return try { jsonConverter.fromJson(json, jsonConverter.typedList(UsedCardInfo::class.java))!! } catch (ex: Exception) { - preferences.edit(true) { remove(USED_CARDS_INFO) } + preferences.edit(true) { remove(USED_CARDS_INFO_V2) } mutableListOf() } } companion object { + private const val USED_CARDS_INFO_V2 = "usedCardsInfo_v2" private const val USED_CARDS_INFO = "usedCardsInfo" - private const val SCANNED_CARDS_IDS_KEY = "scannedCardIds" } + + private class UserCardInfoToV2( + private val storage: UsedCardsPrefStorage, + ) : Migration { + override fun migrate() { + val restoredCardsInfo = restore() + if (restoredCardsInfo.isEmpty()) return + + val newCardsInfo = restoredCardsInfo.map { cardInfo -> + UsedCardInfo( + cardId = cardInfo.cardId, + isScanned = cardInfo.isScanned, + isActivationStarted = true, + isActivationFinished = !cardInfo.isActivationStarted, + ) + }.toMutableList() + storage.save(newCardsInfo) + } + + private fun restore(): MutableList { + val json = storage.preferences.getString(USED_CARDS_INFO, null) ?: return mutableListOf() + return try { + storage.jsonConverter.fromJson(json, storage.jsonConverter.typedList(UsedCardInfoOld::class.java))!! + } catch (ex: Exception) { + mutableListOf() + } finally { + storage.preferences.edit(true) { remove(USED_CARDS_INFO) } + } + } + } + + private data class UsedCardInfo( + val cardId: String, + val isScanned: Boolean = false, + val isActivationStarted: Boolean = false, + val isActivationFinished: Boolean = false, + ) + + private data class UsedCardInfoOld( + val cardId: String, + val isScanned: Boolean = false, + val isActivationStarted: Boolean = false, + ) } -private data class UsedCardInfo( - val cardId: String, - val isScanned: Boolean = false, - val isActivationStarted: Boolean = false, -) \ No newline at end of file +private interface Migration { + fun migrate() +} \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/common/MoshiConverter.kt b/network/src/main/java/com/tangem/network/common/MoshiConverter.kt index 1f967032e3..47b5cc4b64 100644 --- a/network/src/main/java/com/tangem/network/common/MoshiConverter.kt +++ b/network/src/main/java/com/tangem/network/common/MoshiConverter.kt @@ -1,38 +1,29 @@ package com.tangem.network.common -import com.squareup.moshi.FromJson +import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi -import com.squareup.moshi.ToJson -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.common.json.MoshiJsonConverter -import com.tangem.common.json.TangemSdkAdapter import retrofit2.Converter import retrofit2.converter.moshi.MoshiConverterFactory -import java.math.BigDecimal /** [REDACTED_AUTHOR] */ -class MoshiConverter { +object MoshiConverter { - companion object { - fun createFactory(moshi: Moshi = defaultMoshi()): Converter.Factory = MoshiConverterFactory.create(moshi) + var INSTANCE = MoshiJsonConverter() + private set - fun defaultMoshi(): Moshi = Moshi.Builder() - .add(BigDecimalAdapter) - .add(KotlinJsonAdapterFactory()) - .add(TangemSdkAdapter.DerivationPathAdapter()) - .add(TangemSdkAdapter.DerivationNodeAdapter()) - .build() - - fun sdkMoshi(): Moshi = MoshiJsonConverter.INSTANCE.moshi + fun reInitInstance( + adapters: List = listOf(), + typedAdapters: Map, JsonAdapter<*>> = mapOf(), + ) { + INSTANCE = MoshiJsonConverter(adapters, typedAdapters) } -} -object BigDecimalAdapter { - @FromJson - fun fromJson(string: String) = BigDecimal(string) + fun createFactory(moshi: Moshi = INSTANCE.moshi): Converter.Factory = MoshiConverterFactory.create(moshi) - @ToJson - fun toJson(value: BigDecimal) = value.toString() + fun defaultMoshi(): Moshi = INSTANCE.moshi + + fun sdkMoshi(): Moshi = MoshiJsonConverter.INSTANCE.moshi } \ No newline at end of file