From 5899667c14e5310a7445d5394d54de6d1059c2ab Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Oct 2022 18:36:42 +0500 Subject: [PATCH 001/372] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 20 ++++- ...GlobalAnalyticsHandler.kt => Analytics.kt} | 87 +++++++------------ .../analytics/AnalyticsHandlersFactory.kt | 23 +++++ .../common/analytics/AnalyticsInterfaces.kt | 3 - .../analytics/GlobalAnalyticsEventHandler.kt | 18 ++++ .../GlobalAnalyticsEventHandlerBuilder.kt | 79 ----------------- .../common/analytics/api/EventHandlerApi.kt | 41 +++++---- .../amplitude/AmplitudeAnalyticsHandler.kt | 17 +++- .../handlers/amplitude/AmplitudeLogClient.kt | 5 +- .../appsFlyer/AppsFlyerAnalyticsHandler.kt | 18 +++- .../handlers/appsFlyer/AppsFlyerLogClient.kt | 5 +- .../firebase/FirebaseAnalyticsHandler.kt | 31 +++++-- .../handlers/firebase/FirebaseLogClient.kt | 5 +- .../tap/common/redux/global/GlobalState.kt | 4 +- .../tap/common/shop/TangemShopService.kt | 2 +- .../com/tangem/tap/domain/TangemSdkManager.kt | 2 +- .../tap/domain/twins/TwinCardsManager.kt | 6 +- .../walletconnect/WalletConnectSdkHelper.kt | 8 +- .../details/redux/DetailsMiddleware.kt | 4 +- .../redux/OnboardingSaltPayMiddleware.kt | 16 ++-- .../send/redux/middlewares/SendMiddleware.kt | 2 +- .../redux/middlewares/WalletMiddleware.kt | 2 +- 22 files changed, 204 insertions(+), 194 deletions(-) rename app/src/main/java/com/tangem/tap/common/analytics/{GlobalAnalyticsHandler.kt => Analytics.kt} (60%) create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandlersFactory.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/analytics/AnalyticsInterfaces.kt create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandler.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandlerBuilder.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index c0d6e566b4..1913ac6e16 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -14,7 +14,12 @@ 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.AnalyticsHandlersFactory +import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder +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 @@ -102,14 +107,21 @@ class TapApplication : Application(), ImageLoaderFactory { } private fun initAnalytics(application: Application, config: Config) { - val globalAnalyticsHandler = GlobalAnalyticsEventHandlerBuilder( + val factory = AnalyticsHandlersFactory() + factory.addBuilder(AmplitudeAnalyticsHandler.Builder()) + factory.addBuilder(AppsFlyerAnalyticsHandler.Builder()) + factory.addBuilder(FirebaseAnalyticsHandler.Builder()) + + val buildData = AnalyticsHandlerBuilder.Data( application = application, config = config, isDebug = BuildConfig.DEBUG, logConfig = LogConfig.analyticsHandlers, jsonConverter = MoshiJsonConverter.INSTANCE, - ).default() - store.dispatch(GlobalAction.SetGlobalAnalyticsHandler(globalAnalyticsHandler)) + ) + factory.build(buildData).forEach { + Analytics.addHandler(it.id(), it) + } } private fun initFeedbackManager(context: Context, preferencesStorage: PreferencesStorage, config: Config) { diff --git a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt similarity index 60% rename from app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt rename to app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt index d1bef7d01b..9eaee82a3a 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt @@ -7,68 +7,43 @@ 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 { +/** +[REDACTED_AUTHOR] + */ +object Analytics : GlobalAnalyticsEventHandler { - fun attachToAllEvents(key: String, value: String) + private val handlers = mutableMapOf() - 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 analyticsHandlers: List + get() = handlers.values.toList() private val attachToAllEventsParams: MutableMap = mutableMapOf() + override fun id(): String = analyticsHandlers.joinToString(", ") { it.id() } + + override fun addHandler(name: String, handler: AnalyticsEventHandler) { + handlers[name] = handler + } + + override fun removeHandler(name: String): AnalyticsEventHandler? { + return handlers.remove(name) + } + 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 send(event: String, params: Map) { + analyticsHandlers.forEach { it.send(event, params) } } - override fun handleAnalyticsEvent( - event: AnalyticsEvent, - card: Card?, - blockchain: String?, - ) { - analyticsHandlers.forEach { it.handleAnalyticsEvent(event, card, blockchain) } + override fun send(event: AnalyticsEvent, card: Card?, blockchain: String?) { + analyticsHandlers.forEach { it.send(event, card, blockchain) } } override fun handleAnalyticsEvent( @@ -77,40 +52,42 @@ class GlobalAnalyticsHandler( card: Card?, blockchain: String?, ) { - analyticsHandlers.forEach { it.handleAnalyticsEvent(event, params, card, blockchain) } + analyticsHandlers.forEach { + it.handleAnalyticsEvent(event, params, card, blockchain) + } } - override fun handleErrorEvent(error: Throwable, params: Map) { + override fun send(error: Throwable, params: Map) { analyticsHandlers.filterIsInstance().forEach { it.logErrorEvent(error, params) } } - override fun handleCardSdkErrorEvent( + override fun send( error: TangemSdkError, action: AnalyticsAnOld.ActionToLog, params: Map, card: Card?, ) { analyticsHandlers.filterIsInstance().forEach { - it.handleCardSdkErrorEvent(error, action, params, card) + it.send(error, action, params, card) } } - override fun handleBlockchainSdkErrorEvent( + override fun send( error: BlockchainError, action: AnalyticsAnOld.ActionToLog, params: Map, card: Card?, ) { analyticsHandlers.filterIsInstance().forEach { - it.handleBlockchainSdkErrorEvent(error, action, params, card) + it.send(error, action, params, card) } } - override fun handleShopifyOrderEvent(order: Storefront.Order) { + override fun send(order: Storefront.Order) { analyticsHandlers.filterIsInstance().forEach { - it.handleShopifyOrderEvent(order) + it.send(order) } } @@ -136,7 +113,7 @@ fun GlobalAnalyticsEventHandler.logWcEvent(event: AnalyticsAnOld.WcAnalyticsEven AnalyticsParamAnOld.WALLET_CONNECT_ACTION.param to event.action?.name, AnalyticsParamAnOld.ERROR_DESCRIPTION.param to event.error.message, ).filterNotNull() - handleErrorEvent(event.error, params) + send(event.error, params) } is AnalyticsAnOld.WcAnalyticsEvent.InvalidRequest -> handleAnalyticsEvent( diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandlersFactory.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandlersFactory.kt new file mode 100644 index 0000000000..a287f6fac2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandlersFactory.kt @@ -0,0 +1,23 @@ +package com.tangem.tap.common.analytics + +import com.tangem.tap.common.analytics.api.AnalyticsEventHandler +import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder + +/** +[REDACTED_AUTHOR] + */ +class AnalyticsHandlersFactory { + + private val builders = mutableListOf() + + fun addBuilder(builder: AnalyticsHandlerBuilder) { + builders.add(builder) + } + + fun build(data: AnalyticsHandlerBuilder.Data): List { + val handlers = builders.mapNotNull { it.build(data) } + builders.clear() + + return handlers + } +} \ 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/GlobalAnalyticsEventHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandler.kt new file mode 100644 index 0000000000..139ffa5582 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandler.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.common.analytics + +import com.tangem.tap.common.analytics.api.AnalyticsEventHandler +import com.tangem.tap.common.analytics.api.ErrorEventHandler +import com.tangem.tap.common.analytics.api.SdkErrorEventHandler +import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler + +interface GlobalAnalyticsEventHandler : AnalyticsEventHandler, + ErrorEventHandler, + SdkErrorEventHandler, + ShopifyOrderEventHandler { + + fun addHandler(name: String, handler: AnalyticsEventHandler) + + fun removeHandler(name: String): AnalyticsEventHandler? + + fun attachToAllEvents(key: String, value: String) +} 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/api/EventHandlerApi.kt b/app/src/main/java/com/tangem/tap/common/analytics/api/EventHandlerApi.kt index 08e1609e3c..ea77e75dfd 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,31 +1,30 @@ package com.tangem.tap.common.analytics.api +import android.app.Application 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.common.json.MoshiJsonConverter +import com.tangem.domain.common.AnalyticsHandlersLogConfig 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.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 id(): String - fun handleAnalyticsEvent( - event: AnalyticsEvent, - card: Card? = null, - blockchain: String? = null, - ) { - handleEvent( + fun send(event: String, params: Map = emptyMap()) + + fun send(event: AnalyticsEvent, card: Card? = null, blockchain: String? = null) { + send( event = prepareEventString(event.category, event.event), params = prepareParams(card, blockchain, event.params), ) @@ -38,7 +37,7 @@ interface AnalyticsEventHandler { card: Card? = null, blockchain: String? = null, ) { - handleEvent(event.event, prepareParams(card, blockchain, params)) + send(event.event, prepareParams(card, blockchain, params)) } fun prepareParams( @@ -57,7 +56,7 @@ interface AnalyticsEventHandler { } interface ErrorEventHandler { - fun handleErrorEvent( + fun send( error: Throwable, params: Map = emptyMap(), ) @@ -66,7 +65,7 @@ interface ErrorEventHandler { interface SdkErrorEventHandler : CardSdkErrorEventHandler, BlockchainSdkErrorEventHandler interface CardSdkErrorEventHandler { - fun handleCardSdkErrorEvent( + fun send( error: TangemSdkError, action: AnalyticsAnOld.ActionToLog, params: Map = emptyMap(), @@ -75,7 +74,7 @@ interface CardSdkErrorEventHandler { } interface BlockchainSdkErrorEventHandler { - fun handleBlockchainSdkErrorEvent( + fun send( error: BlockchainError, action: AnalyticsAnOld.ActionToLog, params: Map = mapOf(), @@ -84,5 +83,17 @@ interface BlockchainSdkErrorEventHandler { } interface ShopifyOrderEventHandler { - fun handleShopifyOrderEvent(order: Storefront.Order) + fun send(order: Storefront.Order) } + +interface AnalyticsHandlerBuilder { + fun build(data: Data): AnalyticsEventHandler? + + data class Data( + val application: Application, + val config: Config, + val isDebug: Boolean, + val logConfig: AnalyticsHandlersLogConfig, + val jsonConverter: MoshiJsonConverter, + ) +} \ 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..5a03cc2f36 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.AnalyticsHandlerBuilder class AmplitudeAnalyticsHandler( private val client: AmplitudeAnalyticsClient, ) : AnalyticsEventHandler { - 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): AnalyticsEventHandler? = when { + !data.isDebug -> AmplitudeClient(data.application, data.config.amplitudeApiKey) + data.isDebug && data.logConfig.appsFlyer -> 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..008b6189b9 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 @@ -5,22 +5,34 @@ 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.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler class AppsFlyerAnalyticsHandler( private val client: AppsFlyerAnalyticsClient, ) : AnalyticsEventHandler, ShopifyOrderEventHandler { - 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)) + override fun send(order: Storefront.Order) { + send(ORDER_EVENT, OrderToParamsConverter().convert(order)) } companion object { const val ORDER_EVENT = AFInAppEventType.PURCHASE + const val ID = "AppsFlyer" + } + + class Builder : AnalyticsHandlerBuilder { + override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsEventHandler? = when { + !data.isDebug -> AppsFlyerClient(data.application, data.config.appsFlyerDevKey) + data.isDebug && data.logConfig.appsFlyer -> AppsFlyerLogClient(data.jsonConverter) + else -> null + }?.let { AppsFlyerAnalyticsHandler(it) } } } 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..65ef31ac8e 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 @@ -9,6 +9,7 @@ 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.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 @@ -19,15 +20,17 @@ class FirebaseAnalyticsHandler( private val client: FirebaseAnalyticsClient, ) : AnalyticsEventHandler, ErrorEventHandler, SdkErrorEventHandler, ShopifyOrderEventHandler { - 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(error: Throwable, params: Map) { client.logErrorEvent(error, params) } - override fun handleCardSdkErrorEvent( + override fun send( error: TangemSdkError, action: AnalyticsAnOld.ActionToLog, params: Map, @@ -36,11 +39,11 @@ class FirebaseAnalyticsHandler( val model = CardSdkErrorConverter.Model(error, action, params, prepareParams(card)) val converter = CardSdkErrorConverter() converter.convert(model)?.let { - handleErrorEvent(it.throwable, it.params) + send(it.throwable, it.params) } } - override fun handleBlockchainSdkErrorEvent( + override fun send( error: BlockchainError, action: AnalyticsAnOld.ActionToLog, params: Map, @@ -50,12 +53,24 @@ class FirebaseAnalyticsHandler( val model = BlockchainSdkErrorConverter.Model(error, action, params, prepareParams(card)) val converter = BlockchainSdkErrorConverter(CardSdkErrorConverter()) converter.convert(model)?.let { - handleErrorEvent(it.throwable, it.params) + send(it.throwable, it.params) } } - override fun handleShopifyOrderEvent(order: Storefront.Order) { - handleEvent(FirebaseClient.ORDER_EVENT, OrderToParamsConverter().convert(order)) + override fun send(order: Storefront.Order) { + send(FirebaseClient.ORDER_EVENT, OrderToParamsConverter().convert(order)) + } + + companion object { + const val ID = "Firebase" + } + + class Builder : AnalyticsHandlerBuilder { + override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsEventHandler? = when { + !data.isDebug -> FirebaseClient() + data.isDebug && data.logConfig.firebase -> FirebaseLogClient(data.jsonConverter) + else -> null + }?.let { FirebaseAnalyticsHandler(it) } } } 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/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index a02172db23..b31226d6da 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,6 +1,7 @@ package com.tangem.tap.common.redux.global import com.tangem.domain.common.ScanResponse +import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.feedback.FeedbackManager @@ -27,11 +28,10 @@ data class GlobalState( val dialog: StateDialog? = null, val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(), val resources: AndroidResources = AndroidResources(), - val analyticsHandler: GlobalAnalyticsEventHandler = GlobalAnalyticsEventHandler.stub(), + val analyticsHandler: GlobalAnalyticsEventHandler = Analytics, 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..d26f38e508 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 @@ -199,7 +199,7 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) { suspend fun waitForCheckout(productType: ProductType, analyticsHandler: GlobalAnalyticsEventHandler?) { val result = shopifyService.checkout(true, checkouts[productType]!!.id) result.onSuccess { - ifNotNull(analyticsHandler, it.order) { handler, order -> handler.handleShopifyOrderEvent(order) } + ifNotNull(analyticsHandler, it.order) { handler, order -> handler.send(order) } } } 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 296b47185b..94bf9fcaec 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -91,7 +91,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co } is CompletionResult.Failure -> (result.error as? TangemSdkError)?.let { error -> - analyticsHandler?.handleCardSdkErrorEvent( + analyticsHandler?.send( error = error, action = AnalyticsAnOld.ActionToLog.Scan, ) 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..7a39dc917d 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 @@ -37,7 +37,7 @@ class TwinCardsManager( is CompletionResult.Success -> currentCardPublicKey = response.data.wallet.publicKey.toHexString() is CompletionResult.Failure -> { (response.error as? TangemSdkError)?.let { error -> - analyticsHandler?.handleCardSdkErrorEvent( + analyticsHandler?.send( error, AnalyticsAnOld.ActionToLog.CreateWallet, card = card @@ -67,7 +67,7 @@ class TwinCardsManager( } is CompletionResult.Failure -> { (response.error as? TangemSdkError)?.let { error -> - analyticsHandler?.handleCardSdkErrorEvent( + analyticsHandler?.send( error, AnalyticsAnOld.ActionToLog.CreateWallet, card = card @@ -87,7 +87,7 @@ class TwinCardsManager( is CompletionResult.Success -> Result.Success(response.data) is CompletionResult.Failure -> { (response.error as? TangemSdkError)?.let { error -> - analyticsHandler?.handleCardSdkErrorEvent( + analyticsHandler?.send( error, AnalyticsAnOld.ActionToLog.WriteIssuerData, card = card 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 f90eb7cbb9..c09bfcbfe9 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 @@ -143,7 +143,7 @@ class WalletConnectSdkHelper { HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash } is SimpleResult.Failure -> { - store.state.globalState.analyticsHandler?.handleBlockchainSdkErrorEvent( + store.state.globalState.analyticsHandler?.send( result.error, AnalyticsAnOld.ActionToLog.WalletConnectTransaction, ) @@ -173,7 +173,7 @@ class WalletConnectSdkHelper { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent( + store.state.globalState.analyticsHandler?.send( error, AnalyticsAnOld.ActionToLog.WalletConnectSign, ) @@ -202,7 +202,7 @@ class WalletConnectSdkHelper { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent( + store.state.globalState.analyticsHandler?.send( error, AnalyticsAnOld.ActionToLog.WalletConnectTransaction, ) @@ -278,7 +278,7 @@ class WalletConnectSdkHelper { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent( + store.state.globalState.analyticsHandler?.send( error, AnalyticsAnOld.ActionToLog.WalletConnectSign, ) 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 309c2900d3..719e666a32 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 @@ -110,7 +110,7 @@ class DetailsMiddleware { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent( + store.state.globalState.analyticsHandler?.send( error, AnalyticsAnOld.ActionToLog.PurgeWallet, card = store.state.detailsState.scanResponse?.card, @@ -154,7 +154,7 @@ class DetailsMiddleware { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent( + store.state.globalState.analyticsHandler?.send( error = error, action = AnalyticsAnOld.ActionToLog.ChangeSecOptions, params = mapOf( 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 b969f87719..fdd09a7dd8 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 @@ -77,7 +77,7 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App } } is OnboardingSaltPayAction.RegisterCard -> { - analyticsHandler.handleAnalyticsEvent(Onboarding.ButtonConnect()) + analyticsHandler.send(Onboarding.ButtonConnect()) handleInProgress = true val state = getState() @@ -151,7 +151,7 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App is OnboardingSaltPayAction.TrySetPin -> { try { assertPinValid(action.pin, getState().pinLength) - analyticsHandler.handleAnalyticsEvent(Onboarding.PinCodeSet()) + analyticsHandler.send(Onboarding.PinCodeSet()) store.dispatch(OnboardingSaltPayAction.SetPin(action.pin)) store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.CardRegistration)) } catch (error: SaltPayActivationError) { @@ -159,7 +159,7 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App } } is OnboardingSaltPayAction.Claim -> { - analyticsHandler.handleAnalyticsEvent(Onboarding.ButtonClaim()) + analyticsHandler.send(Onboarding.ButtonClaim()) val state = getState() handleInProgress = true @@ -192,7 +192,7 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App } handleInProgress = false - analyticsHandler.handleAnalyticsEvent(Onboarding.ClaimWasSuccessfully()) + analyticsHandler.send(Onboarding.ClaimWasSuccessfully()) dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimInProgress)) dispatchOnMain(OnboardingSaltPayAction.RefreshClaim) } @@ -242,10 +242,10 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App 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()) + SaltPayActivationStep.KycStart -> analyticsHandler.send(Onboarding.KYCStarted()) + SaltPayActivationStep.KycWaiting -> analyticsHandler.send(Onboarding.KYCInProgress()) + SaltPayActivationStep.KycReject -> analyticsHandler.send(Onboarding.KYCRejected()) + SaltPayActivationStep.Claim -> analyticsHandler.send(Onboarding.ClaimScreenOpened()) else -> {} } } 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..49bbefe866 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 @@ -250,7 +250,7 @@ private fun sendTransaction( feeAmount = feeAmount, destinationAddress = destinationAddress, ) - store.state.globalState.analyticsHandler?.handleBlockchainSdkErrorEvent( + store.state.globalState.analyticsHandler?.send( error = sendResult.error, action = AnalyticsAnOld.ActionToLog.SendTransaction, params = mapOf(AnalyticsParamAnOld.BLOCKCHAIN to walletManager.wallet.blockchain.currency), 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 ca2323f38e..5e34717f5d 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 @@ -186,7 +186,7 @@ class WalletMiddleware { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandler?.handleCardSdkErrorEvent( + store.state.globalState.analyticsHandler?.send( error, AnalyticsAnOld.ActionToLog.CreateWallet, card = store.state.detailsState.scanResponse?.card From f5bad316181c7cd0bc851a6a8071f7e0244fe0ee Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Oct 2022 12:02:07 +0500 Subject: [PATCH 002/372] Updated on 2026-08-14 --- .../tap/common/redux/global/GlobalAction.kt | 2 -- .../common/redux/global/GlobalMidlleware.kt | 1 - .../tap/common/redux/global/GlobalReducer.kt | 1 - .../tap/common/redux/global/GlobalState.kt | 3 --- .../tap/common/shop/TangemShopService.kt | 7 +++--- .../com/tangem/tap/domain/TangemSdkManager.kt | 14 +++++------ .../tap/domain/twins/TwinCardsManager.kt | 9 ++++---- .../walletconnect/WalletConnectManager.kt | 13 ++++++----- .../walletconnect/WalletConnectSdkHelper.kt | 21 ++++------------- .../details/redux/DetailsMiddleware.kt | 5 ++-- .../tap/features/home/redux/HomeMiddleware.kt | 5 ++-- .../twins/redux/TwinCardsMiddleware.kt | 1 - .../redux/OnboardingSaltPayMiddleware.kt | 23 +++++++++---------- .../send/redux/middlewares/SendMiddleware.kt | 5 ++-- .../tap/features/shop/redux/ShopMiddleware.kt | 7 +----- .../redux/middlewares/WalletMiddleware.kt | 3 ++- .../redux/middlewares/WarningsMiddleware.kt | 3 ++- .../ui/adapters/WarningMessagesAdapter.kt | 8 +++---- 18 files changed, 54 insertions(+), 77 deletions(-) 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 a805bbd798..bd346b13a6 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 @@ -163,7 +163,6 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di is GlobalAction.ScanCard -> { scope.launch { 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 b31226d6da..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,8 +1,6 @@ package com.tangem.tap.common.redux.global import com.tangem.domain.common.ScanResponse -import com.tangem.tap.common.analytics.Analytics -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 @@ -28,7 +26,6 @@ data class GlobalState( val dialog: StateDialog? = null, val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(), val resources: AndroidResources = AndroidResources(), - val analyticsHandler: GlobalAnalyticsEventHandler = Analytics, val userCountryCode: String? = null, ) : StateType 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 d26f38e508..87dacd7bc1 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,7 @@ 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.extensions.filterNotNull import com.tangem.tap.common.shop.data.ProductType import com.tangem.tap.common.shop.data.TangemProduct @@ -196,10 +195,10 @@ 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.send(order) } + it.order?.let { Analytics.send(it) } } } 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 94bf9fcaec..55ec76e903 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -26,10 +26,10 @@ 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.Analytics 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.domain.tasks.CreateWalletAndRescanTask import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ScanProductTask @@ -45,18 +45,17 @@ 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) + Analytics.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,18 +69,17 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co } private fun sendScanResultsToAnalytics( - analyticsHandler: GlobalAnalyticsEventHandler?, result: CompletionResult, ) { when (result) { is CompletionResult.Success -> { - analyticsHandler?.handleAnalyticsEvent( + Analytics.handleAnalyticsEvent( event = AnalyticsEventAnOld.CARD_IS_SCANNED, card = result.data.card, blockchain = result.data.walletData?.blockchain, ) if (DemoHelper.isDemoCard(result.data)) { - analyticsHandler?.handleAnalyticsEvent( + Analytics.handleAnalyticsEvent( event = AnalyticsEventAnOld.DEMO_MODE_ACTIVATED, params = mapOf(AnalyticsParamAnOld.CARD_ID.param to result.data.card.cardId), card = result.data.card, @@ -91,7 +89,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co } is CompletionResult.Failure -> (result.error as? TangemSdkError)?.let { error -> - analyticsHandler?.send( + Analytics.send( error = error, action = AnalyticsAnOld.ActionToLog.Scan, ) 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 7a39dc917d..fc80f3c5b0 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 @@ -14,14 +14,13 @@ 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.Analytics 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, assetReader: AssetReader, - val analyticsHandler: GlobalAnalyticsEventHandler? ) { private val currentCardId: String = card.cardId @@ -37,7 +36,7 @@ class TwinCardsManager( is CompletionResult.Success -> currentCardPublicKey = response.data.wallet.publicKey.toHexString() is CompletionResult.Failure -> { (response.error as? TangemSdkError)?.let { error -> - analyticsHandler?.send( + Analytics.send( error, AnalyticsAnOld.ActionToLog.CreateWallet, card = card @@ -67,7 +66,7 @@ class TwinCardsManager( } is CompletionResult.Failure -> { (response.error as? TangemSdkError)?.let { error -> - analyticsHandler?.send( + Analytics.send( error, AnalyticsAnOld.ActionToLog.CreateWallet, card = card @@ -87,7 +86,7 @@ class TwinCardsManager( is CompletionResult.Success -> Result.Success(response.data) is CompletionResult.Failure -> { (response.error as? TangemSdkError)?.let { error -> - analyticsHandler?.send( + Analytics.send( error, AnalyticsAnOld.ActionToLog.WriteIssuerData, card = card 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 6bd6cddada..f9ed7f61a5 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,6 +4,7 @@ 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.Analytics import com.tangem.tap.common.analytics.AnalyticsAnOld import com.tangem.tap.common.analytics.logWcEvent import com.tangem.tap.common.extensions.dispatchOnMain @@ -223,7 +224,7 @@ class WalletConnectManager { } private fun onSessionClosed(session: WCSession) { - store.state.globalState.analyticsHandler?.logWcEvent( + Analytics.logWcEvent( AnalyticsAnOld.WcAnalyticsEvent.Session( AnalyticsAnOld.WcSessionEvent.Disconnect, sessions[session]?.peerMeta?.url ) @@ -372,7 +373,7 @@ class WalletConnectManager { ), ) } - store.state.globalState.analyticsHandler?.logWcEvent( + Analytics.logWcEvent( AnalyticsAnOld.WcAnalyticsEvent.Session( AnalyticsAnOld.WcSessionEvent.Connect, peer.url, ), @@ -387,7 +388,7 @@ class WalletConnectManager { } client.onEthSendTransaction = { id: Long, transaction: WCEthereumTransaction -> Timber.d("onEthSendTransaction: $transaction") - store.state.globalState.analyticsHandler?.logWcEvent( + Analytics.logWcEvent( AnalyticsAnOld.WcAnalyticsEvent.Action( AnalyticsAnOld.WcAction.SendTransaction ) @@ -405,7 +406,7 @@ class WalletConnectManager { } client.onEthSignTransaction = { id: Long, transaction: WCEthereumTransaction -> Timber.d("onEthSignTransaction: $transaction") - store.state.globalState.analyticsHandler?.logWcEvent( + Analytics.logWcEvent( AnalyticsAnOld.WcAnalyticsEvent.Action( AnalyticsAnOld.WcAction.SignTransaction ) @@ -423,7 +424,7 @@ class WalletConnectManager { } client.onEthSign = { id: Long, message: WCEthereumSignMessage -> Timber.d("onEthSign: $message") - store.state.globalState.analyticsHandler?.logWcEvent( + Analytics.logWcEvent( AnalyticsAnOld.WcAnalyticsEvent.Action( AnalyticsAnOld.WcAction.PersonalSign ) @@ -499,7 +500,7 @@ class WalletConnectManager { val message = EthSignHelper.tryToParseEthTypedMessage(request) if (message != null) { Timber.d("onEthSign_v4: $message") - store.state.globalState.analyticsHandler?.logWcEvent( + Analytics.logWcEvent( AnalyticsAnOld.WcAnalyticsEvent.Action( AnalyticsAnOld.WcAction.PersonalSign ) 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 c09bfcbfe9..36e8fc2a1d 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,6 +24,7 @@ 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.Analytics import com.tangem.tap.common.analytics.AnalyticsAnOld import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.toFormattedString @@ -143,10 +144,7 @@ class WalletConnectSdkHelper { HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash } is SimpleResult.Failure -> { - store.state.globalState.analyticsHandler?.send( - result.error, - AnalyticsAnOld.ActionToLog.WalletConnectTransaction, - ) + Analytics.send(result.error, AnalyticsAnOld.ActionToLog.WalletConnectTransaction) Timber.e(result.error as BlockchainSdkError) null } @@ -173,10 +171,7 @@ class WalletConnectSdkHelper { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandler?.send( - error, - AnalyticsAnOld.ActionToLog.WalletConnectSign, - ) + Analytics.send(error, AnalyticsAnOld.ActionToLog.WalletConnectSign) } Timber.e(result.error.customMessage) null @@ -202,10 +197,7 @@ class WalletConnectSdkHelper { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandler?.send( - error, - AnalyticsAnOld.ActionToLog.WalletConnectTransaction, - ) + Analytics.send(error, AnalyticsAnOld.ActionToLog.WalletConnectTransaction) } Timber.e(result.error.customMessage) null @@ -278,10 +270,7 @@ class WalletConnectSdkHelper { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandler?.send( - error, - AnalyticsAnOld.ActionToLog.WalletConnectSign, - ) + Analytics.send(error, AnalyticsAnOld.ActionToLog.WalletConnectSign) } 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 719e666a32..6df912853f 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,6 +3,7 @@ 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.Analytics import com.tangem.tap.common.analytics.AnalyticsAnOld import com.tangem.tap.common.analytics.AnalyticsParamAnOld import com.tangem.tap.common.extensions.dispatchNotification @@ -110,7 +111,7 @@ class DetailsMiddleware { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandler?.send( + Analytics.send( error, AnalyticsAnOld.ActionToLog.PurgeWallet, card = store.state.detailsState.scanResponse?.card, @@ -154,7 +155,7 @@ class DetailsMiddleware { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandler?.send( + Analytics.send( error = error, action = AnalyticsAnOld.ActionToLog.ChangeSecOptions, params = mapOf( 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 e7be6e2145..8df39141c7 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 @@ -6,6 +6,7 @@ import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.extensions.withIOContext 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.AnalyticsEventAnOld import com.tangem.tap.common.analytics.AnalyticsParamAnOld import com.tangem.tap.common.analytics.GetCardSourceParamsAnOld @@ -102,7 +103,7 @@ 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( + Analytics.handleAnalyticsEvent( event = AnalyticsEventAnOld.GET_CARD, params = mapOf(AnalyticsParamAnOld.SOURCE.param to GetCardSourceParamsAnOld.WELCOME.param), ) @@ -149,7 +150,7 @@ private fun onScanSuccess(scanResponse: ScanResponse) { val tapWalletManager = globalState.tapWalletManager tapWalletManager.updateConfigManager(scanResponse) - globalState.analyticsHandler.attachToAllEvents(AnalyticsParam.BatchId, scanResponse.card.batchId) + Analytics.attachToAllEvents(AnalyticsParam.BatchId, scanResponse.card.batchId) store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) 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..35f7e1d7bd 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 @@ -139,7 +139,6 @@ private fun handle(action: Action, dispatch: DispatchFunction) { val manager = TwinCardsManager( card = getScanResponse().card, assetReader = action.reader, - analyticsHandler = store.state.globalState.analyticsHandler ) store.dispatch(TwinCardsAction.CardsManager.Set(manager)) 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 fdd09a7dd8..d5442968ca 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,7 +10,7 @@ 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.dispatchOnMain import com.tangem.tap.common.extensions.isPositive @@ -59,7 +59,6 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App fun getAppState(): AppState = appState()!! fun getOnboardingWalletState(): OnboardingWalletState = getAppState().onboardingWalletState fun getState(): OnboardingSaltPayState = getOnboardingWalletState().onboardingSaltPayState!! - val analyticsHandler = getAppState().globalState.analyticsHandler when (action) { is OnboardingSaltPayAction.Update -> { @@ -77,7 +76,7 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App } } is OnboardingSaltPayAction.RegisterCard -> { - analyticsHandler.send(Onboarding.ButtonConnect()) + Analytics.send(Onboarding.ButtonConnect()) handleInProgress = true val state = getState() @@ -151,7 +150,7 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App is OnboardingSaltPayAction.TrySetPin -> { try { assertPinValid(action.pin, getState().pinLength) - analyticsHandler.send(Onboarding.PinCodeSet()) + Analytics.send(Onboarding.PinCodeSet()) store.dispatch(OnboardingSaltPayAction.SetPin(action.pin)) store.dispatch(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.CardRegistration)) } catch (error: SaltPayActivationError) { @@ -159,7 +158,7 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App } } is OnboardingSaltPayAction.Claim -> { - analyticsHandler.send(Onboarding.ButtonClaim()) + Analytics.send(Onboarding.ButtonClaim()) val state = getState() handleInProgress = true @@ -192,7 +191,7 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App } handleInProgress = false - analyticsHandler.send(Onboarding.ClaimWasSuccessfully()) + Analytics.send(Onboarding.ClaimWasSuccessfully()) dispatchOnMain(OnboardingSaltPayAction.SetStep(SaltPayActivationStep.ClaimInProgress)) dispatchOnMain(OnboardingSaltPayAction.RefreshClaim) } @@ -233,19 +232,19 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App } } } - is OnboardingSaltPayAction.SetStep -> handleAnalytics(analyticsHandler, action.newStep) + is OnboardingSaltPayAction.SetStep -> handleAnalytics(action.newStep) else -> { /* do nothing, only reduce */ } } } -private fun handleAnalytics(analyticsHandler: GlobalAnalyticsEventHandler, step: SaltPayActivationStep) { +private fun handleAnalytics(step: SaltPayActivationStep) { when (step) { - SaltPayActivationStep.KycStart -> analyticsHandler.send(Onboarding.KYCStarted()) - SaltPayActivationStep.KycWaiting -> analyticsHandler.send(Onboarding.KYCInProgress()) - SaltPayActivationStep.KycReject -> analyticsHandler.send(Onboarding.KYCRejected()) - SaltPayActivationStep.Claim -> analyticsHandler.send(Onboarding.ClaimScreenOpened()) + SaltPayActivationStep.KycStart -> Analytics.send(Onboarding.KYCStarted()) + SaltPayActivationStep.KycWaiting -> Analytics.send(Onboarding.KYCInProgress()) + SaltPayActivationStep.KycReject -> Analytics.send(Onboarding.KYCRejected()) + SaltPayActivationStep.Claim -> Analytics.send(Onboarding.ClaimScreenOpened()) else -> {} } } 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 49bbefe866..1e886dd335 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,6 +17,7 @@ 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.Analytics import com.tangem.tap.common.analytics.AnalyticsAnOld import com.tangem.tap.common.analytics.AnalyticsEventAnOld import com.tangem.tap.common.analytics.AnalyticsParamAnOld @@ -221,7 +222,7 @@ private fun sendTransaction( when (sendResult) { is SimpleResult.Success -> { - store.state.globalState.analyticsHandler?.handleAnalyticsEvent( + Analytics.handleAnalyticsEvent( event = AnalyticsEventAnOld.TRANSACTION_IS_SENT, card = card, blockchain = walletManager.wallet.blockchain.currency, @@ -250,7 +251,7 @@ private fun sendTransaction( feeAmount = feeAmount, destinationAddress = destinationAddress, ) - store.state.globalState.analyticsHandler?.send( + Analytics.send( error = sendResult.error, action = AnalyticsAnOld.ActionToLog.SendTransaction, params = mapOf(AnalyticsParamAnOld.BLOCKCHAIN to walletManager.wallet.blockchain.currency), 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/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index 5e34717f5d..157c8786b4 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 @@ -11,6 +11,7 @@ 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.Analytics import com.tangem.tap.common.analytics.AnalyticsAnOld import com.tangem.tap.common.extensions.copyToClipboard import com.tangem.tap.common.extensions.dispatchDebugErrorNotification @@ -186,7 +187,7 @@ class WalletMiddleware { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - store.state.globalState.analyticsHandler?.send( + Analytics.send( error, AnalyticsAnOld.ActionToLog.CreateWallet, card = store.state.detailsState.scanResponse?.card 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..4a30816169 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,6 +8,7 @@ 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.Analytics import com.tangem.tap.common.analytics.AnalyticsEventAnOld import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.isGreaterThan @@ -81,7 +82,7 @@ class WarningsMiddleware { preferencesStorage.appRatingLaunchObserver.foundWalletWithFunds() } if (preferencesStorage.appRatingLaunchObserver.isReadyToShow()) { - store.state.globalState.analyticsHandler?.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_DISPLAYED) + Analytics.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_DISPLAYED) addWarningMessage(WarningMessagesManager.appRatingWarning(), true) } } 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..6a38cb2622 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,6 +7,7 @@ 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.Analytics import com.tangem.tap.common.analytics.AnalyticsEventAnOld import com.tangem.tap.common.extensions.getActivity import com.tangem.tap.common.extensions.getColor @@ -107,14 +108,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.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_DISMISS) store.dispatch(GlobalAction.HideWarningMessage(warning)) store.dispatch(WalletAction.Warnings.AppRating.RemindLater) } binding.btnCanBeBetter.setOnClickListener { - analyticsHandler?.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_NEGATIVE) + Analytics.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_NEGATIVE) store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow) store.dispatch(GlobalAction.HideWarningMessage(warning)) store.dispatch(GlobalAction.SendEmail(RateCanBeBetterEmail())) @@ -122,7 +122,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.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_POSITIVE) store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow) val reviewManager = ReviewManagerFactory.create(activity) val task = reviewManager.requestReviewFlow() From 2c19b748b0a0a3df33bfeffd8cc4b4c5aa96d704 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Oct 2022 12:27:15 +0500 Subject: [PATCH 003/372] Updated on 2026-08-14 --- .../tap/features/home/redux/HomeMiddleware.kt | 2 +- .../features/onboarding/OnboardingHelper.kt | 4 +- .../features/onboarding/OnboardingManager.kt | 22 ++++++--- .../products/note/OnboardingNoteFragment.kt | 1 + .../note/redux/OnboardingNoteAction.kt | 1 + .../note/redux/OnboardingNoteMiddleware.kt | 8 ++++ .../OnboardingOtherCardsFragment.kt | 1 + .../redux/OnboardingOtherCardsAction.kt | 1 + .../redux/OnboardingOtherCardsMiddleware.kt | 10 +++- .../twins/redux/TwinCardsMiddleware.kt | 23 ++++++--- .../wallet/redux/OnboardingWalletAction.kt | 1 - .../redux/OnboardingWalletMiddleware.kt | 30 +++++++----- .../saltPay/redux/OnboardingSaltPayAction.kt | 12 ++--- .../redux/OnboardingSaltPayMiddleware.kt | 47 ++++++++++++++----- .../saltPay/redux/OnboardingSaltPayReducer.kt | 2 +- .../wallet/ui/OnboardingWalletFragment.kt | 3 +- .../tap/persistence/UsedCardsPrefStorage.kt | 18 +++---- 17 files changed, 127 insertions(+), 59 deletions(-) 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 8df39141c7..5f77bee19f 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 @@ -166,7 +166,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/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index f2227fcc32..48d1e70e68 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 @@ -19,10 +19,10 @@ class OnboardingHelper { if (!response.twinsIsTwinned()) { true } else { - cardInfoStorage.activationIsStarted(response.card.cardId) + cardInfoStorage.isActivationStarted(response.card.cardId) } } - response.card.hasWallets() -> cardInfoStorage.activationIsStarted(response.card.cardId) + response.card.hasWallets() -> cardInfoStorage.isActivationStarted(response.card.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..865726f3dc 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 @@ -41,6 +41,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) } 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 d990941bd5..a71a55f30a 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,6 +4,8 @@ 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.Onboarding import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchOpenUrl @@ -58,6 +60,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() @@ -78,6 +85,7 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch 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) } } 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..960062acab 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 @@ -33,6 +33,7 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment { + if (!onboardingManager.isActivationStarted(card.cardId)) { + Analytics.send(Onboarding.Started()) + } + } is OnboardingOtherCardsAction.LoadCardArtwork -> { scope.launch { val artworkUrl = onboardingManager.loadArtworkUrl() @@ -60,6 +67,7 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) { } is OnboardingOtherCardsAction.SetStepOfScreen -> { if (action.step == OnboardingOtherCardsStep.Done) { + Analytics.send(Onboarding.Finished()) onboardingManager.activationFinished(card.cardId) postUi(200) { store.dispatch(OnboardingOtherCardsAction.Confetti.Show) } } @@ -71,7 +79,7 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) { when (result) { is CompletionResult.Success -> { 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 35f7e1d7bd..864549e54d 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 @@ -6,6 +6,8 @@ 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.Onboarding import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchOpenUrl @@ -76,6 +78,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 +88,18 @@ 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() -> TwinCardsStep.CreateFirstWallet twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done else -> TwinCardsStep.TopUpWallet } @@ -105,8 +115,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 = { @@ -163,7 +173,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 -> { @@ -221,11 +231,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)) @@ -239,7 +249,6 @@ private fun handle(action: Action, dispatch: DispatchFunction) { store.dispatch(TwinCardsAction.Balance.SetNonCriticalError(loadedBalance.error)) } } - } is TwinCardsAction.Balance.Set -> { if (action.balance.balanceIsToppedUp()) { 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 0de2a90471..728e86280e 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 @@ -7,7 +7,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 48e9c32f2f..ebdae723ea 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.primaryCardIsSaltPay @@ -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 @@ -87,13 +97,6 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: } } } - 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() @@ -148,9 +151,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)) } @@ -313,6 +313,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) } is BackupAction.FinishBackup -> { if (!onboardingWalletState.isSaltPay) { + Analytics.send(Onboarding.Finished()) finishCardActivation(backupState, card) } } @@ -320,7 +321,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 -> { @@ -351,7 +355,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 +381,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 d5442968ca..ea6bec2c95 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 @@ -12,13 +12,16 @@ import com.tangem.network.api.paymentology.KYCStatus import com.tangem.network.api.paymentology.RegistrationResponse 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,9 +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!! 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 @@ -232,23 +249,31 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App } } } - is OnboardingSaltPayAction.SetStep -> handleAnalytics(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(step: SaltPayActivationStep) { - when (step) { - SaltPayActivationStep.KycStart -> Analytics.send(Onboarding.KYCStarted()) - SaltPayActivationStep.KycWaiting -> Analytics.send(Onboarding.KYCInProgress()) - SaltPayActivationStep.KycReject -> Analytics.send(Onboarding.KYCRejected()) - SaltPayActivationStep.Claim -> Analytics.send(Onboarding.ClaimScreenOpened()) - else -> {} - } -} - suspend fun SaltPayActivationManager.update( currentStep: SaltPayActivationStep, currentAmountToClaim: Amount?, 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/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index 47b6beb3aa..e016d5fe8d 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 @@ -32,6 +32,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 +82,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) } 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..fb7f6a75e1 100644 --- a/app/src/main/java/com/tangem/tap/persistence/UsedCardsPrefStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/UsedCardsPrefStorage.kt @@ -11,7 +11,7 @@ import timber.log.Timber */ class UsedCardsPrefStorage( private val preferences: SharedPreferences, - private val jsonConverter: MoshiJsonConverter + private val jsonConverter: MoshiJsonConverter, ) { internal fun migrate() { @@ -43,14 +43,6 @@ class UsedCardsPrefStorage( save(foundItem, restoredList) } - fun isActivationFinished(cardId: String): Boolean { - return !(findCardInfo(cardId)?.isActivationStarted ?: true) - } - - fun activationIsStarted(cardId: String): Boolean { - return findCardInfo(cardId)?.isActivationStarted ?: false - } - fun activationFinished(cardId: String) { val restoredList = restore() val foundItem = findCardInfo(cardId, restoredList)?.copy(isActivationStarted = false) @@ -59,6 +51,14 @@ class UsedCardsPrefStorage( save(foundItem, restoredList) } + fun isActivationFinished(cardId: String): Boolean { + return !(findCardInfo(cardId)?.isActivationStarted ?: true) + } + + fun isActivationStarted(cardId: String): Boolean { + return findCardInfo(cardId)?.isActivationStarted ?: false + } + private fun findCardInfo(cardId: String, list: MutableList? = null): UsedCardInfo? { val findInList = list ?: restore() return findInList.firstOrNull { it.cardId == cardId } From cc4292319d29cfaaf42efd86f03ebf31046db052 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Oct 2022 18:02:29 +0500 Subject: [PATCH 004/372] Updated on 2026-08-14 --- .../note/redux/OnboardingNoteMiddleware.kt | 5 +++++ .../redux/OnboardingOtherCardsMiddleware.kt | 15 +++++++++++---- .../products/twins/redux/TwinCardsMiddleware.kt | 7 ++++++- .../wallet/redux/OnboardingWalletMiddleware.kt | 3 +++ 4 files changed, 25 insertions(+), 5 deletions(-) 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 a71a55f30a..f55bf9668a 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 @@ -81,6 +81,9 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch } is OnboardingNoteAction.SetStepOfScreen -> { when (action.step) { + OnboardingNoteStep.CreateWallet -> { + Analytics.send(Onboarding.CreateWallet.ScreenOpened()) + } OnboardingNoteStep.TopUpWallet -> { store.dispatch(OnboardingNoteAction.Balance.Update) } @@ -92,11 +95,13 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch } } is OnboardingNoteAction.CreateWallet -> { + Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) scope.launch { val result = tangemSdkManager.createProductWallet(scanResponse) 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) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt index efb2226659..ff988cc8d5 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt @@ -66,18 +66,25 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) { store.dispatch((OnboardingOtherCardsAction.SetStepOfScreen(step))) } is OnboardingOtherCardsAction.SetStepOfScreen -> { - if (action.step == OnboardingOtherCardsStep.Done) { - Analytics.send(Onboarding.Finished()) - 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 -> { + Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) scope.launch { val result = tangemSdkManager.createProductWallet(onboardingManager.scanResponse) withMainContext { when (result) { is CompletionResult.Success -> { + Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully()) val updatedResponse = onboardingManager.scanResponse.copy( card = result.data.card, ) 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 864549e54d..9865fc9c06 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 @@ -99,7 +99,10 @@ private fun handle(action: Action, dispatch: DispatchFunction) { CreateTwinWalletMode.CreateWallet -> { if (preferencesStorage.wasTwinsOnboardingShown()) { val step = when { - !scanResponse.twinsIsTwinned() -> TwinCardsStep.CreateFirstWallet + !scanResponse.twinsIsTwinned() -> { + Analytics.send(Onboarding.CreateWallet.ScreenOpened()) + TwinCardsStep.CreateFirstWallet + } twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done else -> TwinCardsStep.TopUpWallet } @@ -146,6 +149,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { } } is TwinCardsAction.Wallet.LaunchFirstStep -> { + Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) val manager = TwinCardsManager( card = getScanResponse().card, assetReader = action.reader, @@ -155,6 +159,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { 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 { 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 4e34f48b54..ab507c9a6e 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 @@ -94,6 +94,7 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: } else -> { store.dispatch(OnboardingWalletAction.GetToCreateWalletStep) + Analytics.send(Onboarding.CreateWallet.ScreenOpened()) } } } @@ -109,12 +110,14 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: } } is OnboardingWalletAction.CreateWallet -> { + Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) scope.launch { scanResponse ?: return@launch val result = tangemSdkManager.createProductWallet(scanResponse) 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, From d963d65f2ec591cc717e533e1f8e318257009dcf Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Oct 2022 19:49:32 +0500 Subject: [PATCH 005/372] Updated on 2026-08-14 --- .../tap/common/analytics/events/Onboarding.kt | 20 +++++++++---------- .../redux/OnboardingWalletMiddleware.kt | 9 +++++++++ 2 files changed, 19 insertions(+), 10 deletions(-) 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/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index ab507c9a6e..e468e440f5 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 @@ -169,6 +169,7 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: if (onboardingWalletState.isSaltPay) { BackupAction.StartBackup } else { + Analytics.send(Onboarding.Backup.ScreenOpened()) BackupAction.IntroduceBackup } } else { @@ -224,6 +225,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) { @@ -269,7 +271,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 { @@ -278,6 +284,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) @@ -315,6 +322,7 @@ 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) @@ -340,6 +348,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) } From 7d0835f28c7c6bdf702733ad646ba36d004aec69 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Oct 2022 23:15:07 +0500 Subject: [PATCH 006/372] Updated on 2026-08-14 --- .../domain/tasks/product/ScanProductTask.kt | 4 +- .../features/onboarding/OnboardingHelper.kt | 5 +- .../tap/persistence/PreferencesStorage.kt | 5 - .../tap/persistence/UsedCardsPrefStorage.kt | 97 ++++++++++++++----- 4 files changed, 78 insertions(+), 33 deletions(-) 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..a5d9fc3932 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 @@ -162,9 +162,9 @@ private class ScanWalletProcessor( 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 && activationInProgress && card.wallets.isNotEmpty()) { StartPrimaryCardLinkingTask().run(session) { linkingResult -> when (linkingResult) { is CompletionResult.Success -> { 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/persistence/PreferencesStorage.kt b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt index 723af0df99..40e4cd1bd2 100644 --- a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt @@ -33,11 +33,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 saveDisclaimerAccepted() { preferences.edit { putBoolean(DISCLAIMER_ACCEPTED_KEY, true) } } 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 From a71116a0aa5312c44b3c38a914d98be67deccbdb Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 28 Oct 2022 23:15:23 +0500 Subject: [PATCH 007/372] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index c8f577f2ab..cf6ea50867 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit c8f577f2ab48e0d6c49f79b6cff21eabe67cd63e +Subproject commit cf6ea50867477f97655da9da47ff94987e8f3354 From 7b90bb0848dd1bcabb6210d90562016312055916 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 29 Oct 2022 00:22:43 +0500 Subject: [PATCH 008/372] Updated on 2026-08-14 --- .../java/com/tangem/tap/persistence/PreferencesStorage.kt | 8 -------- 1 file changed, 8 deletions(-) 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 2250427b09..32ff1aeb79 100644 --- a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt @@ -33,14 +33,6 @@ class PreferencesStorage(applicationContext: Application) { fun getCountOfLaunches(): Int = preferences.getInt(APP_LAUNCH_COUNT_KEY, 1) - fun saveDisclaimerAccepted() { - preferences.edit { putBoolean(DISCLAIMER_ACCEPTED_KEY, true) } - } - - fun wasDisclaimerAccepted(): Boolean { - return preferences.getBoolean(DISCLAIMER_ACCEPTED_KEY, false) - } - fun saveTwinsOnboardingShown() { preferences.edit { putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true) } } From 9f031964463ec2d4d4a8e9d6851bbc13469524ab Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 30 Oct 2022 20:41:30 +0500 Subject: [PATCH 009/372] Updated on 2026-08-14 --- .../features/onboarding/products/wallet/ui/BackupAnimator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 936112008744895f8e42d6e696a824b589c61906 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 30 Oct 2022 20:43:03 +0500 Subject: [PATCH 010/372] Updated on 2026-08-14 --- .../wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 90c9d33f06..f550b730ac 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 @@ -66,7 +66,7 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App when (action) { is OnboardingSaltPayAction.Init -> { - if (getOnboardingWalletState().isSaltPay) return + if (!getOnboardingWalletState().isSaltPay) return val onboardingManager = getOnboardingManager().guard { // onboardingManager may be null if it's started from standard Wallet unfinished backup return @@ -289,7 +289,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) From 085ffaf2c5d0be5d5b8be04613eb719d4da0df72 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 30 Oct 2022 20:43:48 +0500 Subject: [PATCH 011/372] Updated on 2026-08-14 --- .../tap/domain/tasks/product/ScanProductTask.kt | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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 a5d9fc3932..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,6 +157,7 @@ private class ScanWalletProcessor( } } } + private fun startLinkingForBackupIfNeeded( card: Card, session: CardSession, @@ -164,7 +165,9 @@ private class ScanWalletProcessor( ) { val activationInProgress = preferencesStorage.usedCardsPrefStorage.isActivationInProgress(card.cardId) - if (card.backupStatus == Card.BackupStatus.NoBackup && activationInProgress && 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() From 41703c9f113ac0545b5bb5aeb19c42450cce72d5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 30 Oct 2022 20:44:26 +0500 Subject: [PATCH 012/372] Updated on 2026-08-14 --- .../analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5a03cc2f36..06f77c15b3 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 @@ -20,7 +20,7 @@ class AmplitudeAnalyticsHandler( class Builder : AnalyticsHandlerBuilder { override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsEventHandler? = when { !data.isDebug -> AmplitudeClient(data.application, data.config.amplitudeApiKey) - data.isDebug && data.logConfig.appsFlyer -> AmplitudeLogClient(data.jsonConverter) + data.isDebug && data.logConfig.amplitude -> AmplitudeLogClient(data.jsonConverter) else -> null }?.let { AmplitudeAnalyticsHandler(it) } } From 08b9e7f8f4fc8362f548cc4201bba699b7fcad72 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 31 Oct 2022 11:27:31 +0500 Subject: [PATCH 013/372] Updated on 2026-08-14 --- .../onboarding/products/note/OnboardingNoteFragment.kt | 7 ++++++- .../products/note/redux/OnboardingNoteMiddleware.kt | 1 - .../products/otherCards/OnboardingOtherCardsFragment.kt | 7 ++++++- .../otherCards/redux/OnboardingOtherCardsMiddleware.kt | 1 - .../onboarding/products/twins/redux/TwinCardsMiddleware.kt | 1 - .../onboarding/products/twins/ui/TwinsCardsFragment.kt | 3 +++ .../products/wallet/redux/OnboardingWalletMiddleware.kt | 1 - .../wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt | 3 --- .../products/wallet/saltPay/ui/OnboardingSaltPayView.kt | 4 ++++ .../products/wallet/ui/OnboardingWalletFragment.kt | 7 ++++++- 10 files changed, 25 insertions(+), 10 deletions(-) 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 865726f3dc..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 @@ -109,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/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index f55bf9668a..6e1d907be1 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 @@ -95,7 +95,6 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch } } is OnboardingNoteAction.CreateWallet -> { - Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) scope.launch { val result = tangemSdkManager.createProductWallet(scanResponse) withMainContext { 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 960062acab..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 @@ -69,7 +71,10 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment { - Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) scope.launch { val result = tangemSdkManager.createProductWallet(onboardingManager.scanResponse) withMainContext { 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 9865fc9c06..a872bfef97 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 @@ -149,7 +149,6 @@ private fun handle(action: Action, dispatch: DispatchFunction) { } } is TwinCardsAction.Wallet.LaunchFirstStep -> { - Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) val manager = TwinCardsManager( card = getScanResponse().card, assetReader = action.reader, 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/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index e468e440f5..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 @@ -110,7 +110,6 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: } } is OnboardingWalletAction.CreateWallet -> { - Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) scope.launch { scanResponse ?: return@launch val result = tangemSdkManager.createProductWallet(scanResponse) 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 f550b730ac..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 @@ -93,8 +93,6 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App } } is OnboardingSaltPayAction.RegisterCard -> { - Analytics.send(Onboarding.ButtonConnect()) - handleInProgress = true val state = getState() @@ -175,7 +173,6 @@ private fun handleOnboardingSaltPayAction(anyAction: Action, appState: () -> App } } is OnboardingSaltPayAction.Claim -> { - Analytics.send(Onboarding.ButtonClaim()) val state = getState() handleInProgress = true 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/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index e016d5fe8d..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 @@ -171,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) From 03eeebd2806f7da4df18ad2374e114f2e42b0fba Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 31 Oct 2022 12:05:13 +0500 Subject: [PATCH 014/372] Updated on 2026-08-14 --- .../com/tangem/tap/features/home/HomeFragment.kt | 14 ++++++++++++-- .../tap/features/home/redux/HomeMiddleware.kt | 2 ++ .../features/wallet/ui/dialogs/ScanFailsDialog.kt | 3 +++ 3 files changed, 17 insertions(+), 2 deletions(-) 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 839c7a3828..313471aa5d 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 @@ -27,6 +29,7 @@ class HomeFragment : Fragment(), StoreSubscriber { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + Analytics.send(IntroductionProcess.ScreenOpened()) store.dispatch(HomeAction.Init) } @@ -78,9 +81,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 5a4e3d2ca2..460c05f0a2 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 @@ -14,6 +14,7 @@ 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.events.IntroductionProcess import com.tangem.tap.common.entities.IndeterminateProgressButton import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOpenUrl @@ -161,6 +162,7 @@ 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) 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) { _, _ -> } From 1d5360126656ad9d84ed65566c1b1af5181025c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 31 Oct 2022 12:30:11 +0500 Subject: [PATCH 015/372] Updated on 2026-08-14 --- .../tap/common/extensions/WalletManager.kt | 5 ++--- .../note/redux/OnboardingNoteMiddleware.kt | 17 +++++++++++++++-- .../products/twins/redux/TwinCardsMiddleware.kt | 4 ++-- 3 files changed, 19 insertions(+), 7 deletions(-) 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/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index 6e1d907be1..ef0c5a743a 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 @@ -5,12 +5,14 @@ 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.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 @@ -85,6 +87,7 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch Analytics.send(Onboarding.CreateWallet.ScreenOpened()) } OnboardingNoteStep.TopUpWallet -> { + Analytics.send(Onboarding.Topup.ScreenOpened()) store.dispatch(OnboardingNoteAction.Balance.Update) } OnboardingNoteStep.Done -> { @@ -159,11 +162,21 @@ 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 + val walletManager = noteState.walletManager.guard { + store.dispatchDebugErrorNotification("NPE: WalletManager") + return + } + + val blockchain = walletManager.wallet.blockchain + val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain) + Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType)) + + val topUpUrl = walletManager.getTopUpUrl() ?: return store.dispatchOpenUrl(topUpUrl) } OnboardingNoteAction.Done -> { 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 a872bfef97..83514174a1 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 @@ -12,7 +12,7 @@ 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 @@ -268,7 +268,7 @@ 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 store.dispatchOpenUrl(topUpUrl) } TwinCardsAction.Done -> { From 67ad64b128a598313bd884542de69ebdf545b0e7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 31 Oct 2022 12:44:29 +0500 Subject: [PATCH 016/372] Updated on 2026-08-14 --- .../products/twins/redux/TwinCardsMiddleware.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 83514174a1..bf3cb1296c 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 @@ -139,8 +139,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() @@ -149,6 +151,7 @@ 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, @@ -199,6 +202,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 { From 8f61c0b19f91cc65edfbec1672c9dfaad46aaf1c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 31 Oct 2022 15:45:44 +0500 Subject: [PATCH 017/372] Updated on 2026-08-14 --- .../common/analytics/events/AnalyticsParam.kt | 9 ---- .../tap/common/analytics/events/Settings.kt | 6 ++- .../details/ui/details/DetailsFragment.kt | 4 +- .../details/ui/details/DetailsScreen.kt | 8 +-- .../details/ui/details/DetailsScreenState.kt | 50 +++++++++++-------- .../details/ui/details/DetailsViewModel.kt | 14 ++++-- .../walletconnect/WalletConnectViewModel.kt | 8 ++- 7 files changed, 59 insertions(+), 40 deletions(-) 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..cca03e068f 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 @@ -41,15 +41,6 @@ sealed class AnalyticsParam { object LongTap : SecurityMode("Long Tap") } - 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") - } - companion object Key { const val Blockchain = "blockchain" const val BatchId = "batch_id" 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..fff6794867 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] */ @@ -18,9 +20,9 @@ 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( 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/WalletConnectViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt index 6bbde4c1ae..9f4c8dd659 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt @@ -1,5 +1,7 @@ package com.tangem.tap.features.details.ui.walletconnect +import com.tangem.tap.common.analytics.Analytics +import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession @@ -12,7 +14,10 @@ class WalletConnectViewModel(private val store: Store) { state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) }, isLoading = state.loading, onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, state.sessions) }, - onAddSession = { copiedUri -> store.dispatch(WalletConnectAction.StartWalletConnect(copiedUri)) }, + onAddSession = { copiedUri -> + Analytics.send(Settings.ButtonStartWalletConnectSession()) + store.dispatch(WalletConnectAction.StartWalletConnect(copiedUri)) + }, ) } @@ -20,6 +25,7 @@ class WalletConnectViewModel(private val store: Store) { sessions .firstOrNull { it.session.toUri() == sessionUri } ?.let { baseSession -> + Analytics.send(Settings.ButtonStopWalletConnectSession()) store.dispatch(WalletConnectAction.DisconnectSession(baseSession.session)) } } From 1b2c087d0858b0c973bdac26963a9b85ae8f57fc Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 31 Oct 2022 15:49:34 +0500 Subject: [PATCH 018/372] Updated on 2026-08-14 --- .../ui/walletconnect/WalletConnectScreen.kt | 14 ++++++++++++-- .../ui/walletconnect/WalletConnectViewModel.kt | 8 +------- 2 files changed, 13 insertions(+), 9 deletions(-) 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/details/ui/walletconnect/WalletConnectViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt index 9f4c8dd659..6bbde4c1ae 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt @@ -1,7 +1,5 @@ package com.tangem.tap.features.details.ui.walletconnect -import com.tangem.tap.common.analytics.Analytics -import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession @@ -14,10 +12,7 @@ class WalletConnectViewModel(private val store: Store) { state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) }, isLoading = state.loading, onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, state.sessions) }, - onAddSession = { copiedUri -> - Analytics.send(Settings.ButtonStartWalletConnectSession()) - store.dispatch(WalletConnectAction.StartWalletConnect(copiedUri)) - }, + onAddSession = { copiedUri -> store.dispatch(WalletConnectAction.StartWalletConnect(copiedUri)) }, ) } @@ -25,7 +20,6 @@ class WalletConnectViewModel(private val store: Store) { sessions .firstOrNull { it.session.toUri() == sessionUri } ?.let { baseSession -> - Analytics.send(Settings.ButtonStopWalletConnectSession()) store.dispatch(WalletConnectAction.DisconnectSession(baseSession.session)) } } From e4ff41dc503e7fc84792d8354e144456e8a8d364 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 31 Oct 2022 20:25:53 +0500 Subject: [PATCH 019/372] Updated on 2026-08-14 --- .../tap/common/analytics/events/AnalyticsParam.kt | 10 ++++++++++ .../tangem/tap/common/analytics/events/Settings.kt | 4 ++-- .../tap/features/details/redux/DetailsMiddleware.kt | 12 +++++++++++- .../details/ui/cardsettings/CardSettingsViewModel.kt | 6 ++++++ 4 files changed, 29 insertions(+), 3 deletions(-) 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 cca03e068f..5e3f8a81c5 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,5 +1,7 @@ package com.tangem.tap.common.analytics.events +import com.tangem.tap.features.details.redux.SecurityOption + sealed class AnalyticsParam { sealed class CurrencyType(val value: String) { @@ -39,6 +41,14 @@ 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 + } + } } companion object Key { 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 fff6794867..b24d6ef042 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 @@ -35,12 +35,12 @@ sealed class Settings( 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) : CardSettings( event = "Security Mode Changed", params = mapOf("Mode" to mode.value), ) 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 6df912853f..13caeece31 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 @@ -6,6 +6,8 @@ import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.AnalyticsAnOld import com.tangem.tap.common.analytics.AnalyticsParamAnOld +import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState @@ -107,6 +109,7 @@ class DetailsMiddleware { val result = tangemSdkManager.resetToFactorySettings(card) when (result) { is CompletionResult.Success -> { + Analytics.send(Settings.CardSettings.FactoryResetFinished()) store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) } is CompletionResult.Failure -> { @@ -133,6 +136,10 @@ class DetailsMiddleware { is DetailsAction.ManageSecurity.OpenSecurity -> { store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsSecurity)) } + is DetailsAction.ManageSecurity.SelectOption -> { + val modeParam = AnalyticsParam.SecurityMode.from(action.option) + Analytics.send(Settings.CardSettings.SecurityModeChanged(modeParam)) + } is DetailsAction.ManageSecurity.SaveChanges -> { val cardId = store.state.detailsState.scanResponse?.card?.cardId val selectedOption = @@ -176,7 +183,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 -> {} From d7b674ea892ebbf31114df58d944ee1c723b86c6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Nov 2022 14:07:13 +0500 Subject: [PATCH 020/372] Updated on 2026-08-14 --- .../common/analytics/events/AnalyticsParam.kt | 4 +- .../common/analytics/events/ManageTokens.kt | 46 ++++++++-- .../addCustomToken/AddCustomTokenFragment.kt | 28 +++--- .../features/tokens/redux/TokensMiddleware.kt | 4 + .../features/tokens/ui/AddTokensFragment.kt | 54 ++++++----- .../tokens/ui/compose/CurrenciesScreen.kt | 91 +++++++++++++------ 6 files changed, 159 insertions(+), 68 deletions(-) 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 5e3f8a81c5..9996f479b7 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 @@ -10,7 +10,7 @@ sealed class AnalyticsParam { class Token(token: com.tangem.blockchain.common.Token) : CurrencyType(token.symbol) } - // Multicurrency or CurrencyType + // MultiCurrency or CurrencyType sealed class CardCurrency(val value: String) { object MultiCurrency : CardCurrency("Multicurrency") class SingleCurrency(type: CurrencyType) : CardCurrency(type.value) @@ -43,7 +43,7 @@ sealed class AnalyticsParam { object LongTap : SecurityMode("Long Tap") companion object { - fun from(option: SecurityOption): SecurityMode = when(option){ + fun from(option: SecurityOption): SecurityMode = when (option) { SecurityOption.AccessCode -> AccessCode SecurityOption.PassCode -> Passcode SecurityOption.LongTap -> LongTap 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..9f3ec194b7 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,28 @@ 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( + "Network" to network.toNetworkId(), + "Symbol" to network.currency, + "DerivationPath" to derivationPath?.rawPath, + ).filterNotNull() + is CustomCurrency.CustomToken -> mapOf( + "Network" to network.toNetworkId(), + "Symbol" to token.symbol, + "DerivationPath" to derivationPath?.rawPath, + "Token" to token.id, + "Address" to token.contractAddress, + ).filterNotNull() + } + } + } + } } } \ No newline at end of file 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 9817b4db49..7bf90e4bfd 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 @@ -21,6 +21,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 @@ -52,6 +54,7 @@ class TokensMiddleware { handleAddingCustomToken(action) } is TokensAction.SetSearchInput -> { + Analytics.send(ManageTokens.TokenSearched()) handleLoadCurrencies( scanResponse = store.state.globalState.scanResponse, newSearchInput = action.searchInput @@ -345,6 +348,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 From 7aa118cec40934fbf46e8bc58427e569d6df7d0f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Nov 2022 14:21:12 +0500 Subject: [PATCH 021/372] Updated on 2026-08-14 --- .../tangem/tap/common/analytics/events/ManageTokens.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 9f3ec194b7..77cfc78626 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 @@ -44,15 +44,13 @@ sealed class ManageTokens( private fun convertToParam(customCurrency: CustomCurrency): Map = with(customCurrency) { return when (this) { is CustomCurrency.CustomBlockchain -> mapOf( - "Network" to network.toNetworkId(), - "Symbol" to network.currency, + "Token" to network.currency, "DerivationPath" to derivationPath?.rawPath, ).filterNotNull() is CustomCurrency.CustomToken -> mapOf( - "Network" to network.toNetworkId(), - "Symbol" to token.symbol, + "Token" to token.symbol, "DerivationPath" to derivationPath?.rawPath, - "Token" to token.id, + "NetworkId" to network.toNetworkId(), "Address" to token.contractAddress, ).filterNotNull() } From a63f4797fe4cd999b236f0d902227e069956c1f2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Nov 2022 14:29:00 +0500 Subject: [PATCH 022/372] Updated on 2026-08-14 --- .../tangem/tap/features/details/redux/DetailsMiddleware.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 13caeece31..23edb2035c 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 @@ -136,10 +136,6 @@ class DetailsMiddleware { is DetailsAction.ManageSecurity.OpenSecurity -> { store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsSecurity)) } - is DetailsAction.ManageSecurity.SelectOption -> { - val modeParam = AnalyticsParam.SecurityMode.from(action.option) - Analytics.send(Settings.CardSettings.SecurityModeChanged(modeParam)) - } is DetailsAction.ManageSecurity.SaveChanges -> { val cardId = store.state.detailsState.scanResponse?.card?.cardId val selectedOption = @@ -155,6 +151,8 @@ class DetailsMiddleware { when (result) { is CompletionResult.Success -> { selectedOption?.let { + val paramValue = AnalyticsParam.SecurityMode.from(it) + Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue)) store.dispatch(GlobalAction.UpdateSecurityOptions(it)) } store.dispatch(NavigationAction.PopBackTo()) From 37648e0d92c35212ae385e880401c50b2c5e6b2e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Nov 2022 17:02:08 +0500 Subject: [PATCH 023/372] Updated on 2026-08-14 --- .../tangem/tap/common/analytics/events/ManageTokens.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 77cfc78626..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 @@ -45,13 +45,13 @@ sealed class ManageTokens( return when (this) { is CustomCurrency.CustomBlockchain -> mapOf( "Token" to network.currency, - "DerivationPath" to derivationPath?.rawPath, + "Derivation Path" to derivationPath?.rawPath, ).filterNotNull() is CustomCurrency.CustomToken -> mapOf( "Token" to token.symbol, - "DerivationPath" to derivationPath?.rawPath, - "NetworkId" to network.toNetworkId(), - "Address" to token.contractAddress, + "Derivation Path" to derivationPath?.rawPath, + "Network Id" to network.toNetworkId(), + "Contract Address" to token.contractAddress, ).filterNotNull() } } From 8a4a0d77e9bc4b97b05eab4ea973a873618d9e23 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Nov 2022 17:10:55 +0500 Subject: [PATCH 024/372] Updated on 2026-08-14 --- .../java/com/tangem/tap/common/extensions/UI.kt | 3 ++- .../tap/features/wallet/ui/WalletFragment.kt | 3 +++ .../features/wallet/ui/adapters/WalletAdapter.kt | 14 ++++++++------ .../features/wallet/ui/wallet/MultiWalletView.kt | 3 +++ 4 files changed, 16 insertions(+), 7 deletions(-) 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/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index 87a31563fa..a3d82b0de2 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,8 @@ 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.events.Portfolio import com.tangem.tap.common.extensions.show import com.tangem.tap.common.recyclerView.SpaceItemDecoration import com.tangem.tap.common.redux.global.GlobalAction @@ -152,6 +154,7 @@ 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/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index 9d4fe68894..891dba9126 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,8 @@ 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.Portfolio import com.tangem.tap.common.extensions.animateVisibility import com.tangem.tap.common.extensions.formatAmountAsSpannedString import com.tangem.tap.common.extensions.getQuantityString @@ -85,6 +87,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( From d57d48e68164f8c37ceade0e1ebee386421b3bb9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Nov 2022 18:03:12 +0500 Subject: [PATCH 025/372] Updated on 2026-08-14 --- .../tangem/tap/common/analytics/events/AnalyticsParam.kt | 1 + .../com/tangem/tap/common/analytics/events/MainScreen.kt | 2 +- .../wallet/redux/middlewares/AppCurrencyMiddleware.kt | 4 ++++ .../com/tangem/tap/features/wallet/ui/WalletFragment.kt | 6 ++++-- .../features/wallet/ui/adapters/WarningMessagesAdapter.kt | 5 +++++ .../tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt | 3 +++ 6 files changed, 18 insertions(+), 3 deletions(-) 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 9996f479b7..17b2a3484b 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 @@ -8,6 +8,7 @@ sealed class AnalyticsParam { 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) } // MultiCurrency or CurrencyType 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/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/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index a3d82b0de2..97fba31222 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 @@ -17,6 +17,7 @@ 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.events.MainScreen import com.tangem.tap.common.analytics.events.Portfolio import com.tangem.tap.common.extensions.show import com.tangem.tap.common.recyclerView.SpaceItemDecoration @@ -54,6 +55,8 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber Date: Tue, 1 Nov 2022 19:14:20 +0500 Subject: [PATCH 026/372] Updated on 2026-08-14 --- .../tap/common/analytics/events/Token.kt | 32 +++++++++-- .../middlewares/MultiWalletMiddleware.kt | 2 + .../middlewares/TradeCryptoMiddleware.kt | 39 ++++++++----- .../middlewares/WalletDialogsMiddleware.kt | 12 +++- .../redux/middlewares/WalletMiddleware.kt | 2 + .../wallet/ui/WalletDetailsFragment.kt | 56 +++++++++++-------- 6 files changed, 98 insertions(+), 45 deletions(-) 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..b0c9a0db1d 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] */ @@ -12,10 +14,30 @@ sealed class Token( 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 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, @@ -37,7 +59,7 @@ sealed class Token( class ButtonQRCode : Send("Button - QR Code") class ButtonSwapCurrency : Send("Button - Swap Currency") - class TransactionSent(token: AnalyticsParam.CurrencyType) : Send( + class TransactionSent(token: CurrencyType) : Send( event = "Transaction Sent", params = mapOf("Token" to token.value), ) 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 ba68023190..3433d447d5 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,7 @@ 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.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.safeUpdate @@ -109,6 +110,7 @@ class MultiWalletMiddleware { WalletDialog.RemoveWalletDialog( currencyTitle = currency.currencyName, onOk = { + Analytics.send(com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken()) store.dispatch( WalletAction.MultiWallet.RemoveWallet( currency = currency, 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 319c940a52..182c75c9bd 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,6 +2,9 @@ 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.redux.AppState @@ -19,7 +22,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 +52,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 +65,7 @@ class TradeCryptoMiddleware { exchangeManager.buyErc20TestnetTokens( card = card, walletManager = walletManager, - token = currency.token + token = currency.token, ) } return @@ -73,7 +76,7 @@ class TradeCryptoMiddleware { blockchain = currency.blockchain, cryptoCurrencyName = currency.currencySymbol, fiatCurrencyName = appCurrency.code, - walletAddress = addresses[0].address + walletAddress = addresses[0].address, )?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) } } @@ -85,29 +88,37 @@ 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 + walletAddress = addresses[0].address, )?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) } } 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)) } 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 d53809be9c..64d2122fa2 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,14 +27,14 @@ 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 -> { 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 157c8786b4..a4f353a6f7 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 @@ -13,6 +13,7 @@ import com.tangem.operations.attestation.Attestation import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.AnalyticsAnOld +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.dispatchOnMain @@ -247,6 +248,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/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)) From bb19d52b2d9e2b3fc8b8a5c2ca1e0cad14ab6ccd Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Nov 2022 20:43:38 +0500 Subject: [PATCH 027/372] Updated on 2026-08-14 --- .../onboarding/AddressInfoBottomSheetDialog.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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, ) } } From 9a9d7efe656e277f3d634245529f58fcf980026c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Nov 2022 20:49:56 +0500 Subject: [PATCH 028/372] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/common/DialogManager.kt | 3 +-- .../wallet/redux/middlewares/TradeCryptoMiddleware.kt | 10 +++++++--- .../RussianCardholdersWarningBottomSheetDialog.kt | 3 +++ 3 files changed, 11 insertions(+), 5 deletions(-) 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 5ae8d9ce04..ed8d4542b9 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -152,8 +152,7 @@ class DialogManager : StoreSubscriber { primaryButtonRes = state.dialog.primaryButtonRes, primaryButtonAction = state.dialog.onOk, ) - is WalletDialog.RussianCardholdersWarningDialog -> - RussianCardholdersWarningBottomSheetDialog(context) + is WalletDialog.RussianCardholdersWarningDialog -> RussianCardholdersWarningBottomSheetDialog(context) else -> null } dialog?.show() 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 182c75c9bd..8a951b246c 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 @@ -7,6 +7,7 @@ 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 @@ -77,7 +78,10 @@ class TradeCryptoMiddleware { cryptoCurrencyName = currency.currencySymbol, fiatCurrencyName = appCurrency.code, walletAddress = addresses[0].address, - )?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) } + )?.let { + store.dispatchOpenUrl(it) + Analytics.send(Token.Topup.ScreenOpened()) + } } private fun proceedSellAction() { @@ -96,7 +100,7 @@ class TradeCryptoMiddleware { cryptoCurrencyName = currency.currencySymbol, fiatCurrencyName = appCurrency.code, walletAddress = addresses[0].address, - )?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) } + )?.let { store.dispatchOpenUrl(it) } } private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) { @@ -127,6 +131,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/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt index a41dce9228..526b991006 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,6 +4,8 @@ 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.Token import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.features.wallet.redux.WalletAction @@ -16,6 +18,7 @@ class RussianCardholdersWarningBottomSheetDialog(context: Context) : BottomSheet override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + Analytics.send(Token.Topup.P2PScreenOpened()) binding = DialogRussiansCardholdersWarningBinding .inflate(LayoutInflater.from(context)) .also { setContentView(it.root) } From e761c047d1f0249d7a891f488aa1cf115c2caeca Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Nov 2022 20:51:04 +0500 Subject: [PATCH 029/372] Updated on 2026-08-14 --- .../wallet/redux/middlewares/TradeCryptoMiddleware.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 8a951b246c..a18c14fae9 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 @@ -100,7 +100,10 @@ class TradeCryptoMiddleware { cryptoCurrencyName = currency.currencySymbol, fiatCurrencyName = appCurrency.code, walletAddress = addresses[0].address, - )?.let { store.dispatchOpenUrl(it) } + )?.let { + store.dispatchOpenUrl(it) + Analytics.send(Token.Withdraw.ScreenOpened()) + } } private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) { From bd3ecd6e1ad773cffc94ad662c31174d5b3579b4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Nov 2022 21:02:14 +0500 Subject: [PATCH 030/372] Updated on 2026-08-14 --- .../common/analytics/events/AnalyticsParam.kt | 3 +- .../tap/common/analytics/events/Token.kt | 4 +- .../send/redux/middlewares/SendMiddleware.kt | 3 ++ .../tap/features/send/ui/SendFragment.kt | 40 ++++++++++++++++--- 4 files changed, 42 insertions(+), 8 deletions(-) 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 17b2a3484b..08cc1d9e5b 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 @@ -8,7 +8,8 @@ sealed class AnalyticsParam { 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 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 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 b0c9a0db1d..7648552745 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 @@ -59,9 +59,9 @@ sealed class Token( class ButtonQRCode : Send("Button - QR Code") class ButtonSwapCurrency : Send("Button - Swap Currency") - class TransactionSent(token: CurrencyType) : Send( + class TransactionSent(type: CurrencyType) : Send( event = "Transaction Sent", - params = mapOf("Token" to token.value), + params = mapOf("Token" to type.value), ) } 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 1e886dd335..c45924449f 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 @@ -21,6 +21,8 @@ import com.tangem.tap.common.analytics.Analytics 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.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 @@ -222,6 +224,7 @@ private fun sendTransaction( when (sendResult) { is SimpleResult.Success -> { + Analytics.send(Token.Send.TransactionSent(AnalyticsParam.CurrencyType.Amount(amountToSend))) Analytics.handleAnalyticsEvent( event = AnalyticsEventAnOld.TRANSACTION_IS_SENT, card = card, 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())) From c204af29737419436c6da74d78c90c1271236685 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Nov 2022 11:56:53 +0500 Subject: [PATCH 031/372] Updated on 2026-08-14 --- .../tangem/tap/common/analytics/Analytics.kt | 5 ++- .../common/analytics/api/EventHandlerApi.kt | 3 +- .../tap/common/analytics/events/Shop.kt | 34 +++++++++++++-- .../appsFlyer/AppsFlyerAnalyticsHandler.kt | 34 +++------------ .../firebase/FirebaseAnalyticsHandler.kt | 31 +++----------- .../tap/common/shop/TangemShopService.kt | 41 +++++++++---------- .../tap/features/shop/ui/ShopFragment.kt | 4 ++ 7 files changed, 72 insertions(+), 80 deletions(-) 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 index 9eaee82a3a..386213f341 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt @@ -11,6 +11,7 @@ import com.tangem.tap.common.analytics.api.ErrorEventLogger import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler import com.tangem.tap.common.analytics.events.AnalyticsEvent import com.tangem.tap.common.extensions.filterNotNull +import com.tangem.tap.common.shop.data.ProductType /** [REDACTED_AUTHOR] @@ -85,9 +86,9 @@ object Analytics : GlobalAnalyticsEventHandler { } } - override fun send(order: Storefront.Order) { + override fun send(order: Storefront.Order, productType: ProductType) { analyticsHandlers.filterIsInstance().forEach { - it.send(order) + it.send(order, productType) } } 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 ea77e75dfd..5eda855ec2 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 @@ -13,6 +13,7 @@ import com.tangem.tap.common.analytics.AnalyticsParamAnOld 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.common.shop.data.ProductType import com.tangem.tap.domain.configurable.config.Config /** @@ -83,7 +84,7 @@ interface BlockchainSdkErrorEventHandler { } interface ShopifyOrderEventHandler { - fun send(order: Storefront.Order) + fun send(order: Storefront.Order, productType: ProductType) } interface AnalyticsHandlerBuilder { 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..90fdf6e08f 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,10 @@ package com.tangem.tap.common.analytics.events +import com.shopify.buy3.Storefront +import com.tangem.common.Converter +import com.tangem.tap.common.extensions.filterNotNull +import com.tangem.tap.common.shop.data.ProductType + /** [REDACTED_AUTHOR] */ @@ -10,13 +15,36 @@ sealed class Shop( class ScreenOpened : IntroductionProcess("Shop Screen Opened") - class Purchased(productSku: String) : Shop( + class Purchased(params: Map) : Shop( event = "Purchased", - params = mapOf("SKU" to productSku), + params = params, ) class Redirected(partnerName: String) : Shop( event = "Redirected", params = mapOf("Partner" to partnerName), ) -} \ No newline at end of file +} + +class OrderToParamsConverter : Converter, Map> { + override fun convert(value: Pair): Map { + val order = value.first + val productType = value.second + + val code = (order.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code + 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}" + + return mapOf( + "Coupon Code" to code, + "SKU" to sku, + "Count" to count, + "Amount" to amount, + ).filterNotNull() + } +} 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 008b6189b9..8264b4c501 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,12 +1,12 @@ 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.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler +import com.tangem.tap.common.analytics.events.OrderToParamsConverter +import com.tangem.tap.common.analytics.events.Shop +import com.tangem.tap.common.shop.data.ProductType class AppsFlyerAnalyticsHandler( private val client: AppsFlyerAnalyticsClient, @@ -18,12 +18,12 @@ class AppsFlyerAnalyticsHandler( client.logEvent(event, params) } - override fun send(order: Storefront.Order) { - send(ORDER_EVENT, OrderToParamsConverter().convert(order)) + override fun send(order: Storefront.Order, productType: ProductType) { + val eventParams = OrderToParamsConverter().convert(order to productType) + send(Shop.Purchased(eventParams)) } companion object { - const val ORDER_EVENT = AFInAppEventType.PURCHASE const val ID = "AppsFlyer" } @@ -34,26 +34,4 @@ class AppsFlyerAnalyticsHandler( else -> null }?.let { AppsFlyerAnalyticsHandler(it) } } -} - -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 - } } \ No newline at end of file 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 65ef31ac8e..1c79942d4f 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,9 +1,7 @@ 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 @@ -15,6 +13,9 @@ 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.events.OrderToParamsConverter +import com.tangem.tap.common.analytics.events.Shop +import com.tangem.tap.common.shop.data.ProductType class FirebaseAnalyticsHandler( private val client: FirebaseAnalyticsClient, @@ -57,8 +58,9 @@ class FirebaseAnalyticsHandler( } } - override fun send(order: Storefront.Order) { - send(FirebaseClient.ORDER_EVENT, OrderToParamsConverter().convert(order)) + override fun send(order: Storefront.Order, productType: ProductType) { + val eventParams = OrderToParamsConverter().convert(order to productType) + send(Shop.Purchased(eventParams)) } companion object { @@ -72,25 +74,4 @@ class FirebaseAnalyticsHandler( else -> null }?.let { FirebaseAnalyticsHandler(it) } } -} - -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 - } } \ No newline at end of file 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 87dacd7bc1..3d2c060786 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 @@ -22,7 +22,6 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) { private val shopifyService = ShopifyService(application, shopifyShop) - private val checkouts = mutableMapOf() private val variants = mutableMapOf() @@ -40,24 +39,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)) @@ -85,7 +87,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, ) } @@ -104,7 +106,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 { @@ -119,7 +121,7 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) { private suspend fun completeTokenizedPayment( paymentData: PaymentData, - productType: ProductType + productType: ProductType, ): Result { val checkout = checkouts[productType]!! val googlePayResponse = @@ -140,19 +142,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, ) } @@ -181,12 +182,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()!!) } @@ -197,9 +198,7 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) { suspend fun waitForCheckout(productType: ProductType) { val result = shopifyService.checkout(true, checkouts[productType]!!.id) - result.onSuccess { - it.order?.let { Analytics.send(it) } - } + result.onSuccess { checkout -> checkout.order?.let { Analytics.send(it, productType) } } } companion object { 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()) From 48343b3c604483811203a922494e278f81db8221 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Nov 2022 12:30:20 +0500 Subject: [PATCH 032/372] Updated on 2026-08-14 --- .../tangem/tap/domain/walletconnect/WalletConnectManager.kt | 3 +++ 1 file changed, 3 insertions(+) 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 f9ed7f61a5..31e16004cd 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 @@ -6,6 +6,7 @@ import com.tangem.common.extensions.guard import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.AnalyticsAnOld +import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.common.analytics.logWcEvent import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.global.GlobalAction @@ -224,6 +225,7 @@ class WalletConnectManager { } private fun onSessionClosed(session: WCSession) { + Analytics.send(WalletConnect.SessionDisconnected()) Analytics.logWcEvent( AnalyticsAnOld.WcAnalyticsEvent.Session( AnalyticsAnOld.WcSessionEvent.Disconnect, sessions[session]?.peerMeta?.url @@ -373,6 +375,7 @@ class WalletConnectManager { ), ) } + Analytics.send(WalletConnect.NewSessionEstablished()) Analytics.logWcEvent( AnalyticsAnOld.WcAnalyticsEvent.Session( AnalyticsAnOld.WcSessionEvent.Connect, peer.url, From dec6afa96a4adfbcfb8fa08fafb7a9731d909854 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Nov 2022 14:57:52 +0500 Subject: [PATCH 033/372] Updated on 2026-08-14 --- .../java/com/tangem/tap/common/analytics/events/Token.kt | 7 ++++++- .../wallet/redux/middlewares/MultiWalletMiddleware.kt | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) 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 7648552745..9da6abe399 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 @@ -12,9 +12,14 @@ sealed class Token( ) : AnalyticsEvent(category, event, params) { class Refreshed : Token("Token", "Refreshed") - class ButtonRemoveToken : Token("Token", "Button - Remove Token") class ButtonExplore : Token("Token", "Button - Explore") + 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", 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 3433d447d5..3fe4e1b3d0 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 @@ -6,6 +6,8 @@ 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 @@ -110,7 +112,7 @@ class MultiWalletMiddleware { WalletDialog.RemoveWalletDialog( currencyTitle = currency.currencyName, onOk = { - Analytics.send(com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken()) + Analytics.send(ButtonRemoveToken(AnalyticsParam.CurrencyType.Currency(currency))) store.dispatch( WalletAction.MultiWallet.RemoveWallet( currency = currency, From c2b57b28b0506ce12bce12741bf46d3a3cca0d87 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Nov 2022 17:52:24 +0500 Subject: [PATCH 034/372] Updated on 2026-08-14 --- .../converters/ShopOrderToEventConverter.kt | 27 ++++++++++++++ .../tap/common/analytics/events/Shop.kt | 37 ++++--------------- .../appsFlyer/AppsFlyerAnalyticsHandler.kt | 8 ++-- .../firebase/FirebaseAnalyticsHandler.kt | 8 ++-- 4 files changed, 43 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt 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/Shop.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt index 90fdf6e08f..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,9 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.shopify.buy3.Storefront -import com.tangem.common.Converter import com.tangem.tap.common.extensions.filterNotNull -import com.tangem.tap.common.shop.data.ProductType /** [REDACTED_AUTHOR] @@ -15,36 +12,18 @@ sealed class Shop( class ScreenOpened : IntroductionProcess("Shop Screen Opened") - class Purchased(params: Map) : Shop( + class Purchased(sku: String, count: String, amount: String, couponCode: String?) : Shop( event = "Purchased", - params = params, + params = mapOf( + "SKU" to sku, + "Count" to count, + "Amount" to amount, + "Coupon Code" to couponCode, + ).filterNotNull(), ) class Redirected(partnerName: String) : Shop( event = "Redirected", params = mapOf("Partner" to partnerName), ) -} - -class OrderToParamsConverter : Converter, Map> { - override fun convert(value: Pair): Map { - val order = value.first - val productType = value.second - - val code = (order.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code - 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}" - - return mapOf( - "Coupon Code" to code, - "SKU" to sku, - "Count" to count, - "Amount" to amount, - ).filterNotNull() - } -} +} \ No newline at end of file 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 8264b4c501..fe23cfd753 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 @@ -4,8 +4,7 @@ import com.shopify.buy3.Storefront import com.tangem.tap.common.analytics.api.AnalyticsEventHandler import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler -import com.tangem.tap.common.analytics.events.OrderToParamsConverter -import com.tangem.tap.common.analytics.events.Shop +import com.tangem.tap.common.analytics.converters.ShopOrderToEventConverter import com.tangem.tap.common.shop.data.ProductType class AppsFlyerAnalyticsHandler( @@ -19,8 +18,9 @@ class AppsFlyerAnalyticsHandler( } override fun send(order: Storefront.Order, productType: ProductType) { - val eventParams = OrderToParamsConverter().convert(order to productType) - send(Shop.Purchased(eventParams)) + //TODO: make sending this event through filters + val event = ShopOrderToEventConverter().convert(order to productType) + send(event) } companion object { 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 1c79942d4f..f2abc8ca76 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 @@ -13,8 +13,7 @@ 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.events.OrderToParamsConverter -import com.tangem.tap.common.analytics.events.Shop +import com.tangem.tap.common.analytics.converters.ShopOrderToEventConverter import com.tangem.tap.common.shop.data.ProductType class FirebaseAnalyticsHandler( @@ -59,8 +58,9 @@ class FirebaseAnalyticsHandler( } override fun send(order: Storefront.Order, productType: ProductType) { - val eventParams = OrderToParamsConverter().convert(order to productType) - send(Shop.Purchased(eventParams)) + //TODO: make sending this event through filters + val event = ShopOrderToEventConverter().convert(order to productType) + send(event) } companion object { From 9c3cd6f2d3df061a26a931bdd0fabd2bb22d2878 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Nov 2022 19:26:45 +0500 Subject: [PATCH 035/372] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 4 +++ .../tangem/tap/common/analytics/Analytics.kt | 30 ++++++++++++------- .../analytics/GlobalAnalyticsEventHandler.kt | 9 ++++-- .../common/analytics/api/EventFilterApi.kt | 12 ++++++++ .../common/analytics/api/EventHandlerApi.kt | 6 ---- .../filters/ShopPurchasedEventFilter.kt | 25 ++++++++++++++++ .../appsFlyer/AppsFlyerAnalyticsHandler.kt | 12 +------- .../firebase/FirebaseAnalyticsHandler.kt | 12 +------- .../tap/common/shop/TangemShopService.kt | 8 ++++- 9 files changed, 76 insertions(+), 42 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/api/EventFilterApi.kt create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/filters/ShopPurchasedEventFilter.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 1913ac6e16..5417042486 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -17,6 +17,7 @@ import com.tangem.tap.common.AssetReader import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.AnalyticsHandlersFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder +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 @@ -122,6 +123,9 @@ class TapApplication : Application(), ImageLoaderFactory { factory.build(buildData).forEach { Analytics.addHandler(it.id(), it) } + listOf( + ShopPurchasedEventFilter(), + ).forEach { Analytics.addFilter(it) } } private fun initFeedbackManager(context: Context, preferencesStorage: PreferencesStorage, config: Config) { 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 index 386213f341..57617de12b 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt @@ -1,23 +1,22 @@ 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.AnalyticsEventFilter 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.ErrorEventLogger -import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler import com.tangem.tap.common.analytics.events.AnalyticsEvent import com.tangem.tap.common.extensions.filterNotNull -import com.tangem.tap.common.shop.data.ProductType /** [REDACTED_AUTHOR] */ object Analytics : GlobalAnalyticsEventHandler { + private val eventFilters = mutableListOf() private val handlers = mutableMapOf() private val analyticsHandlers: List @@ -35,6 +34,14 @@ object Analytics : GlobalAnalyticsEventHandler { return handlers.remove(name) } + override fun addFilter(filter: AnalyticsEventFilter) { + eventFilters.add(filter) + } + + override fun removeFilter(filter: AnalyticsEventFilter) { + eventFilters.remove(filter) + } + override fun attachToAllEvents(key: String, value: String) { attachToAllEventsParams[key] = value } @@ -44,7 +51,16 @@ object Analytics : GlobalAnalyticsEventHandler { } override fun send(event: AnalyticsEvent, card: Card?, blockchain: String?) { - analyticsHandlers.forEach { it.send(event, card, blockchain) } + val eventFilter = eventFilters.firstOrNull { it.canBeAppliedTo(event) } + + when { + eventFilter == null -> analyticsHandlers.forEach { it.send(event, card, blockchain) } + eventFilter.canBeSent(event) -> { + analyticsHandlers + .filter { eventFilter.canBeConsumedBy(it, event) } + .forEach { it.send(event) } + } + } } override fun handleAnalyticsEvent( @@ -86,12 +102,6 @@ object Analytics : GlobalAnalyticsEventHandler { } } - override fun send(order: Storefront.Order, productType: ProductType) { - analyticsHandlers.filterIsInstance().forEach { - it.send(order, productType) - } - } - override fun prepareParams(card: Card?, blockchain: String?, params: Map): Map { return super.prepareParams(card, blockchain, params).toMutableMap().apply { putAll(attachToAllEventsParams) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandler.kt index 139ffa5582..287f991527 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandler.kt @@ -1,18 +1,21 @@ package com.tangem.tap.common.analytics +import com.tangem.tap.common.analytics.api.AnalyticsEventFilter import com.tangem.tap.common.analytics.api.AnalyticsEventHandler import com.tangem.tap.common.analytics.api.ErrorEventHandler import com.tangem.tap.common.analytics.api.SdkErrorEventHandler -import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler interface GlobalAnalyticsEventHandler : AnalyticsEventHandler, ErrorEventHandler, - SdkErrorEventHandler, - ShopifyOrderEventHandler { + SdkErrorEventHandler { fun addHandler(name: String, handler: AnalyticsEventHandler) fun removeHandler(name: String): AnalyticsEventHandler? + fun addFilter(filter: AnalyticsEventFilter) + + fun removeFilter(filter: AnalyticsEventFilter) + fun attachToAllEvents(key: String, value: String) } 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..a4e93fa75b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/api/EventFilterApi.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.common.analytics.api + +import com.tangem.tap.common.analytics.events.AnalyticsEvent + +/** +[REDACTED_AUTHOR] + */ +interface AnalyticsEventFilter { + fun canBeAppliedTo(event: AnalyticsEvent): Boolean + fun canBeSent(event: AnalyticsEvent): Boolean + fun canBeConsumedBy(handler: AnalyticsEventHandler, event: AnalyticsEvent): 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 5eda855ec2..494747287e 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,7 +1,6 @@ package com.tangem.tap.common.analytics.api import android.app.Application -import com.shopify.buy3.Storefront import com.tangem.blockchain.common.BlockchainError import com.tangem.common.card.Card import com.tangem.common.core.TangemSdkError @@ -13,7 +12,6 @@ import com.tangem.tap.common.analytics.AnalyticsParamAnOld 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.common.shop.data.ProductType import com.tangem.tap.domain.configurable.config.Config /** @@ -83,10 +81,6 @@ interface BlockchainSdkErrorEventHandler { ) } -interface ShopifyOrderEventHandler { - fun send(order: Storefront.Order, productType: ProductType) -} - interface AnalyticsHandlerBuilder { fun build(data: Data): AnalyticsEventHandler? 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..5267782ddc --- /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.AnalyticsEventHandler +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 canBeConsumedBy(handler: AnalyticsEventHandler, 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/appsFlyer/AppsFlyerAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt index fe23cfd753..b91da358d6 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,15 +1,11 @@ package com.tangem.tap.common.analytics.handlers.appsFlyer -import com.shopify.buy3.Storefront import com.tangem.tap.common.analytics.api.AnalyticsEventHandler import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder -import com.tangem.tap.common.analytics.api.ShopifyOrderEventHandler -import com.tangem.tap.common.analytics.converters.ShopOrderToEventConverter -import com.tangem.tap.common.shop.data.ProductType class AppsFlyerAnalyticsHandler( private val client: AppsFlyerAnalyticsClient, -) : AnalyticsEventHandler, ShopifyOrderEventHandler { +) : AnalyticsEventHandler { override fun id(): String = ID @@ -17,12 +13,6 @@ class AppsFlyerAnalyticsHandler( client.logEvent(event, params) } - override fun send(order: Storefront.Order, productType: ProductType) { - //TODO: make sending this event through filters - val event = ShopOrderToEventConverter().convert(order to productType) - send(event) - } - companion object { const val ID = "AppsFlyer" } 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 f2abc8ca76..9a4a8bf839 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,6 +1,5 @@ package com.tangem.tap.common.analytics.handlers.firebase -import com.shopify.buy3.Storefront import com.tangem.blockchain.common.BlockchainError import com.tangem.common.card.Card import com.tangem.common.core.TangemSdkError @@ -10,15 +9,12 @@ import com.tangem.tap.common.analytics.api.AnalyticsEventHandler 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.ShopOrderToEventConverter -import com.tangem.tap.common.shop.data.ProductType class FirebaseAnalyticsHandler( private val client: FirebaseAnalyticsClient, -) : AnalyticsEventHandler, ErrorEventHandler, SdkErrorEventHandler, ShopifyOrderEventHandler { +) : AnalyticsEventHandler, ErrorEventHandler, SdkErrorEventHandler { override fun id(): String = ID @@ -57,12 +53,6 @@ class FirebaseAnalyticsHandler( } } - override fun send(order: Storefront.Order, productType: ProductType) { - //TODO: make sending this event through filters - val event = ShopOrderToEventConverter().convert(order to productType) - send(event) - } - companion object { const val ID = "Firebase" } 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 3d2c060786..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 @@ -5,6 +5,7 @@ import android.content.Intent import com.google.android.gms.wallet.PaymentData import com.shopify.buy3.Storefront 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 @@ -198,7 +199,12 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) { suspend fun waitForCheckout(productType: ProductType) { val result = shopifyService.checkout(true, checkouts[productType]!!.id) - result.onSuccess { checkout -> checkout.order?.let { Analytics.send(it, productType) } } + result.onSuccess { checkout -> + checkout.order?.let { + val event = ShopOrderToEventConverter().convert(it to productType) + Analytics.send(event) + } + } } companion object { From bd0adfea6ffc5d403816862ed99b391900d3f55b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Nov 2022 21:24:22 +0500 Subject: [PATCH 036/372] Updated on 2026-08-14 --- .../onboarding/products/twins/redux/TwinCardsMiddleware.kt | 6 ++++++ 1 file changed, 6 insertions(+) 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 bf3cb1296c..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,5 +1,6 @@ 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 @@ -7,6 +8,7 @@ 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 @@ -273,6 +275,10 @@ private fun handle(action: Action, dispatch: DispatchFunction) { } is TwinCardsAction.TopUp -> { val topUpUrl = twinCardsState.walletManager?.getTopUpUrl() ?: return + + val currencyType = AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin) + Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType)) + store.dispatchOpenUrl(topUpUrl) } TwinCardsAction.Done -> { From 7d7b6106a0673e93a8d9c02866b72aeebe450de3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 2 Nov 2022 21:48:54 +0500 Subject: [PATCH 037/372] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 2 + .../converters/BasicSignInEventConverter.kt | 38 +++++++++++++++++++ .../converters/ParamCardCurrencyConverter.kt | 31 +++++++++++++++ .../common/analytics/events/AnalyticsEvent.kt | 1 + .../tap/common/analytics/events/Basic.kt | 2 +- .../analytics/filters/BasicSignInFilter.kt | 27 +++++++++++++ .../redux/reducers/TotalBalanceOperations.kt | 27 +++++++++---- .../tap/features/wallet/ui/WalletFragment.kt | 14 +++++++ 8 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/converters/BasicSignInEventConverter.kt create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/filters/BasicSignInFilter.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 5417042486..36fcd97572 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -17,6 +17,7 @@ import com.tangem.tap.common.AssetReader import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.AnalyticsHandlersFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder +import com.tangem.tap.common.analytics.filters.BasicSignInFilter 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 @@ -125,6 +126,7 @@ class TapApplication : Application(), ImageLoaderFactory { } listOf( ShopPurchasedEventFilter(), + BasicSignInFilter(), ).forEach { Analytics.addFilter(it) } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicSignInEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicSignInEventConverter.kt new file mode 100644 index 0000000000..e3d07461d0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicSignInEventConverter.kt @@ -0,0 +1,38 @@ +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.domain.tokens.UserWalletId +import com.tangem.tap.features.wallet.redux.ProgressState +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +class BasicSignInEventConverter( + private val scanResponse: ScanResponse, + private val progressState: ProgressState, + private val totalCryptoAmount: BigDecimal, +) : Converter { + + override fun convert(value: Any?): Basic.SignedIn? { + if (progressState == ProgressState.Loading || progressState == ProgressState.Refreshing) return null + val cardCurrency = ParamCardCurrencyConverter().convert(scanResponse) ?: return null + + val cardState = when { + totalCryptoAmount.isZero() -> AnalyticsParam.CardState.Empty + else -> AnalyticsParam.CardState.Full + } + + return Basic.SignedIn( + state = cardState, + currency = cardCurrency, + batch = scanResponse.card.batchId, + ).apply { + filterData = UserWalletId(scanResponse.card.wallets.first().publicKey).stringValue + } + } +} \ 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/events/AnalyticsEvent.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsEvent.kt index 0451fdd68c..8e5cf0e942 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 @@ -7,4 +7,5 @@ sealed class AnalyticsEvent( val category: String, val event: String, val params: Map = mapOf(), + var filterData: Any? = null, ) \ 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..cba32f8061 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 @@ -8,7 +8,7 @@ sealed class Basic( params: Map = mapOf(), ) : AnalyticsEvent("Basic", event, params) { - sealed class SignedIn( + class SignedIn( state: AnalyticsParam.CardState, currency: AnalyticsParam.CardCurrency, batch: String, 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..c885cf7ae6 --- /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.AnalyticsEventHandler +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 canBeConsumedBy(handler: AnalyticsEventHandler, event: AnalyticsEvent): Boolean = true +} \ 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/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index 97fba31222..b44dcebc1d 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 @@ -17,6 +17,7 @@ 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.events.MainScreen import com.tangem.tap.common.analytics.events.Portfolio import com.tangem.tap.common.extensions.show @@ -33,6 +34,7 @@ import com.tangem.tap.features.wallet.redux.ErrorType import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.redux.reducers.calculateTotalCryptoAmount import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView import com.tangem.tap.features.wallet.ui.wallet.SaltPaySingleWalletView @@ -119,6 +121,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber Date: Thu, 3 Nov 2022 17:42:20 +0500 Subject: [PATCH 038/372] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../common/analytics/converters/BasicSignInEventConverter.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index a1658496e7..cf6ea50867 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit a1658496e777b611fc990ef2bc1a1a1fd48bc1e6 +Subproject commit cf6ea50867477f97655da9da47ff94987e8f3354 diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicSignInEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicSignInEventConverter.kt index e3d07461d0..083fd47f08 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicSignInEventConverter.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicSignInEventConverter.kt @@ -5,7 +5,7 @@ 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.domain.tokens.UserWalletId +import com.tangem.tap.domain.extensions.getUserWalletId import com.tangem.tap.features.wallet.redux.ProgressState import java.math.BigDecimal @@ -32,7 +32,7 @@ class BasicSignInEventConverter( currency = cardCurrency, batch = scanResponse.card.batchId, ).apply { - filterData = UserWalletId(scanResponse.card.wallets.first().publicKey).stringValue + filterData = scanResponse.card.getUserWalletId() } } } \ No newline at end of file From d2d725c4e9927ce8e6b7b9f1334b460cf54ba31e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 3 Nov 2022 17:45:42 +0500 Subject: [PATCH 039/372] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 2 + .../converters/BasicEventConverter.kt | 76 +++++++++++++++++++ .../converters/BasicSignInEventConverter.kt | 38 ---------- .../common/analytics/events/AnalyticsParam.kt | 7 +- .../tap/common/analytics/events/Basic.kt | 2 +- .../analytics/filters/BasicTopUpFilter.kt | 50 ++++++++++++ .../tap/features/wallet/ui/WalletFragment.kt | 10 +-- .../tap/persistence/PreferencesStorage.kt | 2 + .../tap/persistence/ToppedUpWalletStorage.kt | 57 ++++++++++++++ 9 files changed, 195 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/converters/BasicEventConverter.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/analytics/converters/BasicSignInEventConverter.kt create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/filters/BasicTopUpFilter.kt create mode 100644 app/src/main/java/com/tangem/tap/persistence/ToppedUpWalletStorage.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 36fcd97572..be1837114f 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -18,6 +18,7 @@ import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.AnalyticsHandlersFactory 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 @@ -127,6 +128,7 @@ class TapApplication : Application(), ImageLoaderFactory { listOf( ShopPurchasedEventFilter(), BasicSignInFilter(), + BasicTopUpFilter(preferencesStorage.toppedUpWalletStorage), ).forEach { Analytics.addFilter(it) } } 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/BasicSignInEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicSignInEventConverter.kt deleted file mode 100644 index 083fd47f08..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/converters/BasicSignInEventConverter.kt +++ /dev/null @@ -1,38 +0,0 @@ -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.domain.extensions.getUserWalletId -import com.tangem.tap.features.wallet.redux.ProgressState -import java.math.BigDecimal - -/** -[REDACTED_AUTHOR] - */ -class BasicSignInEventConverter( - private val scanResponse: ScanResponse, - private val progressState: ProgressState, - private val totalCryptoAmount: BigDecimal, -) : Converter { - - override fun convert(value: Any?): Basic.SignedIn? { - if (progressState == ProgressState.Loading || progressState == ProgressState.Refreshing) return null - val cardCurrency = ParamCardCurrencyConverter().convert(scanResponse) ?: return null - - val cardState = when { - totalCryptoAmount.isZero() -> AnalyticsParam.CardState.Empty - else -> AnalyticsParam.CardState.Full - } - - return Basic.SignedIn( - state = cardState, - currency = cardCurrency, - batch = scanResponse.card.batchId, - ).apply { - filterData = scanResponse.card.getUserWalletId() - } - } -} \ 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 08cc1d9e5b..71e6eb6c09 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 @@ -18,9 +18,10 @@ sealed class AnalyticsParam { 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) { 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 cba32f8061..f1e3927530 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 @@ -9,7 +9,7 @@ sealed class Basic( ) : AnalyticsEvent("Basic", event, params) { class SignedIn( - state: AnalyticsParam.CardState, + state: AnalyticsParam.CardBalanceState, currency: AnalyticsParam.CardCurrency, batch: String, ) : Basic( 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..5b0638231e --- /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.AnalyticsEventHandler +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 canBeConsumedBy(handler: AnalyticsEventHandler, 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/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index b44dcebc1d..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 @@ -18,6 +18,7 @@ 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 @@ -34,7 +35,6 @@ import com.tangem.tap.features.wallet.redux.ErrorType import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState -import com.tangem.tap.features.wallet.redux.reducers.calculateTotalCryptoAmount import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView import com.tangem.tap.features.wallet.ui.wallet.SaltPaySingleWalletView @@ -231,13 +231,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber = 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 { + jsonConverter.fromJson(json, jsonConverter.typedList(BasicTopUpFilter.Data::class.java))!! + } catch (ex: Exception) { + preferences.edit(true) { remove(KEY) } + mutableSetOf() + } + } + + companion object { + private const val KEY = "userWalletsInfo" + } +} \ No newline at end of file From 431852f30c12e5f97ae3ea7539bf3e3cc5c7dc97 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 8 Nov 2022 14:53:46 +0500 Subject: [PATCH 040/372] Updated on 2026-08-14 --- .../tangem/tap/common/analytics/Analytics.kt | 60 ++++++++++++++----- 1 file changed, 44 insertions(+), 16 deletions(-) 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 index 57617de12b..6e773bceab 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt @@ -3,6 +3,7 @@ package com.tangem.tap.common.analytics import com.tangem.blockchain.common.BlockchainError import com.tangem.common.card.Card import com.tangem.common.core.TangemSdkError +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.BlockchainSdkErrorEventHandler @@ -10,12 +11,20 @@ import com.tangem.tap.common.analytics.api.CardSdkErrorEventHandler import com.tangem.tap.common.analytics.api.ErrorEventLogger import com.tangem.tap.common.analytics.events.AnalyticsEvent import com.tangem.tap.common.extensions.filterNotNull +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.launch +import java.util.concurrent.Executors /** [REDACTED_AUTHOR] */ object Analytics : GlobalAnalyticsEventHandler { + internal val analyticsScope: CoroutineScope by lazy { createScope() } + private val eventFilters = mutableListOf() private val handlers = mutableMapOf() @@ -47,18 +56,22 @@ object Analytics : GlobalAnalyticsEventHandler { } override fun send(event: String, params: Map) { - analyticsHandlers.forEach { it.send(event, params) } + analyticsScope.launch { + analyticsHandlers.forEach { it.send(event, params) } + } } override fun send(event: AnalyticsEvent, card: Card?, blockchain: String?) { - val eventFilter = eventFilters.firstOrNull { it.canBeAppliedTo(event) } + analyticsScope.launch { + val eventFilter = eventFilters.firstOrNull { it.canBeAppliedTo(event) } - when { - eventFilter == null -> analyticsHandlers.forEach { it.send(event, card, blockchain) } - eventFilter.canBeSent(event) -> { - analyticsHandlers - .filter { eventFilter.canBeConsumedBy(it, event) } - .forEach { it.send(event) } + when { + eventFilter == null -> analyticsHandlers.forEach { it.send(event, card, blockchain) } + eventFilter.canBeSent(event) -> { + analyticsHandlers + .filter { eventFilter.canBeConsumedBy(it, event) } + .forEach { it.send(event) } + } } } } @@ -69,14 +82,18 @@ object Analytics : GlobalAnalyticsEventHandler { card: Card?, blockchain: String?, ) { - analyticsHandlers.forEach { - it.handleAnalyticsEvent(event, params, card, blockchain) + analyticsScope.launch { + analyticsHandlers.forEach { + it.handleAnalyticsEvent(event, params, card, blockchain) + } } } override fun send(error: Throwable, params: Map) { - analyticsHandlers.filterIsInstance().forEach { - it.logErrorEvent(error, params) + analyticsScope.launch { + analyticsHandlers.filterIsInstance().forEach { + it.logErrorEvent(error, params) + } } } @@ -86,8 +103,10 @@ object Analytics : GlobalAnalyticsEventHandler { params: Map, card: Card?, ) { - analyticsHandlers.filterIsInstance().forEach { - it.send(error, action, params, card) + analyticsScope.launch { + analyticsHandlers.filterIsInstance().forEach { + it.send(error, action, params, card) + } } } @@ -97,8 +116,10 @@ object Analytics : GlobalAnalyticsEventHandler { params: Map, card: Card?, ) { - analyticsHandlers.filterIsInstance().forEach { - it.send(error, action, params, card) + analyticsScope.launch { + analyticsHandlers.filterIsInstance().forEach { + it.send(error, action, params, card) + } } } @@ -107,6 +128,13 @@ object Analytics : GlobalAnalyticsEventHandler { putAll(attachToAllEventsParams) } } + + private fun createScope(): CoroutineScope { + val name = "Analytics" + val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() + val exceptionHandler = FeatureCoroutineExceptionHandler.create(name) + return CoroutineScope(Job() + dispatcher + CoroutineName(name) + exceptionHandler) + } } fun GlobalAnalyticsEventHandler.logWcEvent(event: AnalyticsAnOld.WcAnalyticsEvent) { From de93d0f3503a9fd2c3dc6ed1f755a1ffeba07693 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 8 Nov 2022 18:26:18 +0500 Subject: [PATCH 041/372] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../tangem/tap/common/analytics/Analytics.kt | 67 +++++++++++++------ .../analytics/GlobalAnalyticsEventHandler.kt | 21 ------ .../analytics/api/ErrorEventHandlerApi.kt | 37 ++++++++++ .../common/analytics/api/EventFilterApi.kt | 7 +- .../common/analytics/api/EventHandlerApi.kt | 41 +----------- .../analytics/api/ParamsInterceptorApi.kt | 17 +++++ .../common/analytics/events/AnalyticsParam.kt | 4 +- .../tap/common/analytics/events/Basic.kt | 2 +- .../analytics/filters/BasicSignInFilter.kt | 2 +- .../analytics/filters/BasicTopUpFilter.kt | 2 +- .../filters/ShopPurchasedEventFilter.kt | 2 +- .../BatchIdParamsInterceptor.kt | 21 ++++++ .../tap/features/home/redux/HomeMiddleware.kt | 4 +- 14 files changed, 140 insertions(+), 89 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandler.kt create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/api/ErrorEventHandlerApi.kt create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/api/ParamsInterceptorApi.kt create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/BatchIdParamsInterceptor.kt diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index cf6ea50867..a1658496e7 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit cf6ea50867477f97655da9da47ff94987e8f3354 +Subproject commit a1658496e777b611fc990ef2bc1a1a1fd48bc1e6 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 index 6e773bceab..3950438219 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt @@ -6,9 +6,15 @@ import com.tangem.common.core.TangemSdkError 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.AnalyticsHandlerHolder 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.ParamsInterceptor +import com.tangem.tap.common.analytics.api.ParamsInterceptorHolder +import com.tangem.tap.common.analytics.api.SdkErrorEventHandler import com.tangem.tap.common.analytics.events.AnalyticsEvent import com.tangem.tap.common.extensions.filterNotNull import kotlinx.coroutines.CoroutineName @@ -21,18 +27,27 @@ import java.util.concurrent.Executors /** [REDACTED_AUTHOR] */ +interface GlobalAnalyticsEventHandler : AnalyticsEventHandler, + ErrorEventHandler, + SdkErrorEventHandler, + AnalyticsHandlerHolder, + AnalyticsFilterHolder, + ParamsInterceptorHolder { + + fun send(event: AnalyticsEvent, card: Card? = null, blockchain: String? = null) +} + object Analytics : GlobalAnalyticsEventHandler { - internal val analyticsScope: CoroutineScope by lazy { createScope() } + private val analyticsScope: CoroutineScope by lazy { createScope() } - private val eventFilters = mutableListOf() private val handlers = mutableMapOf() + private val analyticsFilters = mutableSetOf() + private val paramsInterceptors = mutableMapOf() private val analyticsHandlers: List get() = handlers.values.toList() - private val attachToAllEventsParams: MutableMap = mutableMapOf() - override fun id(): String = analyticsHandlers.joinToString(", ") { it.id() } override fun addHandler(name: String, handler: AnalyticsEventHandler) { @@ -44,33 +59,39 @@ object Analytics : GlobalAnalyticsEventHandler { } override fun addFilter(filter: AnalyticsEventFilter) { - eventFilters.add(filter) + analyticsFilters.add(filter) } - override fun removeFilter(filter: AnalyticsEventFilter) { - eventFilters.remove(filter) + override fun removeFilter(filter: AnalyticsEventFilter): Boolean { + return analyticsFilters.remove(filter) } - override fun attachToAllEvents(key: String, value: String) { - attachToAllEventsParams[key] = value + override fun addParamsInterceptor(interceptor: ParamsInterceptor) { + paramsInterceptors[interceptor.id()] = interceptor + } + + override fun removeParamsInterceptor(interceptor: ParamsInterceptor): ParamsInterceptor? { + return paramsInterceptors.remove(interceptor.id()) } override fun send(event: String, params: Map) { analyticsScope.launch { - analyticsHandlers.forEach { it.send(event, params) } + analyticsHandlers.forEach { it.send(event, params.interceptParams()) } } } override fun send(event: AnalyticsEvent, card: Card?, blockchain: String?) { analyticsScope.launch { - val eventFilter = eventFilters.firstOrNull { it.canBeAppliedTo(event) } + val eventString = prepareEventString(event.category, event.event) + val eventParams = prepareParams(card, blockchain, event.interceptParams()) + val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(event) } when { - eventFilter == null -> analyticsHandlers.forEach { it.send(event, card, blockchain) } + eventFilter == null -> analyticsHandlers.forEach { it.send(eventString, eventParams) } eventFilter.canBeSent(event) -> { analyticsHandlers - .filter { eventFilter.canBeConsumedBy(it, event) } - .forEach { it.send(event) } + .filter { handler -> eventFilter.canBeConsumedByHandler(handler, event) } + .forEach { it.send(eventString, eventParams) } } } } @@ -84,7 +105,7 @@ object Analytics : GlobalAnalyticsEventHandler { ) { analyticsScope.launch { analyticsHandlers.forEach { - it.handleAnalyticsEvent(event, params, card, blockchain) + it.handleAnalyticsEvent(event, params.interceptParams(), card, blockchain) } } } @@ -123,17 +144,21 @@ object Analytics : GlobalAnalyticsEventHandler { } } - override fun prepareParams(card: Card?, blockchain: String?, params: Map): Map { - return super.prepareParams(card, blockchain, params).toMutableMap().apply { - putAll(attachToAllEventsParams) - } + private fun AnalyticsEvent.interceptParams(): Map = params.interceptParams() + + private fun Map.interceptParams(): Map { + return this.toMutableMap().apply { interceptParams() } + } + + private fun MutableMap.interceptParams() { + paramsInterceptors.values.forEach { it.intercept(this) } } private fun createScope(): CoroutineScope { val name = "Analytics" val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() - val exceptionHandler = FeatureCoroutineExceptionHandler.create(name) - return CoroutineScope(Job() + dispatcher + CoroutineName(name) + exceptionHandler) + val exHandler = FeatureCoroutineExceptionHandler.create(name) + return CoroutineScope(Job() + dispatcher + CoroutineName(name) + exHandler) } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandler.kt deleted file mode 100644 index 287f991527..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/GlobalAnalyticsEventHandler.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.tap.common.analytics - -import com.tangem.tap.common.analytics.api.AnalyticsEventFilter -import com.tangem.tap.common.analytics.api.AnalyticsEventHandler -import com.tangem.tap.common.analytics.api.ErrorEventHandler -import com.tangem.tap.common.analytics.api.SdkErrorEventHandler - -interface GlobalAnalyticsEventHandler : AnalyticsEventHandler, - ErrorEventHandler, - SdkErrorEventHandler { - - fun addHandler(name: String, handler: AnalyticsEventHandler) - - fun removeHandler(name: String): AnalyticsEventHandler? - - fun addFilter(filter: AnalyticsEventFilter) - - fun removeFilter(filter: AnalyticsEventFilter) - - fun attachToAllEvents(key: String, value: String) -} diff --git a/app/src/main/java/com/tangem/tap/common/analytics/api/ErrorEventHandlerApi.kt b/app/src/main/java/com/tangem/tap/common/analytics/api/ErrorEventHandlerApi.kt new file mode 100644 index 0000000000..3279075ea4 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/api/ErrorEventHandlerApi.kt @@ -0,0 +1,37 @@ +package com.tangem.tap.common.analytics.api + +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.AnalyticsParamAnOld + +/** +[REDACTED_AUTHOR] + */ +interface ErrorEventHandler { + fun send( + error: Throwable, + params: Map = emptyMap(), + ) +} + +interface SdkErrorEventHandler : CardSdkErrorEventHandler, BlockchainSdkErrorEventHandler + +interface CardSdkErrorEventHandler { + fun send( + error: TangemSdkError, + action: AnalyticsAnOld.ActionToLog, + params: Map = emptyMap(), + card: Card? = null, + ) +} + +interface BlockchainSdkErrorEventHandler { + fun send( + error: BlockchainError, + action: AnalyticsAnOld.ActionToLog, + params: Map = mapOf(), + card: Card? = null, + ) +} \ 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 index a4e93fa75b..c86313164b 100644 --- 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 @@ -8,5 +8,10 @@ import com.tangem.tap.common.analytics.events.AnalyticsEvent interface AnalyticsEventFilter { fun canBeAppliedTo(event: AnalyticsEvent): Boolean fun canBeSent(event: AnalyticsEvent): Boolean - fun canBeConsumedBy(handler: AnalyticsEventHandler, event: AnalyticsEvent): Boolean + fun canBeConsumedByHandler(handler: AnalyticsEventHandler, 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 494747287e..5095d83dfa 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,15 +1,10 @@ package com.tangem.tap.common.analytics.api import android.app.Application -import com.tangem.blockchain.common.BlockchainError import com.tangem.common.card.Card -import com.tangem.common.core.TangemSdkError import com.tangem.common.json.MoshiJsonConverter import com.tangem.domain.common.AnalyticsHandlersLogConfig -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.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 @@ -22,13 +17,6 @@ interface AnalyticsEventHandler { fun send(event: String, params: Map = emptyMap()) - fun send(event: AnalyticsEvent, card: Card? = null, blockchain: String? = null) { - send( - event = prepareEventString(event.category, event.event), - params = prepareParams(card, blockchain, event.params), - ) - } - @Deprecated("Migrate to AnalyticsEvent") fun handleAnalyticsEvent( event: AnalyticsEventAnOld, @@ -45,7 +33,6 @@ interface AnalyticsEventHandler { params: Map = emptyMap(), ): Map = mapOf( AnalyticsParam.Firmware to card?.firmwareVersion?.stringValue, - AnalyticsParam.BatchId to card?.batchId, AnalyticsParam.Blockchain to blockchain, ).filterNotNull() + params @@ -54,31 +41,9 @@ interface AnalyticsEventHandler { } } -interface ErrorEventHandler { - fun send( - error: Throwable, - params: Map = emptyMap(), - ) -} - -interface SdkErrorEventHandler : CardSdkErrorEventHandler, BlockchainSdkErrorEventHandler - -interface CardSdkErrorEventHandler { - fun send( - error: TangemSdkError, - action: AnalyticsAnOld.ActionToLog, - params: Map = emptyMap(), - card: Card? = null, - ) -} - -interface BlockchainSdkErrorEventHandler { - fun send( - error: BlockchainError, - action: AnalyticsAnOld.ActionToLog, - params: Map = mapOf(), - card: Card? = null, - ) +interface AnalyticsHandlerHolder { + fun addHandler(name: String, handler: AnalyticsEventHandler) + fun removeHandler(name: String): AnalyticsEventHandler? } interface AnalyticsHandlerBuilder { 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/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index 71e6eb6c09..0d76d1dd08 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 @@ -55,8 +55,10 @@ sealed class AnalyticsParam { } companion object Key { + const val BatchId = "Batch" + + // old keys const val Blockchain = "blockchain" - const val BatchId = "batch_id" const val Firmware = "firmware" } } \ 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 f1e3927530..229093235d 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 @@ -17,7 +17,7 @@ sealed class Basic( params = mapOf( "State" to state.value, "Currency" to currency.value, - "Batch" to batch, + AnalyticsParam.BatchId to batch, ), ) 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 index c885cf7ae6..372f2787a3 100644 --- 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 @@ -23,5 +23,5 @@ class BasicSignInFilter : AnalyticsEventFilter { return canBeSent } - override fun canBeConsumedBy(handler: AnalyticsEventHandler, event: AnalyticsEvent): Boolean = true + override fun canBeConsumedByHandler(handler: AnalyticsEventHandler, 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 index 5b0638231e..fea71037d9 100644 --- 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 @@ -39,7 +39,7 @@ class BasicTopUpFilter( } } - override fun canBeConsumedBy(handler: AnalyticsEventHandler, event: AnalyticsEvent): Boolean = true + override fun canBeConsumedByHandler(handler: AnalyticsEventHandler, event: AnalyticsEvent): Boolean = true data class Data( val walletId: String, 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 index 5267782ddc..945dbefab4 100644 --- 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 @@ -16,7 +16,7 @@ class ShopPurchasedEventFilter : AnalyticsEventFilter { override fun canBeSent(event: AnalyticsEvent): Boolean = true - override fun canBeConsumedBy(handler: AnalyticsEventHandler, event: AnalyticsEvent): Boolean { + override fun canBeConsumedByHandler(handler: AnalyticsEventHandler, event: AnalyticsEvent): Boolean { return when (handler) { is AppsFlyerAnalyticsHandler, is FirebaseAnalyticsHandler -> true else -> false 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/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 0148a2cbf7..16d6ee9efd 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 @@ -13,8 +13,8 @@ import com.tangem.tap.common.analytics.Analytics 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.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 @@ -165,7 +165,7 @@ private fun onScanSuccess(scanResponse: ScanResponse) { val tapWalletManager = globalState.tapWalletManager tapWalletManager.updateConfigManager(scanResponse) - Analytics.attachToAllEvents(AnalyticsParam.BatchId, scanResponse.card.batchId) + Analytics.addParamsInterceptor(BatchIdParamsInterceptor(scanResponse.card.batchId)) store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) From fec0f149526f4e4818bf7036a482fc7a5b02ce48 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 8 Nov 2022 18:37:46 +0500 Subject: [PATCH 042/372] Updated on 2026-08-14 --- .../com/tangem/tap/common/analytics/Analytics.kt | 4 ++-- .../tap/common/analytics/api/EventFilterApi.kt | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) 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 index 3950438219..276ef9059f 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt @@ -87,11 +87,11 @@ object Analytics : GlobalAnalyticsEventHandler { val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(event) } when { - eventFilter == null -> analyticsHandlers.forEach { it.send(eventString, eventParams) } + eventFilter == null -> analyticsHandlers.forEach { handler -> handler.send(eventString, eventParams) } eventFilter.canBeSent(event) -> { analyticsHandlers .filter { handler -> eventFilter.canBeConsumedByHandler(handler, event) } - .forEach { it.send(eventString, eventParams) } + .forEach { handler -> handler.send(eventString, eventParams) } } } } 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 index c86313164b..eb0f9587ce 100644 --- 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 @@ -4,10 +4,23 @@ 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: AnalyticsEventHandler, event: AnalyticsEvent): Boolean } From 5030c571c5f297b2f963f1bc54517e3f3e6e7625 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Nov 2022 06:08:40 +0500 Subject: [PATCH 043/372] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 23 ++- .../tangem/tap/common/analytics/Analytics.kt | 145 +++--------------- .../common/analytics/AnalyticsEventAnOld.kt | 71 --------- .../tap/common/analytics/AnalyticsFactory.kt | 37 +++++ .../analytics/AnalyticsHandlersFactory.kt | 23 --- .../analytics/api/ErrorEventHandlerApi.kt | 37 ----- .../common/analytics/api/EventFilterApi.kt | 2 +- .../common/analytics/api/EventHandlerApi.kt | 43 ++---- .../converters/AnalyticsErrorConverter.kt | 70 +++++++++ .../converters/SdkErrorConverters.kt | 88 ----------- .../common/analytics/events/AnalyticsEvent.kt | 3 +- .../common/analytics/events/AnalyticsParam.kt | 13 +- .../tap/common/analytics/events/Basic.kt | 8 +- .../tap/common/analytics/events/Settings.kt | 15 +- .../tap/common/analytics/events/Token.kt | 9 +- .../common/analytics/events/WalletConnect.kt | 6 +- .../analytics/filters/BasicSignInFilter.kt | 4 +- .../analytics/filters/BasicTopUpFilter.kt | 4 +- .../filters/ShopPurchasedEventFilter.kt | 4 +- .../amplitude/AmplitudeAnalyticsHandler.kt | 6 +- .../appsFlyer/AppsFlyerAnalyticsHandler.kt | 6 +- .../firebase/FirebaseAnalyticsHandler.kt | 59 +++---- .../com/tangem/tap/domain/TangemSdkManager.kt | 32 +--- .../tap/domain/twins/TwinCardsManager.kt | 44 +----- .../walletconnect/WalletConnectManager.kt | 52 +++---- .../walletconnect/WalletConnectSdkHelper.kt | 41 ++--- .../details/redux/DetailsMiddleware.kt | 28 +--- .../tap/features/home/redux/HomeMiddleware.kt | 7 - .../send/redux/middlewares/SendMiddleware.kt | 20 +-- .../redux/middlewares/WalletMiddleware.kt | 12 +- .../redux/middlewares/WarningsMiddleware.kt | 34 ++-- .../ui/adapters/WarningMessagesAdapter.kt | 10 +- 32 files changed, 298 insertions(+), 658 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventAnOld.kt create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/AnalyticsFactory.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandlersFactory.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/analytics/api/ErrorEventHandlerApi.kt create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/converters/AnalyticsErrorConverter.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/analytics/converters/SdkErrorConverters.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index be1837114f..568ca4af60 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -15,7 +15,7 @@ import com.tangem.network.common.MoshiConverter import com.tangem.tap.common.AndroidAssetReader import com.tangem.tap.common.AssetReader import com.tangem.tap.common.analytics.Analytics -import com.tangem.tap.common.analytics.AnalyticsHandlersFactory +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 @@ -110,10 +110,14 @@ class TapApplication : Application(), ImageLoaderFactory { } private fun initAnalytics(application: Application, config: Config) { - val factory = AnalyticsHandlersFactory() - factory.addBuilder(AmplitudeAnalyticsHandler.Builder()) - factory.addBuilder(AppsFlyerAnalyticsHandler.Builder()) - factory.addBuilder(FirebaseAnalyticsHandler.Builder()) + 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, @@ -122,14 +126,7 @@ class TapApplication : Application(), ImageLoaderFactory { logConfig = LogConfig.analyticsHandlers, jsonConverter = MoshiJsonConverter.INSTANCE, ) - factory.build(buildData).forEach { - Analytics.addHandler(it.id(), it) - } - listOf( - ShopPurchasedEventFilter(), - BasicSignInFilter(), - BasicTopUpFilter(preferencesStorage.toppedUpWalletStorage), - ).forEach { Analytics.addFilter(it) } + factory.build(Analytics, buildData) } private fun initFeedbackManager(context: Context, preferencesStorage: PreferencesStorage, config: Config) { 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 index 276ef9059f..03cb8437bb 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt @@ -1,22 +1,14 @@ package com.tangem.tap.common.analytics -import com.tangem.blockchain.common.BlockchainError -import com.tangem.common.card.Card -import com.tangem.common.core.TangemSdkError 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.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.ParamsInterceptor import com.tangem.tap.common.analytics.api.ParamsInterceptorHolder -import com.tangem.tap.common.analytics.api.SdkErrorEventHandler import com.tangem.tap.common.analytics.events.AnalyticsEvent -import com.tangem.tap.common.extensions.filterNotNull import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -28,33 +20,26 @@ import java.util.concurrent.Executors [REDACTED_AUTHOR] */ interface GlobalAnalyticsEventHandler : AnalyticsEventHandler, - ErrorEventHandler, - SdkErrorEventHandler, AnalyticsHandlerHolder, AnalyticsFilterHolder, - ParamsInterceptorHolder { - - fun send(event: AnalyticsEvent, card: Card? = null, blockchain: String? = null) -} + ParamsInterceptorHolder object Analytics : GlobalAnalyticsEventHandler { private val analyticsScope: CoroutineScope by lazy { createScope() } - private val handlers = mutableMapOf() - private val analyticsFilters = mutableSetOf() + private val handlers = mutableMapOf() private val paramsInterceptors = mutableMapOf() + private val analyticsFilters = mutableSetOf() - private val analyticsHandlers: List + private val analyticsHandlers: List get() = handlers.values.toList() - override fun id(): String = analyticsHandlers.joinToString(", ") { it.id() } - - override fun addHandler(name: String, handler: AnalyticsEventHandler) { + override fun addHandler(name: String, handler: AnalyticsHandler) { handlers[name] = handler } - override fun removeHandler(name: String): AnalyticsEventHandler? { + override fun removeHandler(name: String): AnalyticsHandler? { return handlers.remove(name) } @@ -74,84 +59,29 @@ object Analytics : GlobalAnalyticsEventHandler { return paramsInterceptors.remove(interceptor.id()) } - override fun send(event: String, params: Map) { + override fun send(event: AnalyticsEvent) { analyticsScope.launch { - analyticsHandlers.forEach { it.send(event, params.interceptParams()) } - } - } - - override fun send(event: AnalyticsEvent, card: Card?, blockchain: String?) { - analyticsScope.launch { - val eventString = prepareEventString(event.category, event.event) - val eventParams = prepareParams(card, blockchain, event.interceptParams()) - + event.params = applyParamsInterceptors(event) val eventFilter = analyticsFilters.firstOrNull { it.canBeAppliedTo(event) } + when { - eventFilter == null -> analyticsHandlers.forEach { handler -> handler.send(eventString, eventParams) } + eventFilter == null -> analyticsHandlers.forEach { handler -> handler.send(event) } eventFilter.canBeSent(event) -> { analyticsHandlers .filter { handler -> eventFilter.canBeConsumedByHandler(handler, event) } - .forEach { handler -> handler.send(eventString, eventParams) } + .forEach { handler -> handler.send(event) } } } } } - override fun handleAnalyticsEvent( - event: AnalyticsEventAnOld, - params: Map, - card: Card?, - blockchain: String?, - ) { - analyticsScope.launch { - analyticsHandlers.forEach { - it.handleAnalyticsEvent(event, params.interceptParams(), card, blockchain) - } - } - } + private fun applyParamsInterceptors(event: AnalyticsEvent): MutableMap { + val interceptedParams = event.params.toMutableMap() + paramsInterceptors.values + .filter { it.canBeAppliedTo(event) } + .forEach { it.intercept(interceptedParams) } - override fun send(error: Throwable, params: Map) { - analyticsScope.launch { - analyticsHandlers.filterIsInstance().forEach { - it.logErrorEvent(error, params) - } - } - } - - override fun send( - error: TangemSdkError, - action: AnalyticsAnOld.ActionToLog, - params: Map, - card: Card?, - ) { - analyticsScope.launch { - analyticsHandlers.filterIsInstance().forEach { - it.send(error, action, params, card) - } - } - } - - override fun send( - error: BlockchainError, - action: AnalyticsAnOld.ActionToLog, - params: Map, - card: Card?, - ) { - analyticsScope.launch { - analyticsHandlers.filterIsInstance().forEach { - it.send(error, action, params, card) - } - } - } - - private fun AnalyticsEvent.interceptParams(): Map = params.interceptParams() - - private fun Map.interceptParams(): Map { - return this.toMutableMap().apply { interceptParams() } - } - - private fun MutableMap.interceptParams() { - paramsInterceptors.values.forEach { it.intercept(this) } + return interceptedParams } private fun createScope(): CoroutineScope { @@ -160,43 +90,4 @@ object Analytics : GlobalAnalyticsEventHandler { val exHandler = FeatureCoroutineExceptionHandler.create(name) return CoroutineScope(Job() + dispatcher + CoroutineName(name) + exHandler) } -} - -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() - send(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/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/AnalyticsHandlersFactory.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandlersFactory.kt deleted file mode 100644 index a287f6fac2..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsHandlersFactory.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.tap.common.analytics - -import com.tangem.tap.common.analytics.api.AnalyticsEventHandler -import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder - -/** -[REDACTED_AUTHOR] - */ -class AnalyticsHandlersFactory { - - private val builders = mutableListOf() - - fun addBuilder(builder: AnalyticsHandlerBuilder) { - builders.add(builder) - } - - fun build(data: AnalyticsHandlerBuilder.Data): List { - val handlers = builders.mapNotNull { it.build(data) } - builders.clear() - - return handlers - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/api/ErrorEventHandlerApi.kt b/app/src/main/java/com/tangem/tap/common/analytics/api/ErrorEventHandlerApi.kt deleted file mode 100644 index 3279075ea4..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/api/ErrorEventHandlerApi.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.tap.common.analytics.api - -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.AnalyticsParamAnOld - -/** -[REDACTED_AUTHOR] - */ -interface ErrorEventHandler { - fun send( - error: Throwable, - params: Map = emptyMap(), - ) -} - -interface SdkErrorEventHandler : CardSdkErrorEventHandler, BlockchainSdkErrorEventHandler - -interface CardSdkErrorEventHandler { - fun send( - error: TangemSdkError, - action: AnalyticsAnOld.ActionToLog, - params: Map = emptyMap(), - card: Card? = null, - ) -} - -interface BlockchainSdkErrorEventHandler { - fun send( - error: BlockchainError, - action: AnalyticsAnOld.ActionToLog, - params: Map = mapOf(), - card: Card? = null, - ) -} \ 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 index eb0f9587ce..04d6dbbb01 100644 --- 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 @@ -21,7 +21,7 @@ interface AnalyticsEventFilter { /** * Performs a check to see if the event can be dispatched by a specific handler */ - fun canBeConsumedByHandler(handler: AnalyticsEventHandler, event: AnalyticsEvent): Boolean + fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean } interface AnalyticsFilterHolder { 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 5095d83dfa..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,53 +1,44 @@ package com.tangem.tap.common.analytics.api import android.app.Application -import com.tangem.common.card.Card import com.tangem.common.json.MoshiJsonConverter import com.tangem.domain.common.AnalyticsHandlersLogConfig -import com.tangem.tap.common.analytics.AnalyticsEventAnOld -import com.tangem.tap.common.analytics.events.AnalyticsParam -import com.tangem.tap.common.extensions.filterNotNull +import com.tangem.tap.common.analytics.events.AnalyticsEvent import com.tangem.tap.domain.configurable.config.Config /** [REDACTED_AUTHOR] */ interface AnalyticsEventHandler { + fun send(event: AnalyticsEvent) +} + +interface AnalyticsHandler : AnalyticsEventHandler { fun id(): String fun send(event: String, params: Map = emptyMap()) - @Deprecated("Migrate to AnalyticsEvent") - fun handleAnalyticsEvent( - event: AnalyticsEventAnOld, - params: Map = emptyMap(), - card: Card? = null, - blockchain: String? = null, - ) { - send(event.event, prepareParams(card, blockchain, params)) + override fun send(event: AnalyticsEvent) { + send(prepareEventString(event), event.params) } - fun prepareParams( - card: Card? = null, - blockchain: String? = null, - params: Map = emptyMap(), - ): Map = mapOf( - AnalyticsParam.Firmware to card?.firmwareVersion?.stringValue, - AnalyticsParam.Blockchain to blockchain, - ).filterNotNull() + params + fun prepareEventString(event: AnalyticsEvent): String = "[${event.category}] ${event.event}" +} - fun prepareEventString(category: String, event: String): String { - return "[$category] $event" - } +interface ErrorEventHandler { + fun send( + error: Throwable, + params: Map = emptyMap(), + ) } interface AnalyticsHandlerHolder { - fun addHandler(name: String, handler: AnalyticsEventHandler) - fun removeHandler(name: String): AnalyticsEventHandler? + fun addHandler(name: String, handler: AnalyticsHandler) + fun removeHandler(name: String): AnalyticsHandler? } interface AnalyticsHandlerBuilder { - fun build(data: Data): AnalyticsEventHandler? + fun build(data: Data): AnalyticsHandler? data class Data( val application: Application, 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/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/events/AnalyticsEvent.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsEvent.kt index 8e5cf0e942..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,6 +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 0d76d1dd08..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 @@ -54,11 +54,16 @@ sealed class AnalyticsParam { } } + 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 BatchId = "Batch" - - // old keys - const val Blockchain = "blockchain" - const val Firmware = "firmware" + 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 229093235d..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,7 +6,8 @@ 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) { class SignedIn( state: AnalyticsParam.CardBalanceState, @@ -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/Settings.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt index b24d6ef042..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 @@ -9,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") @@ -28,10 +29,15 @@ sealed class Settings( 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") @@ -40,9 +46,10 @@ sealed class Settings( params = mapOf("Type" to type.value), ) - 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/Token.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt index 9da6abe399..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 @@ -9,7 +9,8 @@ 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 ButtonExplore : Token("Token", "Button - Explore") @@ -57,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(type: CurrencyType) : Send( + class TransactionSent(type: CurrencyType, error: Throwable? = null) : Send( event = "Transaction Sent", 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 index 372f2787a3..a582ea34f1 100644 --- 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 @@ -1,7 +1,7 @@ package com.tangem.tap.common.analytics.filters import com.tangem.tap.common.analytics.api.AnalyticsEventFilter -import com.tangem.tap.common.analytics.api.AnalyticsEventHandler +import com.tangem.tap.common.analytics.api.AnalyticsHandler import com.tangem.tap.common.analytics.events.AnalyticsEvent import com.tangem.tap.common.analytics.events.Basic @@ -23,5 +23,5 @@ class BasicSignInFilter : AnalyticsEventFilter { return canBeSent } - override fun canBeConsumedByHandler(handler: AnalyticsEventHandler, event: AnalyticsEvent): Boolean = true + 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 index fea71037d9..be780ccd2d 100644 --- 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 @@ -2,7 +2,7 @@ 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.AnalyticsEventHandler +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 @@ -39,7 +39,7 @@ class BasicTopUpFilter( } } - override fun canBeConsumedByHandler(handler: AnalyticsEventHandler, event: AnalyticsEvent): Boolean = true + override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean = true data class Data( val walletId: String, 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 index 945dbefab4..dffa123240 100644 --- 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 @@ -1,7 +1,7 @@ package com.tangem.tap.common.analytics.filters import com.tangem.tap.common.analytics.api.AnalyticsEventFilter -import com.tangem.tap.common.analytics.api.AnalyticsEventHandler +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 @@ -16,7 +16,7 @@ class ShopPurchasedEventFilter : AnalyticsEventFilter { override fun canBeSent(event: AnalyticsEvent): Boolean = true - override fun canBeConsumedByHandler(handler: AnalyticsEventHandler, event: AnalyticsEvent): Boolean { + override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean { return when (handler) { is AppsFlyerAnalyticsHandler, is FirebaseAnalyticsHandler -> true else -> false 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 06f77c15b3..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,11 +1,11 @@ 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 id(): String = ID @@ -18,7 +18,7 @@ class AmplitudeAnalyticsHandler( } class Builder : AnalyticsHandlerBuilder { - override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsEventHandler? = when { + 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 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 b91da358d6..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,11 +1,11 @@ package com.tangem.tap.common.analytics.handlers.appsFlyer -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 AppsFlyerAnalyticsHandler( private val client: AppsFlyerAnalyticsClient, -) : AnalyticsEventHandler { +) : AnalyticsHandler { override fun id(): String = ID @@ -18,7 +18,7 @@ class AppsFlyerAnalyticsHandler( } class Builder : AnalyticsHandlerBuilder { - override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsEventHandler? = when { + 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 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 9a4a8bf839..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,20 +1,14 @@ package com.tangem.tap.common.analytics.handlers.firebase -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.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.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 { +) : AnalyticsHandler, ErrorEventHandler { override fun id(): String = ID @@ -22,43 +16,32 @@ class FirebaseAnalyticsHandler( client.logEvent(event, params) } + 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 send( - 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 { - send(it.throwable, it.params) - } - } - - override fun send( - 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 { - send(it.throwable, it.params) - } - } - companion object { const val ID = "Firebase" } class Builder : AnalyticsHandlerBuilder { - override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsEventHandler? = when { + override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when { !data.isDebug -> FirebaseClient() data.isDebug && data.logConfig.firebase -> FirebaseLogClient(data.jsonConverter) else -> null 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 194d6d5815..7d6db12a26 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -27,14 +27,11 @@ import com.tangem.operations.pins.CheckUserCodesCommand import com.tangem.operations.pins.CheckUserCodesResponse import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.tap.common.analytics.Analytics -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.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 @@ -49,8 +46,6 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co additionalBlockchainsToDerive: Collection? = null, messageRes: Int? = null, ): CompletionResult { - Analytics.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), @@ -71,29 +66,10 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co private fun sendScanResultsToAnalytics( result: CompletionResult, ) { - when (result) { - is CompletionResult.Success -> { - Analytics.handleAnalyticsEvent( - event = AnalyticsEventAnOld.CARD_IS_SCANNED, - card = result.data.card, - blockchain = result.data.walletData?.blockchain, - ) - if (DemoHelper.isDemoCard(result.data)) { - Analytics.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 -> - Analytics.send( - error = error, - action = AnalyticsAnOld.ActionToLog.Scan, - ) - } } } 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 fc80f3c5b0..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,19 +7,16 @@ 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.Analytics -import com.tangem.tap.common.analytics.AnalyticsAnOld import com.tangem.tap.tangemSdkManager class TwinCardsManager( - private val card: Card, + card: Card, assetReader: AssetReader, ) { @@ -34,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 -> - Analytics.send( - error, - AnalyticsAnOld.ActionToLog.CreateWallet, - card = card - ) - } - } + is CompletionResult.Failure -> {} } return response } @@ -57,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 -> - Analytics.send( - error, - AnalyticsAnOld.ActionToLog.CreateWallet, - card = card - ) - } - } + is CompletionResult.Failure -> {} } return response } @@ -80,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 -> - Analytics.send( - error, - AnalyticsAnOld.ActionToLog.WriteIssuerData, - card = card - ) - } - Result.fromTangemSdkError(response.error) - } + is CompletionResult.Failure -> Result.fromTangemSdkError(response.error) } } @@ -102,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 31e16004cd..8b66d5ce12 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 @@ -5,9 +5,7 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.guard import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.analytics.Analytics -import com.tangem.tap.common.analytics.AnalyticsAnOld import com.tangem.tap.common.analytics.events.WalletConnect -import com.tangem.tap.common.analytics.logWcEvent import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError @@ -226,11 +224,6 @@ class WalletConnectManager { private fun onSessionClosed(session: WCSession) { Analytics.send(WalletConnect.SessionDisconnected()) - Analytics.logWcEvent( - AnalyticsAnOld.WcAnalyticsEvent.Session( - AnalyticsAnOld.WcSessionEvent.Disconnect, sessions[session]?.peerMeta?.url - ) - ) sessions.remove(session) walletConnectRepository.removeSession(session) store.dispatchOnMain(WalletConnectAction.RemoveSession(session)) @@ -376,11 +369,6 @@ class WalletConnectManager { ) } Analytics.send(WalletConnect.NewSessionEstablished()) - Analytics.logWcEvent( - AnalyticsAnOld.WcAnalyticsEvent.Session( - AnalyticsAnOld.WcSessionEvent.Connect, peer.url, - ), - ) } } } @@ -391,11 +379,11 @@ class WalletConnectManager { } client.onEthSendTransaction = { id: Long, transaction: WCEthereumTransaction -> Timber.d("onEthSendTransaction: $transaction") - Analytics.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( @@ -409,11 +397,11 @@ class WalletConnectManager { } client.onEthSignTransaction = { id: Long, transaction: WCEthereumTransaction -> Timber.d("onEthSignTransaction: $transaction") - Analytics.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( @@ -427,11 +415,11 @@ class WalletConnectManager { } client.onEthSign = { id: Long, message: WCEthereumSignMessage -> Timber.d("onEthSign: $message") - Analytics.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( @@ -503,11 +491,11 @@ class WalletConnectManager { val message = EthSignHelper.tryToParseEthTypedMessage(request) if (message != null) { Timber.d("onEthSign_v4: $message") - Analytics.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 36e8fc2a1d..a95fd872e9 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 @@ -25,7 +25,7 @@ import com.tangem.common.extensions.toHexString import com.tangem.crypto.CryptoUtils import com.tangem.operations.sign.SignHashCommand import com.tangem.tap.common.analytics.Analytics -import com.tangem.tap.common.analytics.AnalyticsAnOld +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 @@ -88,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, @@ -101,7 +101,7 @@ class WalletConnectSdkHelper { isEnoughFundsToSend = (balance - total) >= BigDecimal.ZERO, session = session.session, id = id, - type = type + type = type, ) return WcTransactionData( type = type, @@ -109,7 +109,7 @@ class WalletConnectSdkHelper { session = session, id = id, walletManager = walletManager, - dialogData = dialogData + dialogData = dialogData, ) } @@ -123,7 +123,7 @@ class WalletConnectSdkHelper { val blockchain = session.wallet.getBlockchainForSession() return factory.makeWalletManager( blockchain = blockchain, - publicKey = publicKey + publicKey = publicKey, ) } @@ -137,14 +137,14 @@ class WalletConnectSdkHelper { private suspend fun sendTransaction(data: WcTransactionData): String? { val result = (data.walletManager as TransactionSender).send( transactionData = data.transaction, - signer = CommonSigner(tangemSdk) + signer = CommonSigner(tangemSdk), ) return when (result) { SimpleResult.Success -> { HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash } is SimpleResult.Failure -> { - Analytics.send(result.error, AnalyticsAnOld.ActionToLog.WalletConnectTransaction) + (result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.TransactionError(it)) } Timber.e(result.error as BlockchainSdkError) null } @@ -156,13 +156,13 @@ class WalletConnectSdkHelper { transactionData = data.transaction, nonce = null, blockchain = data.walletManager.wallet.blockchain, - gasLimit = null + gasLimit = null, ) ?: return null val command = SignHashCommand( hash = dataToSign.hash, walletPublicKey = data.walletManager.wallet.publicKey.seedKey, - derivationPath = data.walletManager.wallet.publicKey.derivationPath + derivationPath = data.walletManager.wallet.publicKey.derivationPath, ) val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message()) return when (result) { @@ -170,9 +170,7 @@ class WalletConnectSdkHelper { HEX_PREFIX + result.data } is CompletionResult.Failure -> { - (result.error as? TangemSdkError)?.let { error -> - Analytics.send(error, AnalyticsAnOld.ActionToLog.WalletConnectSign) - } + (result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.SignError(it)) } Timber.e(result.error.customMessage) null } @@ -183,7 +181,7 @@ class WalletConnectSdkHelper { val command = SignHashCommand( hash = data, walletPublicKey = session.wallet.walletPublicKey ?: return null, - derivationPath = session.wallet.derivationPath + derivationPath = session.wallet.derivationPath, ) val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message()) return when (result) { @@ -192,13 +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 -> - Analytics.send(error, AnalyticsAnOld.ActionToLog.WalletConnectTransaction) - } + (result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.TransactionError(it)) } Timber.e(result.error.customMessage) null } @@ -223,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, ) } @@ -269,9 +264,7 @@ class WalletConnectSdkHelper { ) } is CompletionResult.Failure -> { - (result.error as? TangemSdkError)?.let { error -> - Analytics.send(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 835a392ee2..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 @@ -4,8 +4,6 @@ import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.tap.common.analytics.Analytics -import com.tangem.tap.common.analytics.AnalyticsAnOld -import com.tangem.tap.common.analytics.AnalyticsParamAnOld import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchDialogShow @@ -129,11 +127,7 @@ class DetailsMiddleware { } is CompletionResult.Failure -> { (result.error as? TangemSdkError)?.let { error -> - Analytics.send( - error, - AnalyticsAnOld.ActionToLog.PurgeWallet, - card = store.state.detailsState.scanResponse?.card, - ) + Analytics.send(Settings.CardSettings.FactoryResetFinished(error)) } } } @@ -160,30 +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 { - val paramValue = AnalyticsParam.SecurityMode.from(it) - Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue)) - 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 -> - Analytics.send( - 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) } 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 16d6ee9efd..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 @@ -10,9 +10,6 @@ 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.Analytics -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.IntroductionProcess import com.tangem.tap.common.analytics.paramsInterceptor.BatchIdParamsInterceptor import com.tangem.tap.common.entities.IndeterminateProgressButton @@ -107,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)) } - Analytics.handleAnalyticsEvent( - event = AnalyticsEventAnOld.GET_CARD, - params = mapOf(AnalyticsParamAnOld.SOURCE.param to GetCardSourceParamsAnOld.WELCOME.param), - ) } } } 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 c45924449f..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 @@ -18,9 +18,6 @@ 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.Analytics -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.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.dispatchDialogShow @@ -222,14 +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 -> { - Analytics.send(Token.Send.TransactionSent(AnalyticsParam.CurrencyType.Amount(amountToSend))) - Analytics.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) { @@ -254,15 +247,10 @@ private fun sendTransaction( feeAmount = feeAmount, destinationAddress = destinationAddress, ) - Analytics.send( - 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/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index 78c837ba71..07d6994a7a 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 @@ -5,14 +5,12 @@ 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.Analytics -import com.tangem.tap.common.analytics.AnalyticsAnOld import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.copyToClipboard import com.tangem.tap.common.extensions.dispatchDebugErrorNotification @@ -189,15 +187,7 @@ class WalletMiddleware { globalState.scanResponse?.copy(card = result.data) scanNoteResponse?.let { store.onCardScanned(scanNoteResponse) } } - is CompletionResult.Failure -> { - (result.error as? TangemSdkError)?.let { error -> - Analytics.send( - error, - AnalyticsAnOld.ActionToLog.CreateWallet, - card = store.state.detailsState.scanResponse?.card, - ) - } - } + is CompletionResult.Failure -> {} } } } 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 4a30816169..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,8 +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.Analytics -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 @@ -55,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, ) } } @@ -70,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 } } @@ -82,7 +77,6 @@ class WarningsMiddleware { preferencesStorage.appRatingLaunchObserver.foundWalletWithFunds() } if (preferencesStorage.appRatingLaunchObserver.isReadyToShow()) { - Analytics.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_DISPLAYED) addWarningMessage(WarningMessagesManager.appRatingWarning(), true) } } @@ -119,11 +113,7 @@ class WarningsMiddleware { if (remainingSignatures != null && remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING ) { - addWarningMessage( - WarningMessagesManager.remainingSignaturesNotEnough( - remainingSignatures - ) - ) + addWarningMessage(WarningMessagesManager.remainingSignaturesNotEnough(remainingSignatures)) } } @@ -179,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 } @@ -208,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/ui/adapters/WarningMessagesAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt index 90abb0d255..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 @@ -8,7 +8,6 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.google.android.play.core.review.ReviewManagerFactory import com.tangem.tap.common.analytics.Analytics -import com.tangem.tap.common.analytics.AnalyticsEventAnOld import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.extensions.getActivity @@ -29,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) } @@ -63,7 +62,7 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi tvMessage.text = getString( resId = warning.messageResId, default = warning.message, - formatArgs = warning.messageFormatArg + formatArgs = warning.messageFormatArg, ) } @@ -101,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 @@ -112,13 +111,11 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi binding.btnClose.show() binding.btnClose.setOnClickListener { Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Closed)) - Analytics.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_DISMISS) store.dispatch(GlobalAction.HideWarningMessage(warning)) store.dispatch(WalletAction.Warnings.AppRating.RemindLater) } binding.btnCanBeBetter.setOnClickListener { Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Disliked)) - Analytics.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_NEGATIVE) store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow) store.dispatch(GlobalAction.HideWarningMessage(warning)) store.dispatch(GlobalAction.SendEmail(RateCanBeBetterEmail())) @@ -127,7 +124,6 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi val activity = binding.root.context.getActivity() ?: return@setOnClickListener Analytics.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Liked)) - Analytics.handleAnalyticsEvent(AnalyticsEventAnOld.APP_RATING_POSITIVE) store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow) val reviewManager = ReviewManagerFactory.create(activity) val task = reviewManager.requestReviewFlow() From 14a2156262096ec5160cbadad25df1161ec373ae Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 9 Nov 2022 22:17:30 +0500 Subject: [PATCH 044/372] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index cf6ea50867..a1658496e7 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit cf6ea50867477f97655da9da47ff94987e8f3354 +Subproject commit a1658496e777b611fc990ef2bc1a1a1fd48bc1e6 From a6e04a43443f402f4aafea96ee9824164be28665 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 11 Nov 2022 13:54:04 +0300 Subject: [PATCH 045/372] Updated on 2026-08-14 --- .gitignore | 1 + app/build.gradle | 187 ------------------ app/build.gradle.kts | 182 +++++++++++++++++ app/proguard-rules.pro | 2 +- build.gradle | 31 --- build.gradle.kts | 30 +++ buildSrc/build.gradle.kts | 7 + buildSrc/src/main/java/AppConfig.kt | 8 + buildSrc/src/main/java/ClasspathDependency.kt | 9 + buildSrc/src/main/java/ConfigFieldBuilder.kt | 26 +++ buildSrc/src/main/java/Dependency.kt | 77 ++++++++ buildSrc/src/main/java/Versions.kt | 68 +++++++ common/{build.gradle => build.gradle.kts} | 4 +- dependencies.gradle | 14 -- domain/build.gradle | 88 --------- domain/build.gradle.kts | 98 +++++++++ domain/proguard-rules.pro | 2 +- network/build.gradle | 31 --- network/build.gradle.kts | 26 +++ settings.gradle | 4 - settings.gradle.kts | 4 + 21 files changed, 540 insertions(+), 359 deletions(-) delete mode 100644 app/build.gradle create mode 100644 app/build.gradle.kts delete mode 100644 build.gradle create mode 100644 build.gradle.kts create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/src/main/java/AppConfig.kt create mode 100644 buildSrc/src/main/java/ClasspathDependency.kt create mode 100644 buildSrc/src/main/java/ConfigFieldBuilder.kt create mode 100644 buildSrc/src/main/java/Dependency.kt create mode 100644 buildSrc/src/main/java/Versions.kt rename common/{build.gradle => build.gradle.kts} (67%) delete mode 100644 dependencies.gradle delete mode 100644 domain/build.gradle create mode 100644 domain/build.gradle.kts delete mode 100644 network/build.gradle create mode 100644 network/build.gradle.kts delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore index dd64e70b22..dd33a7dab3 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ local.properties **/fastlane/Preview.html **/fastlane/screenshots **/fastlane/test_output +/buildSrc/build/ diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 70b6edcebd..0000000000 --- a/app/build.gradle +++ /dev/null @@ -1,187 +0,0 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' -apply plugin: 'com.google.gms.google-services' -apply plugin: 'com.google.firebase.crashlytics' -apply from: '../dependencies.gradle' - -android { - compileSdkVersion 33 - defaultConfig { - applicationId "com.tangem.wallet" - versionCode project.hasProperty('versionCode') ? project.property('versionCode') as int : 1 - versionName project.hasProperty('versionName') ? project.property('versionName') : "1.0.0.-SNAPSHOT" - minSdkVersion 21 - targetSdkVersion 33 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - - buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false' - buildConfigField 'Boolean', environmentConfig.logEnabled, 'false' - } - buildFeatures { - compose true - viewBinding true - } - buildTypes { - release { - debuggable false - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - signingConfig signingConfigs.debug - buildConfigField 'String', environmentConfig.environment, '\"prod\"' - buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false' - buildConfigField 'Boolean', environmentConfig.logEnabled, 'false' - } - debug { - debuggable true - minifyEnabled false - applicationIdSuffix ".dev" - signingConfig signingConfigs.debug - firebaseCrashlytics { - mappingFileUploadEnabled false - } - buildConfigField 'String', environmentConfig.environment, '\"dev\"' - buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'true' - buildConfigField 'Boolean', environmentConfig.logEnabled, 'true' - } - debug_beta { - initWith release - debuggable false - versionNameSuffix "-beta" - applicationIdSuffix ".debug" - buildConfigField 'String', environmentConfig.environment, '\"prod\"' - buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false' - buildConfigField 'Boolean', environmentConfig.logEnabled, 'false' - } - - } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - coreLibraryDesugaringEnabled false - } - composeOptions { - kotlinCompilerExtensionVersion '1.1.0' - } - packagingOptions { - exclude 'lib/x86_64/darwin/libscrypt.dylib' - exclude 'lib/x86_64/freebsd/libscrypt.so' - exclude 'lib/x86_64/linux/libscrypt.so' - } -} - -repositories { - maven { url "https://jitpack.io" } -} - -dependencies { - implementation fileTree(include: ['*.aar'], dir: 'libs') - implementation implementation(project(path: ':domain')) - implementation implementation(project(path: ':network')) - implementation implementation(project(path: ':common')) - - implementation 'androidx.core:core-ktx:1.9.0' - implementation 'androidx.appcompat:appcompat:1.5.1' - implementation 'androidx.fragment:fragment-ktx:1.5.3' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'com.google.android.material:material:1.6.1' - implementation 'com.google.android.play:core:1.10.3' - implementation 'com.google.android.play:core-ktx:1.8.1' - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - - implementation "com.tangem:blockchain:${versions.tangem_blockchain_sdk}" - implementation "com.tangem.tangem-sdk-kotlin:core:${versions.tangem_card_sdk}" - implementation "com.tangem.tangem-sdk-kotlin:android:${versions.tangem_card_sdk}" - - // WebView - implementation "androidx.browser:browser:1.4.0" - - implementation 'com.jakewharton.timber:timber:4.7.1' - - //lifecycle - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1" - implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1" - - implementation "org.rekotlin:rekotlin:1.0.4" - - // QR - implementation 'com.google.zxing:core:3.3.3' - implementation 'me.dm7.barcodescanner:zxing:1.9.8' - - // Camera - implementation 'com.otaliastudios:cameraview:2.6.3' - - // Image Loading - implementation 'io.coil-kt:coil:2.1.0' - implementation 'io.coil-kt:coil-compose:2.1.0' - - // Firebase - implementation platform('com.google.firebase:firebase-bom:26.0.0') - implementation 'com.google.firebase:firebase-analytics-ktx' - implementation 'com.google.firebase:firebase-crashlytics-ktx' - - // Analytics - implementation 'com.appsflyer:af-android-sdk:6.5.1' - implementation 'com.amplitude:android-sdk:2.36.1' - - // WalletConnect - implementation 'com.github.salomonbrys.kotson:kotson:2.5.0' - - // Network and Json - //TODO: refactoring: remove it when all network services moved to the network module - implementation 'com.squareup.retrofit2:retrofit:2.8.1' - implementation 'com.squareup.retrofit2:converter-moshi:2.6.0' - implementation 'com.squareup.moshi:moshi:1.12.0' - implementation "com.squareup.moshi:moshi-kotlin:1.12.0" - - implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3")) - implementation("com.squareup.okhttp3:okhttp") - implementation("com.squareup.okhttp3:logging-interceptor") - - // Support - implementation("com.zendesk:chat:3.3.5") - implementation("com.zendesk:messaging:5.2.4") - - //Crypto - implementation "com.madgag.spongycastle:core:1.58.0.0" - - // animation - implementation "com.airbnb.android:lottie:3.4.0" - - // Shopify - implementation('com.shopify.mobilebuysdk:buy3:12.0.0') { - exclude group: "com.shopify.graphql.support" - exclude module: 'joda-time' - } - - // Google Pay - implementation 'com.google.android.gms:play-services-wallet:19.1.0' - - //Compose - implementation 'androidx.activity:activity-compose:1.5.0' - implementation 'androidx.compose.material:material:1.1.1' - implementation 'androidx.compose.animation:animation:1.1.1' - implementation 'androidx.compose.foundation:foundation:1.2.0-rc03' - implementation 'androidx.compose.ui:ui:1.1.1' - implementation 'androidx.compose.ui:ui-tooling:1.1.1' - implementation "com.google.accompanist:accompanist-appcompat-theme:0.23.0" - implementation "com.google.accompanist:accompanist-systemuicontroller:0.23.0" - - implementation "com.github.skydoves:androidveil:1.1.2" - - implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6' - - // Shared preferences (encrypted) - implementation 'at.favre.lib:armadillo:0.9.0' - - testImplementation 'junit:junit:4.13.2' - testImplementation "com.google.truth:truth:1.1.3" - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000000..218986fc34 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,182 @@ +plugins { + id("com.android.application") + kotlin("android") + kotlin("kapt") + id("com.google.gms.google-services") + id("com.google.firebase.crashlytics") +} + +android { + compileSdk = AppConfig.compileSdkVersion + + defaultConfig { + applicationId = AppConfig.packageName + minSdk = AppConfig.minSdkVersion + targetSdk = AppConfig.targetSdkVersion + + versionCode = if (project.hasProperty("versionCode")) { + project.property("versionCode") as Int + } else { + AppConfig.versionCode + } + + versionName = if (project.hasProperty("versionName")) { + project.property("versionName") as String + } else { + AppConfig.versionName + } + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildFeatures { + compose = true + viewBinding = true + } + + buildTypes { + release { + isDebuggable = false + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") + + BuildConfigFieldFactory( + fields = listOf( + Field.Environment("prod"), + Field.TestActionEnabled(false), + Field.LogEnabled(false), + ), + builder = ::buildConfigField, + ).create() + } + + debug { + isDebuggable = true + isMinifyEnabled = false + applicationIdSuffix = ".dev" + + configure { + // disable mapping file uploads (default=true if minifying) + mappingFileUploadEnabled = false + } + + BuildConfigFieldFactory( + fields = listOf( + Field.Environment("dev"), + Field.TestActionEnabled(true), + Field.LogEnabled(true), + ), + builder = ::buildConfigField, + ).create() + } + + create("debug_beta") { + initWith(getByName("release")) + versionNameSuffix = "-beta" + applicationIdSuffix = ".debug" + } + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + isCoreLibraryDesugaringEnabled = false + } + + composeOptions { + kotlinCompilerExtensionVersion = Versions.compose + } + + packagingOptions { + resources.excludes += "lib/x86_64/darwin/libscrypt.dylib" + resources.excludes += "lib/x86_64/freebsd/libscrypt.so" + resources.excludes += "lib/x86_64/linux/libscrypt.so" + } +} + +repositories { + maven(url = "https://jitpack.io") +} + +dependencies { + implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar")))) + implementation(project(":domain")) + implementation(project(":network")) + implementation(project(":common")) + + /** AndroidX libraries */ + implementation(AndroidX.coreKtx) + implementation(AndroidX.appCompat) + implementation(AndroidX.fragmentKtx) + implementation(AndroidX.constraintLayout) + implementation(AndroidX.browser) + implementation(AndroidX.lifecycleRuntimeKtx) + implementation(AndroidX.lifecycleCommonJava8) + implementation(AndroidX.lifecycleViewModelKtx) + implementation(AndroidX.lifecycleLiveDataKtx) + implementation(AndroidX.activityCompose) + + /** Compose libraries */ + implementation(Compose.material) + implementation(Compose.animation) + implementation(Compose.foundation) + implementation(Compose.ui) + implementation(Compose.uiTooling) + implementation(Compose.coil) + + /** Firebase libraries */ + implementation(platform(Firebase.bom)) + implementation(Firebase.firebaseAnalytics) + implementation(Firebase.firebaseCrashlytics) + + /** Tangem libraries */ + implementation(Tangem.blockchain) + implementation(Tangem.cardCore) + implementation(Tangem.cardAndroid) + + /** Other libraries */ + implementation(Library.materialComponent) + implementation(Library.googlePlayCore) + implementation(Library.googlePlayCoreKtx) + coreLibraryDesugaring(Library.desugarJdkLibs) + implementation(Library.timber) + implementation(Library.reKotlin) + implementation(Library.zxingQrCore) + implementation(Library.zxingQrBarcodeScanner) + implementation(Library.otaliastudiosCameraView) + implementation(Library.coil) + implementation(Library.appsflyer) + implementation(Library.amplitude) + implementation(Library.kotsonGsonExt) + //TODO: refactoring: remove it when all network services moved to the datasource module + implementation(Library.retrofit) + implementation(Library.retrofitMoshiConverter) + implementation(Library.moshi) + implementation(Library.moshiKotlin) + implementation(Library.okHttp) + implementation(Library.okHttpLogging) + implementation(Library.zendeskChat) + implementation(Library.zendeskMessaging) + implementation(Library.spongecastleCryptoCore) + implementation(Library.lottie) + implementation(Library.shopifyBuySdk) { + exclude(group = "com.shopify.graphql.support") + exclude(module = "joda-time") + } + implementation(Library.accompanistAppCompatTheme) + implementation(Library.accompanistSystemUiController) + implementation(Library.skydovesVeil) + implementation(Library.viewBindingDelegate) + implementation(Library.armadillo) + implementation(Library.googlePlayServicesWallet) + + /** Testing libraries */ + testImplementation(Test.junit) + testImplementation(Test.truth) + androidTestImplementation(Test.junitAndroidExt) + androidTestImplementation(Test.espresso) +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index f1b424510d..2f9dc5a47e 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 64f57c2248..0000000000 --- a/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -buildscript { - apply from: 'dependencies.gradle' - - repositories { - google() - jcenter() - maven { url 'https://maven.fabric.io/public' } - } - dependencies { - classpath "com.android.tools.build:gradle:${versions.build_gradle}" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin" - classpath "com.github.dcendents:android-maven-gradle-plugin:2.1" - classpath 'com.google.gms:google-services:4.3.10' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' - } -} - -allprojects { - repositories { - google() - jcenter() - mavenLocal() - maven { url "https://nexus.tangem-tech.com/repository/maven-releases/" } - maven { url 'https://jitpack.io' } - maven { url 'https://zendesk.jfrog.io/zendesk/repo' } - } -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000000..0cc8e9c419 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,30 @@ +buildscript { + repositories { + google() + mavenCentral() + maven(url = "https://maven.fabric.io/public") + } + + dependencies { + classpath(ClasspathDependency.AndroidGradlePlugin) + classpath(ClasspathDependency.KotlinGradlePlugin) + classpath(ClasspathDependency.AndroidMavenGradlePlugin) + classpath(ClasspathDependency.GoogleServices) + classpath(ClasspathDependency.GoogleFirebaseCrashlytics) + } +} + +allprojects { + repositories { + google() + jcenter() // unable to replace with mavenCentral() due to rekotlin and com.otaliastudios:cameraview + mavenLocal() + maven("https://nexus.tangem-tech.com/repository/maven-releases/") + maven("https://jitpack.io") + maven("https://zendesk.jfrog.io/zendesk/repo") + } +} + +tasks.register("clean", Delete::class) { + delete(rootProject.buildDir) +} \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000000..b22ed732fd --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() +} \ No newline at end of file diff --git a/buildSrc/src/main/java/AppConfig.kt b/buildSrc/src/main/java/AppConfig.kt new file mode 100644 index 0000000000..9a7e91768d --- /dev/null +++ b/buildSrc/src/main/java/AppConfig.kt @@ -0,0 +1,8 @@ +object AppConfig { + const val packageName = "com.tangem.wallet" + const val versionCode = 1 + const val versionName = "1.0.0.-SNAPSHOT" + const val minSdkVersion = 21 + const val targetSdkVersion = 33 + const val compileSdkVersion = 33 +} \ No newline at end of file diff --git a/buildSrc/src/main/java/ClasspathDependency.kt b/buildSrc/src/main/java/ClasspathDependency.kt new file mode 100644 index 0000000000..23fcf71450 --- /dev/null +++ b/buildSrc/src/main/java/ClasspathDependency.kt @@ -0,0 +1,9 @@ +object ClasspathDependency { + const val AndroidGradlePlugin = "com.android.tools.build:gradle:" + Versions.androidGradlePlugin + const val AndroidMavenGradlePlugin = "com.github.dcendents:android-maven-gradle-plugin:" + + Versions.androidMavenGradlePlugin + const val GoogleFirebaseCrashlytics = "com.google.firebase:firebase-crashlytics-gradle:" + + Versions.googleFirebaseCrashlytics + const val GoogleServices = "com.google.gms:google-services:" + Versions.googleServices + const val KotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:" + Versions.kotlin +} \ No newline at end of file diff --git a/buildSrc/src/main/java/ConfigFieldBuilder.kt b/buildSrc/src/main/java/ConfigFieldBuilder.kt new file mode 100644 index 0000000000..42c1f16c56 --- /dev/null +++ b/buildSrc/src/main/java/ConfigFieldBuilder.kt @@ -0,0 +1,26 @@ +typealias ConfigFieldBuilder = (String, String, String) -> Unit + +class BuildConfigFieldFactory( + private val fields: List, + private val builder: ConfigFieldBuilder, +) { + fun create() { + fields.forEach { field -> builder(field.type, field.name, field.value) } + } +} + +/** + * Build config field + * + * @property type field type + * @property name field name + * @property value field value + */ +sealed class Field(val type: String, val name: String, val value: String) { + + class Environment(value: String) : Field("String", "ENVIRONMENT", "\"$value\"") + + class TestActionEnabled(isEnabled: Boolean) : Field("Boolean","TEST_ACTION_ENABLED", isEnabled.toString()) + + class LogEnabled(isEnabled: Boolean) : Field("Boolean","LOG_ENABLED", isEnabled.toString()) +} \ No newline at end of file diff --git a/buildSrc/src/main/java/Dependency.kt b/buildSrc/src/main/java/Dependency.kt new file mode 100644 index 0000000000..25c0e3787a --- /dev/null +++ b/buildSrc/src/main/java/Dependency.kt @@ -0,0 +1,77 @@ +object AndroidX { + const val activityCompose = "androidx.activity:activity-compose:" + Versions.androidxActivityCompose + const val appCompat = "androidx.appcompat:appcompat:" + Versions.androidxAppCompat + const val browser = "androidx.browser:browser:" + Versions.androidxBrowser + const val constraintLayout = "androidx.constraintlayout:constraintlayout:" + Versions.androidxConstraintLayout + const val coreKtx = "androidx.core:core-ktx:" + Versions.androidxCore + const val fragmentKtx = "androidx.fragment:fragment-ktx:" + Versions.androidxFragment + const val lifecycleCommonJava8 = "androidx.lifecycle:lifecycle-common-java8:" + Versions.androidxLifecycle + const val lifecycleLiveDataKtx = "androidx.lifecycle:lifecycle-livedata-ktx:" + Versions.androidxLifecycle + const val lifecycleRuntimeKtx = "androidx.lifecycle:lifecycle-runtime-ktx:" + Versions.androidxLifecycle + const val lifecycleViewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:" + Versions.androidxLifecycle +} + +object Compose { + const val animation = "androidx.compose.animation:animation:" + Versions.compose + const val coil = "io.coil-kt:coil-compose:" + Versions.coil + const val foundation = "androidx.compose.foundation:foundation:" + Versions.composeFoundation + const val material = "androidx.compose.material:material:" + Versions.compose + const val ui = "androidx.compose.ui:ui:" + Versions.compose + const val uiTooling = "androidx.compose.ui:ui-tooling:" + Versions.compose +} + +object Firebase { + const val bom = "com.google.firebase:firebase-bom:" + Versions.firebase + const val firebaseAnalytics = "com.google.firebase:firebase-analytics-ktx" + const val firebaseCrashlytics = "com.google.firebase:firebase-crashlytics-ktx" +} + +object Library { + const val accompanistAppCompatTheme = "com.google.accompanist:accompanist-appcompat-theme:" + Versions.accompanist + const val accompanistSystemUiController = + "com.google.accompanist:accompanist-systemuicontroller:" + Versions.accompanist + const val amplitude = "com.amplitude:android-sdk:" + Versions.amplitude + const val appsflyer = "com.appsflyer:af-android-sdk:" + Versions.appsflyer + const val armadillo = "at.favre.lib:armadillo:" + Versions.armadillo + const val coil = "io.coil-kt:coil:" + Versions.coil + const val coroutine = "org.jetbrains.kotlinx:kotlinx-coroutines-core:" + Versions.coroutine + const val desugarJdkLibs = "com.android.tools:desugar_jdk_libs:" + Versions.desugarJdkLibs + const val googlePlayCore = "com.google.android.play:core:" + Versions.googlePlayCore + const val googlePlayCoreKtx = "com.google.android.play:core-ktx:" + Versions.googlePlayCoreKtx + const val googlePlayServicesWallet = + "com.google.android.gms:play-services-wallet:" + Versions.googlePlayServicesWallet + const val kotsonGsonExt = "com.github.salomonbrys.kotson:kotson:" + Versions.kotsonGsonExt + const val lottie = "com.airbnb.android:lottie:" + Versions.lottie + const val materialComponent = "com.google.android.material:material:" + Versions.googleMaterialComponent + const val moshi = "com.squareup.moshi:moshi:" + Versions.moshi + const val moshiKotlin = "com.squareup.moshi:moshi-kotlin:" + Versions.moshi + const val okHttp = "com.squareup.okhttp3:okhttp:" + Versions.okhttp + const val okHttpLogging = "com.squareup.okhttp3:logging-interceptor:" + Versions.okhttp + const val otaliastudiosCameraView = "com.otaliastudios:cameraview:" + Versions.otaliastudiosCameraView + const val shopifyBuySdk = "com.shopify.mobilebuysdk:buy3:" + Versions.shopifyBuySdk + const val skydovesVeil = "com.github.skydoves:androidveil:" + Versions.skydovesAndroidVeil + const val spongecastleCryptoCore = "com.madgag.spongycastle:core:" + Versions.spongycastleCryptoCore + const val reKotlin = "org.rekotlin:rekotlin:" + Versions.rekotlin + const val retrofit = "com.squareup.retrofit2:retrofit:" + Versions.retrofit + const val retrofitMoshiConverter = "com.squareup.retrofit2:converter-moshi:" + Versions.retrofitMoshiConverter + const val timber = "com.jakewharton.timber:timber:" + Versions.timber + const val viewBindingDelegate = + "com.github.kirich1409:viewbindingpropertydelegate-noreflection:" + Versions.viewBindingDelegate + const val zendeskChat = "com.zendesk:chat:" + Versions.zendeskChat + const val zendeskMessaging = "com.zendesk:messaging:" + Versions.zendeskMessaging + const val zxingQrBarcodeScanner = "me.dm7.barcodescanner:zxing:" + Versions.zxingQrBarcodeScanner + const val zxingQrCore = "com.google.zxing:core:" + Versions.zxingQrCode +} + +object Tangem { + const val blockchain = "com.tangem:blockchain:" + Versions.tangemBlockchainSdk + const val cardAndroid = "com.tangem.tangem-sdk-kotlin:android:" + Versions.tangemCardSgk + const val cardCore = "com.tangem.tangem-sdk-kotlin:core:" + Versions.tangemCardSgk +} + +object Test { + const val espresso = "androidx.test.espresso:espresso-core:" + Versions.espresso + const val junit = "junit:junit:" + Versions.junit + const val junitAndroidExt = "androidx.test.ext:junit:" + Versions.junitAndroidExt + const val truth = "com.google.truth:truth:" + Versions.truth +} \ No newline at end of file diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt new file mode 100644 index 0000000000..d7d89a1fcd --- /dev/null +++ b/buildSrc/src/main/java/Versions.kt @@ -0,0 +1,68 @@ +object Versions { + // region Classpath + const val androidGradlePlugin = "7.1.3" + const val androidMavenGradlePlugin = "2.1" + const val googleFirebaseCrashlytics = "2.7.1" + const val googleServices = "4.3.10" + const val kotlin = "1.6.10" + // endregion Classpath + + // region AndroidX + const val androidxActivityCompose = "1.5.0" + const val androidxAppCompat = "1.5.1" + const val androidxBrowser = "1.4.0" + const val androidxConstraintLayout = "2.1.4" + const val androidxCore = "1.9.0" + const val androidxFragment = "1.5.3" + const val androidxLifecycle = "2.5.1" + // endregion AndroidX + + // region Compose + const val compose = "1.1.0" + const val composeFoundation = "1.2.0-rc03" + // endregion Compose + + // region Other libraries + const val accompanist = "0.23.0" + const val amplitude = "2.36.1" + const val appsflyer = "6.5.1" + const val armadillo = "0.9.0" + const val coil = "2.1.0" + const val coroutine = "1.5.2" + const val desugarJdkLibs = "1.1.5" + const val firebase = "26.0.0" + const val googleMaterialComponent = "1.6.1" + const val googlePlayCore = "1.10.3" + const val googlePlayCoreKtx = "1.8.1" + const val googlePlayServicesWallet = "19.1.0" + const val kotsonGsonExt = "2.5.0" + const val lottie = "3.4.0" + const val moshi = "1.12.0" + const val okhttp = "4.9.3" + const val otaliastudiosCameraView = "2.6.3" + const val rekotlin = "1.0.4" + const val retrofit = "2.8.1" + const val retrofitMoshiConverter = "2.6.0" + const val shopifyBuySdk = "12.0.0" + const val skydovesAndroidVeil = "1.1.2" + const val spongycastleCryptoCore = "1.58.0.0" + const val timber = "4.7.1" + const val viewBindingDelegate = "1.5.6" + const val zendeskChat = "3.3.5" + const val zendeskMessaging = "5.2.4" + const val zxingQrBarcodeScanner = "1.9.8" + const val zxingQrCode = "3.3.3" + // endregion Other libraries + + // region Tangem + const val tangemBlockchainSdk = "develop-133" + const val tangemCardSgk = "develop-165" + // endregion Tangem + + // region Testing + const val espresso = "3.4.0" + const val junit = "4.13.2" + const val truth = "1.1.3" + const val junitAndroidExt = "1.1.3" + // endregion Testing +} \ No newline at end of file diff --git a/common/build.gradle b/common/build.gradle.kts similarity index 67% rename from common/build.gradle rename to common/build.gradle.kts index 0412225730..e8259cee84 100644 --- a/common/build.gradle +++ b/common/build.gradle.kts @@ -1,6 +1,6 @@ plugins { - id 'java-library' - id 'org.jetbrains.kotlin.jvm' + id("java-library") + id("org.jetbrains.kotlin.jvm") } java { diff --git a/dependencies.gradle b/dependencies.gradle deleted file mode 100644 index ba7e3c39b6..0000000000 --- a/dependencies.gradle +++ /dev/null @@ -1,14 +0,0 @@ -ext.versions = [ - kotlin : '1.6.10', - build_gradle : '7.1.3', - tangem_card_sdk : 'develop-165', - // tangem_card_sdk : '0.0.1', - tangem_blockchain_sdk: 'develop-133', - // tangem_blockchain_sdk: '0.0.1', -] - -ext.environmentConfig = [ - environment : "ENVIRONMENT", - testActionEnabled: "TEST_ACTION_ENABLED", - logEnabled : "LOG_ENABLED", -] diff --git a/domain/build.gradle b/domain/build.gradle deleted file mode 100644 index 83c9fe6ff1..0000000000 --- a/domain/build.gradle +++ /dev/null @@ -1,88 +0,0 @@ -plugins { - id 'com.android.library' - id 'org.jetbrains.kotlin.android' -} - -apply from: '../dependencies.gradle' - -android { - compileSdk 31 - - defaultConfig { - minSdk 21 - targetSdk 31 - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles "consumer-rules.pro" - buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false' - buildConfigField 'Boolean', environmentConfig.logEnabled, 'false' - } - - buildTypes { - release { - debuggable false - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false' - buildConfigField 'Boolean', environmentConfig.logEnabled, 'false' - } - debug { - debuggable true - minifyEnabled false - buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'true' - buildConfigField 'Boolean', environmentConfig.logEnabled, 'true' - } - debug_beta { - initWith release - buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'true' - buildConfigField 'Boolean', environmentConfig.logEnabled, 'true' - } - } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -// composeOptions { -// kotlinCompilerExtensionVersion '1.1.0' -// } - packagingOptions { - exclude 'lib/x86_64/darwin/libscrypt.dylib' - exclude 'lib/x86_64/freebsd/libscrypt.so' - exclude 'lib/x86_64/linux/libscrypt.so' - } -} - -dependencies { - implementation implementation(project(path: ':network')) - implementation implementation(project(path: ':common')) - - implementation "com.tangem:blockchain:${versions.tangem_blockchain_sdk}" - implementation "com.tangem.tangem-sdk-kotlin:core:${versions.tangem_card_sdk}" - implementation "com.tangem.tangem-sdk-kotlin:android:${versions.tangem_card_sdk}" - - // Kotlin coroutines - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' - - // State management - implementation "org.rekotlin:rekotlin:1.0.4" - - // Network - //TODO: it must depends from network module - implementation 'com.squareup.retrofit2:retrofit:2.8.1' - implementation 'com.squareup.retrofit2:converter-moshi:2.6.0' - implementation 'com.squareup.moshi:moshi:1.12.0' - implementation "com.squareup.moshi:moshi-kotlin:1.12.0" - implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2' - - // Logs - implementation 'com.jakewharton.timber:timber:4.7.1' - - // Tests - testImplementation 'junit:junit:4.13.2' - testImplementation "com.google.truth:truth:1.1.3" - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' -} \ No newline at end of file diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts new file mode 100644 index 0000000000..6e1c48864e --- /dev/null +++ b/domain/build.gradle.kts @@ -0,0 +1,98 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + compileSdk = AppConfig.compileSdkVersion + + defaultConfig { + minSdk = AppConfig.minSdkVersion + targetSdk = AppConfig.targetSdkVersion + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") + + BuildConfigFieldFactory( + fields = listOf( + Field.Environment("prod"), + Field.TestActionEnabled(false), + Field.LogEnabled(false), + ), + builder = ::buildConfigField, + ).create() + } + + debug { + isMinifyEnabled = false + + BuildConfigFieldFactory( + fields = listOf( + Field.Environment("prod"), + Field.TestActionEnabled(true), + Field.LogEnabled(true), + ), + builder = ::buildConfigField, + ).create() + } + + create("debug_beta") { + initWith(getByName("release")) + BuildConfigFieldFactory( + fields = listOf( + Field.Environment("release"), + Field.TestActionEnabled(true), + Field.LogEnabled(true), + ), + builder = ::buildConfigField, + ).create() + } + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + packagingOptions { + resources.excludes += "lib/x86_64/darwin/libscrypt.dylib" + resources.excludes += "lib/x86_64/freebsd/libscrypt.so" + resources.excludes += "lib/x86_64/linux/libscrypt.so" + } +} + +dependencies { + implementation(project(":network")) + implementation(project(":common")) + + /** Tangem libraries */ + implementation(Tangem.blockchain) + implementation(Tangem.cardCore) + implementation(Tangem.cardAndroid) + + /** Other libraries */ + implementation(Library.reKotlin) + //TODO: refactoring: remove it when all network services moved to the datasource module + implementation(Library.retrofit) + implementation(Library.retrofitMoshiConverter) + implementation(Library.moshi) + implementation(Library.moshiKotlin) + implementation(Library.okHttpLogging) + implementation(Library.timber) + implementation(Library.coroutine) + + /** Testing libraries */ + testImplementation(Test.junit) + testImplementation(Test.truth) + androidTestImplementation(Test.junitAndroidExt) + androidTestImplementation(Test.espresso) +} \ No newline at end of file diff --git a/domain/proguard-rules.pro b/domain/proguard-rules.pro index 481bb43481..ff59496d81 100644 --- a/domain/proguard-rules.pro +++ b/domain/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/network/build.gradle b/network/build.gradle deleted file mode 100644 index e7fccceda6..0000000000 --- a/network/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -plugins { - id 'java-library' - id 'org.jetbrains.kotlin.jvm' -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -dependencies { - implementation implementation(project(path: ':common')) - - // Tangem sdk's - implementation "com.tangem.tangem-sdk-kotlin:core:${versions.tangem_card_sdk}" - - // Kotlin coroutines - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' - - // Logs - implementation 'com.jakewharton.timber:timber:4.7.1' - - // Network - implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3")) - implementation("com.squareup.okhttp3:okhttp") - implementation("com.squareup.okhttp3:logging-interceptor") - implementation 'com.squareup.retrofit2:retrofit:2.8.1' - implementation 'com.squareup.retrofit2:converter-moshi:2.6.0' - implementation 'com.squareup.moshi:moshi:1.13.0' - implementation "com.squareup.moshi:moshi-kotlin:1.13.0" -} \ No newline at end of file diff --git a/network/build.gradle.kts b/network/build.gradle.kts new file mode 100644 index 0000000000..0c82757e05 --- /dev/null +++ b/network/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + id("java-library") + id("org.jetbrains.kotlin.jvm") +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + implementation(project(":common")) + + /** Tangem libraries */ + implementation(Tangem.cardCore) + + /** Other libraries */ + implementation(Library.retrofit) + implementation(Library.retrofitMoshiConverter) + implementation(Library.moshi) + implementation(Library.moshiKotlin) + implementation(Library.okHttp) + implementation(Library.okHttpLogging) + implementation(Library.timber) + implementation(Library.coroutine) +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 9b33b9462c..0000000000 --- a/settings.gradle +++ /dev/null @@ -1,4 +0,0 @@ -include ':app' -include ':domain' -include ':network' -include ':common' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000000..5e820bbbfc --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,4 @@ +include(":app") +include(":domain") +include(":network") +include(":common") \ No newline at end of file From df818fa72a6149c0ca087799a98dc0bee5c57663 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 11 Nov 2022 14:07:23 +0300 Subject: [PATCH 046/372] Updated on 2026-08-14 --- app/build.gradle.kts | 6 +++ .../java/com/tangem/tap/TapApplication.kt | 2 + build.gradle.kts | 4 ++ buildSrc/src/main/java/Dependency.kt | 2 + buildSrc/src/main/java/Versions.kt | 1 + core/datasource/.gitignore | 1 + core/datasource/build.gradle.kts | 46 +++++++++++++++++++ core/datasource/src/main/AndroidManifest.xml | 2 + .../datasource/api/referral/ReferralApi.kt | 7 +++ .../com/tangem/datasource/di/NetworkModule.kt | 44 ++++++++++++++++++ settings.gradle.kts | 3 +- 11 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 core/datasource/.gitignore create mode 100644 core/datasource/build.gradle.kts create mode 100644 core/datasource/src/main/AndroidManifest.xml create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/referral/ReferralApi.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 218986fc34..fe20c4709a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,6 +4,7 @@ plugins { kotlin("kapt") id("com.google.gms.google-services") id("com.google.firebase.crashlytics") + id("com.google.dagger.hilt.android") } android { @@ -95,6 +96,7 @@ android { resources.excludes += "lib/x86_64/darwin/libscrypt.dylib" resources.excludes += "lib/x86_64/freebsd/libscrypt.so" resources.excludes += "lib/x86_64/linux/libscrypt.so" + resources.excludes += "META-INF/gradle/incremental.annotation.processors" } } @@ -138,6 +140,10 @@ dependencies { implementation(Tangem.cardCore) implementation(Tangem.cardAndroid) + /** DI */ + implementation(Library.hilt) + implementation(Library.hiltKapt) + /** Other libraries */ implementation(Library.materialComponent) implementation(Library.googlePlayCore) diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index c0d6e566b4..56e6a36cd4 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -33,6 +33,7 @@ import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.persistence.PreferencesStorage import com.tangem.wallet.BuildConfig import com.zendesk.logger.Logger +import dagger.hilt.android.HiltAndroidApp import org.rekotlin.Store import timber.log.Timber import zendesk.chat.Chat @@ -50,6 +51,7 @@ lateinit var shopService: TangemShopService lateinit var assetReader: AssetReader lateinit var userTokensRepository: UserTokensRepository +@HiltAndroidApp class TapApplication : Application(), ImageLoaderFactory { override fun onCreate() { diff --git a/build.gradle.kts b/build.gradle.kts index 0cc8e9c419..75b1a4e453 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,6 +14,10 @@ buildscript { } } +plugins { + id("com.google.dagger.hilt.android") version "2.44" apply false +} + allprojects { repositories { google() diff --git a/buildSrc/src/main/java/Dependency.kt b/buildSrc/src/main/java/Dependency.kt index 25c0e3787a..bd91e1bdaa 100644 --- a/buildSrc/src/main/java/Dependency.kt +++ b/buildSrc/src/main/java/Dependency.kt @@ -40,6 +40,8 @@ object Library { const val googlePlayCoreKtx = "com.google.android.play:core-ktx:" + Versions.googlePlayCoreKtx const val googlePlayServicesWallet = "com.google.android.gms:play-services-wallet:" + Versions.googlePlayServicesWallet + const val hilt = "com.google.dagger:hilt-android:" + Versions.hilt + const val hiltKapt = "com.google.dagger:hilt-compiler:" + Versions.hilt const val kotsonGsonExt = "com.github.salomonbrys.kotson:kotson:" + Versions.kotsonGsonExt const val lottie = "com.airbnb.android:lottie:" + Versions.lottie const val materialComponent = "com.google.android.material:material:" + Versions.googleMaterialComponent diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index d7d89a1fcd..56e26ff54e 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -35,6 +35,7 @@ object Versions { const val googlePlayCore = "1.10.3" const val googlePlayCoreKtx = "1.8.1" const val googlePlayServicesWallet = "19.1.0" + const val hilt = "2.44" const val kotsonGsonExt = "2.5.0" const val lottie = "3.4.0" const val moshi = "1.12.0" diff --git a/core/datasource/.gitignore b/core/datasource/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/datasource/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts new file mode 100644 index 0000000000..2d5eb60c3e --- /dev/null +++ b/core/datasource/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + id("com.android.library") + kotlin("android") + kotlin("kapt") + id("com.google.dagger.hilt.android") +} + +android { + + defaultConfig { + compileSdk = AppConfig.compileSdkVersion + minSdk = AppConfig.minSdkVersion + targetSdk = AppConfig.targetSdkVersion + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + isCoreLibraryDesugaringEnabled = false + } +} + +dependencies { + + /** DI */ + implementation(Library.hilt) + implementation(Library.hiltKapt) + + /** Coroutines */ + implementation(Library.coroutine) + + /** Logging */ + implementation(Library.timber) + + /** Network */ + implementation(Library.retrofit) + implementation(Library.retrofitMoshiConverter) + implementation(Library.moshi) + implementation(Library.moshiKotlin) + implementation(Library.okHttp) + implementation(Library.okHttpLogging) +} \ No newline at end of file diff --git a/core/datasource/src/main/AndroidManifest.xml b/core/datasource/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..d6401c6174 --- /dev/null +++ b/core/datasource/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/referral/ReferralApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/referral/ReferralApi.kt new file mode 100644 index 0000000000..ccaa203e9c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/referral/ReferralApi.kt @@ -0,0 +1,7 @@ +package com.tangem.datasource.api.referral + +/** + * Api for referral functionality + */ +interface ReferralApi { +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt new file mode 100644 index 0000000000..823d3e63d8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -0,0 +1,44 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.api.referral.ReferralApi +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class NetworkModule { + + @Provides + @Singleton + fun provideReferralApi(okHttpClient: OkHttpClient): ReferralApi { + return Retrofit.Builder() + .addConverterFactory( + MoshiConverterFactory.create(), + ) + .baseUrl(PROD_REFERRAL_BASE_URL) + .client(okHttpClient) + .build() + .create(ReferralApi::class.java) + } + + @Provides + @Singleton + fun provideOkHttpClient(): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor( + HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC), + ) + .build() + } + + private companion object { + const val PROD_REFERRAL_BASE_URL = "" + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 5e820bbbfc..658531b8f2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,5 @@ include(":app") include(":domain") include(":network") -include(":common") \ No newline at end of file +include(":common") +include(":core:datasource") \ No newline at end of file From 241f8d217c3ecfac0ae29fb53493c966ee8a00f1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 11 Nov 2022 15:25:06 +0300 Subject: [PATCH 047/372] Updated on 2026-08-14 --- buildSrc/src/main/java/Dependency.kt | 1 + buildSrc/src/main/java/Versions.kt | 1 + core/datasource/build.gradle.kts | 3 ++ .../datasource/api/referral/ReferralApi.kt | 17 +++++++- .../api/referral/models/ReferralResponse.kt | 42 +++++++++++++++++++ .../api/referral/models/StartReferralBody.kt | 11 +++++ 6 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/referral/models/StartReferralBody.kt diff --git a/buildSrc/src/main/java/Dependency.kt b/buildSrc/src/main/java/Dependency.kt index bd91e1bdaa..84f6380c8c 100644 --- a/buildSrc/src/main/java/Dependency.kt +++ b/buildSrc/src/main/java/Dependency.kt @@ -42,6 +42,7 @@ object Library { "com.google.android.gms:play-services-wallet:" + Versions.googlePlayServicesWallet const val hilt = "com.google.dagger:hilt-android:" + Versions.hilt const val hiltKapt = "com.google.dagger:hilt-compiler:" + Versions.hilt + const val jodatime = "net.danlew:android.joda:" + Versions.jodatime const val kotsonGsonExt = "com.github.salomonbrys.kotson:kotson:" + Versions.kotsonGsonExt const val lottie = "com.airbnb.android:lottie:" + Versions.lottie const val materialComponent = "com.google.android.material:material:" + Versions.googleMaterialComponent diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index 56e26ff54e..b52ccb4d7b 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -36,6 +36,7 @@ object Versions { const val googlePlayCoreKtx = "1.8.1" const val googlePlayServicesWallet = "19.1.0" const val hilt = "2.44" + const val jodatime = "2.12.1" const val kotsonGsonExt = "2.5.0" const val lottie = "3.4.0" const val moshi = "1.12.0" diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 2d5eb60c3e..ba85cac77f 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -43,4 +43,7 @@ dependencies { implementation(Library.moshiKotlin) implementation(Library.okHttp) implementation(Library.okHttpLogging) + + /** Time */ + implementation(Library.jodatime) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/referral/ReferralApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/referral/ReferralApi.kt index ccaa203e9c..42445f931d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/referral/ReferralApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/referral/ReferralApi.kt @@ -1,7 +1,22 @@ package com.tangem.datasource.api.referral +import com.tangem.datasource.api.referral.models.ReferralResponse +import com.tangem.datasource.api.referral.models.StartReferralBody +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query + /** - * Api for referral functionality + * Api for referral feature */ interface ReferralApi { + + /** Returns referral status by [walletId] */ + @GET("referral/{walletId}") + suspend fun getReferralStatus(@Query("name") walletId: String): ReferralResponse + + /** Make user referral, requires [StartReferralBody] */ + @POST("referral") + suspend fun startReferral(@Body startReferralBody: StartReferralBody): ReferralResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt new file mode 100644 index 0000000000..16c6cd1fa6 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt @@ -0,0 +1,42 @@ +package com.tangem.datasource.api.referral.models + +import com.squareup.moshi.Json +import org.joda.time.DateTime + +/** + * Main response class for referral API + * contains all necessary info about users program status + */ +data class ReferralResponse( + @Json(name = "conditions") val conditions: Conditions, +) + +data class Conditions( + @Json(name = "conditions") val award: Int, + @Json(name = "discount") val discount: Int, + @Json(name = "discountType") val discountType: DiscountType, + @Json(name = "tosLink") val tosLink: String, + @Json(name = "tokens") val tokens: List, + @Json(name = "referral") val referral: Referral, +) + +data class Token( + @Json(name = "id") val id: String, + @Json(name = "name") val name: String, + @Json(name = "symbol") val symbol: String, + @Json(name = "networkId") val networkId: String?, + @Json(name = "contractAddress") val contractAddress: String?, + @Json(name = "decimalCount") val decimalCount: Int, +) + +data class Referral( + @Json(name = "shareLink") val shareLink: String, + @Json(name = "address") val address: String, + @Json(name = "promocode") val promocode: String, + @Json(name = "walletsPurchased") val walletsPurchased: Int, + @Json(name = "termsAcceptedAt") val termsAcceptedAt: DateTime?, +) + +enum class DiscountType { + PERCENTAGE, VALUE +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/StartReferralBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/StartReferralBody.kt new file mode 100644 index 0000000000..98a0a8ab90 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/StartReferralBody.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.api.referral.models + +import com.squareup.moshi.Json + +/** Body for make user referral */ +data class StartReferralBody( + @Json(name = "walletId") val walletId: String, + @Json(name = "networkId") val networkId: String, + @Json(name = "tokenId") val tokenId: String, + @Json(name = "address") val address: String, +) \ No newline at end of file From ec62375be4a1108b58d8368b554065e930d20c68 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 14 Nov 2022 22:36:50 +0500 Subject: [PATCH 048/372] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/common/analytics/Analytics.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 03cb8437bb..8a6ee52f27 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/Analytics.kt @@ -11,7 +11,7 @@ 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.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.launch import java.util.concurrent.Executors @@ -88,6 +88,6 @@ object Analytics : GlobalAnalyticsEventHandler { val name = "Analytics" val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() val exHandler = FeatureCoroutineExceptionHandler.create(name) - return CoroutineScope(Job() + dispatcher + CoroutineName(name) + exHandler) + return CoroutineScope(SupervisorJob() + dispatcher + CoroutineName(name) + exHandler) } } \ No newline at end of file From d4c34f6af90c6420198cf3d54d6d768739654951 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 15 Nov 2022 00:17:49 +0500 Subject: [PATCH 049/372] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/common/DialogManager.kt | 2 +- .../products/note/redux/OnboardingNoteMiddleware.kt | 9 ++++++--- .../tangem/tap/features/wallet/redux/WalletAction.kt | 5 ++++- .../redux/middlewares/WalletDialogsMiddleware.kt | 2 +- .../tap/features/wallet/redux/models/WalletDialog.kt | 5 ++++- .../RussianCardholdersWarningBottomSheetDialog.kt | 12 +++++++++--- 6 files changed, 25 insertions(+), 10 deletions(-) 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/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index 57940fe421..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 @@ -13,6 +13,7 @@ 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.getTopUpUrl import com.tangem.tap.common.extensions.onCardScanned import com.tangem.tap.common.postUi import com.tangem.tap.common.redux.AppDialog @@ -28,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 @@ -174,13 +176,14 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch return } - val topUpUrl = noteState.walletManager.getToUpUrl() ?: return + val topUpUrl = walletManager.getTopUpUrl() ?: return + val blockchain = walletManager.wallet.blockchain if (globalState.userCountryCode == RUSSIA_COUNTRY_CODE) { - store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog(topUpUrl)) + val dialogData = WalletDialog.RussianCardholdersWarningDialog.Data(topUpUrl, blockchain) + store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog(dialogData)) return } - val blockchain = walletManager.wallet.blockchain val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain) Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType)) 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/WalletDialogsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt index 33e8e901f8..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 @@ -38,7 +38,7 @@ class WalletDialogsMiddleware { ) } 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/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/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt index 4073120f64..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 @@ -5,16 +5,20 @@ 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) { @@ -36,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)) } From c70f4c5192f06fd808842d0b42dc784ab3f966d9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 15 Nov 2022 02:27:54 +0500 Subject: [PATCH 050/372] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 18 ++++++++-- .../com/tangem/tap/common/moshi/Adapters.kt | 16 +++++++++ .../tap/persistence/PreferencesStorage.kt | 8 ++--- .../tap/persistence/ToppedUpWalletStorage.kt | 21 ++++++++++- .../tangem/network/common/MoshiConverter.kt | 35 +++++++------------ 5 files changed, 69 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/moshi/Adapters.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 568ca4af60..5074f09e8d 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -27,6 +27,7 @@ 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 @@ -38,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 @@ -70,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() @@ -88,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, @@ -124,7 +138,7 @@ class TapApplication : Application(), ImageLoaderFactory { config = config, isDebug = BuildConfig.DEBUG, logConfig = LogConfig.analyticsHandlers, - jsonConverter = MoshiJsonConverter.INSTANCE, + jsonConverter = MoshiConverter.INSTANCE, ) factory.build(Analytics, buildData) } 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/persistence/PreferencesStorage.kt b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt index 38a03e482d..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) { @@ -21,12 +21,12 @@ class PreferencesStorage(applicationContext: Application) { 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, MoshiJsonConverter.INSTANCE) + toppedUpWalletStorage = ToppedUpWalletStorage(preferences, MoshiConverter.INSTANCE) } var chatFirstLaunchTime: Long? diff --git a/app/src/main/java/com/tangem/tap/persistence/ToppedUpWalletStorage.kt b/app/src/main/java/com/tangem/tap/persistence/ToppedUpWalletStorage.kt index 7ec028e8ea..e270b2b820 100644 --- a/app/src/main/java/com/tangem/tap/persistence/ToppedUpWalletStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/ToppedUpWalletStorage.kt @@ -2,7 +2,10 @@ 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 @@ -44,7 +47,9 @@ class ToppedUpWalletStorage( private fun restore(): MutableSet { val json = preferences.getString(KEY, null) ?: return mutableSetOf() return try { - jsonConverter.fromJson(json, jsonConverter.typedList(BasicTopUpFilter.Data::class.java))!! + 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() @@ -54,4 +59,18 @@ class ToppedUpWalletStorage( 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/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 From 2925390cb36d6f83c4a804d167e2034a751676a7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 11 Nov 2022 17:46:58 +0300 Subject: [PATCH 051/372] Updated on 2026-08-14 --- core/datasource/build.gradle.kts | 2 +- features/referral/data/.gitignore | 1 + features/referral/data/build.gradle.kts | 32 +++++++++++++++++++ .../data/src/main/AndroidManifest.xml | 2 ++ features/referral/domain/.gitignore | 1 + features/referral/domain/build.gradle.kts | 32 +++++++++++++++++++ .../domain/src/main/AndroidManifest.xml | 2 ++ features/referral/presentation/.gitignore | 1 + .../referral/presentation/build.gradle.kts | 32 +++++++++++++++++++ .../presentation/src/main/AndroidManifest.xml | 2 ++ settings.gradle.kts | 5 ++- 11 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 features/referral/data/.gitignore create mode 100644 features/referral/data/build.gradle.kts create mode 100644 features/referral/data/src/main/AndroidManifest.xml create mode 100644 features/referral/domain/.gitignore create mode 100644 features/referral/domain/build.gradle.kts create mode 100644 features/referral/domain/src/main/AndroidManifest.xml create mode 100644 features/referral/presentation/.gitignore create mode 100644 features/referral/presentation/build.gradle.kts create mode 100644 features/referral/presentation/src/main/AndroidManifest.xml diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index ba85cac77f..c85c0841b9 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -28,7 +28,7 @@ dependencies { /** DI */ implementation(Library.hilt) - implementation(Library.hiltKapt) + kapt(Library.hiltKapt) /** Coroutines */ implementation(Library.coroutine) diff --git a/features/referral/data/.gitignore b/features/referral/data/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/referral/data/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/referral/data/build.gradle.kts b/features/referral/data/build.gradle.kts new file mode 100644 index 0000000000..4a5dd97a5c --- /dev/null +++ b/features/referral/data/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + id("com.android.library") + kotlin("android") + kotlin("kapt") + id("com.google.dagger.hilt.android") +} + +android { + + defaultConfig { + compileSdk = AppConfig.compileSdkVersion + minSdk = AppConfig.minSdkVersion + targetSdk = AppConfig.targetSdkVersion + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + isCoreLibraryDesugaringEnabled = false + } +} + +dependencies { + + /** DI */ + implementation(Library.hilt) + kapt(Library.hiltKapt) +} \ No newline at end of file diff --git a/features/referral/data/src/main/AndroidManifest.xml b/features/referral/data/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..6f8fe3941b --- /dev/null +++ b/features/referral/data/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/features/referral/domain/.gitignore b/features/referral/domain/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/referral/domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/referral/domain/build.gradle.kts b/features/referral/domain/build.gradle.kts new file mode 100644 index 0000000000..4a5dd97a5c --- /dev/null +++ b/features/referral/domain/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + id("com.android.library") + kotlin("android") + kotlin("kapt") + id("com.google.dagger.hilt.android") +} + +android { + + defaultConfig { + compileSdk = AppConfig.compileSdkVersion + minSdk = AppConfig.minSdkVersion + targetSdk = AppConfig.targetSdkVersion + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + isCoreLibraryDesugaringEnabled = false + } +} + +dependencies { + + /** DI */ + implementation(Library.hilt) + kapt(Library.hiltKapt) +} \ No newline at end of file diff --git a/features/referral/domain/src/main/AndroidManifest.xml b/features/referral/domain/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..ace8d8e355 --- /dev/null +++ b/features/referral/domain/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/features/referral/presentation/.gitignore b/features/referral/presentation/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/referral/presentation/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts new file mode 100644 index 0000000000..4a5dd97a5c --- /dev/null +++ b/features/referral/presentation/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + id("com.android.library") + kotlin("android") + kotlin("kapt") + id("com.google.dagger.hilt.android") +} + +android { + + defaultConfig { + compileSdk = AppConfig.compileSdkVersion + minSdk = AppConfig.minSdkVersion + targetSdk = AppConfig.targetSdkVersion + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + isCoreLibraryDesugaringEnabled = false + } +} + +dependencies { + + /** DI */ + implementation(Library.hilt) + kapt(Library.hiltKapt) +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/AndroidManifest.xml b/features/referral/presentation/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..4c5a0a1f54 --- /dev/null +++ b/features/referral/presentation/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 658531b8f2..878d6a3498 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,4 +2,7 @@ include(":app") include(":domain") include(":network") include(":common") -include(":core:datasource") \ No newline at end of file +include(":core:datasource") +include(":features:referral:data") +include(":features:referral:domain") +include(":features:referral:presentation") \ No newline at end of file From 37054e453d93356a858cfe26b5b957e12a8b78f6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 15 Nov 2022 10:26:15 +0300 Subject: [PATCH 052/372] Updated on 2026-08-14 --- .../api/referral/models/ReferralResponse.kt | 4 +- core/utils/.gitignore | 1 + core/utils/build.gradle.kts | 35 ++++++++++++ core/utils/src/main/AndroidManifest.xml | 2 + .../com/tangem/utils/converter/Converter.kt | 8 +++ .../coroutines/CoroutineDispatcherProvider.kt | 20 +++++++ features/referral/data/build.gradle.kts | 8 +++ .../referral/converters/ReferralConverter.kt | 57 +++++++++++++++++++ .../referral/data/ReferralRepositoryImpl.kt | 43 ++++++++++++++ features/referral/domain/build.gradle.kts | 3 + .../referral/domain/ReferralRepository.kt | 17 ++++++ .../referral/domain/models/ReferralData.kt | 52 +++++++++++++++++ settings.gradle.kts | 1 + 13 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 core/utils/.gitignore create mode 100644 core/utils/build.gradle.kts create mode 100644 core/utils/src/main/AndroidManifest.xml create mode 100644 core/utils/src/main/java/com/tangem/utils/converter/Converter.kt create mode 100644 core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt create mode 100644 features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt create mode 100644 features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt create mode 100644 features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt create mode 100644 features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt index 16c6cd1fa6..ef8823e877 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt @@ -9,15 +9,15 @@ import org.joda.time.DateTime */ data class ReferralResponse( @Json(name = "conditions") val conditions: Conditions, + @Json(name = "referral") val referral: Referral?, ) data class Conditions( - @Json(name = "conditions") val award: Int, + @Json(name = "award") val award: Int, @Json(name = "discount") val discount: Int, @Json(name = "discountType") val discountType: DiscountType, @Json(name = "tosLink") val tosLink: String, @Json(name = "tokens") val tokens: List, - @Json(name = "referral") val referral: Referral, ) data class Token( diff --git a/core/utils/.gitignore b/core/utils/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/utils/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/utils/build.gradle.kts b/core/utils/build.gradle.kts new file mode 100644 index 0000000000..917187555f --- /dev/null +++ b/core/utils/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + id("com.android.library") + kotlin("android") + kotlin("kapt") + id("com.google.dagger.hilt.android") +} + +android { + + defaultConfig { + compileSdk = AppConfig.compileSdkVersion + minSdk = AppConfig.minSdkVersion + targetSdk = AppConfig.targetSdkVersion + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + isCoreLibraryDesugaringEnabled = false + } +} + +dependencies { + + /** DI */ + implementation(Library.hilt) + kapt(Library.hiltKapt) + + /** Coroutines */ + implementation(Library.coroutine) +} \ No newline at end of file diff --git a/core/utils/src/main/AndroidManifest.xml b/core/utils/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..02f6bf24ad --- /dev/null +++ b/core/utils/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt b/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt new file mode 100644 index 0000000000..6af667ae08 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt @@ -0,0 +1,8 @@ +package com.tangem.utils.converter + +interface Converter { + fun convert(value: I): O + fun convertList(input: List): List { + return input.map { convert(it) } + } +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt new file mode 100644 index 0000000000..952424e215 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt @@ -0,0 +1,20 @@ +package com.tangem.utils.coroutines + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import javax.inject.Inject + +interface CoroutineDispatcherProvider { + val main: CoroutineDispatcher + val io: CoroutineDispatcher +} + +class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcherProvider { + override val main: CoroutineDispatcher = Dispatchers.Main + override val io: CoroutineDispatcher = Dispatchers.IO +} + +class TestingCoroutineDispatcherProvider( + override val main: CoroutineDispatcher = Dispatchers.Unconfined, + override val io: CoroutineDispatcher = Dispatchers.Unconfined, +) : CoroutineDispatcherProvider \ No newline at end of file diff --git a/features/referral/data/build.gradle.kts b/features/referral/data/build.gradle.kts index 4a5dd97a5c..497d0af645 100644 --- a/features/referral/data/build.gradle.kts +++ b/features/referral/data/build.gradle.kts @@ -26,6 +26,14 @@ android { dependencies { + /** Project*/ + implementation(project(":core:datasource")) + implementation(project(":core:utils")) + implementation(project(":features:referral:domain")) + + /** Time */ + implementation(Library.jodatime) + /** DI */ implementation(Library.hilt) kapt(Library.hiltKapt) diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt new file mode 100644 index 0000000000..bc55726c4b --- /dev/null +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt @@ -0,0 +1,57 @@ +package com.tangem.feature.referral.converters + +import com.tangem.datasource.api.referral.models.ReferralResponse +import com.tangem.datasource.api.referral.models.Token +import com.tangem.feature.referral.domain.models.DiscountType +import com.tangem.feature.referral.domain.models.ReferralData +import com.tangem.feature.referral.domain.models.ReferralInfo +import com.tangem.feature.referral.domain.models.TokenData +import com.tangem.utils.converter.Converter +import javax.inject.Inject + +class ReferralConverter @Inject constructor() : Converter { + + override fun convert(value: ReferralResponse): ReferralData { + val conditions = value.conditions + val referral = value.referral + val tokenConverter = TokenConverter() + return if (referral != null) { + ReferralData.ParticipantData( + award = conditions.award, + discount = conditions.discount, + discountType = DiscountType.valueOf(conditions.discountType.name), + tosLink = conditions.tosLink, + tokens = tokenConverter.convertList(conditions.tokens), + referral = ReferralInfo( + shareLink = referral.shareLink, + address = referral.address, + promocode = referral.promocode, + walletsPurchased = referral.walletsPurchased, + termsAcceptedAt = referral.termsAcceptedAt, + ), + ) + } else { + ReferralData.NonParticipantData( + award = conditions.award, + discount = conditions.discount, + discountType = DiscountType.valueOf(conditions.discountType.name), + tosLink = conditions.tosLink, + tokens = tokenConverter.convertList(conditions.tokens), + ) + } + } +} + +private class TokenConverter : Converter { + + override fun convert(value: Token): TokenData { + return TokenData( + id = value.id, + name = value.name, + symbol = value.symbol, + networkId = value.networkId, + contractAddress = value.contractAddress, + decimalCount = value.decimalCount, + ) + } +} \ No newline at end of file diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt new file mode 100644 index 0000000000..bf2387f070 --- /dev/null +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -0,0 +1,43 @@ +package com.tangem.feature.referral.data + +import com.tangem.datasource.api.referral.ReferralApi +import com.tangem.datasource.api.referral.models.StartReferralBody +import com.tangem.feature.referral.converters.ReferralConverter +import com.tangem.feature.referral.domain.ReferralRepository +import com.tangem.feature.referral.domain.models.ReferralData +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal class ReferralRepositoryImpl @Inject constructor( + private val referralApi: ReferralApi, + private val referralConverter: ReferralConverter, + private val coroutineDispatcher: CoroutineDispatcherProvider, +) : ReferralRepository { + + override suspend fun getReferralStatus(walletId: String): ReferralData { + return withContext(coroutineDispatcher.io) { + referralConverter.convert(referralApi.getReferralStatus(walletId)) + } + } + + override suspend fun startReferral( + walletId: String, + networkId: String, + tokenId: String, + address: String, + ): ReferralData { + return withContext(coroutineDispatcher.io) { + referralConverter.convert( + referralApi.startReferral( + StartReferralBody( + walletId = walletId, + networkId = networkId, + tokenId = tokenId, + address = address, + ), + ), + ) + } + } +} \ No newline at end of file diff --git a/features/referral/domain/build.gradle.kts b/features/referral/domain/build.gradle.kts index 4a5dd97a5c..561cbe2b15 100644 --- a/features/referral/domain/build.gradle.kts +++ b/features/referral/domain/build.gradle.kts @@ -26,6 +26,9 @@ android { dependencies { + /** Time */ + implementation(Library.jodatime) + /** DI */ implementation(Library.hilt) kapt(Library.hiltKapt) diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt new file mode 100644 index 0000000000..a329077747 --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.referral.domain + +import com.tangem.feature.referral.domain.models.ReferralData + +interface ReferralRepository { + + /** Returns data object of [ReferralData] depends on user program status */ + suspend fun getReferralStatus(walletId: String): ReferralData + + /** Starts user referral program */ + suspend fun startReferral( + walletId: String, + networkId: String, + tokenId: String, + address: String, + ): ReferralData +} \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt new file mode 100644 index 0000000000..ba5f803e59 --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt @@ -0,0 +1,52 @@ +package com.tangem.feature.referral.domain.models + +import org.joda.time.DateTime + +sealed interface ReferralData { + + val award: Int + val discount: Int + val discountType: DiscountType + val tosLink: String + val tokens: List + + /** Data class that used if user is participant of program */ + data class ParticipantData( + override val award: Int, + override val discount: Int, + override val discountType: DiscountType, + override val tosLink: String, + override val tokens: List, + val referral: ReferralInfo, + ) : ReferralData + + /** Data class that used if user is not participant of program */ + data class NonParticipantData( + override val award: Int, + override val discount: Int, + override val discountType: DiscountType, + override val tosLink: String, + override val tokens: List, + ) : ReferralData +} + +data class TokenData( + val id: String, + val name: String, + val symbol: String, + val networkId: String?, + val contractAddress: String?, + val decimalCount: Int, +) + +data class ReferralInfo( + val shareLink: String, + val address: String, + val promocode: String, + val walletsPurchased: Int, + val termsAcceptedAt: DateTime?, +) + +enum class DiscountType { + PERCENTAGE, VALUE +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 878d6a3498..05eaab6228 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,7 @@ include(":domain") include(":network") include(":common") include(":core:datasource") +include(":core:utils") include(":features:referral:data") include(":features:referral:domain") include(":features:referral:presentation") \ No newline at end of file From 56b064b1a021c09073b264de21dd030618e7e452 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 15 Nov 2022 17:26:57 +0300 Subject: [PATCH 053/372] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../tap/common/compose/TangemTypography.kt | 8 +- core/ui/.gitignore | 1 + core/ui/build.gradle.kts | 38 ++++ core/ui/src/main/AndroidManifest.xml | 2 + .../com/tangem/core/ui/components/AppBar.kt | 72 ++++++++ .../com/tangem/core/ui/components/Buttons.kt | 164 ++++++++++++++++++ .../com/tangem/core/ui/components/Cards.kt | 79 +++++++++ .../com/tangem/core/ui/res/ButtonColor.kt | 16 ++ .../main/java/com/tangem/core/ui/res/Color.kt | 45 +++++ .../com/tangem/core/ui/res/TangemTheme.kt | 46 +++++ .../tangem/core/ui/res/TangemTypography.kt | 82 +++++++++ .../java/com/tangem/core/ui/res/TextColor.kt | 20 +++ .../ui}/src/main/res/drawable/ic_back.xml | 0 .../ui}/src/main/res/drawable/ic_tangem.xml | 0 .../main/res/drawable/ill_businessman_3d.webp | Bin 0 -> 76206 bytes core/ui/src/main/res/font/roboto_medium.ttf | Bin 0 -> 168644 bytes core/ui/src/main/res/font/roboto_regular.ttf | Bin 0 -> 168260 bytes core/ui/src/main/res/values/dimens.xml | 31 ++++ settings.gradle.kts | 9 +- 20 files changed, 609 insertions(+), 5 deletions(-) create mode 100644 core/ui/.gitignore create mode 100644 core/ui/build.gradle.kts create mode 100644 core/ui/src/main/AndroidManifest.xml create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/AppBar.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/res/ButtonColor.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/res/Color.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/res/TextColor.kt rename {app => core/ui}/src/main/res/drawable/ic_back.xml (100%) rename {app => core/ui}/src/main/res/drawable/ic_tangem.xml (100%) create mode 100644 core/ui/src/main/res/drawable/ill_businessman_3d.webp create mode 100644 core/ui/src/main/res/font/roboto_medium.ttf create mode 100644 core/ui/src/main/res/font/roboto_regular.ttf create mode 100644 core/ui/src/main/res/values/dimens.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fe20c4709a..847507ba5f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -109,6 +109,7 @@ dependencies { implementation(project(":domain")) implementation(project(":network")) implementation(project(":common")) + implementation(project(":core:ui")) /** AndroidX libraries */ implementation(AndroidX.coreKtx) diff --git a/app/src/main/java/com/tangem/tap/common/compose/TangemTypography.kt b/app/src/main/java/com/tangem/tap/common/compose/TangemTypography.kt index 88b088fa6a..2013295f25 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/TangemTypography.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/TangemTypography.kt @@ -9,13 +9,13 @@ import com.tangem.wallet.R object TangemTypography { val body1 = TextStyle( - fontFamily = FontFamily(Font(R.font.roboto)), + fontFamily = FontFamily(Font(R.font.roboto_regular)), fontSize = 16.0.sp, letterSpacing = 0.5.sp, lineHeight = 24.0.sp, ) val body2 = TextStyle( - fontFamily = FontFamily(Font(R.font.roboto)), + fontFamily = FontFamily(Font(R.font.roboto_regular)), fontSize = 14.0.sp, letterSpacing = 0.25.sp, lineHeight = 20.0.sp, @@ -27,13 +27,13 @@ object TangemTypography { lineHeight = 20.0.sp, ) val caption = TextStyle( - fontFamily = FontFamily(Font(R.font.roboto)), + fontFamily = FontFamily(Font(R.font.roboto_regular)), fontSize = 12.0.sp, letterSpacing = 0.4000000059604645.sp, lineHeight = 16.0.sp, ) val headline1 = TextStyle( - fontFamily = FontFamily(Font(R.font.roboto)), + fontFamily = FontFamily(Font(R.font.roboto_regular)), fontSize = 34.0.sp, letterSpacing = 0.0.sp, lineHeight = 44.0.sp, diff --git a/core/ui/.gitignore b/core/ui/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/ui/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts new file mode 100644 index 0000000000..ee2eab0e2a --- /dev/null +++ b/core/ui/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + id("com.android.library") + kotlin("android") +} + +android { + defaultConfig { + compileSdk = AppConfig.compileSdkVersion + minSdk = AppConfig.minSdkVersion + targetSdk = AppConfig.targetSdkVersion + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + isCoreLibraryDesugaringEnabled = false + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = Versions.compose + } +} + +dependencies { + + /** Compose */ + implementation(Compose.foundation) + implementation(Compose.material) + implementation(Compose.uiTooling) +} \ No newline at end of file diff --git a/core/ui/src/main/AndroidManifest.xml b/core/ui/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..c5a36c7e17 --- /dev/null +++ b/core/ui/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/AppBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/AppBar.kt new file mode 100644 index 0000000000..6810ba11e8 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/AppBar.kt @@ -0,0 +1,72 @@ +package com.tangem.core.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +fun PreviewAppBarWithBackButtonInLightTheme() { + TangemTheme(isDarkTheme = false) { + AppBarWithBackButton(text = "Title", onBackClick = {}) + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +fun PreviewAppBarWithBackButtonInDarkTheme() { + TangemTheme(isDarkTheme = true) { + AppBarWithBackButton(text = "Title", onBackClick = {}) + } +} + +/** + * App bar with back button and optional title + * + * @param text optional title + * @param onBackClick the lambda to be invoked when this icon is pressed + * + * @see Figma component + */ +@Composable +fun AppBarWithBackButton(text: String? = null, onBackClick: () -> Unit) { + Row( + modifier = Modifier + .background(MaterialTheme.colors.primaryVariant) + .padding(all = dimensionResource(R.dimen.spacing16)), + horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing16)), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(R.drawable.ic_back), + contentDescription = null, + modifier = Modifier + .size(size = dimensionResource(R.dimen.size24)) + .clickable { onBackClick() }, + tint = MaterialTheme.colors.onPrimary, + ) + if (!text.isNullOrBlank()) { + Text( + text = text, + color = MaterialTheme.colors.onPrimary, + maxLines = 1, + style = MaterialTheme.typography.subtitle1, + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt new file mode 100644 index 0000000000..e9e49c5c71 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -0,0 +1,164 @@ +package com.tangem.core.ui.components + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.ButtonColorType +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TextColorType +import com.tangem.core.ui.res.buttonColor +import com.tangem.core.ui.res.textColor + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryStartIconButton_Enabled_InLightTheme() { + TangemTheme(isDarkTheme = false) { + PrimaryStartIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryStartIconButton_Enabled_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + PrimaryStartIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryStartIconButton_Disabled_InLightTheme() { + TangemTheme(isDarkTheme = false) { + PrimaryStartIconButton( + text = "Manage tokens", + iconResId = R.drawable.ic_tangem, + enabled = false, + onClicked = {}, + ) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryStartIconButton_Disabled_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + PrimaryStartIconButton( + text = "Manage tokens", + iconResId = R.drawable.ic_tangem, + enabled = false, + onClicked = {}, + ) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryEndIconButton_Enabled_InLightTheme() { + TangemTheme(isDarkTheme = false) { + PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryEndIconButton_Enabled_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryEndIconButton_Disabled_InLightTheme() { + TangemTheme(isDarkTheme = false) { + PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = false, onClicked = {}) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryEndIconButton_Disabled_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = false, onClicked = {}) + } +} + +/** + * Primary button with an icon at the beginning of the layout + * + * @param text button text + * @param iconResId button icon res id + * @param enabled controls the enabled state of the button + * @param onClicked the lambda to be invoked when this button is pressed + * + * @see Figma component + */ +@Composable +fun PrimaryStartIconButton(text: String, @DrawableRes iconResId: Int, enabled: Boolean = true, onClicked: () -> Unit) { + PrimaryButtonRow(enabled = enabled, onClicked = onClicked) { + Icon( + painter = painterResource(id = iconResId), + contentDescription = null, + ) + Spacer(modifier = Modifier.width(dimensionResource(R.dimen.spacing8))) + Text(text = text) + } +} + +/** + * Primary button with an icon at the end of the layout + * + * @param text button text + * @param iconResId button icon res id + * @param enabled controls the enabled state of the button + * @param onClicked the lambda to be invoked when this button is pressed + * + * @see Figma component + */ +@Composable +fun PrimaryEndIconButton(text: String, @DrawableRes iconResId: Int, enabled: Boolean = true, onClicked: () -> Unit) { + PrimaryButtonRow(enabled = enabled, onClicked = onClicked) { + Text(text = text) + Spacer(modifier = Modifier.width(dimensionResource(R.dimen.spacing8))) + Icon( + painter = painterResource(id = iconResId), + contentDescription = null, + ) + } +} + +@Composable +private fun PrimaryButtonRow(enabled: Boolean, onClicked: () -> Unit, content: @Composable (RowScope.() -> Unit)) { + Button( + onClick = onClicked, + modifier = Modifier + .fillMaxWidth() + .height(dimensionResource(R.dimen.size48)), + enabled = enabled, + shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius12)), + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.buttonColor(type = ButtonColorType.PRIMARY), + contentColor = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY2), + disabledBackgroundColor = MaterialTheme.colors.buttonColor(type = ButtonColorType.DISABLED), + disabledContentColor = MaterialTheme.colors.textColor(type = TextColorType.DISABLED), + ), + content = content, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt new file mode 100644 index 0000000000..57a47dff46 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt @@ -0,0 +1,79 @@ +package com.tangem.core.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TextColorType +import com.tangem.core.ui.res.textColor + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_SimpleInfoCard_InLightTheme() { + TangemTheme(isDarkTheme = false) { + SmallInfoCard(startText = "Balance", endText = "0.4405434 BTC") + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_SimpleInfoCard_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + SmallInfoCard(startText = "Balance", endText = "0.4405434 BTC") + } +} + +/** + * Small card with text information attached to the edges + * + * @param startText text information attached to the left edge + * @param endText text information attached to the right edge + * + * @see Figma component + */ +@Composable +fun SmallInfoCard(startText: String, endText: String) { + Surface( + shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius12)), + color = MaterialTheme.colors.secondary, + elevation = dimensionResource(id = R.dimen.elevation2), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(dimensionResource(R.dimen.size48)) + .padding( + horizontal = dimensionResource(id = R.dimen.spacing16), + vertical = dimensionResource(id = R.dimen.spacing12), + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = startText, + color = MaterialTheme.colors.textColor(type = TextColorType.TERTIARY), + maxLines = 1, + style = MaterialTheme.typography.subtitle2 + ) + Text( + text = endText, + color = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY1), + maxLines = 1, + style = MaterialTheme.typography.body2 + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/ButtonColor.kt b/core/ui/src/main/java/com/tangem/core/ui/res/ButtonColor.kt new file mode 100644 index 0000000000..cb6607c8a3 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/ButtonColor.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.res + +import androidx.compose.material.Colors +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +enum class ButtonColorType(val lightColor: Color, val darkColor: Color) { + PRIMARY(lightColor = Dark6, darkColor = Light1), + SECONDARY(lightColor = Light2, darkColor = Dark5), + DISABLED(lightColor = Light2, darkColor = Dark6), + POSITIVE(lightColor = Meadow, darkColor = Meadow), + POSITIVE_DISABLED(lightColor = MagicMint, darkColor = DarkGreen) +} + +@Composable +fun Colors.buttonColor(type: ButtonColorType): Color = if (isSystemInDarkTheme) type.darkColor else type.lightColor \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/Color.kt b/core/ui/src/main/java/com/tangem/core/ui/res/Color.kt new file mode 100644 index 0000000000..4d37ecee45 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/Color.kt @@ -0,0 +1,45 @@ +package com.tangem.core.ui.res + +import androidx.compose.ui.graphics.Color + +// region Base +val Black = Color(0xFF000000) +val White = Color(0xFFFFFFFF) +// endregion Base + +// region Dark +val Dark1 = Color(0xFF919191) +val Dark2 = Color(0xFF656565) +val Dark3 = Color(0xFF494949) +val Dark4 = Color(0xFF3B3B3B) +val Dark5 = Color(0xFF303030) +val Dark6 = Color(0xFF1E1E1E) +// endregion Dark + +// region Light +val Light1 = Color(0xFFF5F5F5) +val Light2 = Color(0xFFEBEBEB) +val Light3 = Color(0xFFD3D3D3) +val Light4 = Color(0xFFC9C9C9) +val Light5 = Color(0xFFB0B0B0) +// endregion Light + +// region Green +val Meadow = Color(0xFF1ACE80) +val MagicMint = Color(0xFFA3EBCC) +val DarkGreen = Color(0xFF06311F) +// endregion Green + +// region Blue +val Cobalt = Color(0xFF0029FF) +val Azure = Color(0xFF007AFF) +// endregion Blue + +// region Red +val Amaranth = Color(0xFFDE1010) +val Flamingo = Color(0xFFED7979) +// endregion Red + +// region Yellow +val Tangerine = Color(0xFFFFB71B) +// endregion Yellow \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt new file mode 100644 index 0000000000..2e065ba6ee --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -0,0 +1,46 @@ +package com.tangem.core.ui.res + +import androidx.compose.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable + +internal val isSystemInDarkTheme: Boolean = false // TODO: use isSystemInDarkTheme() for automatic color detection + +@Composable +fun TangemTheme( + isDarkTheme: Boolean = isSystemInDarkTheme, + content: @Composable () -> Unit, +) { + MaterialTheme( + colors = if (isDarkTheme) DarkColors else LightColors, + typography = TangemTypography, + content = content, + ) +} + +private val LightColors = lightColors( + primary = Light1, + primaryVariant = Light1, + secondary = White, + background = Light1, + surface = White, + error = Amaranth, + onPrimary = Dark6, + onSecondary = Dark6, + onBackground = Dark6, + onSurface = Dark6, +) + +private val DarkColors = darkColors( + primary = Black, + primaryVariant = Black, + secondary = Dark6, + background = Black, + surface = Dark6, + error = Amaranth, + onPrimary = White, + onSecondary = White, + onBackground = White, + onSurface = White, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt new file mode 100644 index 0000000000..89ec283924 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography.kt @@ -0,0 +1,82 @@ +package com.tangem.core.ui.res + +import androidx.compose.material.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.ExperimentalUnitApi +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.sp +import com.tangem.core.ui.R + +private val RobotoFamily = FontFamily( + Font(R.font.roboto_regular, FontWeight.Normal), + Font(R.font.roboto_medium, FontWeight.Medium), +) + +@OptIn(ExperimentalUnitApi::class) +val TangemTypography = Typography( + defaultFontFamily = RobotoFamily, + h1 = TextStyle( + fontSize = 34.sp, + fontWeight = FontWeight.Normal, + letterSpacing = TextUnit(0f, TextUnitType.Sp), + lineHeight = TextUnit(44f, TextUnitType.Sp), + ), + h2 = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Medium, + letterSpacing = TextUnit(0.18f, TextUnitType.Sp), + lineHeight = TextUnit(32f, TextUnitType.Sp), + ), + h3 = TextStyle( + fontSize = 20.sp, + fontWeight = FontWeight.Medium, + letterSpacing = TextUnit(0.15f, TextUnitType.Sp), + lineHeight = TextUnit(24f, TextUnitType.Sp), + ), + subtitle1 = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + letterSpacing = TextUnit(0.15f, TextUnitType.Sp), + lineHeight = TextUnit(24f, TextUnitType.Sp), + ), + subtitle2 = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + letterSpacing = TextUnit(0.5f, TextUnitType.Sp), + lineHeight = TextUnit(24f, TextUnitType.Sp), + ), + body1 = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Normal, + letterSpacing = TextUnit(0.5f, TextUnitType.Sp), + lineHeight = TextUnit(24f, TextUnitType.Sp), + ), + body2 = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + letterSpacing = TextUnit(0.25f, TextUnitType.Sp), + lineHeight = TextUnit(20f, TextUnitType.Sp), + ), + button = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + letterSpacing = TextUnit(0.1f, TextUnitType.Sp), + lineHeight = TextUnit(16f, TextUnitType.Sp), + ), + caption = TextStyle( + fontSize = 12.sp, + fontWeight = FontWeight.Normal, + letterSpacing = TextUnit(0.4f, TextUnitType.Sp), + lineHeight = TextUnit(16f, TextUnitType.Sp), + ), + overline = TextStyle( + fontSize = 10.sp, + fontWeight = FontWeight.Medium, + letterSpacing = TextUnit(1.5f, TextUnitType.Sp), + lineHeight = TextUnit(16f, TextUnitType.Sp), + ), +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TextColor.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TextColor.kt new file mode 100644 index 0000000000..44f94e1157 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TextColor.kt @@ -0,0 +1,20 @@ +package com.tangem.core.ui.res + +import androidx.compose.material.Colors +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +enum class TextColorType(val lightColor: Color, val darkColor: Color) { + PRIMARY1(lightColor = Dark6, darkColor = White), + PRIMARY2(lightColor = White, darkColor = Dark6), + SECONDARY(lightColor = Dark2, darkColor = Light5), + TERTIARY(lightColor = Dark1, darkColor = Dark1), + DISABLED(lightColor = Light4, darkColor = Dark3), + ACCENT(lightColor = Meadow, darkColor = Meadow), + WARNING(lightColor = Amaranth, darkColor = Amaranth), + ATTENTION(lightColor = Tangerine, darkColor = Tangerine), + CONSTANT_WHITE(lightColor = White, darkColor = White) +} + +@Composable +fun Colors.textColor(type: TextColorType): Color = if (isSystemInDarkTheme) type.darkColor else type.lightColor \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_back.xml b/core/ui/src/main/res/drawable/ic_back.xml similarity index 100% rename from app/src/main/res/drawable/ic_back.xml rename to core/ui/src/main/res/drawable/ic_back.xml diff --git a/app/src/main/res/drawable/ic_tangem.xml b/core/ui/src/main/res/drawable/ic_tangem.xml similarity index 100% rename from app/src/main/res/drawable/ic_tangem.xml rename to core/ui/src/main/res/drawable/ic_tangem.xml diff --git a/core/ui/src/main/res/drawable/ill_businessman_3d.webp b/core/ui/src/main/res/drawable/ill_businessman_3d.webp new file mode 100644 index 0000000000000000000000000000000000000000..a018c1b5bb6bfd447600ab7422a74239170983c3 GIT binary patch literal 76206 zcmV)`Kz_ecNk&GdDFFaiMM6+kP&il$0000G0000V1pprd06|PpNb+|801fcR|Nknv zjqJVN-*XC@NiAl{vSnt7<0KAqGRa^j!*gMI_qWV<;oikdb7`4}DKpuf3A5uE;+WZz zEXG!2b-_8`_aEJo1=XjjOa1sIA|`+z`u~Ui|DpeX=>H%3|A+qnq5psA{~!AQhyMSe z|9|NJANv1?{{Nx>f9U@o`u~Ui|Dpf?|6>;zNXpf|zH%u|g!8P~tS{CZ5d>b-N8k{X za-}jfxnbSt_+Ve3i1o#pb7xPVJyowavS7e#^$1I$GPwTwo37i~!b^cLFf_So>y8P( z`NFfWPS9l$11z4;w)=a0U3q@G(|GY26c#?rn|J_ycG5^M{^@&aK6M|oeSxB+PWfYd2bHiU9`L|zd$ZN+)0!JIOG#wZnTQ|9J!-mQA{UNff^Q2^p%PmYG z=ixv7#=kre>@{E{k=q7B-{`tc8#is-ynduaV#;B?=Rq4gGib|L;%6AlqxW zNH}5`+PZn$wyl!`Ve$Og>DgMnl|@G{BP0oo16QgriQ^rA_V>Rwk04;L#UhadqdRZB z^}5m4xpT7%^(;E(dCX(9Es(`hu~b~aGAQi(OY4t5_u{LkGLVRCt4NM8bltrVY^hJp zH*&{3&tr^;Wd#VtRVz#qM{nM}ckkrvlaD=j3JBP1q)3ji@%H!Muz0Q>W1h!o1eQR{ zv_qNTkX9?>J8!#f)4}h2_w@_~>@`m$a>C@@_ia6Yaxvz46j+G%D?ort<-Uo1H{Ota z=ZV)FC}6K)B9Ws_ciy+-{8Se6XaLc^g#gQ8Y;=74&D*jceEW?$LG~IYQVyfHKX~2Q zv(1=CV*)Fngt62=wsFV4o!L|0eYJ&TuRRjT>AUgWH`mTJV;&6-te8Sra^L8dZToi5 zJoebJ94W3TB1MOtcixh`xe#MCIIz-+rJ;X({jTj}ryhNImOxxHB#=|S>A|hDQ_iEo zft6Xv5GJmx+7 zs8@9}R_dD|r#I-y^G}nJ<_lag6W3WUq6h?$~dvDsEJofF= zF%Z}4kj-`XZKydqT38fD8G(m(-ZEKv<%#1C_F5b=+V##YNh=x$j3|%-3w!vcyT^F; zd#4EYS{uUbx_@W66=OtL6iyi_sV_6z7V8a?!3{&s9*;Qudot(=;8OAdhFSA!fRXzYx4G6Mu{;9i$W}{#M<=X z+h2P0!yqG$^{iR}N8zOO%h9=sNXT$;G!gL#ZW7DZe*5QiRq=S$x{ z+yJjRAyw8tc#oY2BMQ2(h}VBA{?Thq@Y)h`DBbqcI1j<1z{|+PpT76EzrT>~v+nh~LO?_a=_>_7)7 zBlGF|fAtyg+7NR7qkqC#8Z6Nf%82~_zy0fY z{W#ER%1+4M^>_aE;~`tiA!%*;d-W_efzDGnglxy(`}Hp-`PvO3TmKImSu~;}m6fpP zFaO4uLtLvtap)h_Guxs=l?nNlfA(9S3vq1*#qhTmvS^ErRVKs@|MQoAE5tP!;Irql zXp0V3MsfY0{o2=)d<}*W-?O(KZPC%nDDMB+&wn?`*IbZyf3$8aI$RmW+kg5$KA+@k zEXd)P&P6-W@ye)r{ZITKCqi6ffqRA;Hl47Hs<++wFEe>91;xqt%|@e62$?YSk?>zw zQ_;L9L|b&mvXTv-`1bRPYbS`Ew=70Pr!0r`y0`zJNTklD^cEX z^Fow6C>gfA=kb^2H4@@Ypotez!?ztCP`*1vs zQ61UReVf0Nt$CmkM>=%D=1s57$ZH%hLdOQ^hSGCpjYAK2at_o98>n4>e>Mx(6GOm^7E%dBar=CkFS0-byCwxKub@)`#C z@%gt`td0%$ZJ$2FS_TgD-Y1Zy0+`MuIz zt#z)s)Wbsz=VtJl2AJ0;$K0XGPIsR_%NmDbY&NPx19{KfDXncFSMNF( z>D;xYem-B@0Ka2AvpY6H_*w%Obf3?aNYU{>H^?( z^9|M>l!tfb#&iL#iMi$)1ZdMn=Yj6vXl*S5d-r`nP44=?aR9TXC%cG`iV&P4pb+di!P$I)&TwMOgGW2IpFFb09IGQVT}RG{dVXkF{t)R6A$`#y2fQ&T;MJ3Q(Y^L&>0;uXPLn{?)4D{t)hUQuWQeTF;2S$xF z;I#x$A6>>qDX_L+s1QR}p_QyLAgUH!#YSn30Yb%e8Pc@|RCNs9h0BelWmKzY@Zi`6WeW0gV4n%BFd0=GS(6-8kq->jum8zkVl={n~q2Ek5UTYjXe?E&# zWYc|3={7j@Fa>Zl^bKzr-&NUQpPhfRdAhNfIocO-VM*0$b+WW~U{k4lwDH*dkr+kd z*kw`bEdC=Z*9G?xM?DU}9F8kvc_av(awo!jr*HIW0v3?wOl z!H7$`L=twiQFX^nH{Ck%xig<@wE~vABy=B|rJf^U3?n1EE9u;cbBnn_ES0%pWmz`v z)BgVT$4?#2K)5@ir0b9d++!p=!lsS4ud4v$xkLzHgD#Jj7-1yY(F|1HGjRPYFTHji z1$Rj((QQZqy~IMf;hxF67Mrim*cOS5L0qW<1O_3+7%LMu-I&Gyls7{X-3?K|>MFEC zZxJ$v9o4-VV2lLjDiw-23~b(<^C&IJbVU=oiAK$&mnb2#wr>5Yjpt4`S`mVRZ9rFn zz~DgQXx=^X%P;^$H0vX|+-*FIJ6X1dsv5)hPoA<8o=;Ky9v8 zN4V=jLYJ|T_6i}KvuVrE4Gcksf`qZeRV)K0L0R0p<1c;pk!Pk-$L>eyDi}3NJwm|| zT|ad1^mC_e5*ZtTxT<9Ui$DzNEjPV9_mYC$?dUo-(;gyj7`ksBN0VBHC0Dz^0703` zH{Ja5?8{1eEaYa=6BKAw#vgd#d^5}P5CXL2f2JKaw5&$z8utC(nXPwh3)U7%Pc+(C;4l z4<}EAkasieAa@aXJeGR7-T8k#YWoe6Ug8bo3=pS zN)oV88xqC}dFiqrTOXT} z>0&g~gl=N1Rq5@KvLwCv#wj8wF>Op30$FKv%a+Hckh>P9k*3{+EbZk9YI*(6U;M#T zBP$_Wh&C=`QpbJ!&NrVV*j)PU;qC-(H3uZRgjP>a;?~62Pi(=hB(*`P-63N_j5`N*PCYK!-HDJb3>kF^WEK^0 zPmYw8NB8YJP|uax0uU>t3=hn{6it^>+5Ct_m(Xag66nnpd1>sr>oZzQK&*(u$TDx6 zeZjFCA%U}*ak`1~X}V18v5|7G+ws{~vn;FHLadmw1JQfRV@Hn$VwWLA1CcJnxk^9) zA@-YTchePa@vEk=;;#QK{ zLaeN^LtyZRsp*-P>@Ud=!`Pgi3vuvO% zDTY4&TMc?)1Fhj3CnpzU$$(gag#os2e{RNf3l-Phmqnvxx{@-A51)E5#2y$FOGA}v zjADiF@1JSWJ>cfKXp1hUtbFXAugE+7WkqB zpyk|ULH;vm$XiJ;LMxVVwQo2%NS9D?U|k+f6rk>dg@q-yHBWULWC?7rxr8EsO*d6? z+}$@i6_a2Ku~G}$()#jIggX#jjIR(_C!4sC4<}e%2k`7O8+MlzhsKX|5rnXfK_Tfc z4<>`9N*YRzS=RJiYrfg2M}vR_+uadlkN5REm&{6UF;G5Fmr$&$MaATcuf2b-6S|Lj z?eWKM`-y(Vu=Ob2L}4(1LOHD@<)jppI*&2Nx)-xNTe4J`E~zd3!WIA-t#H8geNW?# zLRu#J;=&T+%L8MzT;|t~Kk$&PZiD4V9^>SFLlT=#bPEDF8p?y)MsFG3*Efdht(nF` zGuA^|Dp$fpI*g{W*JfWoar|^MN0Nx$4-qR>s+K5*<u0*90o)#|4H>jrL^+_2F4%FN?a=WGb>a#-oQ zb%&8DgrXXn&N~BPBlVRAM=Hamev&A_LNT9jEzC5gTTKArOC%51ay-1(PK>$@7QX(^ z|LSkAPsQkT$0sl(qXQeq`lc6;H|Clc5P6pbU=>zo&oQsRL*0d9+Xv7z{E9Fv1{YVQd_d z$#r)PZ9Mbz;lsHD*xdeVY{JO!w!VR!i&qG$qyweF;l92&y>M!( zWQt#XZHpE(AJFBJTqeKE>XcjLkp8ghpg9O0u|erz5) z5|kJtF0%lE0fdyB$#p-v%SZq9>}-(bE(5S<4lDo_16iiP3%fG0&*8;Ig9MhjXnDy6 z1-6r+v0dny+MfP?CP|adT*RcjZ=zY7&tgEBt5dQK+sAgFI`d`=gu4w7 z)L1Htq7>yK7cvh{d^8bcC@5e9x(Kwy$cSJ&I@!4EF8Hnad8Kg~_{5P+1iGXSX{$pE zYLzPwUSEUQ3Q8~*x?%(v84+THyGP&s`s**8Lo!`NIabO$XOSXC1tE6~;DRQ_d|UPB zP8~WQp=4tdxTFGMEJPBc*}36^{=M_ZLWm%5xGA^kqNZ6#CS}Rqy>EN^a3fa|Y$L=K zE|zr=&PtVAZ!!Pg4vO6aa4D?I0hz*Rn*uL;r1G{pF^3?VD^Vzi$^IMt?BaRJAa0+D zQQZ><#m)?eiFNyGXsF$Y}sNS!4mEo1bg?oJdX-SML@P_MnYi=&ac1zqmx;d zB|#XW%cCX6BytufHvOHW-+%N_u+0rqkzPkB_xm<~Y&OrblCXuY5`n=Yb;R$V_{{Ua zGPjT@?-l^n3eEyWQHD$oSVc@wD)$Y{R!lA|gSY}^kc9)2X=$-GCtTh-Z+B4?-GRwe z%Hwz`;wqN`gmJmNzHe@REEToRz0mE<^kPE`H zb(cjE7xs?6iNlz{)hU2IW3`!M8jaCX_e6Q;B|Ngdlq_O`Syr@15!33g`y4^0?FRBtx_SL$&G`WC^o0Ay8qow?2#l7zRtMpNCC5_r zPjAC{5^SNXKo~1TYyW@u`1ejmtS-t4h1fwUXk zQ0(xK(=;a{3gu!8Fonwl{jC8DWDr-gHJRk+Bce;{U=9-8Imv{uSvZPiMH`g?WY8uw zJBM(G2YsW{SfD~6OfZM$z$#pd?|R2cS}NN@T%E!=C=1p1Y?_XAPo406Oy?YEt(*9{ z7FfXu(N+bs(AFoHronBh0V?|JEtZOvUzjMJKDn_J zqAAu9Hh<(rKn8I&3u6T+(>+0_z9%|oDQ@|Z*A}v@Vhho>P3nB#mWlii6ztAU@-hXo z-l!5(v@q4p=K*Bepo6jKnp(5SofFWvWsVG)HZOa$Txpy^bagZ0Q?+<4hoSaBiicFesvh!=r>GQmAD%RUt~IJqjbbr$(I< z!QMZFTS^Rw_8;=FwSV-$8L5-Ynx#@9Fv1C(dvqU|f`#uI#SWs48g)$&r#dA$uk<}I zfn*EO?qxt?$3xE@oXdnew&EPjR_Nl3)14AH2@@5NSxHAm&o&!~&VAwB z#6ZAUE1qIZ^oJD`au+MdQsveWq-ao^kh?4KB<_q7$9wKRNK0i~D}k^RxBAB3x$pm- z4tB@pR+3O4k5nk!!j($XfK2<8yC@EG^PQ5F3=B*mv!V)y2IsOx2orSt5y1`#{|ji_?J)IZvv4_S6YjS~08ucW!=a@tlx5GH5QMVXT$S0%Fji zg`tY<;x#;c28(XJIf7y*B*lT<(=)A@U{*>Q6kSU8^9(YbxS_QWGz8X4W`S(!U=4-h zWtMS3T{^u<=#22dt{L)dB|%2yrSy6~hj3>WT8){7y>?)ww~{>j!WCiN#5|HwTj`pbq;`nd0ZGxfq(eqB zT3JLMHxAx;_({p`#DLo31j4N7CS(is#SK^#Y+)rRrwyx10-Vfn2ZYRI%UG=X{|q+=;aS&>ar>6^{y%pKvkhOZx=h!#dGkDR6He>GPCdBw=%b(A4A#n`n3uz;m;dGFkc6D#p4a7SLOV9}-)IVVW-@cLmA*k6tt19TPV_GhiVzE&b4cmVJMr&TIwF}w3V^&KEww0B z=IPjTg*Of~vLA64f-!1)1|eE$dbaUrwh)p$Qq&FkLY9w|t#%Yccf%p-%_FJkgyp{e zAdp$fHxC_Xb!^K%Q@=T_VzFXIg;wLqh4rayQQ(Ce&4FOlhOmsf7g&68EYJzdq@~QP z^!@34#2uT(*N=QO9gi`{7_~Jc1ewb73x9kFrFo>_EzFNraJw;DMpn1Nayb5jk~$*f zN-1Y$7mW0u&Z9at6I6@8w*CFW4hE}z86i8mQakhX)7K9sF%!kkOrPIe0kGPMFfzed zU5ce&*dDl~LKo5!U&y$iL8(+H63~hT^i}4a(ZMUrAMd}Dr7|m^N%HBVM=GR53cg%h z94aqq&lX)u%i-|%2Xtwc)e?Eh1x=Apl8hC1o3Yhy(AhKJzU%fNs|x`%e|vop7Y=wSOZ(Evq`z7jN{7?_ zB;>K>R+dfGPiE7Li_t8VYzlRnwjc(yV#!nzyWY`(%%L}Tmp8>2XyYchb@)hMKtx0b zDDpU!ZMMK_H*<66Ui?@;SX~Fp;nCxX00bDLBpppAN&}TjSuy5$-k{+uS|JdW^o4$c zJ<~Yfn4NFhLg3;}ft&~p8?NXBCfAk%&=#PP-F#|epXrvRLs z-%I-y%*+hwkIsB_EMRpLEaL3f76BoQp)cK$Y$#P)T%<8?St^&x!{zbHPzWb$$D6YT zL_sQs3XYwg399A4xbxH7w`G=sF`%o?Krj)7kTw4EiB~q1(j+q-qYW?B`UiGKOKu+~ z%)FK+iTIc6-ZvPqx(61q{*{vwAx4u<-o1bS`tsEI#b(T-0fCUvTQJ68AZ#4If89It z)*}bMxsXA?E6sTER@W&@yixF6&-oBif-Mz*P2Bnr@z~$aI`Wyiw`z>yM}{EOlU} zIX?;}PhS4VKl}C|S=|5&`;{-115Wz(jE|pcooX2bAanU;mX&M_!vp(Dm4oNcwuEqT z0b-ttmEO2J5DDZ@J=K@~gwmJ?Cm4gS3IoB|A`!=Gzb7Ch(e5}5kCxVtjmDT@)CLrt zgrm(9fe7Tu%m3)p5BDipU4W&BzA%)8NTu@5?c1B+45SpyQe7Sd7!j~-HV=HDJox0n z*E1AcY+6}45k@OnbQD+}9~i--v)|tO@VZGWQ;>|2(G}z+!o(nn{LR;1ooj6#3Xq#l zG?1To^v3;QgmrZp0TzF-Q6VfLPmcUgKYve3Vs!;-UrdtVUF+YUyl{B2nM(=SAg)*e zMna1A=!Uy?pIG?)n1v)SEQUs-OrOD8sVrKVO?S`&j?TW;f9v2N7;#k_A@b>yr|UTi zKxcwne{thx+UqkFBn*%z=l|kIKUi^Mb$W*8V~5JYOb*os`AZvvrm08iV=+Gs=K;Y$9|MR+uAKSRe#3%_112VcSUQQUJAemQAfA0M0@qrS- zkxn%d&OTeOy?Z<(HAYvD5ptd+PtTqoP&CWu$?<=5?~n93u{u1Iz4BxzZ@Kd)YRxRm zq*TVBD^Fl>f}D#_Zu{g9e)ILIAmc(}s4d!)7_DUWED5+fu&i2~$?{fZq+AXH42Wf| z%K=^j0?57CZ00#e0@)o4mKJJ@>&gih?eTOA1z(6foc-Ug|BOhij?N*T`))Smb-_)W z9FVzcWe^w`WCATN%*)6{1eC{|3M+h!!E_O$1Ag|IFCYEbx(^QwfQbl!1zsYf<&3t7Fzx&}VV0Cbo z!w+84N4NZVgP4i3ZHKD|fkBDMlWlkX^b<$^U=|@45jkeF&CNzDm(g5(kglU>^5VkD zp}`%M2?53|FW#~NSq2HP>y6pd3$t23uwhtWBRXB>*)z3w3eygc9@$%wFOC>a{o~Kv zo26Kt8|1G)b<^9jS|i3F+No@`Zz3)}Yatg9WM=Ylys|B1g9vvKA}G|eFD*PfIeGK= zE&@O_AY@_T<%E$Tf&u{qac<`1n`e#(Kng^g4%Z6J=GTt=2@=NyV{{c5Asg}Zvl~gB zT^uny_iz6E?mW=BJ4AnL=1*L2h^-_63tepjTO`U99@v-Fejlr#;4_Qc!CJ9^*@blk zcM||dNqPOqu^-IE;XUiuuP^rtFv}VY2!Q|un8k&usrlC%r~5{CPWG1qM4L|74&lwl zH@3eYMG}K`r5PccSUU5AQZf`;;;qK;)bIW6K4W!in4|o|gGcJQl3+uiEec~&NBpjV z?Js@X1Y+?3jx}~8ydoJbvp7cA(FnOYeg62o%NvJ=2FoQby;wfBnhSHybIrLDOA`#8 z5te4^ulC=`QkknZDl`rRLNISVw!ZcBU$(G1Gs}(VHvjiiZWpkDNR7jIL~UW>DHOKQ%BAps2{Jg ztb&MSbkNZu{JVvN?|(n!N{j(rIYtPRmy_R`c^1s2w9kKG+dJ}vPTgvL^221Ua=@0_ zhyW{~Y~V+?-}(Q1JtAB*qIiAorlf2etxN$9oU7vQ17Kkxz^yo+*IKPc%!QCdFp-F{ zD1~adR0)GZONuOv+WeV*(4_&t@z{sgMOmF0&H8W>gD@c4b1aYb z4ZK;y6imxBVjlXjSdopK$E-w`(gq~#>DKGD!!y%U%|%EVU<}A~OOLD$*e4_as$u1U1 zkvIA_V_~f{7DkKHeWf6~i-Cb9GH%4?lH55LE3wqFO~-EO=#jIHPbuVzM(=JuZ+AGdQ7WQ>e1l|hJTCuv^$*P!OL>CGpo*cSfimbT8Se$=-CxO`QV43CRi;-Dsx`LL@H;?ph zFPDuGSE?c4+EJFh9MF2{&G!sM(UF^A+P>`V&gbR=rf{B5hd^F&mbDh^gCKW5E@&`a z%jhIL`SL%x^_HL9mJl5ifrXJl7hz;TU`Hy_t0(_n69pW(tk(4Fm79gtX$fK~OKnY9 z5{&<&SH5xJObB_Au;G!Ux@l8%SZRz%;MlX{*z}58_W9|#*3Qz1zy=VP)BqCH>!(_+ zt^L9lmpi_4_MW~dI&rZ=8V&3`b1p`?K!Av?>45>Gl?IHMB`MK6nv>9c>ie(G|Kjj1 z2y&3X0$+r%FxJxS#H&x9TR>^dx$F+puZ`^%R>$SUsO`yC$erBtkw188(UOZqP;2_> zUAsUStsoXy&DT;O_Kbinbhi2RSl_aFPpJwfVwM$Nf)GOHeC?&$@pZc%XxmQzQ-ZuTPBkCq`H+$5N~L${12FNfctLv1H5cN*TZx!dk{6 z2w-~Q<=U}6K;&0OICf;)CS-M7fLIZO^YYN{$!{(S7YZ}~`uMuOBqCN;8CtK5B5>~r z1_v>B=;fCWWjC+ew_$WF(Q+2cJ8vCdeDUl{&A4?uO%g|Qg}U`@>2}kJixp59D{;Je z=WOFK1-n2HTC>NG-7^*yymA=XNm!^qvmVo9Y6Nk9c5b#bSRLp~%Us}Mb86Xqt>y?Q zF;@&6nBP5Uby^lHsO*VKt2rZ4AV6-`7stx1z-nf51K=JLh@c=&%zo|Y56;XDSFRh` zINUdqRAXMx&NYtB96vi(V%ebRj4Rigd3|&b9k^BqVT9p-3jZjpOD+%*9)IG4n})G4 zS_zDh^Pu00D2cJ>WLYBtd2Hkhv61JQBMM4GRefnWAi!AW%6=i<9Bg%1Vnvl>CD~9v zMWFz-vYFOagIF15069)I0Mms?>5rE|-;;OYyoZdau&5}WxDmxy0>=l{f zXo70luWkOQErC&c0~A$i%|ClEV0vH8MVqT#&1W`j!|GaHFbt2LtRoi(K$#C$H?xu| zk7pYJtM{eNnx}_$C3LZcox<+79iDnd!7dKb(!Z~aTz`G$lEG?c77z(f|L%T5lF@6W zlXL4wx>?IxjEo#crclbYqhn(!7As&3OlQqOFg@35K0mqD>TZcl$=kO7I><#r=IC$V zdDn2r6^v24GC~>U#`E)MAkuqno*vtlSY0gH1lT%yvN?lXAWX>S4jp`dKUjd(o-Du# zt=c!YBHW9mL-kD~bhTxI**bQjbzZ_nB5BPZI`qDN8Dy~9kp;3tXx6^50o03yS}#wo zN8Kzy&eHJVbB_vBBy)u~-#i|E+*v??QTqX5K>u#Oubjk4Pqv>O+hMD_wN!g*CnLxO z!qP(RSaoZujF+@+OO5*L6)C|zSy8X&ds4dE!X)NLX1-sqi9+cJ|7Ky=wtF%sGGMhA zgMeJ=*IpYVaYm1}8ZU1fMcphwHvN^c^V7&h0+1KJKeTDQ5*=vMk!11oOuY}&!~Oim zNn715Ld-||b{sz@3WiMWKW*N(dAvmm#;AQ50m@_b^no+85PP@Gt9@G%y4$iPMsGNJ z)D%n;h|hlc)*aWSS&(Fm+Jn)On3uyVM_<{1a1WQ}Ufny4x?0PZx1K+b3&ygip4~Ar znM4QLupPqjxnqIeZt>`K6Ifj?!RX$_=HYst8+kGaK)YSs-AH+JJ?yV_JHC z?pH>nkoR<%r?+gtbhTw<`qFeR7sbQO&)1nOmoSL7X<1~g^Q|NiJ>J&IbG!P~-9nHl z-Lw1qXHmg`TuGMym4y#&zt4^&27|6HBNQ#AufP7~V<>Un+lAs2`$kZAYx&`!(=Ag# zja>VF+P8C5M2Yt3V9g$xpWmCxrq|ngV$V8T-7OPf!^VTNv4Gf;Yrk3@ykj@c6p%5X ztHVHm7?Z%v_s*Pw;Mn`cYx(Z7=yuDDjnsWg;DP~0aOml?)1Mg{am2|oSEq%DI7wRZ zKPTlaNI5;=q4Darp)MHo^=EO8LTbUxp_#^A1AUHwxY{k{epG<$UNCrK&p1|>3&%9Q zY2XopT|6)-ymi!tBZ4IB^0}XY`CKduD$hUGdT6ykk)TBT+kh;QZv^zN7*X zKwPN?gq$C4zBIIP5)nl&`Lp{gwz^%xZ0JAYR)Mk5*~33LaNXd$`vxJ$l4XQJ7+nf4 z5nz*|EWR-H)j>dH^pHnht8A0#atkw7O6yZnEGS?I%KXUOBV(Jc-@G}IiC!Ww!k0qE zpdiHj^lOI>ZmSHGgB?Xrd6<3qmU7o?Xil;9ZY zMR!IM6%e*TJ?OoRf};T*;F2@ zBCruX>yO;okJaTuW^3ydu>jeCM04@P^ec_S_F#2Od9*rEDU~QGE=Ct75D4VX3PHSf!9QTc|LB0W6j`U<_DXr0Io^I@sJ*qU&u} z(}CK8OwqQNyJ+JJ^~kIFo%^c6>T+SFgY`Oc(Yi+PD|e4a(d9PFU6UR4LgTJ1bhm^d z9j(=_%|h`j?+sYptYt%*=htY#7atmmfo`@Dlt&j9uF;C&&@1ncNmpxGmGb-=Ezp1b zOesp;uF`OA@mj5@FYVkP>1NA7sAMgCZ5CSi<4?!L>Rus3sJ2?>8g29Mesb8Nn=J_C zEW36qdE?Qa%2TV01xPm6Y@7e%&z3E^+ex+Ax^^oj&%E)8Jhi%7681G)m|y+LvPBnL zsGR5c+O6>IsgLD})xEmr+dA{xKR*;@b+hDaxrpKWFZ^r_tYIAMzxkeBQP!G8ZXNyN zpEOuqEa7Xp@ch@d+>-~o*hXC2m7V(1PYxQZJ7qc7HD0*!C=bVg)s+Hg;aV^9!(Vy# z-YB}&F;D0z7kX>P9gef_9{hiVL zmX!p?xFa#AzH;An7F}lp%4=nWd$_d3U_eT$Dko`3%PL357^!E?ENjMSK!BEU$Fca- z;SY=>tIM=(-YkK7xMT^U&C*iRmkz30DfJ~4vY2OaKF&27wLHTtm4va}Szt|ndGLKH zSly_F3ccF_qA{wBjBF`ysf<;sk!IFtwH9JMBq2#Plnx|GWTrVYGjpPOENdEp0vLA= zpw}MR_qHgy(XGXD4;L~RLg?!osf;9pY0AZh5P~Y9s(|bHLTz!rIZc!0h3O1EmroOEPR@0JGE=fg~lAk|a$^iE)~_ zm9?@u88X5h0p{o86Wqhhjb%#z+KIgs0x^qzI^8`_uw*fz)#LLgnh7z_ra z4Sl;dZ@F${-TbK+PkwLyGzKYl23W6tXer8GFrx+g1}gP zLz^cDXX{7m^~@la3e2UIC1bz-gyd$Ii8YBz6Q= z{>@_#ZI`f$lS2&kDn>;`kj?agSDOMv8$<}fEn^D_yZ8lVc)0(e!TqNWoLb1Er64dC zmscQ`3Zf#0fztjByN3E6nSU&Aia;!USd0Jl+{ZUtSVhSphMC5C3aqH-ttw`KLya%ZKm7{qCX6t5l$ zEYn{umr~(ig9Bi>-e_bk5H4l{%M39pIYuCYz+9aI3=qOFLvoB$)D3dMBZectbIZNM z76sTMCe#03A60@?OqQ*OL|R2c#7O@`J02M6H_InO)#2UiZmZ0{{;d~JMiLh^2+rUW zTRuGtWHE%)HsWd&fPutzbh>%Z!}})xzvJJ9AiLlZlLP<$)=wmoRdg(zoc-rP3$0PW zDkQdasEh($B@(vA`fjSsFC0Ig+kgNAPSVNYEopde{yf6POBnZ!?HNBeKbJ*Gn5$i8 zSpjx``IbuS-0Q6W%XEgIkLIy$r1_WRXBh_1VaQ=jZixsj%|4o$v$g)5%?N*AW zykmIVOJ}F^K;a_~&A*-e)nSVQYnPt=OG#Q!pq2GeXwN6BMGASoZ_8hQZN46%1hz0@ zIRgnXkEO}Ka`uhyy%`jXQy`M>S^wwCvyBKP*%oRa0*r%9YjE=C@BOV4UzWhdju`kC zV;`%8B7^y-)lyS3=UeNAi-=K>{n8*tt3WcPbUoA@BM1y2E+~U6Y$i*Ux!IP$#Yh+` zeSKwNM?j`M3!^YJkTP9wfN-HBCO$ryT16(%N^_RBVuhk=sYO$)4hLGh_k1=Zwo(Zr z0CABr79r#gcl_ix55Cq2rWglh=H`#?7{@K8$QEil0$WO*CCT3$IsD2WFDd#~txpu( zW=UW%)!c|&NGv#8Zim`}PhVQ9O9mbN(QqhiO zz`?k>U!~WlKrVK$SS>grO9YsOe7LB9HItVlR)KT$^F;; zPAwKAk~|+7{DpHkLfOWFb}i!&9I1XdEkFLckzDjDq1fP<02b$)8xSreF>rPf1+&^4 z)bib<`wkH)*+N`uVNBw0#C+q}r+w;tmIPseXy3wE!Q|U-9Pa;}MGAkT ztb!xO<>p%DQq=|(58%xP2(R`aduM42m|!ly?2&3IJB27d;L+-BBDRDuE24-)+xybz zYQ?XW791MOE=fuG-p^CZ|pTy~R~ z+x2o*r_jn~?0*Plh?H%il>wPRi1DX}=idBI5~2%S3Qe#Szq$g18kcjkOua&arbrPgxhOl6qjDFgHK z6fKo(AuhWxGNVblIE#u*gwIrQZlzeD6+>78`hWcN8z;^O#o{IxpUi*^ z2ZkmK2+E~%=QfcLS9JpUx`$@x8+pYR;!3rZtglatq7ujzK5_rS^O;g%0xPR*g-F={ z>90Lg%Y}ewRD_15m@+@GkWGj@jjN+;&?H*WkOEN38 z(d6WjT81d>a>2os){>>Ol?pP&Q`BlBt2`q2wkMB)mTF-pc8vqdE+bNYe5e3~q) z7(1*y!cN^9SoewB|He7FYel)qQp1atfziGQs7RQ!d1mTtLR{5BW>7|B1w*8m ziio&v@zi1rtn9*r1L73I#Vo{vV^x|4EYWhKT&7scKx@8SP2^P_fq(uqc`GDF=!#P6 zte7B2ZT!oxkTom9mR2Y$iPjvU~hacW@F2mK5k1>Kg1ab6+fsD(*(viA8 zH>*Wd2znG4(Tg4*P49!2s*ag7qzHy}dRmERbuoLY-azdF3n;^+W(hXb#6mp>FZ}%% zjuVoE?3k89;GUnZghc35*VD5;&S(=FLvxMT_~JwN(S(z%2d-Qd z*Pg58Y3v07B_urp^|7^^ZD7l3eHR3dR#E-$h(MEnuaeD_^|uUKjRHb~;-X50h_MQ- zBS2i-aJAasGPb=FV1@&Ud(ZuwqFD9@F65KxQSjXF4@WkQul5DlQ)PIc2NX&q`S4ND zj1mZyl8yN3k*q&?W{|(Spx~%^P+u!$#j>g|S$SA<@m&taN{FMvS=(<9^%ZF25`WhP zSi-ahQq2IRN@nlbNjtPj81c*S7*qE$G0Hns*E`?PGBJu6MuQWY6;VxbktIQ5(H$DI z7?tkqw51JCkJ6}W!{Wb>9~W4#&O_9isnBSnP-G9Q74_R*P+w|`g=wNC7 z{LNESzc898s)eV*mjnxq8`WR=L+_FpG87(5G7)&|(B93%px=_zajl+pV;YxRp7zL7 z2mwYNWQAHc%8>jh9m1-$C-$Y`75{}BcHkUs1p`TOL}_cZVq>P!xL+Ezzc$uTeXIt_ zK;jxoGQXr&+DuY4WZwYUbVUVdI-J9`*n<|SPj35jW9x5j;`-0Ooc`b`{m>RYSlcWE*h{hu;28HUG;~-)bOwedM4fObQBbJIA!tVrj&cw5tQ=YAmmH+FeyuD z7nR~k>;Kwuv^+ueX{Djla^9tnp=scmXA=nnG8kD*uw%9>y8Z0-WP{q}3(s-7M3{+X zuN35(5CWmA1#7YCsxA{KA%9kR;TO7lI8r9Mn20<>B8FSts!L%hP%KZ$mpJ1R|LMoU zZGhvLP&3Pp0Jo#6?;#UTYYxMnpjXO5VGQ=OAd5IYe@E-Xk}}yeqHy{$i>T1F3N zX2UPJ(0z@NFvBL7c*UMlj{%PsQ5g?^=4C3w{D^IFw`R&#{1 zaaKnz^ENqU%{$Y;!OQ}5^C;twdD`->z9Mg&YEB9To5%hexE|eXGjRSj+mCB8k?U~aE8iO0h`|&)8^p0TN~T*b0=LhA zfCVok?8!x?idF3A1&FQC=)=<7G0)AQ*GxE+9J=Qw$0dMwv#M(;#e%m4rcvzlthiv1 zm-kF0xJnX$FWv*%6v>tcL`FN@=Gu?o88z6@6EHn4UO!_L5*1ye;$CsQ&j9zcEO8}G zthzQaSQ>wG{r8P~DiDsK3{>Qq7W^IIMzExubQN4#C=ePh`tG5?beAI}f7MkquE9pkad2g74&Qq(%EMbD`~Bh0P? zOINS;A`oaw@|pZ+*%Gw~5gETu1iNv|(&-34JJ$q*ck zw#cK0q3I(BN~_VgAdC%T+UKGwv0+&YUM!OoMlFAONy(PkyGVW`GsUQTehX9(RVZ{a z2-hQPgam4`3h3KS!ncYXVCA=MsbT@Jwls<2*oLivDwhop5~P$bwh8Fb6E8cWj4{mV z(0KAn9kr!d{W5GRcX9`xCX`WCf!cKOB~BRnIVtngcR!ctYSdTeaQScfI8%92&7atmd`6UwgD37p`S%O7{2g)~hi zfiJ6e*w^%lSfz%`wHr`ITofYs2miM0%*o6PNnzf4n~5UuE%>oWUG{n~2jpgerPah+ zx|EDUvpK%T(o(iHnlP9VWIdo=wBS8@^y#efN9Y8WrCXed))#tkH6-9%17I5|v2Q(n zGALSI5&Tx~->jH}i5KmFsl`ui{R-L8VcXhFZ~Lc9N<0$-7z&Y0hKVB-L2?C^l&@W8 zg`QQZHoeputWiI`oGYA8FZ!k3emYpTRlrk$V7pu7dBRUK8@JqDS<%v?QW>~QYJAizw zoV|R=!#X!JcZEBPflWJo7*09H#?^1fEkvf4RRRyNg?dXQs%z9V95|_j}4E+()Bz!^Vckatqluj*j$!R>kavD@wSX; z3OOpL)!|)|0<8r5KPv7cAR@ZS(x&*#Kq)p0bbo@vLKmS>G$CfpY8HBwHx*#-x%`Z^ zd~!(XoKQYe-%(GN$ybiH77@}*NpYM~Eu%cLQwAMp^IslfCEX-eH9I_(i%lyLfoA zOHul?$(?=_d9LgQBg}XAdbZsRGh_1DTsIhAHT5H*^3gmr2Y740cNKzhRIWI7`Zik< zOO4?*217S^gqw|N6a2#@g(ztQ8{t{G+|X~@z$tsmjJ5C><_Q$fuhjJnr0#-}|L)If zZM1-MAwb`9(|58FLSFaHA$m)t4j|m{71w9)gK)*5s(eSaMf{bZMtP^|dPJ$(5Vg?u z?ss!Di8{HD4jWY+!mxJLeO7K{vn`K4Q$Uh3`=n06HXOJb2R%y1{mlr_^_%msTvh(( zt#5Q=e4G{`5oQkr5(2nl_se`idsuKST|)0>TN#|)wAL-ot^8G<{uTD@g)>A6I4|7wo36zNo zwjq6pQ-IHLBjRo~c3i9m-7Ud~*3oU{EXqu}ByuGf#}4l_<8m4J`c{|8KRprqRRlz} zw8MSO-XMOEIIN^TBLWYjGIc5$ z>enl%QoUHjfF<tys5v#9!qJ%8!13TD$l$ddA@%)HCi}xT*ikb)vgu1` zq0*2VAF$u-`hp=@tTsgA`=R!aTO=m3MpP>uJ&G+=vFq`aO~!Q6)(Z=Pwsd7QNdl@T zk(^%gvPy~NFcJY*dBB@mUtzj`t(;98%Ym1v`>iK>M|B4;N%upT+w-D>3BHwz46=<6 z398U6>9BDt(uF8Up(2g|t$!Uqu%{&>&{M*cI&;^ck4;tCV#_y~0;vD;`UWaz#;4}I zr6D+z@*pFa!6CZt$Ajhtug-_?DTiSUY+y#rQ?OBTKkb;XoF`A)B@?kNS%1Q~-at^=?~{|q z`p>hm(HsA2*=w3I3Oh($DxEC0SAro8W~`D!jCjC&czqNyiC*x+{bp{&4aYXD2`&%s z&8#L;e1E~uL=n|O$hH14q4P4kLOY)K(ns)7M1sEt$Ezah68-yiodoXA|3=GKh#(k5E7UwEjk(3eNzrpYd8z-Hfe)@`A``6tjQ+qZ5Vu<`nX z4voo0^vqRTzNQc&822b24_0E6>7;6`*2#E{RMnlD#-DZoTg)#a6TD7r$i6;2I;bNn!+_ldhmZx;UF)`daQka$$_N7% z=PV?ss190cpKV3*h+XEZG*Y@`(J+Ef!0`r-QI^!ia9N2_oZP@ifVz@ z*rjpC`o;W6D(J99p1g4zyu7O-vUKcTb&|Z;$7V@%RY`bb)_PT;T z4%iyZb@ad>pdftz-h^aY9a%!siYPvD@MQhIYKv`eLPDm)GhxXSe7#-K(xq+jrP5zP zGyB(zF4F3+gMn|hoXi(i5hnWkkD{pf#+{U@I0-C7Z{pB2 zpv1+}k-4rDFeF05i*K_mMn00{o=9*vTWJd6Lzo!lcLcMnvd-U~3BG(F#jcR97mE?Me~@~ZMzxX-DvNwd3CjQ2NylF`@D65xVugYJ z>WrzKD^*&IKS1PB`ChiIX+KyppMQD0#Uqpu2G+#XmX}2F-e1CR$XB-<0Rs*J>9HMm zGe!J7+M6s|D-T^<3k}wq!PFQfl_c1~94`e#C9ER(2*8QCN$tTvqn^Sgi1q0@Mp7;W zn@OaFz*PXtcKl<%%*8Vg<_Z>M5!7o^*mm;%vXZucsqSubUPVWV8I_eA9*ka>+pUly zd4o&b{<3@qmk^-^W=tT0fp+(5>Q9zEhSOSu1-8a(Fxy{Q8`)h;5}`wijTZwg(>uv| zeVzaE?iSAi)BZ*|E;Fu+Bf9T{!bC~HCvhp2FHtG;wr z%W`L)ARL870^$atR>u$rj@5W&%w4J2-g&C?$)Z65BL+Mu?JjAaDJg#IngvI9gU(GH znL?SZ-Wpv^(=X@;UWT?X7Z*F{-nx4P`!@m-LPBD~KaZ)|G=|;|LUK1zEiz-qwG|4U z8c~~Uu0vlegDfU`V7sQ!+PyE%7IuDyb?rac*K5GiXfK&e>-yjo*qa=NUT^ApGR_mOcL(ZZ$HV{X-QuDdl*)X& z%o4BDSjT~fH*AF}>9`3aN~Y?-OXq`;caN$IjkQ3wUu_16z;-=->(f%k)fyF4`QU&h zysJYJ(-$*g%!C~?8gkDk!gEr&c7v-}?hF07v{fTCk z9*<#)i{z}av{2KyUpH!Roi}vKF8dh!4-H#?9w|#Jt3thx-eJAopVMYFP6a9me8wAr z5o@OE%_GnIW+m&OkLGtY+aUD6uNo&^i%{y4tb|j?bByP6B#EK*XR<1tUCWjdEFh-ljUBJ(nISGq@U*S(5#V zD$Vk*F-o(7u8tXtBtcfz402}`%&>Z@P#qQMP3JYDnHhMT+-7H)+ne}{&qvfrL|huO zLgbHB0%=bN!Yi$Mw&rPFcO=IA@_26qTDWP(- zvP?_5(Xod)_Muvw`=`(3-YHdJB4Lc~xmer|0#b@1k`sZ7Rh+@*OChK$nW_|gKX(%L z+lAe3XSj7+@X!e<>C8K$f}IF4o2`p+1dnP z$iCN=1w^Q(V)c#+;_JiT{jV(&N9v;)qD1^apFx z0sDs6gx8{K&0*Vu*~vCPFr@>I?Bk*lho%1MaUol66NZ9~>){6e;O%5W>UyzcK=c}b zNa?qzl8L@Uk94ea{wY9-&kk*9`>a6XC5d*q^)g~GNys=um6F)}u74s-Y&RlF#iRRV z|9Lg}gUQ^R8F~b-wu70HRlXR_jIjG62o|(OF-5n`km9^ao0^7afI=aG1ptF;j}FaI zH_hWW#^#D%N3Xypljz<%suy5c3A!v$i15$Hf4wM5LW~4^&f{hwDFRN@*?uSHRV2X)_DUfRzyC4$h>ex<(*9C<53CW(K2gNUTJD z&^SKFZ=tqCKz*0@7j_+^{6!CMjStxTK|ZUvDrc3>qPW6QR9`*?ljMWiSs zN@jz~)V5{PTLskr$zZ+eGRl^Gz#ct)yfDUF@K0N$px>C~KVPuGwp+uI6AV=g3(WOH zjjOsq>yqXi`>Rzi;XoQj2ClXyS}?c6U!S(Bgtq@kotO)xJtr@0rA}IZ6jK1)kg<U0$oIbzRvMEO;|f^2 zCSqaYtjDMm)z5^85g#^Kd6^-3H)ckB9K|$wjQ#9lI?#{&3OGp7@&%=nw7PVpn7ijD z&wLvIkQIG|ABPKZti|+V7|5;IRD2!`0(h{`cfm3oiu8V@QDib0dGI+uB^Jf%>jtq_ zVjkQYQ;{=QL+1E{sdjCo8m&z zg?n~!zz%UP;~dK+^+xLY6I=ndK?3%g``gdM6vFkq%Smi)JdofyG(lw`U-z1C>~jWb z2Aad*>gPCaJr@e2B=6(y4BzG!Qs9{Z$7-N^$pJA^B*>5A*&J{aFc-bzNliYMd_so=dl=yR>r z04#xm{}Vtn!T~f!bQu`icD27;t@k_zXklpm(jUo&MVb1rv7+t8!)IcHi;ze)dzW^j z?U!lRv1FZcF5no|W15>R_8W>vT`XcNJfmZn`I+y3>Ivskj0v;J0eJOO7++w3gm~19 z|Bgdh&139UE5y@)FHOn33I?9VQJuHOPdn$2<7FTfqiVt-7HcU8TmD0~_Pr|p2mfk> z;~xbK$B*euzzLcb&psTWN$QIL?29=80zWyceNlLhuPjiDkbs!C*74bJI7eICE3zj{ z!sKxN$&e|T=SqlaV;U+N!l{9EE8TeY=kU_U@WsPAdm%r4k=I8&3aC>zsoJ`3XK0Be zb8@y{o3u*Rgl`v4NWcLi8Ip=7{qh>&@#hfP!Po*OiDR&hhCbGbL0qC0cR&LOD>&}%)Z6(9Gk(c&W zkPUEGgQZ1G!Sh;dmH#%~{{}Po8}UHR%Fy?7Ke4L-LDjKs2IqdjwAsFsH_(|NY4QkvgCOexzin;L{{;O7r zb>jrC{WLHtJCP^AJ*kQC_CAh;pU8}}@Qhym<7vzHRCu4PN_mt!;ICp{YA=dbHft10 zvheM`nx!Nc?fAIg&T;eD$62^kva9{%QapFbn|9VR*M^Nel6j!UvdI3!AY99ZjQ7^m zdQAd8V_OY-f#03DTeq)JKeo)WUim+o1e<(ze={AfWW;I#`B+jr#(7=jBZOl2U-P&P zl(PdK(_Ufbsh%c6n{5DbJ_!L6=G&0E-RdAFP>`$uZS@Wy~Pgs$JQL6YgU8K z(T>*)oN^417*d!ZVqe@i8ecMMJ1`p!ZWHNB5jxi^ADNsi$$E_vN_l{k&Byfm|?=(Tx-aSDeFvw3pPA(M=5XN07;E zhkb><4454^;(J@ZP#uL}Rlhx10wwjnf5R8j5;rW+hG5SJUDEZ1u+LIG1g4VS-m0_sUj=qa6LnW! zAia!PR1%r-d!5_A?ME`0%TC^(af~y_Y2p}8-kulbuoQ;q5_ykK8$RD=;&Q*Z!PJH; z(#95`C=lAIm@L&oGCb%yb`0KdOcgFf(zQJz*0zKnE01t*P{;aQ2@GB*-XERx)FA2&`tsw>2f1(gpil4pCRlau$s`?ihgJ+6t);M8 z2&;yDJ+X`?+W7USNS_$+Xy$st^YG2PepJOF51NCUw4H1P@**yZ4s*AMo>N`rAU%4H zt-eB{D4x&h_Ti$KW4pg?O6fheknG7< z8OlRATLHSRYkf}HHT9Hu zjSTgp#1g&vHnF5tLalyNklxJEmraaAB<)X6T{Nvfz%jAyC8n&ugGoV;i;M&u%D~UT z{406Oft|ySCuun{%ENZhnPl*PiYr1RTGhf4^?Fc%Fh^)r>H3x6+$f*Ka7E zc!6pSwi%L=$I&gf_xP2e2X++iNTJV!z_UtCwABgv%rY}=+jCX^w6g798DB-KalB=f zYPcKlOJ!1>W@P09xHAlE>7&KP{UY*p>N*%S31KR_#B&V*k09_!iCf$~cy9WL_o-S@Ld-CwCkwo6efs=xJiqFf;a_Y}Gnt?-PI_VlC zBl&~{fb1h2pm{HxrWcv&km}p=5^*@?^FWte<}b6dXvibSRoPCf`~seiRp@4U()H@@ zcL$X1Cr6-f4$Xig{PXFqnnPT6v+9xo?dcfKg)IC*eOq|xUmY)DXC6w7#Jqy}0|N0d zHU0F)5yI>bv6N062RGhn8vJ!N+8HGDks6ITzcWmLZk3r(Z>n&-5Rhw*T{cQD{gIg^ zcLJ-%a1|({WL0nH-C0V7Yf0AsaPTHrkT5mBn0gW+EFo0<+Ucm`oMg#i!#p;;jx4`& z{rppR)+1@!KT?v&MTdSZ)Q*k}S6PI2_XjHBD!}Hj}Z*pG7A- z>4riouylq@?CRKAaLrVR6|af(x*8nw0;J9b5Xl+o@yI6Ns+|7(j$6V!o>lmF)(XZy z)kzUysJWe11U7}AUO8Zo$c&AUqmTb9c>Ceui_mLRu_l@jGXK-;E*_ z?bevnb?+g2&Ju@+B0@teh>9TTCU?oH%=U0XYb%F4$g6mSQew3^_w;$aAYk|Jw*);= zJnRyS%2wcC)$G|zy+_S@P4^g-;d0hMFjzA?^S3TR|9L~e%Y-E6IRk=DNiILDMhT_w z7gIzkrFfvmFT#F;tz}BQ?vpsSB{O$LFvMD02J;aW;>{+fCDyxYVb9;tyNs$7-VV6;|0*;eVH+_r+kf=+bR0g1qxwpjBBITF>pTf^KrBTe;p4La zGa^IAn&q;f8Sy)N6X&U_B9oj{538k}Ok4MQB@*s-=%LnvvrN_bwQaq)b=xzzG6%aU zz~zAG*NQa%gKofw zF`w%u&T}23!>Fw3OdR&i41P+6hKC8tqZ6$nHBT6;;HL{Fd@o9cDx$J8rG5Ls;za2b zp`h0vYDA+F-Etw;hy@P`^^^DcC#zbWSRO(pE+P_R9y)!3aZ;x8>M0`Fz z)R2Do5|@^A-!F#h8d!ywFuwkF%K7Y@^R4D8&#QZ5h3FtTVa0140kOk8zy``!Tmm1e^j{r$IxDKD|cDb^}~ zH}yL-p}G&B2M7bh`UlKFxuOm6Yk%Y9yY-0M{kH0!a-+vFPC`Od31z074JMh+CK0-(e!Tbza_MODpJvtlW+d z8c)AFPsIf+APdC4W|2^uc7s-~SuN#A#p;sbN<*y{LMWA0+l}!+c3%^#Vlf{#ul^WXHUw$OrNG~Q@! ztwxJ$RVD5f5&0=+l7%1VXg)WVP7xk4e&dnke!`^zXBNzZ9nwn;v+}+TRUBsfQOmw! z_*6inMBaTW1z7G2gBB}-kxAdK&zX#8aDL4C)N7hG3il7*>@_1xm6al(Y3_FT43%;L zE*UsPxw!n?Y+ZK`Ghp$HzWTa)T7FOE`}=^QYduL+u%M(K58y~y-{-|wyuBYC1=~kU zo%#JF!gER08wb{Z$XGsrdV1GrLTi00M8N1{H=YxJ0_3WC=?0;UsBma8 zu)2P}>jj=`2>rW1cea_v>0IuUIT_v{IeXh+`HnI4tE`O1u}fA0KBS5@Brv0Ahl1lr z=!9*ziKR~ir%Z!Stq2|+e(S+Ttgs&$BJ0o35dx<)*WlLON{hk}9#^k6=u>FrKirD* zPL1O6IB*m&9-D4!Gs0d>^qB!S)4fynsFUKXQd%*haBH#{b|9nHbsBpg2(!&$>Eja| zDc$C$CIVtmk(-T2S5r-=QB#W?Q>zCjsi;%nwU!bHt4>r{&GKmb@|@fTVTzA$J#~+ z1JR^HZs`AB%t0QRxG73^hZxfS#c8zzVG_*UvA4K*)tN)W|8d-O;S1wJjAlGd#YRd> z4v#V=#NJ>c|1IJkBTAr)vt~`E$){B$BjLOo6qXJ+TPeFT2qYaZZqg2r4GeMHB7zCN z&4Zx$${1@UEf*f?QM0-a8DxJxGu$cpvE1d;R&(%z2eAOb2!MfBD8e^B^?d5^JhQ%D ze+B3>H=X4-Umqvx0nhKe0~@mVth?;rLVu|U8^r(WZyD-A@$vrbiRG3m#e49Ar%>}h z@?SCA`sh39=DwNke5x!5V+%&Jj%34=!IDVxzqqgF#uZf^Klx#w^9?`o1Rj?v z@3FjkaPS29((7(Eiwk9(mOfB7b2qUvpi8x)E|#%5pk~y0fCF7b%$gv|y!D`blF35+ zyhu=eMfBctJV>eIng{_ke-^Iv#T=jxFB0Y96bv8h)(G5yROvhY5X&(B(Pj4*bVV{* z7lNoX^u=~z=3S`IL?c>v*OD={WL08kW%kp2ZZJ`z?#EOid}V#(>Sd4&Yv)d%oaycM zP$`M`?Ro*QG&hz}7Iv0XlqbH?FTlmI&0~-e8QlJfF>t5VYO9^Bc7Xz>4s+lpe*I&3 zFC6=5DlbP!nLme#2WAJZn3by~;6uAHCX}M&$i>o9Try+R#RQmf2|6BEoBASU_zIUV z-Vwsqs|O*DWf%_q+20>2Edk}+CEz`T1v$&?PCjV@uFu85BD~$$^xX9!l1NRZeJCa( zUrUsG3&Zt2FouM@)-dDq;s)6x4v_IArp&QG7yUyFm~1fk4^d=<c~~oWZsTLwVTmdrL!?7T)DnNIPhA;a6|B(TH3v zR**x#oCX&+1vAw`7C2yBNjGoCw%%^l8%}maa#X&qkf55=W2U%z#kBgowg4`-gNLPq zRzgrOzYV9fH6xh}1c3r>pYmKI1PMuR>#aPHpdd9_@Q29;;{o^G0cAj6ZUv0&FE2H9 zL5mq;|BwPs-u;%|^;}%CQ4g*%GCuBU1%OxR`ID{?M!EJFpZ~0d-0Jx+bYP%}!>N?U zV7a*C_Q@7>q!1xUz!P@;P`2a&@`|Bd{u7R5UOEFXyE5X>wG<@Cn6}R~sZi}rI$=xC z<#~$)eX5P$%vux7hgxTEOecMXD#yE)?V`nK*TSq;TqI zh-T80HKXtTu#81MMIo#KwbwPK=X=qd>}I(MJZN->h+hbvx2*S+N}R{~4)W?#*ii14 z7@1MMe8Rh>Td4`GJRQkARqBi1rW?RM;>%eR2x7|ix$+Isl}kYW&r43aPiQ{JFv%XR zje`yq^Z0$AdJ>p?rYyvMf*`kiq*}Nk#o0mq+2O(+W+86L#xXv_2`UF>Oa2eONxmF` zfA-f~g>zo7p96+-$qD8;!W3w`uhOKu$Xd1jg#v+EQ|qdmC{p)1V<-pVm!QLD$|vh> zJTWGa-sI-T20hlU+&sm_?kIrcD?8!U;RP5UEg7Zxe*fZ$5%7HHH#56B%V%X%f_ubx2q242 z^wj9`)xO;55hK*|Gch(c0ticx`3zgs9&~=5qPKcNnnB#OS^;Q$t*_dp=o92+@RX}@ z>pDIJR{+^bzTDcTSray{Wvck@2*x7!fn&Fmo=N}*-OHUoLM+?4Zr|sd_9j15dsEp< z(i+xZ#LVX2Pa#f1g7Qw?}2X5X zo=(f{CY*52oZv4^f0ht*{EUZ^rw(hIAr(^k#r9^@l(@4zQMcy0*<^dc2IY~UeCh$_ z=V#KU=Qm)8Dc`LQkKC#mkhNw*qrZ>I32L)HTJ4fdF+BPh+tV#emvlK_mg~43wZVox z-p8YAF(iS_6fuqMu;e~(NUopNc0fZ6Sr!T9FXR;bLi?Sdh@}42xzG6YF_12tzDJB^ zmLQ>@A;kW+z8k4&a9{pyG_~f&_5gd2e;f zD|K&Ky3%Cyu044j{2?}6 z$mG*tkzR-De@&8CR64J3zWr~_tiRAok9tJN_XEU6;48~)O}N{6<&1=K?>t7f51{_o zO73`q5?jB`IWP@ENg~9U<{OuSN3}?!W%` z!Dc$6#~gk?*)HtK*>8xd()uAq5N+tjOV{bSQO&(ijN0%~Ab>Cv;)vXJ_0D~Klfv!i zC1mO|V*wOI1sou^Hk+My9tZ^c%z!;?bRHniwS-grzFHxsz!5xQkLhxHEl=G1QOtkk zLd@the~@t1dU24xZ-@+xj5nJZyZr69)l{9=--%y%1&OI5%aR>KO){O1Gnh2iAwpGOMLzdXC{6YJx-4-DbI@@K7Ssf5R= z=?4y1K8FJxR^-n|B^-=CCj&z`#M$Y-6k;BrcKhSvWS84$mVoe0nS&6JTN*5^&xyr> z@8(l~{fr>``+zPEqt688z+RV!0*OIgInO*(!95;_!Fjh8TjVO9XlZCjh&LRJ! z(d^PfS?K+9#X8#X18qy_GgZp{D){AD#4(!KpV#YtsLQ$kp7AR}TY&!Ksw-!odmR+c z(IPFn`Cc$!UG9lW*k@(*AzCCYswgxagFf?*CA{UPsGFDOiL!@?o2%&fB0c%tKRMrS zz%X>%;c4o?Bc6ok9@N(iKr_7x+U@z5YCU)M^~{w&2vsoR(1HIJ?({(%<%OnBSn<{n zvi9!qB%+ne`Q9qvA!l8W6lN=X2(FEqwExNJS*c(s(D^QaAleShBIk3&2}Ig&2;n{S z5v>wo2PGb`K?AyKTjwW*xw@R9SV9;j54gQnEASLy-tb7KSo{t#EX#H42C~q-#1_#b zo;0gz@t7_XCIs=^BMcW^IZC)O6;#;o*Mlx<)-g%bJvPvbD(v7A=Dg;RyN@=(l8{mP zM|W4nY>v(1yUCv*{#;!UeEmg}GRpa9Pq*Fnf^c#376HW)IW^>!&)rGQD~&_zn0HMT zZ$her*VI+xzJ@SyM{ixrqq5@M(?kGA#UtJ2OUvE7ell&7-@P!?aJkRyu~kkwVU=ux zxa(Ba7HQS(hL7+Pd)Uvb&s%)&?<`fIuNfU~hh^>!5(L?3un;{{Sz*IgX{01SpbGkE z8#c;hKcX#b?;64Wzd@K2ZGHkQqzsN z@?Qt>)iJN1@<{&I=EUer#-q2L{_$Ko2am{0CfRo1!!LQfVp_7O;{9B?MOzgL1Msq`- z3isp~rE;h{iMAySl4#NeE_D}STNsRy4Rt92t^zygsnR#4WAx85pAByKU4BN@IC+kT zR6i&ier`8g?Q77s-p^!NxI2XBTz?j^3@y;QKP?KNM%W3arUQ1GEe|wkYEM4#EUI2o zFoizW?GhI(c?BB2L$n=2raXb>uO@vP;V-uGC3W=Q=wOB3ce5W)lLX#HOtbC8#k&qJ zjPxVQ1N}78CzE12*py4n0Z}|K2I;n?BrI(3-wpZ5x53@-BgX z!DcquVi|$jkFSK}1CfN^Nsii=_5gu^qIXbPHOUJ%p1x@h2lq!AX#OGpgJzOsO6~D# zi{wh{ukc#67U&(+yafiT_Xz|DfrLSzHI{|3ci4|=(C0Q_1TYHvx?|#F3Uqc0{BZg- zx-9*0N{Bz|KIr}c)q@7E!9m&ckRaHar)$)$&vVpkqiB*!VZn~+kM0BEE0#x)2SE7A z?8)u%-`Nljh-xWcc%%Gt=mYS}=pcWE`}q=Z0jMSE5%#-V`Ct(QMS@(;kzYSfnm?Uz zlsZ6*kFcLMAA2CJXD2V=C*fC6!~55=@UR6!eIfxTnFKU~_Cd%Ok)XBp@QY>;iJQ}6 z{)ce>-s$E4uW;6DbmZCvzrhl8!K`Nc_y3GkIP(Xc{I~52^0nLGU!wn#Gs5wps^H~; z=@|_1pyWXwq8=YI`S_rfDz{8mHT3@zPyrj0-4yhE=}nz@2G4GxMHVHN6jSPOv8Mk3 zamtB2fH#C%52X;?!EB;4&vA=?2QJU#uzG#&!4+ z1?AS#l-qB%pFxEmg>dJwh!%{2Jk_lf+o(% z5%HkO0(i~&mQzF38bl?Z*cQBjht84gdNSG%(5TzF|MwIB&&LYcXi^6RDnk^-Re%&# zXP%N*4H@|ww&L~0w9n+!c&>Ls<(pgo%f8=#hh?!KUn%;En*hVFwN}iY1DlOR&tx`n zQpbeaxujRrLgu#@Li?z8a(oN+%kdjfX<@j8VsH-}cWSQ%{@A{>9Dz?u8w-Tx+FT8Y_!1^Ql^Vf(Nhn_9!VUDeW0n6M z!i0&qVVr!Ajz&$1_1X(Ek&)6<6S%C`xsh{a5)Du2WOr8iH@xt~0`(tEyqCgT;5gN~ zU?IRV#JhB~yKW*QmcDuInY32fmAYzip99+nPS5{0-1IcvRK|pX@c9rulK5E(#*C4N ze>)0?yT@b44G@wuhuxG}!ApC|oJZmReVMfS9~rP)dIROm+XfET+T!6rJetADIQ9Ij zV(sGOweMw_femavFJJxGw~X~8A_Ip*WwZ0?UG^J2!-~9ZKE`BWLwwj<1&Ns)c=#ss~8U}s?BIbQy|1RRY@q5E?$`PZfO z7M_UBxh~q66|#1s^Y1A(Q~y`qyNjn&QMQdIe`mR&GEVnl;!mF3Rs1v(A{jw20sFZBx0^BGJ9+ zR~$-!!fFbzWqX3~;ip2}`jbyoxuAG`MC{C-q!nor{P)~V`9gKtt<*y{MTm%n<`Fo9 zIJ4%WxrKex6R-W>`MU|(CLI3(7;}xl&JxtQY`fkrfwf5*BM-$eT` z6b9~p`;Un;uI|MiID^jdbx{56l+G~Z4qd(bTK|C!AvA*rNRQo)g<)+EHczmY;C$!) zd`w_7bf0R)0z*)ToKsbS4uM;5VfENP53#!MByKGa4Hf)+U|q*u!`T(soioZNE-P<| zuYs!37yQ9{(<*cLA4@TjwEo%96uu+7v22$T+Y=<<5Syl`QiM)2Td!|Dyw^k<;N z9)N+H;tu}T(Vw(}(?1Hu(VLI*5qN%f7smGIsT(m%3OK!6I#EBRc~DB)2(}o z$g1nVStn$EJe_&ea0;YeTcb5zfxa(3LG0#u=L^I z2>{|M9*@@11vCw}e^Uxef0{*a_`LYR;;*|iTi8+e9~V$oG2V|o10{Oij*lCP%SSdc z5%*nAEMUNr$^DuzS$QZrdu^R+jutlYe|#bYw`;BCL0|W<-Y<{8H~KNQ5C1%GUSA*; z?Fj>ya0)FTeZb8?q!V{Lr%{2ok+~b5OB5@nN!^-IR^Z0}(qepi)*`t9aTsnRrz(sYY)c zP{JMQ4Pc~?RE%qzfEuoRWslb0+q6)5m1g^TQ*ddfh-;|0^fE(#etis-q9r+jP}PPJ!Tk(2tr z==#PWTbpIgwr$(C?cKI*+jjTv-8OdHwr$(CZFf(f@6MTf?o7<{=UowNMXao>%F3+F zx1PM4a@UH|Yei^p?TF!<>CPGMpw__|XjXJ~$G32^n zi+-r!2vW=FT@#kL8z|VsO}5ALTu_ks@+D<7rlQ-+azUobAt;0 z`y~bFKj_%D?zI{LbPq`^Fa$n{@l)`Th&MrNF*k~L{jX`by1EMx`3GG2%jAFk<2za! zhj57;Kmu1sh!If@nV3*}#0d(-XB}HS%PdmGg?vQmYQZ>NT_D5I$cRZzo(W3}KKU__;(t@}fGs zU(l>>h{*TfSH>q@9*1!44xMwwU8z}wylQw|CBpw-*cY>H36p>9=V80;hVFo;V#6=s z%Y!kYfBGs%UgaK4+AOk@69GDZqh`@*7k9ZpsN%!oSq>`v#wHptgbRf#rtH)wT3Q1+h@M0gY+w>k_cspy*#7)3F`rX>RIQoIP)nw-(9V=$&xmzR_$8Q8nf2 zLCIr~??;Tzt1{G$;rMpaK6-H_LOXCJe>|ER*U`!N`C*pp z?J6~zvxFwcVrR=NKf?)?-rP2kvy-2n3@mZ7Rr(KqF_|+F#s~;kjnDPZoy;c5h41rE zEzo5^uQT^*);HcE=I{T;2W+bV@FAMr^rjgjdwar&VH4p}Q~n8fckqSp&#c|CGdN(Q z)%IYEicfY+$UoxI4Y$9b!(rJ0i_Ilo9ND#cx9@&smf=YzgSL~k49(a%fJ z@Yt6G9A~O{X4a1I(-oBX-J);fNnWA5t^}8N4sZhZQ8A+oaVs9 zZEGWzjlfxw{R`&+-kZ0s*^D34_z3omQnfDt;dG;L^t#n`-4et| zp#rNOX=%v|E(>w;JR3gaUSa@*n4o&4tXjFA(Oi`zqOs4Pq>6RFK_jZCbyTt=zNdx# z!00tVz`sMNDA9*Ce(U>SRfEAqZ4)$U{U+a5PlovV=66)9bw-F8`ESb)yUth5yNd)+ zmKipgKA{F|MUi*q_o{>KlyUwD51YC%Pe@OJ)^GVM|ckdx>7_O}cuNsy5 zL!dF0``F!jZ+1qNlFKN#z{1)-f%W5T-Es>9J@jD3?H9!z66V5CjVa!57{Yn;(*^uE zUs0p(yiJqxsR#N%oMMpMfagID=dj#7ov_0%DEJl&+3mda(FCjV8XzV5p-%b4s%R|r zwd0rrbY{F>KoEE;oQf;t^Z#Rsv{AtIQeRZNDu2{3kAZclhlhu`N0-^lFP{e-6;l3= zfSviEyrek1jQY$b+07wGS>Tl)hJS5T5MygG5g}IbX^*-he*aOl$+jAH2Rb8ex_GVp zSU7RsYlWl8=rcv#_e=Dj{-gdn*^pYU@lxSsc`ME8)l}|KidxVO47!hlEPiNX*HyiT zh+omV?+d&DGWqPVC>o!WI*AcgH_IeruPjXz|F!*#Kx3>@f)E%StwZe$s*AKH{d@5M z?+m=PG@fUA$Rdz{ zIfJ4wRN|)AL_-usQ>aGK$15B)nXyTV155i4@(Fy@kYTC)Im zsnLr{kfYKHy8{|LS>%W*l6k_0di9xbiB^+;o7cNY4VZxH0N6v%s%-WB{>PRNRK*yQ zP^}SpWra|m^wT`mg^oKXJUKOI(E5XJ`7+#h|D>r2&0!dn=mgXXp4IFR7*`*Kj;}M`VhYcB_FcpS+?#!&B zt9`0hF-0z>vpWLUx=w4{1=y}7R6g2&xubu+{2PDaqJH#cdP1xN7Z!B-gvm(QMAw3lKFa$Rx?@>eADVn|oM(F(bP zYcwxxnrKB9UzN)cR}KmL+!_b4ewSHInM*F5QMG2t4Rh~9oiUX3&3Yb>TD4XGjVPJ{ zGWn+D7=@Ege*Ti)aC%uyfw(cip&L_{vyC7eDy>BXb0FzFGgHj{)c-l?_b&%k+*a(# ziBueLib$vRcSwp6q23aGu`$cVRa;qKe~S6`SavF|S;G2B^fdqSw}2KZLv$}jZgKu* zjo!Y0Tl~f8EKs%k)PT}uDyB4V;Lp}1;$sAufNOkq)A&m$(Mf9&y+2q`=Bt5{@;StH zBR_?UANIXj3&%WNe13i@WoK%^1li61%#uOm~7y85=$KGVYTwc}#*CpJD@08=ogekzA|0>9(YX>+_U)r>^%qjNB>vj6d=*j_ zKSs(Kv7*-&!b*q-u0Q$_|482vl+l%P2}j0u(b8>ookN{`tE-E4Uzht*M~DxQi?uK< z{LuRXgbzP%xZk?<<>7rLspoM;O!rA}N~R|w)bKyUpR!=_=-a5T<5*leO46tO#%q2) z2+G3_(KDj{eRWKbxW2w_{0Y|P`1YuzgiNFcwsH+b526E9p)~C=(R*KTVj+J8da78M z60p6jAfI=O^&Q%`2yO8UNDG9mfRVZP=<}v(lb1C>UkcO=TKp+G2(%%0WL(+eHVH4wPwFkv+qL6>i52aI7t9Jhos-hXq0fSAun-C5!})Nnz8S>K z%i9p5`0PJ@sPLK8=_F0!RRvt-J4w4Tlmf1fr%l3@uY5QO(4=n&hog=;YI(n zOECC5%iagG+$)2Sx8Jn1y(6DC{=GkjbU6-6(Xbqmb>9ZBGX!OEI_)6etP z43TUdHZAF-J73S0HJ_XCe+^Gb&9;caTiRTInZa4wR#(l!dH;_}l@t8UCyqWB{2JJa z<_&V$?%rehyhxQl`)7r4H3nuKOc)Cch`>1Ez|K_ii0m9oV#Dfu>?hBO@0=C{vzK{f zDraStqQ3C&Am=W;lEzmg|G*nKV&Q~-&yrQI8hl(9{3G1UH{@|9s0a~d~*({tw z;(h(`nO4gn-0qNd98Eg)k)s&TDb(m5h=DRn7pt^&AAd)VcW7OUq|Q(^A_{ zAD~^K*6@v+yoBd?ac}cqnZ5snazy_GD$p45^4_$`QnxwS2#;?dDJ{bnPkYY>XApg# z;Z+ub5QTeJx9Xaq%PvX-#(j=_m$63(sEc8_x!NQk z-=#lSb&K?!slPU(V!N2hO7%c8O@`vj8Ny0Yq2+rumHY$`43wYl%Y4&K=;iv&tLydZ zoe)YFM*sto{!=y;6*+v0k|+vgzpp$DfMLF7!cgLJ&K3*b;HoNzaCi0lA)rAtaHPwo z3s3vBJ>4GJF%X0f{L}YYl2P{J**ByD*BFT0sG8bDR}FjQ^;+KNq<#yuSh+Fgyi#Np zKKkkvX(^y!9a4l(zTreaww=MgQnSQG!*C1Yp?3k22`=ZZ3&8YU?-2MAf~NqIdri(+ z9ijS(xhK8Tww0_o}+&_^S-%+*SyM2bT14{e2;^(3j1qZ67+Rrjl*v!sO133HIly%1cN`?E z9*O&(^#-hradtQdD6l#fz5=;c86~qLoO6;Xz8;=K*kk9^za9Obh8=E!`cRP@jG)?E zPHXYNy;JF;<6`Iw{bs0z=FUS$l`ur{h$3XaR-0OpQDmkxaBNV89Mcjj?+y4UQf@0a zBljbKe$kKQJu@H)H{{s9_lhhyOnUTBSfDULKPf;vt)y=Q@Yn25Mg~S>iMfy#y64C@I1o({#SH~e4D7V6yHJJr;7>Bw z!b~2Z!>(HAvHa&Qx<+71ZSuOw(QhP&?%Dp5lJK$BIliEDiC6oVWDZ9oTWQSAdj$>} zr0R5*`GI0JPHo~kNvMq~dT(FYSgS2tf2o zBaIM5={ANW8@PlTJ{7$#Qg>Desez?oRR%;yVixnh7}Q4%tX^6#bOi{c@b&SzuOFp~ z=j%m7D7$w~*Vsnf_5F{%y-17dXRR1|$j5Kl#b+0z`Q(jU$4?wR1LLd=(#oXPmsbCC zBe>}Q2Aafo_R$p!V=-yd>x&Y547q|)7-KLveDRmR0 zj6TX7*}n+ZB7DpfOzq{QlOTYxei7pe3e@~0AI#~;3^dDzcyKL*(*GH!0KnIWN)EvP z2fzeYB!|*2>w1jVD%_JVSrF!WZ$Z5zXOPiuDXwj+`EM!<7oY_ZdD|K~*-h_CMvaBN zL^zGfAL0{PeECdd<{HG(IgBm6;NDfD#~o(}0{{FCkSfD&BVv&L{HWlj!n1Y4DNkwv zz+ClOgdvcF!OEX>58!QD2W*CPwS1b(ds}QHkhe%vz3pk!ydM$}c)qlTp$l7`_$4O2 z?Kf>RrsQ%=6@JgDWp(BVLN>01n=FXW1qntpb1-K@qZgxA0yjF$iu^7kdeE=QM2f<^ z$dQ5I){wrI;<&+0eQYw(V6OW5+O<|?W|c26=TUK2iNnHiW-{#WxfX$pK{ia36WC^; z=Is-1w6dK}o(73sFoe{0rb=FJ6roUt!YdozKSkhok?#sFKC$1sQ$ zMZfO-S#jY8?!g@HN?n)ihHsaGCFrFmMNAkMg z9IM~%^4Sesuj?1bFG3eGl>MttNDtIVCd!l`=7Y}+V3 z3+2rDM;X$o()hIf$srT|Um{PcDENE_4x$#m4u!CmveSm~LDYhI=mNXOy04FyAxs)A z(88QJq?`cpG?#RRImn_(=Ay3deExtCzGlLZ?s~;mH!DuKG)S*9o)?PbCVThdl7ko1 z9d@P#>dq*iA%=vx9P<-7dY3+a3Q+_U9@BAT5?d#`)e1^wF}I=z9h+Z3hR0qm#{4p$ zK8G2pPVHdWW(tV!4z5dsiZq0`T+g-}=w<&%4JdfJtPd)D-a~2+n&5x+wt3r!Hao`M z8tY|@Bs;1e&aeIaJbj5tOq!l5p^N@e(L=$ zf0SKNQheNRTvsZYb0U=qwnbWIAB#_;#`gWo>Fk)08ZDwI$_V(WXH^{Wb_y;j>o+Hl zdwRL-c8Vxm5~^0X?ztC2$ulQ%OYLv#-NBVQMTWY<0~Pk2Q5QS(KfBi{aq$)n$ERv} z8%gZ4_SO7t3!gj4G&(X>GkffV>9EGoHvAK4aT(gq@|c3MUA?( zX|y4Ya&o8nFARgy>W?X{>UcsDQ#ESsGB*c$Gha@t^bsO%6aHlqk$d~TeTtXju+u+B z))R@FE6Gm}a^vO`+I`R$IsT8rg5TxdEJR%(O|(E5)t!T!GWM9j>2DComxB?K3`KZc z>k2Dw=y{JO7T`cm1d49CZm%S^9(R6nkvA=3785dBV~B}x&Jn8lVt`ee1pf>K;Pvtk z$<RVm5dDk5a?cD*g_O|PMFd$Em)>-P|6E9t~ju5)PtR0*(-gmuK4Ga*xKK@xMEya z>hbs&VYj^VmQZ1R>ycZY5pS1VPgN8DDIvI(*KfRsr(Q#gair6-p`2y#HkbWG;5fEg z+EPQ}DSMc+6PJTC`tE%3xA+qc7#gaW*$3N3=LfvEYzjKS?zPdq_3H+|kag=J)pY>x z;nCS-yl6CvC}uq|TDO%}S2FhD6}(gfDUe)u38_UUNnF(w`cE)!{g7U(NePG)WBUZq z!r@4+I6S^6fq!WQP{f-rdHIS(Az;P*{;CUF@tx`gN!7&$Vd(Gu^8^vtsfnHfp`QM* zvkYDm8WD-BR9`cCoq-CKa_IOtE9q6NO*|NOwFsc^C;O^gtj)8wpBYX&zg~l zzhnZyAKc#TjQnyq?PDM$_l{GPI!pB$>}3VLwOu&e6*g%Nn)9`;WARJ{7^h%Tsi4jf z9vpMc?RL%3Pk)5RK%O6|&z|%SL0SVq-d`R;(TmQvKy-Q5k_ut9!iIu(_v@G~+m7NT zkKX8;?^-Z^YrgWQR zKlIP28#ELEMzfo0)TywW6FSeNotLlvTPwabm?Kd0K8*bPHJse_TBe_b+v=o1*iG)) zwP5^{&qnr%pvE&wdVRjr_~B2toMO6LXgJwkMxpMt4I-l6AswgpNL>`*&q|~*);WWk zeB|x5N!(w-(wmRKhv2J^vjs2hZ#7qc+kFZS9gK{o7}FYUVUqel2hj$=XnA;LezTI3 z5Cs0J27vzN@leBrfEBMW3CLoXtz|Sh%||dJp?br^VoprqWaqwJq z16*K`0S3ONq`0Se6j@%<8qRegxSL5_zsM|cLQ~v3vk2morn&(G?A#%4U(9T4d>GyM zobb9|<#dx}sg>=)ma26F_v4&53zN!5y?R0Ek@jSx1O*7#G1rgS+0VeMvH2Hjp>wXF zUoM)d5GI($cd`rBiLe}E7n=y@9w*TyPO1BARNS#NX+y{ed7ycpi4QM~L3$(os+LJw zq@s2#b(m70yTHx$xeVTs8?a!LdE2c92G3#ok`~_5bsLZJ=XxNamcaRqn`KO%KDuv% zh(vheF@&x-%sM(hp+DQaE<;&jxPe{*uQo5Slyy^WnwEPZC|DIC&a-#FLs(q=;Z|Xf z?8Q$Xb2t6Y^eY^3${2xf$Aeyk*?jnCn7#I|EkO5YUHrxt2iM8tOq+3`@yYM2y=lmd zOec}pejQ}V)@5zY4=S zF7;qw=Tk`Ij2BDjqs!q@v{Fd0KC3`dUhSac8_r1ed>()#T;MqvnB$5K@B^lr6N*ZB zu;hFdU9#dx2 zuB(f1kVIm)=;UWnflbQ{L>Zb+D?4#N=XG7b(M|z0?1%AuIzX{x7nC%>fW|vbi(MK}2zdFriP!2pqO}YJU{*i@5Q_ zU+M%QAaq-XFS7tE-<%!!1)SWa`XS<(8RrEbIE?=8mvwB6NL;aENS*V5x;sz!$9D^d zF8aTLQ9E4__lI;{X1U*vfc`i^1UCRKW^+X^s>}=bxJXTRuR($n7BLv920+{L;EQ~h z&hZ1mlRqV(+^s=Ns*{s9$YU7Th{uCP&uKh;_bBDv^}t`{=kqRZRx#@cq;hDU`KE}9 z*4H4u+-dg81@<+y=>=!FjAtomoY)LA5VDVy<>hnoW2&Gp6FMh<1N7-FEC%w;u%D2WsK-a*r z@y5-N=c%Y~G4_yp$H$o!w{TV4P?y~jx@e#%y2#jCtA%{x@BlbPSZ#MS7(ET+owR#T z$3=a5!BmtF){9+@#Lvuhmsw2!oU2=4Bz@zn29toqp;cO&K1^ewv&c{V^6J2d%htowr0XCKr{OvzHkjAfu|%zRcoKfEH>$&NaSZYbx&l} z%p$q32L}L1Zz^mh3BfK;&OSTvKM2)3hua?faGwr}lk(QS>*z7seXa<*BHpzcv<(s< zZ^=7b**Fbl$VA38)xT{9f*Uc)+{uAqkVs84EDgBf8P+r2z{lcRbhM+>PYn3V|KYve zxI0unoNq9er+ZJL)|$P(MO8M{g8{_vzobvtlB>E@TWPEH6eR( zuZ49<8Gcs$O&RF$giW%6R9@h0SH`_gP4l{ysQ|n#=VXXlM6(XQrjb>AOKKqMj8YMd zp4lgNP4Rgv{5{5C*w6}ZVZc%MQ<1I zO&QOSYSZGD0_;e-GyV>P7!lixkt5-}r3$R+HG-_IYTMeh!pXP>jlF&U=}sF19hUAd zR+xq0el(QK!`A@d2sIm#DFI+POstQ+?C;D!Da(y!J0=F%1f^=bfo8BJj1e9BC%{FH z=o~(myB}i=-4F~3-BwC&=IknK{F+ssFN#ucJnti?kL-q9-vS7qv7S`Vrzr@)(sFcY z@+OBl=1MUzE`&fi>3Y41hw3U!Gsrz8u48rXk4Nq#F~U07Mi}4elGKntKW&hFR-d~G zd_hBppI6!oCS)KgU@oP8VF%Ksy3~1qqtx!Sf?T;y)`a}I*4)|eYhO(uL;J4WgJo`P zZVZHU{+fC9`v*wz)ndkWZf+*V^QSevP=K=(9pb!+Vr);|E0&j!x{1c#_r2D5{ z4Tj4S%X+s$`I56PEwuX{kb`Z*$fwp!^5YoTLG7M>4&r`&WcrFRqC1kfuMK0&nU>%w;4w&u-lx zO)|}V1;40>{bcE|S`-MY$v05-mVLWBRI9b$%JmJp4;HJTC{v_c{RcbXjznE>PtZ;87$|ryZrSTN%C{>_ zqRZrtT%1tb7tBiY!VJbB3GlszS9KOd4FHN(wp8zfsxD`2dgRd!c*g3E(MZfbwflKv zstR}M%X7-$%NnyjMl*IL32r}es@y?qKl9(l*lCFtkUAtIcC*J}0kExdACSq~Bc zE;~>)SHWW3z~i8gE10N*XO|CO4h`XZ7Ny&f$?p`cBVn(u43M)qGTJ7*Ko%f4J$xMN zqb$b+)V;lCd7vy{p;4L_yucopj)x{pVAL# z5sDjsEn;yrOba4pwq)t|TvPekT|%aHM}SOw~^Bm)q^i>R1j z*>*YgQphDj)j4kry&1MZfe7Qrbat2{Qt}&EIp`!n#s`v4C@BHP#P41jgDGy9i_Ms$ zx(LUb`HXmrPJz*nj;Xz+c?HD{wE+Mj%|!B8@f%&vA=A_Q)iNbw>+XI-0r)l%kcJfB zAM__Obp{Bi@*Q-IYSc`o4I1yU;nNB+G|o{V$Ut42-CfeMrDN`P?SKUTmSI7jcEJg= zaf$<5+Wl2=h;0W}1d|V)d%$Ewlv&!RIg&aZtP7O{UERJL+73M(T?L+ng7Z|EG=aSV z$S)Ro2S6X&fxvW!8O|$pGG_^<@=AqefFeXddU9q}s5BP50I8XF20D|?%re3eHWaxM)H2Zq28JDv1*9W8 zUZl?r`5rh7Ai3tf&5pFf?-zYw-Tbmze8tlymf;L#qi44vlL&ekTEvlZ%{eGM3ibKl zCCr0`*`B!+tGrYVhC-q=^s7oaCC(&q+@$_-0WNJCnS&J|mVWt6*gapQ!(&K&O+SSXEN764U97l^*oKY$i&?`2niH0@{%5K}S<)8Cfji#Nv z0m*~MFl2M~lLCru=N2p616u?Y=f^hf1oq@t_IJyLQ`>}*DI%-k3bNo$rpw6n@nyG+ z|J|_JTYHN{@I06DxtpBupHz58{^^$>-{dQ*?UZ_3i{Jo(#J_4V7f!Z%!kJ%7CaLZ7Kh7b7WW@>`>>?peJ z(t7&H&E|v4Xr>jd(mnmzxVs`*;i@3bTRs;cT`{(5!(wk=ineMpQ>H_eRMIyE? zW{Tj8&LU7X#rhhR$z7jNo99;M9`+r8CUUxv&-&{OYx{<5>F{n~hNnDzR;lKU_eLC+ z`oBd$I{>)^liRO(F8F87*$BnBS4q%GtZ?mcY2{sI6XFNEx(2NN8hbBa)4R2)KN- zwv+vGA!Xil+@`v5A;sBst-1wl>qMoZw-Nb56b-;S7~C_LC%D{A#(M^$&#Qr+trO;Q z(8J-<+jG2&7^aftljI=P^VH=0c7Ldz^Bz+cdF)+FIA7xQoY&|rqftst1KsQ*Z0Its zWjzj{L$$yi`gRVsIuC%M@n7`nL2?u-B$0338nqEtMuIbPkYc))MkyQ&ukM7FdAddM zg+l1l&QfD*@ag>D%NE<$cSZNk>x~0JEsv)ZT~8vQlQguQ;?IApM9Ui^_$`DFg@7$S zCS*Nk&nS~oBx5aZW-!q_UPB?YExO+3Mi-$`c}K_Ieq@%iu2dZxY4)dd)gd=gJo0@P2!UQ zLa3~Ar9DNMGBai7tdP@XZ<9hBxrfx5OCpi^sl0EoBFG95ulKO$Y&Q+CWU@D97$WET zii~D&gbb(Xga#|kJU`rK=g$zTU4T2U?NbvScYBSL{orm?&=m=r5aQ@Hq2h>#H0l`@ zju=IX?@=FWa}Y@z*<_&Iiq@uR=MmtBjK6qay+1(+gPJ(Y zvx?))S?F;p`r~8*XW-`Dt+yhU)w+1+nw4s)r`TPOv79U)z0uUc&Zo91!e%SnF_)QS zJjM&HVn4Avp`q%Xds#8YPvvaE8s%&n!tl(T8nRk+LLOphr|TeuAF*fRppU+9Q{5)U zMxs&!hSUowQA5MQphyudx}aL(mni=&hM(rk>lmnSRLvwmucYKns0o{ZqKQJoYC+l-y8l&iRaxm&uiBHo4!*f$CS3}Y-S!xFrPiOF_k=hcuV)9!4GPzmC7tpx zlm2`Jn|W*tTb!hE-3m(TT2mX1L|G9uy&9+5z5uC4|e2q3Yij$%nz` zF5+2g5}9rxd`AoCw=91XP{c*E8w*QpPpz$Vu&&=Y zJA&OD=W-6=+&iy(x9XXNsCC$J(9ra?-=~bClgd6lx&~v?`DL?Qd2m}}0j6E`N1ZxO zsm(|@@qBusduyLMR=Ejx)j?qdF!hrB>Q97~n@7UoM z^N8Y0#6mf5ToAFTpjR1sRm>rNj6#_{I>WA~7z@eGqCcaR;cg4FEHtSQ$ML@+H9NAepWMR`UkIA}x?*i;p zlvY#mFhxBB&;nQPmX6UJoFj93H3t-Ugx&D|*0Doqr+C$saTxkeSE=&3_d-kiLM|Qa z9~p>1xm|21z*LWzFd^e>&2V#AzP1P`e2R{`Y>@FA?C8(>OYhv*aN1ztkaSgO$1uSw zIF`s_;=9+4VXU5JoHk)b)J`7vd)z4*5*_CB+u#HVsXSa)OhACigR(4PfV9YKg>F72 zlCwoM0{6-=5HXjyt0HlbD+nqeEiLA%Al?bS^waOiVAD-V3Vt^2Z~{Rej@O^@r&Zq?KJUECT&&HVHfaTbUlO zReS=Nnqr@bjI~$gRx@#!B-MhNJ+Gz!mqaTEppy=Epn0ocZ1yI6|Iyz%f!_cbOK8IL zq?ew>kW>Ap6-@B}%lIJP+yV|*e%_qplo4^z{Ja<#;1(8QX?iWzdE48;2OIsXtT3Jh z*JHda?l^%$vM%BioomIsqZ0h%$h_d=BkcRz_itnr=-MD<(m|O01Y-0$GxsP9p-q(X z!s|jXfIx{UO1OJoZ*{Hcel;~w@8^IeUBi~NP#(phPC@_^^9*2{Pe}Vplyn0gyC0+5 z=b||05#edZ!`fM}33|XIBG3Lsp89#O_EjDSltUq+wqP*DQk*gVqG&t9zBh0{E{kEJ zKUoHGg6w6P^ozmQ@3^7tcp%bNT{70>28uiLgb&oAwc=zQ8yoCW&1Q6owcu&J-=?;0 zE^IPHy?Bh3U2u31O`j~>LFGt`o9URkPE>A~Q5D(Dt4o|ce!d$4SO9o~0JxuE**Z+* z0g=;FyTz{Fa>f0PwePQq#MbaB1zCB7WxyLOF);gdu+jmUB~gkJ7ktCnW|+|w;np>= zL4Suv>~U?BzuSl2s34ku>v3XdQ1R0f9bL;Z3P?57L8a~NhoI6TTd`>xg6WZqS$+MD z7}4QPC8f*y%%KGY(9zS>B|9VKgf~6L&mhSw!Pi-Wj(g3);Q6~<(swV>2vx4GUi~_{ zQ6pGaAh3(dExV4?N;-T6`Hd z$bgQ1a@S*+yFp9YEq!*M0Ju7Z}ox`9E)=!2d1-$?l#ApQ0WQf{l z>B@6=L&g$4=XQYu*9=&VU7^vaXkB*na7z{I@Q>4JDM@-w*eSSLyg$$Y#9+S5%hpZi znrE}gV)WarNR9<_<+m6COpI*BWnniy?~TN5kn1EO%d?lyh)hiBK4?+%-pC+_sMj|W zoj_9d0}f;uBRJgroHu6LxvHU7d3F&$jjn+E(jwOO*VoH3T??~H4of|$TeFElC`D9j zHP_to@M-~1!9RF9m`}dj7BuKQOB5_w`h-NjI5ejb#Cu z+j}MbBW=eqg|O;Z#?-pPaPXkB3f#Ptg%2EKhQRM^Rs|vg^nfLdFrzr>ScVUc$WT9`1bs5T%E8A@|02)zq4RnKjom3A@_L|AKF< zI5r1 zY?>y`PHFXg?#OMdvygum(zSkNhsVA}G6a@g?|I8k_HmjD2Mv=k6KJV$R}?Qhgs=qR$s}{t zcxlgfMxK+{I@YcaMuT4HthUhRP7P=Ih`LJngxZ6SfKN0-*DWk&VGm+trXzfMs64E!n+~5P zTI%H?%GnR;o9>1|d}bvx_KUon&!SKT_J+^^Ts=~5-TR2*`J0-P=FMR#!?lgiJ^KRt zoxU^(V821Keh>Stss5?m4Sn_DdkN$=Foi8?kyWAEi6gZEStgdasBkDsR_ z`GqinhV3bFsAvSaSGDD+aj|Hs`NPK&|JYX6dfTRB^1iBeS05L5LKfOWC-X%-x-5lZ zg}YCAaw_PQ+H%>@_{343jt3@P{#QC+e6%o|L|)Ta^bZbw1=hLq&Za;FPA@-cuUW)$ z;-P1Q`loNdKv~4io1uh(dtQC>mf{zfT7aQdPt9a`KaPT`=Zd80VG=sh$U+HB;ke&S zrRUissEfMFzQ9pGTXT821qIRFWNLY418sn|e+4O%j>)zaxtGHnA}=DB!=XejGVNpb z@jQB3W=aWBOy%#G*AQ%>Y&tG=LOz51zrQZFF&i6u5oO1f@;fvdmmeUoB!^;93$f-3 z(FIXK9qx4u`0sGuG`T$GqurcC<*7r-SS15)!oaNCkuatO5{T)Y5V0|swwW7<8Q zYMZ0?Y}TV)B~NH@DW-Z;$m&AQ@Zw0W-!d=B_B#brVI^$oBjYujxl!ZO|6)4@1m#I$ zxre>dOvMHtCi}6uDr70$PSr*nbUohA^VDj(u!FjRLKn4t${-J$-oRlrVf(mD#IGq9 zQoj2oGbC`L?gfu7XHX>ccb@4lfYNN0ol#kRH_m z;`e#kZ}M2~0J@!v&k!BWt`Fja5&1|wX8N2nv#rg8pv^DEEhZ5lgnL4F4V_ZR+Y!*q ztcx&9djEOOwtjB#C;em~aG8aPL{40aS5hMWIu`dOt|V3v|*M36cQZmevWz!*E!s3ZAZWFbFPzHm2Bx zZ+ek@x=qJ@a_+v@2r|mUxWLeSk__hIjg|jADaT8cmyAkML*y_E%0ZU3jY3?df z{4cCqA#nNn1uytMKGQ=UDZk$YH#auO@qL$oXMDzYeT8TdOodOCv4u~;?|w1@W~|r1 z(tp=l&Z5AQEC}bfE5A+YuAHj7YB-&FA;?-c$~Ar&bB;FqkKqlA1`{flaErqPdPBL$ zQfJ7(De5(^(QmzH_y)UWnqoB>Zr-twffQfkax=dVM+*W=4h$4W&$BXN#WTv(+8^Mz+*SZ9R>PV)%_)VOw(SJJ0|HAhk#CX z;JjLuDwXZk<4*I%L6vzg4XaeTz9H?AC*}4)4%o|sqjnJuu1b}-@z~)tSY0N~zPI*Q ztxC!#?+JIevxoBMu;MQ<@{<7#D+DLrDJN+HnPTpy`Boy3sY`pBz5NoT{{=Nb%D=HO z#OK;>sw@xeVQ;0JBWV&@NA(x^4$^f}jKVz6pZg~c=}&E|8i^g`9x5PMjYQsj)*L5!HKK0{)R?tynfsE*jGNCK zR>g#z<>8NDd<~YJjj0C>jT!g~@6;B7s_JiLMfJ$e&_=)GHCPg=nkP=9hRONvZgmfNvu-M6d5zStJj5 z8J8X4{^QDQ=6D3l=Z(f+<_jsWcSzRvA6BR8-@t4qRXZbM*=$AoAi(|>e^)Y_=xRqa znU4r-w7s+mkL*o^8*zC$U8QM{f#wW^tpDA7Tl6bluH7#CPmoydrkvtf*&m}gj@Wns zkfU4xcMU7&N8vnEjrvvX0sCnnd8a&Lq6W0eU>!cZ z%8ZVCK{3)iO+LW-rqgbM0@^dAKWLDojJWmVu9sN~RC4l`(xI&C+@rkh5`b|9mI#}! z@ib~0r{;>2Wu-WkmIb~aV#C%amS^IeFA#^_=F%9N25RPk%LhA>6r$R87^&SF49Rvu ze7>AhLOUpezd^s*6R3$!PAp4w7Rfg;2C<9QEeYR6e#?wT07uZQ}PNX^)gt@%yM0>n%1v7*@I)iHd% zSF{7b6i2Cw*)S8gTTTn8L;;a@-y2_#wu3tYdSmW^P)Vk|L|TzL{|F|Qqj{o*cNH=< zTBr)zkh$ha16XC!+8PyWSQEfSYrW=`pb^;Hqmgh9(9}#g0%J)c&y9eUU*xR`F1Nnb`_RY~ORxJ>eqyf|5QR zBH7<7)YPw#8FCjK+rbnrrvqLT-oF1gq-qx`(?Y5FU~r`};uh_?M+ceHFZ-M%);b+C z^^TLNS0ui*|5UD%P;k_>B9YFUqaE-szeD44wb;1uOf8yM7XQWPjR52oeg9!sj?b|hK}f1 zVT)#tn%Y4L{R3cEpjSvzDlFXZBmjt6p(Lo0DkO!L0j{;H4nz(^=grJie)WmGV z5QEWFg;SsWE}5Qfczzpjslog(nhXr8$eb3Qh4{W3YWW&G`{O=*q0-io zt;YWW-q#I6Y#s#V!D4Y2Q~Ep|y<4XpCI0Gmc;HLl(~9PJI`#504sTDw{Yk^GMHL4d z%$v(L^pXT2@;RB|9miAZ6Elvoq5mP=!gIs7s3T&xLY!)*pj?BZTo(6q@R9%k1dhDs zTzg(_40gD7W0a}K7OftnykATvk)Mfu8VGrGb|_a9bO2pTWs;6nKi&| zUDjn{7S?S}3&=SWsci~+24PrFU3D?Uor7h-+ig#vqV5#}U-&!cF;V-UN>A@?2+^Q*dIU(#;g<=CexyxtZCSrm9knL6z3 z^e1<2qR>~aP^V%f?)3`Y`#WZW;4K=8GVf@3{s%wZ`zMfq<~d5^_GpXfgKu*L=OCa3 zz=pEV$G7{n0uJ~(D;7Ya8e7bb8V)H^H!#b41xp|4C?T6n5J%_6hic5GNv4Gyryto;Hb9#FzSMw6!L@IE?fuyA@ z5YG0p*u>e(+T|0w0i36>AnZXoZqnT`68<3G(d-XM10ezA&3}Wv;0htd==>Ciu)raM zThO*?2OmC#(VL+w*Tu9|;ef3+2(+1TdFt}Dz2E3WbIO!}lL+QbwhdE(s)C)IjS$tx zz8}^7%V@ZH|h#9>z+)LY~TOv>_+ZyXrNwX(e9I-+lVtGZ4+)5Ew zu{No~UDZ9AAJeuL%d`qJ4Pq35enFEMicXnT8-oZZfLZm5M`VXX6!}@F5iNC)Q+=KR z_D=y%OGd-$P>dZV30phGqZF?L{i6|f2Lg*J1nBi{*Q*UyX+x242iTGGyLrs!$}dv5)EqWe$!Ys3Gf)%8Uz1N7Y-z;$Sc)xj-ZfKB^#A|_lFDc1 zab7tN7#PZ$pA8Qyov&wFip1vI)U%aTG_Z-WG3li{5}0G1v7a?v;w3!6>Hm4+aUm7bX6tX3dsreAPHvETrgK z&X=NL;`&BBucb||$?}|Pt%I_9+*2>sk@X6*AZT*;U4XSpkgX^V4hS)X@ylR6aJ)eE z?r^!9T1-n_+Nn}}IDdR)otJ+j&_M3`X!E${DJ{RJ}U%z*iLtoyw6>OLP zyP2uqw_6CBN>ZU5Qtsjah z#;&dV1$Y0RDl=Jr-}l7fQb)J1oMb4d^qbu7v#d8D*@L@G){Z>l`7k3ir(z#b2_ufhc`Nu$=9E-M?JWL6}zgrb?;|H z@2E)P!K;33QjyCqnIPw9ioX)j=s9!eV9fF$;vxvWt8d$fV3t}=9!s8PMmP0Gf3nQ6 z2L}aOb1k$yTwS^K|Lw!pL6ishvrX@`;NQ~;EWF%#Yr)9GO2X~z6jRYEb-fL@<~%oV zfpLCgj447HonscRezymtA#R?H*kCJ8%Y^tg*HF6L=fV~OjNA(J$B2j*pG@9fv7^x~ zy`cuoO1>)>(&a*b*vxK!gEO`d&g>ALlW6q*>c&1rGaY3Y!gV!xTC#|vjGXh@RFWXq zF(pHXx*(hM3Z;Lk19BKV!U>_0@cj)u`kTJCvlE-Z1uw}~IW4F`a&5FF$J}4OpN58t zjz21h!hbcnVd8mqO{LS$%Y~Let&EaWVJJy&9>>4{07o^7s1~Bq-^0TGbB~=uUfL=O zgxK;ZgPqEh`RAU;uoMjH>h{!8UKRe?nAAbwQ4ioj``&!BenaY-TN{)6q(WM~*VW zV-;4H1uGyEiZ&?$^$B|H`64?T6w>qG+~HRi4Ivf|!&;gxzd1Hy!AxrWZCC~{C2j zG8>w>;V`gTvwAfe0Pdu=1}mhqcb&6WlwpS$O$By^h76a7I<>$kR{KG>ld8E8Ay>@- z2$S{$#Cc9}Gsi-))sBpO73(Lh&>4_DjTFV;=XC@$3X;h;MLlc)xtpW7@ zj;WLDnoZ1vp34Br?1ZnLW5?>uPOpp_+_26i%0rE{L|T4VAFee8CB?9%BNJCWCuG1@ z7dQnafRrgAzp4XW;DaI6xf*Y~40v1g6NA9zX^j4;ftHYq^}Id^hllig`Ig;5xLM3qZ%X4>9gR^@c-MkJR0z`M@K3+4Mk2Cop;LXPW&#U(eP+68kt z$heQ*2L)@IG{|0fzq7-^X7Tbo^s**IUOVzi$2glS#~f!%gg@+wycE>CN!&+A{eKBuU*bCA*D(OQ)5-(+Q~LzPSkv zcBkN>Cs!t>tmyk2ITJ#z9$^sE$id?&D; zb@sd*At}dV&1v5{;pg00i3;eWu#rsGC*fXu(FQn?-VBbE2^%X;46&CcWlA{-Yyx&x zd{+&h2W|47YnR8_KR;3*S0+j-49%B?X8Mr*%KZw$@HbfXGO?%Z>1tZ>m{cFC&Fs0$n* z940^37WXs^>h=mZ4RDO&3l#s~R7aw6+as`Gygs?xZ~{uOooC-+-s2(Z&Ii5fl+Up(kStc)4N}_ zV)M^xTd~4CvLm&yLJ#t{m0RlA;paI}Uo$oHBOl#jX?7 zC8Y_?qe#dR4F!nHM?rshP3GcOhT81vcvf&kcSAU-w4>#}{fq@cuyDoSIy;H-nb*as zR`t40uA7x`+T)WT>M#D9oWP)YgGvt9JV1_SM6+R;@~U;86E?F5OFf*~Fu%QG>J7_h z*q?x#w?MFi$@b(Op78{%Z9UxCXurawz|e5qkHrkxWIgJ81P>TR5X$M)#s}5%^d{)zO#9 zHOd(^oB#nK{n*QZlmv}1c`)WdtLksy#X@bs*U(<{b`NEN&KW{p~cr7sf#tJYuf(Uw_UHn7h^BKl8ZBULc z**QCv^@rP9ntRKGvHFwDdfdH)_HHr_$%{G256LbNnq#&f_H7p{YK|2zu1r+TFHO>w zZ#JV(R*6SMT2@?@vO)O1;!0Oi#^H8H&-K=#ie7s5fHJJsW@|jLX(0?*0jn?d#E!Xn zkkJbEEMlkSks}6$wUQfbs+7?Y;XhNEFw<~wN(6zLwcN1XYG27LAV7~Y|$!EBeR^gbVl)ZEOd zwAzksnD}0~z%}ulD8?sQNY`-wdCbzq(Q4p>5#4&^1%_-St)!7n2MpKkc%>~1?Lw-5 zcBCFunu3vWNT-SAh8wLaN}ixd3HV=pmyZ!@ZlO+T%k`juHyO-s1G6ZNlM ziDxT@9Ff4m(tidFNytFm}np?re0?q}6s2pM{fgmC8dceK4C-&w3 zWyf=hm@NFHbZvB(obWVLto1Uhs15oHqW-cj2Rs@FE`c-OaQOwJ%aILp*y)RQg2@YM z%!Wr3_bvjhN`pE)h`FH!M9@OQ-rIP-VimbHijb=Qi<)LRdw;jJ|27f-Lt*2Auq6D- zZ!BK{^w6t(W!gIKBXr$_aZ5*V2m}BF>eJ8w7P;z>1#QbR&)jff#E!Ly9^^N0&{+#v zsVoQqLBSN@kwRGg$a~6#T8l9gZ4)E=^Pk@5dcua#G9Z^TQk=ZVX8SFJT$BKTAQYBP z7^EgH_ZAAk^-*3$76z!ninniLQ8#6Zvo}io@d?zyHVmOs*^J+L?UkM3K|~oe+TjgF zVj^;Vpg@pUU&^Z$DHy?71t&6_j>T?KiMnQelQt@BZFN&m=jb@GRAjgeLD^I;t^MtTV@p(|@6&53-bZP*&He zs@!cTL47&5yGLSO$DesV4n5dg3EuQ&2UH6ohTkgdi(bm0RQ@1|amown+;aBFNq_!MAbK*Mj!tB=a4%`I=?Rrf*bg>Vz zdGcyAu32exB-JMAsGY%?E(t^WlZa)Ed`v^7@?EWJ}=yXrYjX5JG|27-r)IM;@AEXq8}O9 zJY${wUNPsp@$Fv}?P0o&<-77pk~NV~-QIgVI=H*-Z2*1Ry0W zcgX&Q8L~Mt%=?{wkoc73$gosQ20U+*m^^gs2R9lpK2P^#hL%R~x_0Zx9PkQ-MnqY; zO*UG)Z{$}T<6fu1TVG#Br?gDpHf6*4C0gH@^?5*j0)R1>NaI^5+^#49;OgKav%m5T z1kq}X$kuih4bE!3-Put@mb`iqhk9r@O#LftXKZ_}h^G=*%L=e>INg#5-2a@J=wNEd z0m(eXmfgOP)hW}+ef0=Jg6gCM-yG4v_{r~PZnMcGpsB?f zOFy-L!WawLPStpm9SE-T{l81Q?Id+pcU z+g7X%JSs~)sN7mw^t+ushSzM4^ahF@Kr`l`#t+2j$R!+YT`hpKoHHczL7a zQ`;VTf)-lK<$P0Y-oGwYp;Q?sJsc$5IRguLjEc<=qCr{zQkyI|R0S{J8&{DE<1|?ukhh$BAm+{e{e55SUS4{@B@#e`M+~KUXfk$E z>52-W_IIyqHjjdB*k7l%={~Lg@$rH}npu z+6TGxno2N{MmzI2l$4x4L(&f=rMt)?ASC2Fi9(7CM#8Lk`9Uw~mC=8s+JF>*Jtk_t z(>rJwX_{do#&zVS)VgBdp=K-%-eyHw#jB7!qv>vIGv~qyBTtwmyV;{F&6oy0=kBFo zy}u61NZ+(PI-SYknQ^#!BAPCp^-^W{)m170$c+frIDQp?@E#XsK0-2$kI21l@-4a# zpnb)j-g9JA`D8=OqGn>rTJ@1uCcu(A|0QG!3$}tvdmJEb4z;(bAP;{-q^B7B2E5_A ziMwIJ*E&V?EMxp*K~j&3n&@3cF!Vqq^Dee!(I(W>pt}LQl96$AGVzKrPX-C29f)%KKFYCCU8$ENc z_l20=r9|`&@+BK}Udp7? z*bySINa&T;MQbA*aB8z=HoE*AjyNf~S4E;Quu&boJ%stbzNf0cL-*u&9;-FRwStO{ zZ@f#Sm%PAlm5Nv^Kj2{}Al!T6d0~A$lsn*$f=Ll(<`xL@8A}0uEg;Ui_BY#cv%w1= zjFw--x~6U5Isv%X7HHe@NwT{>}MLOLc9?QS9Ku@dN#J4Uip7S?fZAA6R z%ZNT@j1L`39oSgdf!^xB0Mb2Er%7P9kg=dCA*B$cRY^ew)y2105O>#D{I>OZ3dTlj zFaS|581$IX*9xcrKrn|#r1juNIgH=vQa2{8uBHB+?#&3->sIvE(2n&D+f%|km03LQ zuCa9ASsQaN-1nq1uc#qC`0>6?jWk*p>jAxQlQ{c_;VTZa>4vMAa`Zj4|10l0Vl)XM zpEG<5?=Dts#kj-XpdP(%^lKb{(xyLJw>qvwg}B^Bh5oQa3y7fJvOg4dr$WKVG_qPw zuzN*tes!L#m_Avx&hb{!FPz%+&^{@cW`s1fm2DL-MI?ojIpmhec!kk`my^m?Quo?7 zeYq2QiNd{ z0stRKW`r3-TQ1$=_JfV+ucVo)Eg4|;ed=9&5O34WrJDo5` z=;Q<9vprtic!)zIM(!ZIJIB$Pn~X3J3p~l7Mn4QiFat6Vhz5=%DLFySj_V_2C~V|h z1nemUxo_7xOTHn=wdC0MtIOry?Gvyk22-GLL#~QWU{svIi z{1kdcS;b)}Y^|~0!F*FQu*}c~3-=;_A8EnU)jNB=`~M&kj3LJYXbhNCH2+Boj@(a~G~N343xB-|0-8ydc(ljCULQhDxLL4RO7=>vGn< z_mE05Z|){S%4q`utbA18@UhdYzC(*$sv=@A^*(y-FtGqS1=9Q--xaI*IY|~&WqCz1S&Y2Yi&JTF z=NUGgS;!x}&&2h>lP7BDE#ClSH84`EG8tIUR0mP>f4#j}=18~RC`AxZz6jIg-#>{- zy(DqVbDpUj9XqhuQM$%=G0y69z#Dawdx|;4H!Nguj$UM)rLVlZX1wR=BId$#2XUm? zD^F57e=q=F0fL<|BnBl<<`qUQ+Tg58HwjJRiW&_ff(hj$(!~GT#NW4nf z<6NP=&bT4+!gt3lwVnav+$VtzJ*~Gxn1DvpINYvh*`jDYgf%ZWJ4}U1$*}g`jS8q7 zraBgW9Z@_Z;*pu>qRbhI+4r}%C`wLTxgbAiG*YNJ39_FRKUhm~|B2rtXv!lM5rabi zG!gJY3!bugql{WFfA+2@4_Jlx%5Z;IK)^JNVN{QYGmY?Vu@YOYrOwR}d>+mSbn3)j zV)bZ-eml=o!iW4uv&PnwQV2JjW`>{FZYOv^#fTd$^lZ4{ry!niZjW@V@q!m@M8QsU zY~}i~Dg5Pa)*P8}9dtqc^z2(xL+jPo&G~ZgMFSf8v0)nG6g48aex+X1cUf=awtQ%O75J@ZRu(Lpz}L&Zi_X*HtZ|N> zgHmJq5Z5Uik}4t-YJ9SiuFmb+oHJY4h-7t>g@|+)sNH@dT!FSn0r0%KqUjO67DvEU zUKWX@1y!W#sy@ph@eHeo;dnhWGZ@K0Ous__^43OEpL&OOzWm;%Q8R`v>6c+&fL&=? zJU#05fq5>ORP-!)?7CmB2KF@ymv_N6YU3%#kDj4fxHk1m)Bh)BeZRqDMXHJcqB7?3 z+*?dKYaFx9R=4H>h_(USf{gTDJBx5KR&3OLl>38)(nb#f?U4Y2Wztk_7X{6MRL9_ z3lS@<`cz8{Bd{%ee<_!x+M4un-_ur?5PFtj!i3qud_u(~qna9PT?|Q*CElSzUetUN zg_+B3PnaPGULWR?#U@k}3<{{ru7~;Myi>CE1R+y%j2oQPbu?b2n^y<;2s=ok!kWfj zlYJ_sFx-t7Z;+I9?#d^XmZZxgZtcD_%uSGj%FqSL=r(|T*s;j@|3FzBi1bS?)>tK9 zYRqw~-^N8ufbKg6y!iJqhgIx#8w6X0KO)2)WQ)+yIz7GOQPGLzAei(97s|=SWv`PK zTm9txXgN!5#7R!d2Cx`#r7G=JG?slgLnAm8-&ohZkY+Gny9!@|XTmcotI97U3&^`x zPN+EnmpV(1uZ4S8Dhl#fXtvT=Bz2_pUOfbSo}MDp4V-4|In* z0*0`_{~2I=DA=Kv^Xa=qpf!e^CUzgAaor?Ll~vIIqiQAPmcl+A=_Wms_TKw-7ro%9 zv<1~r3A47f9UM;2VfKiut4(QLq7oy^=DGNW`+$_IoQ2EFWUz|P+f_c19sjXK4r8C5K@ zDSg`qv}k?V8W0kcFRZ*rC4yCc82hIX6>4qN4|KIJSQXL{3p z*~pIv9L=Z~Tp8T*c1=qJbI$2C_E4%~UrAoaLwZxTYzref{mbN%1R zM4&OZF*E#?0J5+lkeehl$1t;)D(i#C;$Vf*IZq&*{`EZ)wbp|kypF_0L1qnNsk9od# zv&G$eV}rTndNA3_C~Bqio&yBTN8O6Y)M8TTz%x5eCz~L)n7L{=S&X2t$80Pu8)sst zZVMqQOD^Z1+a!((9I$s1n|r3B+5ChARzIPUv6`=HQto9nUN_ZTm>(%**0pTc_0R%Q^ac zUq?EP*Zts6iU+)jKll2j$t^o=jLdrZ%%6&HkK`h=eGiLz`~n(aBTN%qy}k11$!Ycm znTR6TSY*m1KIms#x=fzTQZ8H&FwIWk`WER_od7)Sgt3x5Z7a8pMLx0!ch^PZ z;&>~LA@5wBOj=9D#RHY^c|V(X6f|}ZmCfA7%Y93<#G&`c{+Ju)04{=fhL0D!yO zcK1j}Ni!z55`9P0^7y~-`N5?9ji&&L*jO_V2WQ1z5TBZ)t$^*C{$LXVXp!Op@TOgi zKv|vP@lL6FOCPdcgNbt3IZlDlpJeq8zQ#^q*_Uc{?p;`{{UYmijm0o- z_FY||*}L!-N+1>|M#@4DCZX)pQ*%gZ$xC*Vuav6v`?*!YwAi+wz4x?!y-tUO-2?9j1$bJPr@(}5)z@8?D@Djt zaw_z=n&Cpqf-F%3>b>DE0DWSY8xG()rAcSNiD>zQ#;J7OqF|Hpn1Ap-!)$yQP34A$uu;)E2yx9^&}Y>>HDCKM|azAfslr zT8UCk7MvOg>uyfE;$T!UxlKGH&5LE4Gi$k7T|Ki93C?X-c!q<}xPEo0-yQ;Gbc<4O z$^%vx;myUyuM)c|BFkx{Bd0s6kP=7X!jgR2z%Y=QQY17UXK?b;g*BTsiixoQv_ zh=%`?pG*alP{PC-+w9X&w!e9j*Ts{&j9^r%wVk8wgSKsl*sOirayrSI?CUI$YGJGH z&nJ!7%$#?5`rhyJoI$C0>b=B=Ou0cscaXk^RTw6q>5}XmPa~36C9rFO5090y<8;|EYVW(q~aPw+eK2yy@>bwws;WrMjF_N@~dokRId4M%5bK|UV$sNkuDpHlCR?tWt2Z)awIcXOX^5JMdk)92wJw23}fE7 zqA|3fZP^9N{=Eiq%!`Md@_=1xGuhh=S?@}Cd;Rwub4$N6{{s=v_SH<7h)NX{1qPn5 zNjfqEQ(!~EHXXn*Q*UC;Xy|!^a1*pFf>o;Eh&YdtZs%TE9bbT)b>HdVIbq`$ys`Fn zr@=H;X|Plnac5+lAqXu^9cT2Xk%oMBp0@vV6-DzynI@o=YPpviV5_%>46eh;I*W?q z^?@3l40__tx=bYZW6{2*y8jZnEK>eu827BF=igY#WeinwCEdkW3=bX~!(UA9&qCxP zletci@p8XE2cQ4}1FXt>jXJ{XZ5-H8tk?9>Yv~JGaCT*U(Jyhw(O6;1zv^VGc6uov zBdElAF_e&uk+#O^-LMGa_S6#?~E-kxm9TwxrqbM89~TekufvXUPK|^Bajys zv2}R5_iTLZARbX7aZkgBF9Y4rrm^8& zW>*bPS^N@k|2~$cwTo%9qmYSo@^s^6I4wXCeI2z3ep9R1GEfT>bxC8@1hG7MRe_eo z*?D6udN_ZG_$iL4K$Xe6rN%t@4~AqtKd&qtl+y^#F;^kTUgYPCxrV{}M0RktT5x1Eq&0w#aX|xxUqV@ek!tzM3-8BID^Mjz8Km%J0 z8s2I$f*y@%&}^UOPLzm+f^sR=1%O5vCFL@h=gZOH!~7+UJo}fcdqAbdOT87A+EFzY zvuvmqG5Y?ghGii!y_e;Xo)A8?vAo8o|Dq6(V6L5m^b^KVTT@fdAA|isq}=dY1D+le zIb_oMiC9swz(S$`T>`@70LDDK+%k{6iF$ZSJo*dEPTfw~_L@V+;ad#ZtBwgczbZzj z|GLN>FPgUrTYMA3n}SM&Vy>({z&N@E^H)N4g8i1^(lCXM}{nHJviB!d+a}d zB-9pUXO;`{n|uGy-vq+$zxdB;w?ZE%i{Fe*_G11W1Kcv;?MC3eL~dx66N%U>2 z1wTWJKs+j5eeA;zj@Iq`clOz2`5^6AmIWm%<>rN!J@D0L>d_zvd6p@CLM0$}q2vww z*aP8Y{=|TNVJFslnc{*i=A(rCya=%1r;wBj$qDGlIraq5SXZ3FD3|eN^^R_^gHaXe?DMb+Ba?&Pi zWu8Ig`i?#(J!SKmrD_muM^|VAW1x0{dUt2LW3RfNlc zobLYrVXc4@o`tY?-@KJwHoah!^IxKW8d(Th9cfaaCb}Pni?56Xd+yzJ()%0Y0i4h%&%S3&2b^cx=?2D1v6$ z(PqUQi(?v<&xRwo_lzmB^3+HN2%yeJTV4Qd-!r>P(gNOT$c=gkgL(7vN3KbuG*6%v zBh>u={66>iL-2ZC2dA@^(_B<&W#WEz9B9u?`p@?8XoE!_GF;~n0+O9PS4~`9Cm;X- z8>UCAb{}xWU;Z2TED-i{|`3osTg^;~vLSbcCsb_g~hj9L4eZ8?E2qZSLP%iYCaj-xd?2 ze^tL4=<6s$5luePL)rFH9-I#2OS)!mqFr}TMVgRvwqMI7Cv62s5hrqR-CsvFxu(o0 zoq33Uw8QcYjKN0vfkqu-vThnPeNmDk@7302{AjAk5ztg=yfLL{F{gu7;)X^Dk;ca+ z52B+Y7e5a&O&&KSnk_f%C-5`p1(A(ef$~LiMsJrX=f4anh@zywqtQrf0z~Lk8le{K zR|#)aBc=f!m_oK8SNjK`=;%w)on&5)qB-7Tk>LQ9J%i`%m!%}`D!q^m4oXA zsiCb1M-94_jm1y8oPewEZn!$l3I>)?BbsVrxrI9OfeVs-(#Lf@oC@%A+LyAgpW|dZ z@VaFt@UO4BNfztr8Tl~-s6~+9kj?nE=H(%&fGJ#hiLWy@qq);j)(zcW_^DL?ap8`7 z7pw{8%G{619fBwM2l0L^HAg`;c@LA|QzMO-Su15bR_#HzZ;oc-236Eb{Qy+H0$D{x zjO@0^#mFoXikQ1nGrc*k!|*#+A3d<&vzQ`<;I5RK$prqx+C*?(JXc`WikF{tdXWW= z;vq`CkJXr5t^CB*3oMe1ZdwFD=|~8e@oxw9A^RYCHIuHc3q{SVo-QxYL%_AxNLwI! z#9~Xjrjnuwbqb~-?`wvLN$%m(43lRMMy__-w zAq11Z(sK;FnF`QoQM(=J8}(MZ0fNK+dO_E(3-UZGT+*XBfMXZs6URJ@C=+t3-h%Jmo1cqym>L z!A~QRe~3dLX3`bPwdIJ7uj}RN1z0f#Ek02BvjoMYI-n|onCF+PoPBDlPBlAsTF#8* zC6#%1Qlofu>9l5Fgr^Q-G_G*Dh2zI}f#ac?pF9(LGbG5GP#1ciFtEqlzf%k2-B0qa zu&p+sY6^)##HM}F4tsuB?2)3|8TB=IlX@ieY@E)R`cv^4d_*rx+sz7_ zs}<3uuX~XAB}pc7wq3*yiy)A)nb<5IasNQBIz`U1=u<_tblzm=cpH17lRTC6_h#BG z+9`M(GIRM}0JB&IKq#FRY=nl=Or>+9v&O_2@Mv|Dbtf6GVa|-gi%17>KmnDMQ0JFR zXft}RH4JY{1V!ZC(Ey<5kqv##)N4aSCDvM|q#jBk-=`vN9~8{UMD+=mj))S5$1HIx zNOjjJ(zwcSQ(E`uF*9x>FOC*m6VPSeVZRb`K(A)*R}Y_BFd=8wEl>QUEO&w0k>xUA z00XW51;|EKjY!QVCV^l;s3^G_|9O{3hxWpV=9cMF2OUSXxzn< zD1AQvdySTU(qq<7x)O>*H)bDu$bT57@B0)GKY)%P` z?@Ei`Mo3;1*XnaL4X~?^5o5}j_Vk7#$!pPVe*ZH*3kv?dsLgdXP%2&i1e1t565gf1 zN^yaF(bW$O(#Inuaw)~`g=*yV_m7s}`veh_9tf6GV4nr_;tnSu#Fh;_EA@DRJT|GX z7vILgSVA%YH{{ei#-*m?Z+(&8K^hCN z@P0ALgBPa|+B^2{53i>&(!j(V+7_)m=`A^cXyp?Zq$`J+=Be;&PnC z-+hKI5ASrt`?< z9zl~Rg;^ys-V^A`)JMCsmzws4B1v!Tb(@HVSP!tvpU&Z|C)IlhFFkt1%tJzPU$046 z+E-qKD`Y&jINld!puZ#_c}53DYXKHyC}iaV-4Fpnlo;&&>SL6{V;@1hR>nHA>e+Bs z&G~-e026QENG)+CcW#viT!oQL3=0vAXnz5sW8?aaU9}W5>){;^-q)%G|A!mBAf%jA zHFFL(AX&Q^`C+fBiA{&cp-;58MG6ZFciX?q-H0?=OqDpoC+Lxtd43q(kwB$~sDv%p zmMqGeFN|jtkYlG50?O_hZb&-e|5b$wZE(53t8VQpludTU$46eHg6Iq97%*7a%#J{h zE?%# z8(uLO=3e*fxP@{50001rh%Tq2v?*{PbRU!xUao8tjLF6~mqKj)Xy$6ISPn+}cQ7UMR8avb;m?l2)Z>BUk60D2?TdXlJxH#BYO=0!S9qhVapO6Pl z{jHwkY`&NIsTSyH?N(JIL4{a^DDvXhqQ0n+Sf=_z*%QUEA!ZYOfP9=onYz;t7e?|L zZ#H}y}WUCD6*{L9*#eBy27U6%T`jSPyZXM z%eX)WVwko1roE3*Ml6VDI*)UE;0lm^GdguV(yGYJEry+w%f*&3h@X2D(72r38*Ii$ z{ss8-oEzB=&M%A?8Z!2>p^P9|W%NE){@cm!jsQ~dNE<>xTEEh8Q}b`~Pm3cxSYxXU z?cI#XR+Q8bR3v!khqPWESe^4OpL?!W7mk{?j@XVTI4<g~v(v&uweYe3fN*V2-ii#0#-Cb{^<`}UWYDTME7 zh!z33?ILw)ewO@IF-os z9|sreXsH3!)6U&0(Kh6KnGR&d);7T)Edx!xVseB7wGc5;y=_{iQ`B&zu0z#9j~Sjw zHHMp~m9_+I_7Kt7nsv@3u0uF+xZWZaKn{T|W>Zm+g2-xOy$PQbW#6^lv`Wlo|}5_@7W;U+P1<=zfa4r+7QF7^jA{q z0{%jab2;fZ@S8&?(CaUjnO1FaHC*HB!1(JPQ|I8s0ZK&Y|A0UH$q9mn%TLat>zDul z0DQ&uC$d6}q6!0j<6STd%fYHUHbsB<}qwwG)S;ZZ=_W1du5_sOCbiDM4ixp<# zRitX*e&}XXN2Ih`#1OzFB;BJCkjTvzl8j@;fjhPfsaoM>aposoWL>%3+BF4HS>!Up zucxaFX(nA;+c5HYeSf?E&J5YpYZl5!5ZV2CR`aOVcd?5dgeWk)D@a-Xu}QFUapi43 zHskz-au6LCVIP7#?b`AfYC_>|KSN99W7loNq562LU>(IC$73_J+OsK*(pfW>MGJlT zCU_|pk=K_vt#Q`e{POd35Q}V>#c-uL%zb*qez=hkXqyj|KswIEY>S>?&v}&s8WqD6 zhAmojxaACueQk7xl?jVeKD8mD9jVyAe*j$3jzt~>i)wxN*TW;If{;5%aLUE&p$k`c z)E^Ul5LSd=jw_pc`-7)`LF?5Xlte$3@Q@)UQzvyv*?phR83O!_w4dp627>S z`OK&&V`m%dR*o9XkU?j!>lQg*9EC6FfB4X=9z+7~Kr;=SB;S%Zpg`NQPv+u->ji15CnM~oVh&qyrJF(LP_lNv6&n?JxY|1rPxv- zBgJ{A-6%Fh_uJ?Io$cUD2IrJ+E)=gu@EYB&S#ALcp+wZ3(m}pQ6Rv(x>SK71ayet| zN==9HCV6@o@`0|%PKX{(7-fC*SdYpkz#_F~jNIGxO;$+Z4J z+e0#mEq*_6_jO}3ydTJn)Ls2rvVv54`b3gGkm$SLBN+hTji;n;LiG*Hsie2dg8gfh z!}gFmzta*2Sve!)R?{hjyXt2TquMVDJj*<^;;2-`(IOAqw_cC0TnCbI}ln0`U@C+|B~>RO%8lp5!2JcZBhv|JHnWt;qrxJtsjqLYgun7Bd3M?x>y zZXnx6)zMzkJz#|bSMu!SdqA1S1DqI@&-s?kZz%hi=N`BlHl;%2*hrg70f<^iwR<|+ z4Cx9E8I~M+|5AnT1>-aOL>f#5&d}qpU;qFBye9)=m)NPN#dEquX~#R0O{gvsp^19i zCD7DW2Mj-2>yj37SvWM{dYlPN6!M!aXWY%*BVnzGg?o<72j2a$x8AQG%3RrlxR+$A zPb^-&KV_D{M^u)sflO{v8g}0Zl&&pJ9dA>g00OFfKD@#^%z4r$3DRg?U@Z&tJ>KLM=Y|4om6ZNVDgr5 zd@9swxPx%5Ph~n1?t{DaF0fDOV4suf5usO3oGQS8zJ)h!@@^IDD4w2C>Ic=I1tRp* z!NwoWpjVNl9v{#O^NRRAj<2A}zIodfpKNDAQ?yU0@m8Y!-Ece^)(pe91+-K=2vXI> z(t2=C*O)Q5QH~Nd;Eil9(Dn8(g=5e_Kb^0-LyF`F*HJ$;nkO+uOLfb5w%c=$Ld(l? zqBr!)<$FZTh0~B%8WSHhfZu)0dwYKQ_6*HE=F!$v5L^HSX22Dc$v0$uY8TxBX>;$` z&-v&{jc${Ii~t%EkqG#su5*5$S*$)GN*w+%NeeKQ53Ql`9lDHQ}8~M@kOilKk@YHxI8xI$9baOYUN548BjV*<5 zkk@_aG4J?ywUS3XdE&`V$-$sCyRTm!W$FhFS}JjR0N`Ki$Kxe+3Vp0^lm9!h%PMM4fIGufT(3%P}MtW={CBCf?YPk~$Ok&KWlc{=ga%B}<#UyYS5dFMmmG%8eS2M{t7X%SC&6x5q(GfB29IDrwwCH7rqr(lvOlBUlwTk;=&TB_M2#~ z8%aHFkjYhF#`oV19FuZ-$I|!w$s7tM!*b^*jNrZ>oQ^1qTjjuK-v7^jE_`aBXv-*7 z)%~gse&8{t1~d(R;o>|_&s@e019&PapYsD(orfBPC%e+DZ*#IU>Vi!=v#G^sqC}mG zAwG&8D$DNiIDk)+DKUnS0bABWNA8TO?tW@Yq8N{2 zjyL`J`&IFr%a+h{QHIiy#xxhtr=RWFuVJr6aH$f_PGntB{Uywm?Ogv*Fc&-OiMm$^*P<3-bRFWCcX!K_+9X=lSR zP?4k7j=bsZh-yI6Qge6v1Q=XO$(AI8&}eWUdlLc?X%eVMP-e#D>;AmdAt6T!l1iw> zLu4IO!!wh~#s1DMlMM7y_KOtO9Rm6XN^&H%7ccZqUM`9YiZ2sYl_%KOk9J2-0C{i8-09ccq zD3aiOO%fX&;lgLtMDbRWu+2*uXFcy$M`CC-6-e6r|nB0-I%FkbHUu!BBd|lt`j>1U!Wd~SN34wA& zPTRPa;|J78H*Va59x2yA9dFUR;ule?tdZN67OIuA1C+UgtgS_@D#7IXtScCv=;njO z?#z&%aXj((%^4>jihYO57NON1yumHm%f*`vFzOjo1RvHU-<=T_i-LtY5D**lIh%v! z-+}dy;tvL~!GM8=+8S~a{8;{jHpMi;CsAm#e)-uG!*TuJ!xf)Yw6)RwE49uZ=o6r2 zc21t2DSNll7rP0@%;_iEYSB;w>WYyZI)HH&98g`Y|LWEZqe(yxC2#C6B=Vc*AV2rV zbF^6?0011dZFbuy??~i-W@Fguzc~@Ww_&$QG#bPo%{y9_EG3B!N!RVna2%=rzk3c% zA?KY^_($kc;j5IP@$nDtrmfv;jLQ*W>mi;CjtJhaUN4okaoTW=gpp&aRm}1m7^B}q zsmj1=WYfYzFWQ9*foQ0GZ0(i%#T_iOG5?uWY0-PbNQ9RZnn?XHN9or zoA{R+%d*bKdciL)%KZ$@u5>UEax)Wf&eU>-Smp$tw$SZ9wfle>!)TFU`TJr0mUpO^ zP^|1*E_m=|=*HYS3DRx%eoTDgR{Bu?KS>j!pujSy`6^TY ze@a|10p@!xLLN9C+)C@tNp-)r#G=*;*o7AN_htSfF0QIgo{dM*J&RqKrwQN@D5cqw zxuEC(01|X=CRSDqb$LL~Yg*>Nhl#iz%+J3ku#gkoRp(&}aMgtR#H`@+o0P(n{X;<< zm@={s%il*1h0JmtN0`b4 z4qOS~ddELh7q4UTb;4;Fplrdf3Xw=$k5+6|20J6lafX z(XT8+=kaUcfY4y9j0dIYI1l)eCVymCwE_~F>Wfj@*j(F9;jp@65UJG-HSc=HeT>d* zQ2AOGM_4OF+?>R=E9{DDiMlp>W(tQLC{re1Y(?=%c8E&0F3}l=32-jyNet-jjIO=} zg}ltLWp_j^M%R3q;vkQV6`9h`+X|DZD++ATJ@H9T-l1!dS5-){uAfbBoayYF0R2K} zKMZX5nxW)!-NG^CyC1&kN-U!M_hme_bu{?23cPlvnRd0EV2=5??H)mHDF90EOMjTO zn!d>F7YPR*K77U<`+IhPIj;WJA1h+jjfpo&{L2!nk`8*UMSbaa628 z?1p$2h(F!UogmRv)kzh%$YE!6PY-{!S+US5t-(OI5$h4IeR&iO`vDRu|LcO|-_Nif zuM+1mlHm;s1o4mxsBc(FC>9q%kH8pd~8jeT;DMYE}aqZxzdp;qXx@(?V znq&g<_NGysAG@jC5jmq}K;`K6^Sgw0gqcNaKEx#t$XDb1`j|~P2QN4_!x5}m7&WoH zjh>Dhr#RZ*hD#y`d^7#%=h*TUZ6sJeUcjScEWSwAD0zS!lffUeYsxpr^}Pp3<^&uEl3e&v4uO`F?VIm`{v-_q=Z0Cp69 zYi4G15(s;mk}v@xe^OpcINqLT1yFa}^MuiXs1&pv4z#&(_u+f61LX|O*ckjFvhyVF zr$REV=%;5$M#_!Gx`_|8IWZ18R6T2p6^vv(j!3r+Tl~5-$PgLJKW-e(gbq3rEG^iJ9=z;`9@(c7;k?IFu zmt6=%B87LBQ$ds$K@G41{#lgsG7VBb3LoKpj|#T+K_sgk#N%UjHNsVGhaXPX54jm^ z%>!IVhNZAa1nry7B_ z=>)a)c6nMkw?=-Bv4|;qJ9^c{f%(+`JwytA5eXRcbFaam4Z{J_G(I>+WBLuoCU2_J~+W6xqbD-N(MF5X&W(8>c@Kn#&3WwFD?>8I{i; z3oJTGIF~&sbk38c(_G5pA@8~v zi>`r>vM_I z#(+Z=vS_ONpb_j&4)=@=BlgACAw<|#`=&yFavVj2JY&un@Y;rQ(m74yNO|m7h4*wg zgXup7#6?6wr#z{Tx=zv+q`w06w9Wj$jM9caPdmts0jwGeLKT5{S5S~StPrfnz?nav zEXk23Q_owOp%aS#RZK0je+^Xj`2;~bo_P0#JP)C|_UD>_RWy%bB$6VxqX@%?2zx$+ zBk+_Y*WS9OdAY*xC{R$~fl-T0%56PxIW2`)8oyH~irS*}CVtVsq5NACuj@#DQ8{s8 z+%^zzz!||g&P*LsA)#@9=vtjDmYrryoRaKL;)-&nwiylz~D79n}_!4*T7@k}!Zc*oCUW*H7ks_i~G{iSFcf ztz>Nf8_4ptpK7B)R0Cm3+&;@7JP|;GPRk&I4(8F!&Pp|y zZOg{Ghn2E?F^P$Dz@Xp4Kk5Gr>m*z$HdwrDJyFfXXFHTQpWwA7Pwh_hEdG$lif@uK zZp{Mo2Ja~8IZmBKctBN^4>Yw8VPBuWJ^`PSh@aihfe7#C6Nj3+YM@M2z^4*M@;UTyA$RgX0#8hHquuA818$~)^duD8o< zga!7zaWk@Yc1qPDD~*Q`w7k{@0Z=7<_?}n+R$2RR#V4c?=-^jT*Wk{@EC;pdLQ~!cS?YXmE6_M1 z&aQ({Y+gl&*c=u%j2+a$fG5B5cM}HVSqw@OogZw^jt`Apw$2jh|W$aMFo1})w zAiW(z4|p~dU|sDh##dsf;t40ESLKm<+8-K!R*J;F`$@L_9!fB?MnwbKZ}f3-$z28B z`?*DtZ`p?u+`TiRN|4zR`H}eqhN=|sfkv+ zagYpe7hG9r^?%l!VwxL)$f*(b_pR5u?L;xQ&p){F*V@yQ!wfzB)ZNNUV95Zo`>W z=|9!)p~;v>4$8G2L1;F2+Ke2%Cblj?uhx_Rtkcte*D*N$gh;j^TC`~I$)yIZ^jBWf zqV$CjUqQw6AkWv4@KmFrq?(9`($AD7-Ln&6Y8c-CgGj6N@dEJ>NZm~_<%WmmvJ|l)oZ;wOVsM_B+XE8*0hpA;rus| zgdBi{yq|C~QVg=e#!q2_fs^)$&s_C?0eTD$mM3}wX~I0QEyJ67C*58vK+n?r#74JC zg^Npbm`{g#q1-TBtlAB1BQeyuSn1LW5gL9ZfT`3%c1(rK3scLqhOZ8O2_tR)pQG|Y zX}1FyV_7im9fRwJvxQ2>puF6C#cs77r7YbUQb@8&g+4mDfFnehvtre zwsn~ag;E*|H|^&s@)bCg+vz)1fs`=xxO27Nd8^WJE_;e)|6KB^1)*47!nyRxr9p(a z^ZkR~sWU#R0J$5I?F57vRH|)142Ml7ZsW6*c)=}%T-1)mA1KO@E{Qxnj zLgtYeq+9sz%j*Qa%|=Q+O-`Gk7vg+!`!voM*fiO zcTG{+qC(A5g3%ZnS7E7EP^rnuZjG_FJw#Rf6Ea(v@ya&j2(%BA`DehtOH7}%5RT2l z!2D2M>{f{Uc65Z^yTJ^fOxs@nKhjAo4jz7J%7|UCTF&3l{&^{u;>H3|^h+AA z^Ca%)lM2+p7^K)8DwzAI5A{7qqs}cOo4`l;0*ANxH9mnMOU0Y;XWBHgeed-?mnA@^;s@Vzvi`-0%YfL#u zhYXu~u5xs(Iz%8?+0|cts~g}tT%y4@8=yY9oK`xRR4yQJsxzoGQS)UIc!~qnG=^)MqcTyHF}RB=t<*y=5~;$5aAzah?DDjvUDAyv-C5rV@cWyXoO*^#_-l}uROJ@B%tor*VEqiT+)lUB6lx9$ z{CGQkc<-8Ii{+Hc<;Zb=|LghAPXIS^tB5W!zv+2x)(NTdT%o`L zk&J4W*V%G-E0ibZ(v`v+@LKK5)#JU7dgNbCX$C+Gm^*gfyQ^om7+I{T@p{)7EO^H# z6W*Q)?1LCFhkZxrmXh&MBII*?O0IhRThscA-%L|heg%qPk%(M|6xl@WTsZt3jm6tg zAZhkso4-t?m$zt>PpXmK&TA50GE?&dX+Sh`MRtaWvcBt{em13@oIn7 zt~-CukLE7r8@YoB8Q<}+RL;KLr6nltiDbb@kc!2+t$D$>i8{w_c)SnG$4TRL39K3| zcQZu*5uk1In|3T*^@ zUJ7q$Z+>N%#}Sm$+1Gix_#Q3{v91yPQ7z7>8WST(uH65a4Ow)!&Lph>ke#id7AAGX zsA%&+sqt^Q#sPgKND%TAyeBwsLa;P`|IBKwUBbQZeJ{b1R%LhQ%<1PpvjPzWAqWq$5L3VId-e9! zc1#t-&<%nh_Nd>YdFx%zXZ9Dwa9I#~SE%3m{o3QE%or_*6DkQp=;G$BlT%kc%sniK zQZ<~VThFZSL(e~LnI(u``vk!}wdbgjF%_fCjRi5N0Y2~4XK3H7SXSu%^5RbppWBL``i8&yhG=yJ+)k+&8*7V|Y=qc|6id=c ztW?TXO+1@HpVGs`@4hHeN<^2-qDvcXg$!ydJgcW*Ae~LBDjK{o%1eG8X&Mn1(AYvYfhQ{Qc(Kz<@wPM*dmzE_^Y)PPcrcBo6GXo`>;dQOhSMFzOo|E7!XovYe7qWw zt+;?yJK*HUOgQ-?@kt~je{nyNO}}_@t{{0PDCaqf^F_p?Z=S<9IoC_~sV`OmZ(1Ee z7^u||!oM7r=Mb!paQ;q-JWibW;P~`d{+A}D1>-L?p8w%7I9|MZx5@SW?dPrq14`-qiE6@du73d_rh<~2e& zVX{)Dd<9J5Ez|eLB)zri$KxPgn|=%qV%YQ}{9@SjBdob1DVn|#=pE1A^|R&%>)uU@ z_h#?LTXV~J-o->KB_<{%rdP_yOiv393k!{}lo%gp4h;#DONd|$OK5z0Vq$t)NLX5G zhLkqQS?{+?yEp7P{$g17Zi5CAVQH_7tB2N~znMR2K zJ8?2n9d3QLZpx+qzhGq1&XQaJ8XO= zt0Op%Jr?7SrTAk8csnE6Q8}5fK4(0QFbSYV0wWAK&nM5}lbmZ4i%5YtbHjCq306mm zJV%LSM}jrCtnU31*4$LjVM@i|N*QU83(P-0E-~HCC!d~PDG`K$#JE3-(SO2dW=bOV zo;&Bx-?3xvti3y%zFVV7(}|76pgpr@@7_j#-LU4WO^uf@(I? zSs@A%SW);$sgRn+C|1Ghu;#JH5}*>(R0hT=2Kp4!7$+PD;f!%&iZNzTN_iJ!Pf|=N z_GB%^xJe0CKeGjgv!jUcVD=Pdi@=kkm^HV!hkKOh;#_ZfTXT~=?{e~Gf@xGrfrl(4 zBR$P(vBxHvV@jG*Gl_*Um=t2q%*+5)LA$ntH@;sd&$0f?PW^k5i0WTYyhR_jT1#6{ z7r8fnAUz#DW@<#=A<;G3)~?@%%;~*jS-g%(hipLdg)JmEKiJ~|;=lRT(1$({7u8&e?{xM!`@e5f` z>epU4kT#?b+R|$@A!k-?(vWIzHcGV%ez7rGlKV^$M9VD|8ULt;8K zHZ;~2Z;MTjwWW!BNDiI;m`)`VA4{=!XmgT#hpb@dilPTeXWcw z76_h{!Ss{GY+gUDYFY9FA^qvWW*ukMJGOVgO`M~?=qV#PR!~}DBW=H!MK9=Jk!~URL z14W7|j;k}(Wpm4}560l5={BzDnC$vkb8Vg}ajoV##0;%se4HqyTZ7V4gMyQ|L|JTH zYh}uDkDfn%CO!J&*&```=(y2CrQzeUhe~2^dY+ylnWUnVR3%mE6?!$~s}uWA(gT+- z96HCS1piDdH0_6%R8{396NYBehURgA1QVNroSZ9phNOrRdl#-wyv>xJkR}$splig+ z(ejdUUtPnwTf*|!1QjBL7K$yB=^*tIHeAB?uS-Za=3_Z*)?9DhCBoEq1!#EByUe*t zCmC+70%MlJwKa4QiIrj~+syo*5g=9cmtDxswa`x(?~oabFEXxiJR0P7qg$H$d)TVtj< zoJ$=H0lkSvZ+{&4Gl>8l_55Zb{%mZP7Ag9;R33JER?vx>3 z7w=rBo2$~7Ki#4K{Cs%8unlu(tcgzpRkJahOw;E=2z=Y#N@z?h8%U_tp+ys1e*QHp zFtbi|DHg#!E6jww$_@2QgUza#=MV%BjW?SuMe`6~M1)u=4jx=cSZanfQR-ge@}KtX z1E+qZ1vkE@e~@g_<#CG{(X+puIN9|1Md|ws$UdB!vTfku@4t8M9W-Nl*1*X* zqO0rp?B2abjRL21E_^KqLC=I@_I(xaIPSY+)?w~h8-$T7NH*&*8*|`t9AU}fdT~?) zk$d^jl|i1#G2^Ur+b~!7wIcj8aEo9IwF;*AbZ8tY6-L%QTdBmjO(*YlE9V>X2%SPmr_Vl(Fv={<4u-F%XLij2$$4VRE!(lhB6c*%k@^EfomAt5N#(ZFKO zl|6?zAM;D_Cp~j+l)A7#$?uYD;YBu@$-)-t?psE~?YcUe1U8m*@psosshe|ynDg^9 zI{y@%{|uw4R`{p18Y>zu#0%XN?~)0OKe3`w?iDq}wQ*qTIIL)Hl*j2{{*;ic$byyq z(rUDOepa4w2<0DQ8dycmiw=!&leY>_ng8?k+t+Giq5b^f4j|9tVunTtQI9$#Dh zv(dXX5$V?_pUzl!tW@>-U3zwJoqOf-=bP4zZM>LXe5aur`inwG`i}WK(_$e*sEug# zojP^dnBK8E;xU^t$&NH#OvdD&IZ|GjlNaMsQyN=IC6GTk0+SJM=T`LuT8_{>M;g?a z?wvB$+<4s~tY=NnA!A&4I!R_0mStuHfqld5k{Q}MGf0SylSPX;NM=WN_KlKBiNQfZ zCDT&nAYx`JJu{ev8s@M3?i}A=J#AK_rK^IhlgvxAtdHE0Nt2ib9MlCbCl_;N@JY>FTeocS;s7aIO7dH{} zqB6BzU!DJ94;e;gpQQ_){6RMzC>1xSRK;&De@zle5ZV9Y2ilv?pS_@KYw{%c8aM^4 zZ>kNQYJv5wuQI&HU)OR@8dWv-RPmO1(!<-p---`^m+>{yI~?kD~vGi9d@; z^p$HZ+4?nUO?M-+)Iy9FJ0TG!EQeNS*`xp=43=uBBC3`OnJOEyI-Z!)V1P+GD|b4nwQ`ZmNzNEZGhvKll;Gl zIH=#KDfFMe=6I8e3HJCz#z@StGOm&zh0z=T z?4f@zmX;FRXMd2yaA(l6gkkg7ZB>t`R&`{TDMk6+D1 zLR94RzYhBQLjN~V#4t`Y&^^?xE8a?XMn*pm85xoNJ%?_p1|fYR%JGtojgfgX%gLTo zr0nJ`^y;~-TfVp{UO8)Wd`iz=Zg`cR-zncOc=*q6EuXxG_G3KJgpaAE%9EV)G{alu z5Bi$9KZxv_Ud_seToe+MxnBCA!53svA^($paqSe3I2%a0t|nqzaW1WS$>g|92Qv-K z#!wMs@P^jmI_0f$@pe&gU<|rgl!*lMMA>DfA6y4q(#NiDVk5D?YZbd(b6jlzxaB#x zrG(I4@r`C#5cQ(IP-MQ^#EZDa_kTrWN(oyXXIR_zP z%Vbtae%_q^s(el#ekhS@q*y*FQT$aI`p?c>>5JUct>_K0)Umz;wvzNK!$~7D;`u#d z`RmEh$F!h5DSpyTFH<;Qv!-uZGecCVNw0^jOjUcEd5{IAqDv+60FDt4a*AYbxYL#G za*6Yt;w;x#lf$(U329Jo6CT#%+7hIVpHvHf`GIyhey%jK-s!|w@vHKMkEQ!~87QPE zCZ2y+e{ArKdCZC7>De9YnPy{wkd~HA7k_oqf?M}rIY(sm!ML2#wU-aS8ks$7w4O44 zLTmK`22SFpN?Tz*ZcW|twP%zOxMJNmKo}>8ofWfs)Z3hG7CU1o7EYZ^=vxviW^8e} z=98Ty)%CMj4U3k&mm;Kby}z4K*LWw(R4~3y#H>w-D?JPF<^c2(l~OT*v2Ge!BCc=_ zc3pyusOK>cz^edZ@LR}cSO866`Qd^qpI17$^+($m+ zeAtA&MaB=oaOZ>ZOm36SWD}|HjG(Rd)7BAK+k(w}TEVa!8t3q_YSO2OChq@O_!DC)!d8HQJ?$>nXvUrevUkDX7)ZYC z4-E@TU@8IGwnDlaC^DCBXb9x}Rl*r8LJ_TC5p*#l(X%4B*e(b0#;%v7?=c> zf=M@(SWcgOmP;S4BCAL+=|zH964&S3Hy;v@Y~8+JRBq8@yLXeC*IL~q^>*)~CvKWv zz|Agv)aE&TO@x4@TM2xFj zp=^o-HACUsHc-4&-xgJr827jg8Jr?!LpT!Qu>#BsjPV>|Kp~>VZb@K1n`UG=NnvF@ zB#oFh>h)%Nsd_z!o2j0bIQ={$s&`T zMzBwFLG>h|k76suf?Ab#4Y6Ty7^~t;VTr|A45$Qn^W-98KYi{{G>lS-crtw26SyPu=kX0Ll1nDePvG0?17ja5~H#MD`F9n)oB_04Q&aS zD`9Xnc!rsg7}nE4s}|52@-(>QS&gF|qNm!e-Uy)4leIi|UrY`-6RFclKt258OIIeCMDjVWi zgFk0beMJgR;6eF;;b%@c-;c$B|-}7^AGUK4>6|6KcHxS zhASBK>D#>-edb$HudSH)5ELJ6B*AGI3I3%8lQfgh-2&=(K_Afv`P5NxkA&SI{p1F( zTgXWT-$Cmhmg`}3rLm^JVRXKl)in&u;E!lz=W5;pCbn>^OPIhZX7vfhdUm8bDJp5W z#bri!4oM{?X#;Yaen-DRR{MK$hSnz~rHx{MtAZ;|40HW1-Vv{}Rk2`Hw=gO%)X(_k z45Q-L)^w^iDv*Q?M7<`s4Hq0jnv-xEPN|(lh*VUZ{an9_;nHe$g%-HNd|bg>sHa}R za8iwz^^A-#pNDhEPnbOFL-97D=!DBM;}48LdTc0}D)|(g75{LB$kCTBZI)|Z-oUOn zogNjV%nLEMlDt5L9H>D|4EZ=r1IChh!eZ2xEwmq5ZOZ#+$1w}1PDtT2sTamB!RD}v zKI06{8$+HHJP`6+?DpK`_~&a}*AMyIt^B&kxZv9)8S$ci3M@)7xZ%OLYFpO36;v*$T9DkCnH_yv6TwNmrKkW7 z|0ppDc9pC0G&QHKRYmN!5PN6>3l0&?vU;p!^&7c;(-lIVt?1IN^?>|QUrsoFPb%$l zHJB7LZPx70(e=h2p0oQ<{WiT@*X*#S{fXT)d}%w|-g@s`?$WkiYes<_=-gD$Fc8(P zK1#4!-3nH{7FH$%LveRYBZ9Ut{F9>mhRKhuwBo~`-heUuyY>~rGC~#>q97d3207`iIoW2xbv=iEPhVLaj=8k$uiVuHcwAQDM z;W!#R8q~ehQthC0JmV=bljT1$ZSc~}&3`@f>45y(epv^OKgyq!z2dz(D@IKg6JC?# zF=Cn5!qDj?^~IJ$Gsu;xN7&d~W9+`5awsCknMxSX&p5)YfDWnD=EKtlu7v?gq`*lV znY`~r{mO=^g;(;!FP{RV5 zVB*z`c#thLmX&~57EwZw9t!}B*x?uY(}a8T{_}^cy6?xMxAmV^>$}ap#@onMui+si z{zZw6g$wCJN_)&)J8MGsfztZ<6N1J~06(#!?=5pE>Ok)+me8;we&RD^;3*~(To=4K zT?jUOQoZNSfLj>$8>t?o?m5x|K!o_X&`?%(yS`=1g?%*}m`kPaxqVxjxv%HGvq8tb zKI^v6IgU}k6>Tf~f%*~9bTgH>gha-$ajb}^QmkD9A9rnF2F&srJN zB1)i*y{m+(A3~4mDiBPH4Fyk?vohUrJ)jRP0hs;V9& zrFg)5lk)Q^tv|g168dsg5>dHPqso~L8=`=}pEf2la7{ntcN;5`ond-F{i0~cppH2o zVtMs-cIX6_rVmZd4fGT!Fnb%rMkS?l4S-UEh9VrdWqq9*i78{HP;E2?eYWnko%EhA zZ}N-_-Z}iF3{b_GmC*#kApe@v#6`uEmGNR;Q&Cn%J$h?ahTI;MpWlR(g_Ag#9HYOG zydiYGS#ZwnN?OwDt~ub0`B<4r7>zN5rHw{YKyIpo*7z!Zl>-laEaa%TFh-GeIc9Fv zv~}eEpLW2{q#bD30X{EY+hogKWFwhm5sBloOcoyg!$Z_5GJqAa4E79YWc(pG;y>`Eu1^Gq!F$f67%wJlVHfhX$@l z@xqDG&gXJfHZLwGn3q*(qoAe_GQUMH7^V^c&n7?{eUS@K0=UNpS@$&v4#sPc9MZ)y z<_u|N&hOBBSp$r1!V)^cl`bCZ+jEAKVo0j3tBw%`0x$TU;#Vw+aWNP(+^w+;3RE+6 z22%zv$p8}p!Vgpv?Dh~=DiRpRPpkPipLl=y55(q6#y?p3fIi8e`*O~Lv7_gUN$U&e z(1*WO+2EW@DpK!l$4(yFeDWAZmqWYBDeln?1$>T=&h7OSEvL5XJY?4nxN_C>spbqR z{~!Jj7CVixfx-WgQ|_Ns2Ik-O8F=vgPcNp7Sy8L@%8}E=paOa`+f``#e(+R8Os~xw zkB_G@6S-Y(j+uy<3G*qk-tZ~dObncDunb&cwV7xuVDv5w>+yEi0tO)7hV?MI7U%a5 z2>3RCz|PaZ=T95CxL&=5BWH+-l$4n?@O2__4kgJiw;mf$9*;V#%A~jtGw>JcE8ZL% z{T7dcD-mrCv=lQC!TQv<;_a(Kn21}LIDPHnXDMmhs^HW+!&TT3ZfFlWLu2lm3pZ6Fpi$niMHY&#wx%2{QRT*#m zYi>!804cF0x#p8%gJXFHS_AMx3Fy$fEK>tYGVE?x7_6!I!@yUq4?I3_`dNOzwk>*h zAtg37e{pt3-pI$MF+IEWB8j!?Rw~tg+x(*^KdjfeQH{zq-fcIo-O~Ntws&vaxgndK zSK(uExTzLAnKp``^JIcCptC*vRe^Cn~;Mrfln`D2^zmTJ)cNuFW+veU92udhflDSTL^9p!(m?5=>iZR-N&x zLGki&X@fKv6z;)53m!JgCwxp5Q4+`j7r4&6xAi2E}te+JPbZEixE_kDBvQpWe** z1hW#0gyJ>=Z*qkM(EX$>{qx&z5Q4X*2Z*0|i}Z3ma{WMt(}iM!7>W`3(Z~HLEMbvmx*d@mEBE|;n@F@(&nM|MD|G5y zES^p$X3|N#QxHd=1s#>@H18~@V+5^0lam;MF(0InLE}V2Ao>5C51K62bS@PWTo0t? zr%$gJzd8F6TZPJWwYbDw9og+nC5UIY1$d8q!MV*2$4t!vi~0~I=9)Z#k`iQMNj6Ni zq?BGMK0P%(9ncaxt8-g`g%7if-RXMEsZ-K^N6K#VZv3OGdy;N9e^Tz<*moy&e&yHS z{?xJ4&o^IJ#whk+Ro+KNhv$<1g^FcKDDwWcZS;kid*;P3=4^~P9Ah>`FtnDyk{?Wr zIb@xU*%r?5g$`SCZjf%wrXaSez}d1IcV=RGdLn`chTKmJMKAsubq^(zfmIR|~7mPkV6a!cWRi=Z-u`V|2?V)8yMyGU!$d*J4!x0f-ev3g=8A zC9{xTueabtqqhWhOkXDg@7S4J##CC$?XkT|c}tJ#J_l_!Befa`K_v6toLOoM2IVlPp$fQ`MFklo^+3 z(1)p2i^oG(RWHlhfp66|295{Xc`ck45=(rUjIkRzNMulF5 zzOs)gQHT+`D#c>sI2G73@Vbu1wp^5ARBM32}xGMZBWY`zH2C< z@r(!%i>$#!MCcu)HlV0^5y=t~CN}Rd#(ZBsxAem2moI;QVcjA5zGYO$hgM`o`n8z- zRn+gl$C2pUQ8!6U+)sC-zFpfbn_g(Zaw9}QSz699oqbv7 zgK_f8Q%ZoRFkTe!^?5bKU?r@Mz&s^P{W!=_(!!G4LoBh#6)@y(5^PserHq&&du&Jc z)@7+F9wY_7v`Q*#`DBgPYqIj!zxsFS(AS$}iftXU-oJFodq~GaU%a^V`}YUC_Mh7C z#TP7Q>|FT3)Li}p`Lt5tOR;nrHZ=GQ(QaPQGzbDZ2Vz-RuNaSXFz8sK=MY&auw{}Z z335^?GbxifjP^`BggXS~Hxd#C7dQjn1Ta!^_4Qv~S~q3EtX^4*7c5+J^7NV|3zrY< zF>j%B%;4jj-nv77be^%L zaiEF^)Di>}9Yg>nhcZcorbfP-{SVg?8DIPlaFiGeZS7BY{Z5g)rPrMFGO0u_zoI*c zO=>Q7cV(=ey9#yC@~h^q7B8`Nm<@fF#r2s5T1vzDa|;T7Vg-}35H|P+%oCse;K2j> zLMrBbAYBkyHTMIWh^oL@)VHc5;}TQjUABrbR!0g}5i?{076aCdVW+AR!xI0FDu$x1 zB35B-IGAT{fX)^44dBf=sp?Nst-0lNA4aB@Q!kU7qXP7x&xv3D zk}>1wjUm%cf8hG6Ws^ZYSA14s#BB1#!4AC+?LPnARWf)?*XB(-W~L4PDC^({sk3|S zy!_MU0h7D7YSXUYunz}^MwQBG*)}=dnw;ITJL7-(M=9QP85J8FD$XF55@t{^RUUaTAs&3s;b;w2a+0-_*ZrxsW>sC~sSj^uJ2!VT8ok+-7 zhOktL&aBJ`7i~qa40}v-%UT+iD2+wAH}lkUtsVp;6HvS7P>EFGy5*6>_wvjLn8*arAvY&zHgoq=xf}FU@L767C9z~Y zkdM|NIi_S#0sv}2BUn{%g4|^gn^-$p2U&Bh^R3>U zmBUsvTEG(Pfuv}eiT0cXG<26iGSZMPWri6Fnng?~f~YkRGx%HVW^0-=YTEYQeMZ)5 zxO?yHIUhyR4W-%?8_=Y!c&cuz@%;ynO`DXK63}nTlIiqp_m)#jM$94ADz+DZY;Gsa zm1amgghaCR5fIO0DS__?TQrG`l1@trK>VOr3Jqiki%`QtKEi;rf=9&4QKv7sW=M6& zC?fBI(k$Xk)-6;KeK?;eKJQdx#DkSZ_bMJM@<)wBxig^IH_b{j$^?1X3BSq>_Z%w4 zeR!6pI53+wW{r$`?+~od8l+nqnRY@CL?}dN zF+Xo1)AbXBdDz-CaF>Qapg}{^*>$(qthv38j;WtssY#Q5 z4RHOgC`IPWPDEOPLIbq}4<@82_y*SeZ5?^cMbujlQvmb#_xYNNnRp_h$VS3B$ArbT zq#fPyEnP!;e@jxGd!?4rv@d85nf?VCTY$-fcGbi+qGCD%pSh#r<@TAGF*Ec?ae9Qn zTMY9|T@eLV3sKO?&v=Qs*18Y<_`4{ju@5=*(Ll=b>~41&EEFiliV6_T#fN27s#^Q- zj#-P=4(KtTj1bpMAf~=`%B0uM82RDk5d-_Q8-{Uaii^b^7-vZ!*h_HDr1PXTdExa8 zj8h?Yrf_l~rr`%_k`|;Jk%lRYSveR4H0#Fct#%zDzoEmdD?vb{VQR(rSy%zLgy$!i zOj_z2Q(<N7jZS>5B|U-rxy z+kbRg>4gI(P8yb&o>`_hX#bRW`zJ$} zq>)hi=!{vQ*YNwL$o@qmRt*xmEB@}zRJNLY0X5&>kXGUF87=Eq#D45IMal;x(3ha; z=t7d(!VmKi$||C+cax_)w3XxoW;q>XtvCn4_e0wp&`hy5ae9k~^l#$x5Kf>My>+wj zH6R;ASSY?nc1W#oUc2JuJ}*EGT(x~$>~;sYXQ8gt!bUS2H>+8ziFm1Mvu0J_VLK!c zA&b@`7okN0gmlF_kmc`+>aa!WaWjG=X6D%$6@Lo@?I?s}ci@|=e2}DS!%R}ccjc#R zEVb*`ZxJJ96kFPV)STop<>O8B*tI4@?nKibWSOc07s2MJ@-q)<3{+tJEOV$NGdYrh z6XR_b)|)9Fx=4lu7R@@6&dr;3mb-5_b9!ro^=Hm*GW8fd zsC$d5Jbup+ngDx3k}fF;WdI zhDD4PQp7ddbzCS z-;CrH>3=l?h4G#sC>O-jytkg&)HL2E1BX5n39De{6T^c{4HHL+=?(jS(4#~90r}#y z(^u}T>D0_e{97&8i7RrJG#}7?K$p(FTIc3nJh)|3=LRFvi6=uK7QO`W+d%>Yp(nAD{xu6q51j4ejm&`(W{bF|+%nnC)C*iHsr z+`=vCu{;xYw6XxLh{hl83wb9iKHlt=B1;@fNq9z*MV6*K#UgVz2?F7Z05h1NiJI66 zc!xA4)#%yf0 z#S3xnK;Z63bo0ntA&N?*m8VMM`4cwvVB2LN+Sk|EBU$GpllnwY_y-o@$YlU#G82JB zVf`%j%rr|_uqnjODjlN5R4Iu-H3P&P2N3hV&C@&XSaN112~o~1YPW6K$HZB;K6%-0 z7O>3XiE`B5tUp8EGC*_Vl~2!*zIASrx_f&s%0ETVG`>1ns`@@)&0brFJ8x$&_$_3D zA>b#1fBaBMKox{n5}08MbkCP7Z%v13W(0_F&KkgF1*oqAlJ)snR%0;WB2Z3XjYV=i zYeGg`o{Gdq5}EbKpCpJr&3}=f|8l~T`Qrd?{`KjFFP>3At!V;{oO)+E$J@>ZK4lG1 z8|8$~O1y4=7Or2lvaq3KTV!$qOuSZ$?$Rg^*_F6Z=2$wAR5onBg7$bd3JD3oW-ENd z6*vMcgUOv@7LKcqkA;kT@!-mlZ;HJaR!eF5-T4-~5-XOUl+pczdV5+M# zidek{8yM)1pcapn-K${0s%e=rcfjUn;&>0gWMz>bJPdd^R?k!!1PQ!YELo{6@Rs<> z$@`)!Cs$?woyvvmscYpS?CC`1tCbOB%&&}aUZ1l9Tqj<4IwQ##LMhpk(PEAB>7FkG z)8}puWU;aacjJMcJ;RO$Z#S@qA$vSQUl`#aAEY%Ny5`rqHPdn7>2D{xw(mINh3njp zL#AE0@zmAOd-cpY1FJWzcg?xF>g@7o`;F-=mF+pnO3r45eA2XyW9_<6>v!zip?2`n zLo;TH)b(i2ymeLMwpOlr@SU>P#IE&fH5Y%GJE_DlbdU&r!CN)W<$xL}3#qrLz0OEr zlyXx(0yZH+BI-~=iZw*?8@||!zxa^-0ahd8yi2(-Pddpi5cH) z;=Bcehs-hA>EContF82(r4#SXUh&I>jyuN9${OCP%$d|#W3pTkK`E8Dub8;~!fx2z zb-!ZDa;DQy`jdLI%{%He!hdztS7N;zL}@a-apU1loQqg7fKz9 zhbqJ^%KMO>1X!WOva2ge#@ZfY_poEzmUnh-vz=smcq+X8OhgL|Qvs}|0k!ZptR^?| zny;_b5&wU&3je}t%9%f(238?|faBcH`O`*eP=&wgH9!?C8(iOv22%62Y3pGRRsnK^ zLKd>hvBDxHTJ4)hwvKtJ!D>#`0B+K%o!l?c(&kEXFgEH4WDj8?&#;C8KJ8I3itNXM z=K=hQ<(Pp|M2;Dbi>^bIX(e5)0K5pR!y%G3j+jhm7`?R6G^E%yz@)6~Rnd|RI9X7T zzt_1~gH6g83Mw2=nm&g842I{BrTSlnaGOU$EcglGfK&%6 zFxC)W!(v!B?j_hg6LcWEj`kf>9?QDvH!6V|NB#nS2(s&_Y6Y=ex_e6xSX5S==RJW; z>P#gjO+sg&GbhK1-azMAah@w%-m|gbPu3JS$?ZvdQ$(wU$KgG=RV(JhKUfsW4 zT1~HL$RSS3qOci0tgaTX`19(u5)|N052;W@h8ayyXT5D`1~hpe-%+nxK&h(ZT~nM+ zaS~eI!#_W7`@-*FMLD$K5!zH9%OPyM6KP}Fj0i0f@mVO>f$DgQQ>>)HP_5j__$;$j#ArQAS7=)+rHbmDu1Tc>IIM3T~Ph*3_Uh zG$=EtiH6HmOkNzpoNVW?^JM6b$@bo!=n1l>wh)o@A)%d{!CxI zm^pUz%vocyXG=f5KPH6iA0{TfBFVXJx}fLy@$7l)%A~G)x7Hr)zez;zore#d+kX1_DUK?BG(kwFXxk%%doxz9lqGoPKft z<^~yombyV>wrn|_TfJjM;yWLci_%PIy5pqngygeFy0otcmgcC>ZltnAp*>b+r?~>E z*@;*gnd24p-Yrk(YD%NXXaxGRyaN*axnZ8N56g?|qFtQD=-H|{hoatIa6fY0*z*Hj z(^xG}i?5X>9E#d|3pX}za%5~yJK1%Rbxt4Jt|c0Z+IzSAPn){=TKu*jgIJI6qc%l7 zzN|Oqh`b%zF%EMJi7&~<9OCK3GPn=CFv%Pyv&<`yKt=iM<_5n=w-9V@G5Uy81rmFv zggmDv$UKvaxT^Vt`^$fto1U_*ecyM*@xQG2nLd6|1SWIXKuKy1+{}9cXFYE@CiA zq3+fAcbjWgm;Fc18G2oB`%L}h{w;6UmVdRpavY|ql)MZPUQflM+tbg0R??hPu4px; z0{b|E^OVqFu4mC{AbG~Cm_zxy=zFp1$qf5bBGS*92?Ag7`xyL_OKUA-3q%Gr33rkZi2qYrMYS2-}L zYK~mBV1{zqcEbOpJP2~u0r=Ecuue<^c2~Sq4G5Q8Q-WOYX;;$dqxh-4P7Jtbm^o_z z(%WDxXQaoM#Gq0ImTEUAVSg`G8Je-9C@U|jN>I2@LdJhT^X4;gr|+WCZRa&>Uhn#n zwqt^6<6&}hdgEpEM+FNAAx~z{{v;`VU5z@U}^g;~=d0iz?M$puP zJq*QzLqz(B6}KZ;*$h&`oJR|w5teASiG*a~Y6A|63EwdB`cr5;0^D?xCe=B!a>XeU zxJZ8FyLPQV;eOe0T&)tn8g`p@UrjBY?+YZ+Y?lf_=6espQRcf%XQqXso&{{r^`&|A zx0UqK6;q!zKJ+&d?#-$MT9wx<4CPh%Z27WSV0s82IfZ9_10~GCjKB^EXn+YbVxj*swOFc8dHXnT4(!Z=r-D zx%ay80?E9L&=nr@yE|v5)SXxt|4bdy>!Z$FuO2vOjJ!ZhGnc_xLXe3B)s#RJMhS3J zFoIMcmIhHb^o9-b_Z%`N(8|hy(eX?oFkPf==gAYgb)PWFR=YxSt#^C8Yc4Z>=a?}& z$LA&2s+m%uP92Q9G;KvbL0&KrJENk61+A$IAeaE?P*`yA7-WL?$+7BXrA#uvU8DNt zDug!d)OF;amAh1|YhPGyaL4_Fn4djP{y}PI`U^P~?B@gM2Vb@WfnL#G$zBp8wn|;b z-hn!37ZxE%cZfiggSwa{RN<8>ii4-E+D+<~RHc8}$i(y&le$*9@g4ZPom$ zRPY+S@`Eb8N4KGT4fVr9c#RgJ=Kr-y%FHoBW4IznjdJc_Bej5qEu4kXfs7q=C1DAf4g(YR7kIr3iLWwZz>%?9v2=5huN{a9Tc{M1KDZ@3X)Zdky50ZQVXe@Wa-ST zJ?yDBPk5;difM;>4uNiUXNfWG#3Hm7EX&mX%^D6E(6Cwm-YIdh<;%y$r95revwP!4 zUAi@htx!I$WJ(HX?_4NfqkXixu-4fuU=nOQV2C6aT{nmc%!9^e+Q-w_Ok7>$YlW;= zOJWEtt**>rEN)q3)G^eRX>3tlxv{Nq)>>)Vv8!1F`QcJiRD6T+{n}S+ z=7SQk==78C7gj=bg{^ZY>*q$`T{K~JN%?vnVA7s!W-JJIooQQ#j4#T3c|i%bz|@jQ zZ(`@UxHEsfmhTfEH0k=VSwzw0G`VmdG@`oeu-OAjgEy+-&s7tO*Lu1VJ* z1Coh|-^6rOQO!?tO^VVn@SAjQFUG;~m~y78n^RiLw(YK!VERMy#Tu{2lJ~{CbT!-T?vjM=CLSl6f)O)I(5G?2Sj9{A z3t@}F%y`r=wTARWkwq50SWQ0%3l_ewn>ZAJzkMlUO@{juMC^U1%O$ZwoIWsyPBUf) zSjKcnzcPIco?kFv#8CKQCh3>D1yXnD+tQavj9r0rmL{W3H^%{(&uvB z0^GELp(Z2&s$H(YSIYuD*Gye6PBHEL+WPAkAAvQ;4Vci=>hbBA>Nhml!eQa1T?x;F zq|?qCy~LL;pLWuR&c2r}Nlnj7r8&A7{AUWm7%aGp0Oyzgdo>!yTwjLf*+$NerVyuV zl2ab5@gsH?#|)d|?0!Oxx6Dqvc(#arc8l$~*x3Os7u%KNW0au;!H zkm^haG3}zns0T60h72##yD|J^MkqT@jM0Y4VOF!u!TBXw$*3gI`AA>e;q#hMNws0O z*qlv7o;RP;&93c~&Ra-CY<_lr!^dkjN+Un{aP2yo%8Lr<>Ax44cI=Sn|Gj<#T_7v2 z=(P^BxW{y0{kr{TS8Gcy^9|}^rkyZTJF2aM4n<}NYr;?#3)7~_9dhlUD9Ib@%P?hV zRc0C$@v|O8;LRol^MTZ^Szx`fi9!-yhhau(Qj=+mT@wB6rSm!cRd$j^lbvE6`|)k$ z4{Cl%uSU?hB->6>829^rd^%hjgfR~UU;GCh1__8SExbOYvIoC2i8eB=CdApYBqmUj z;UZA?(Qn9va38~sG-lt?Jc0C4JO-VRfyM|G`-aWM?BJm^wOxWiwD}&77U0{(A4iTS zA1_|GX3}VC`F7x#g~YUZ3o$PqJ@5v#UX3BvU$QQJ|J_%;9??f7u725o?;Sl^k6~B{)x=o1xRs5VmMBC!eUsgN6;deS-Nu6QTTx z)l2j-b?}=Cl5O9CA}x}n_(LGWHnz8C>dGgQOR8t{2~TUXF}=^JGe6%yyT5NRjT$#< ze!Y5gM@*8dI$w{M_m$YRkraRBPkczms%fqh>vjW%z7UWgQCLYw%L8BuLV)=0rzELe z6-ibs1nc?vx7E?+rzEM3Q%T8&j!bh&ULZuIrdRuzq+zd9c;$%9_)e#6X5c|oVsZNw zR(a+7m0+1!=SZs9|LTjL?Z-}iac#@E7dhXNCc`>+o6`Q{VROdy9pB)F>Fl>k*EK2S zj!pgQacf%Hv}44np)~7*Q4{+)AGDe?IBN+x#I!!`ihWF~!Z*SGQ{D`)qOKv=_gb`R z!18H9E;is{u^Pk$eTWz&NfQGe7tlrdbWs6$M=UIOPSZ)A?91rb26>js<`{qr%VL-F zFtwJA3R%3HW4M0Qa${VAnE=4M)u;0V!g%T&fR550YoHV_%`?zpZ={mcJG0nBIn_e! zVUP%S!UbD)ql_J_1w2V{=DZu9HyqY{RsUum9KCx-9QV_Td-UnSIbz3tqvnhqJ!?tU z*g5j=OSXg+JCk0$Qs>&u>F<}L*1Vhr|5w^l7-uRq$2DQ&(W3{(d}%+Rrb$l2X!6uURu^*P63hqqAE-og%VA% z``ekPp+xz^9rne8Bf-E)N^A+1*x4t?hVTYpkfdm{a!=1j8j5vVML&)Ad35XFqh0q` z*H09_7}uf8@NWITxORO)pB__RQ0JF7=_``jX;Mtdu?<#k*}tS(^QP}sYrI=p_HMfl zwbvZR#z#VTI#FUfPnN*$jcN|#?ULdF#TZh83W?(Q9cX1;4;^?c6p%F}XYF?0uZ&bh zdPY(byCIR4WX7gTmuR=P^{O}iPxt2IT21II4(@lhQ5BMK+jZ9DSVDcW4i9YeL3r)x z9xdx1uF<=1z3Q>WQv0`pX9?Cbui6=z}KobP& zPr9uhSye}`ksx?F7y`y`xNivd!%~%ziKWYtFp|r?`uWG#rk4-zy)?P1k{jcO4H-vn z&~oGNjuyq?xaG_NQj3)T4ZE^8!NJ$@DObMy^2)O^dDkvo0_}Gc{wa3nnKk!Lrv^H6 zXe9(i@5f_6`UY^aHnm2XvW?opCRcU7te(NUzHu)QoI9-Wsocm^fM}tWB5AosRc~wF z09WGNlV>Uiz5Tl4wh}1t<}zN_M#%R@Midn~0*>ScC^3n39D^g{!J8y#(3ohlt7OF( zn_Q++R(9D-F99W<(lM@KTrue#Q^ET!3Qn{I4)CN;`ypb8D^m{qzOf`8uwul$=qL%Q z8!-|ZVBCnZFm=Rbpfk!~q4h4Q;#Hn^`gs{pZc1_WU6c^?h{%%OEJX_HsY)>xk{<&? z%3=1fPyj-C)XcVXWPY61-DQ`i&22YsHi?=upRSuiB8J4b6vwximR{jq>S{7+)TYIQ zbBOr!hZE%QOSWx%@I3vw$L!h(;gz!*9C@c#l@_Jr%h!!b|9tt9v*k-I?7sC}In1zp z;h*wVtak`JBH#bEKLBTHwjeCr(8S9D`>(@XFecXRgvUa1U=ZLY0e8V+2!?F}nBR{3 zw8=ZxS9~`ynQ;rD~1M^Z59!>!2z33=bF)#47w9Z-s ziy8{OML;ZXc#p_W=sh@NT?*@fZ8%VQa@%-8a;M;!ofq5QEx3E?vjO=;vSN#@2|2_2 zjFy|xFX^+u7xV#kua$myL1LXZvv+@X@Z6~ljMrPyxzcYKa~$@8EyiO2RfieI%4--5 zJIwPJjHUJL9u?1(FZPizVkF^^c)H1C_`+~cRBi#YV7K#(46*XrC-<|u4V$){ROnWb zOlmSItxUCAPoI!6x8-L4InsUP(Vdag*0inCx^{yTm3wD4D_5#aU?#nm^zrLC%s*%a zT{jUt!S;h|rdZVd;4DnjsdM0kY0Y+OBxYJmmnvXbHoZ>63?rB`AIrQcI8uB=wM;ZQ zd&n$Lq<=JaSs~7ik*@W{}x#b&ZpWai#Ql=yndAbg=yJ9Riy@ZHb&x2UXSVOvT10- zZ83;GHqkxOZfUH`@yga*v9q0bz*4n2)&=p)?dqr+pH`t znkc1(*{s;X6hR6Z{>iW<(%?TvQ0FgSUL)wR7)4#bZTN*0e@O4qKk2D^--@^=mnhVw zai$q^El5c`Y8D>cIR!P6@?x^=&&1btjFC!s;s2I z+P?wHM7r9pB4V+)xUp1xFhVv~(GA7@s+{!ai6d4wXd-^Jd|l46$Ft}i`d5DbiY?-T zk(mKeB(rcc?pw38-qC)ad{si6JbLMZf~EQNn@HSlZF$70RjW=gu)fe7Gh-YXj(vm# zcRkHtoe`HIZ4#PpG89{oCPH2f^DuKvlpkY5M8X=4F~*D7vwlG?k4KdsdsvW0WyK zOGfIt@C`DrVNbX&)wY$y~^&oLmHxDjCU=zmAg028kE*6kqiN zFIV~rV1fMLn<8s~+ zdd1I|#1GmSL*DzSAu5SKAtSI38G%UbE+^=aM21|2)+2@L72j>f2&sU1bD(m@caQ+* zyRj7(9)&>&pqm+bNPPEt*>Tz@~T(2$kV3x zTmxNCMXR{#J=3XTy;vPG4Lk+#8gxpcFkJCc>;1@jmT<4gzXe*e(?u!|f~!+`ikFSA z7RncBzQ!JLVSuU`JWM6p%3CE$GC_$C3oT+VTcu3cY=V8qnaG3t)V&|@!rW$Z=B1-g zJ9T<`>_Wy6b1Xe4S9hMTCbw?fsSkNA_UzoawKQ$rUZ|@Z_wJ!q9+kWHbC+|&?%cJ< zFRfbf>(fQ6nC}@^_`-xFC8T_qV$)U4InGD(sWlgr%MZ_Oun7apqo1Q}=esnaZx=zD zEdqSrC6!$%OwU_?21ij9-efcB- z_dQv@@Dqm@DWI}t*T>6n^ONs>Wj8;`xH|qSyZOn>$7>fpxPIwB4`zJ~j>O)WUbV1F z5%6tr*RP`dW~7@V4HyS4t7P!&kuK!u0F+O{jmv`+ynZKBI7Iq{QxLa9QQVDST^F(i z*fdE5Kt4V_vxb<7or={v(Ih6B{7J=v1Uyr&t*RlZTNK4Gr`e-HtQ@b5`$Vl5kSLJ-c}^XSin;2QBp6k{Lk*xh zM*IhEEy237VQ}SmPyxJ(w2!)1eLPtuPIi*AYoAV{Kj*vdx+XiFQVwRlba@W$z%|fy zeejnvzWQ4n=bDMTTzJv&nL~$6WA>{eGP)Nq*HEFo>gZ|B|9th{x|Fc2M!~akFb?i3 z`Gy+-9U06Q4q_G~hhn@eYSmoMEr=wIpL%EGo5s<+#TYzrr6^LG?QGKH!jN6 z)Gb|0Q5*8d7=49?MIF@KzG`b=P~4h{;uFvmqCR4Cusfaxb4)ZRV(e0vIK@R9yC{ld z1(EW@*DdI3!ZC4+VJS}G$}9uWMX{=Ta$2o8gGwxed~4TY@~u})q90eSxY?;L6lD5W zxAt5ax!0C)xVElIayeIbQf9-`9Qx>)>uYH(j!I8nG(QKVT0i*GjLWaEzXsMbXx@my zli6J#O45%^w=v=XM0b6aAhl-;LZs~&%Y$wM-FPelcGc=X%&x(G!uA*Xl8+kKsRqQj zeJq9!rHS3v(vbg#IHtv7{E}$8`rKLTPp|BD{Q?uEm4^y`5Qoxs;l=1jq!0Z_Old=E zeJRcawIgXC+M97A_G7cG;hZ>8C9c-f$rsXqaWj+~ipI@k&an7Ja4~H0!JHNA8~d%} zPMEwKlVRs^Tg+hXK5YzF&I-z`qy~{w0p@byCg)DN25o{jX(K@XJALe2NjhMA$!6pl zJui-Q&1Zj-Mcm=qAa>(_9h`q-UJVPM$dll0losYF3A($J8djnR)(YC{@0MB0m!%zZ z9Ho-Oanoml6jG_0rI>*zHIzMVDCNMDVlKtmB6-%z>2Bq zBkXv^44{_lW;g2b^tR)crRixg(xBy&vX(lh|GwZg2^}+N+!T7_^+*3qaIPIQbJL=^ zyEYO(Iljlp5gkV#?)&}Oo?BDO?3#4;m%+!!cbPJ=_xK?fFn@Fra_RMEXe72q2GQlA zq{feir7S9Os=KVgMP`eLv9p~g!E$Lf+3Iq+rnp?J#wq(3T;2VVu7HkO?scm9D)F_X>zf?Rh=7mPzhG$EMSJ(-x@?v7nT zRCp}|Y`=v#+R|gE6Stvej+wqB4O{rXOgD_)+e~-3IEz{C3G}z8t{XrR%u+2k?Df}! zFV2KBCO>g~sabA`d7Wl&a5qUJkz(x=-JZdOZgWWLnix|AMqC%+P#WUyv zwtn!Wav1JGml(xbdu9NroX4?DBCFXXRQJOrpL9A4YO3>)rfL2MyUmT>2hqGHU-%%+ zl>pTT31GTRrM_VX8Uv#Csf|-ldT3ysS7{t)k>^hq=*IZw=sY6>765VDA3RQ*YKYSe zPP~?&QM`j$QYHt=0~H+aq1Y#EU!M0}_LSmXlkFNNcq1^=dr8RK>aDk6=Jiq}&2>{! z0=-#*9aB+*GsK%L^fX)IGth5>yJ<09iJV9h=@+tU+1g!Z7ZpXX+zst^2n=JL1q=dXK8ytdN|UyZ*?zt~Ppzd{N&p!3Lr+1i19n4$!! zy|M5GwLL8ivo<(bTs6L#R6sw!y4#MYqNOhpvf6;IX2rs8U7s_itKq1adj*}J!FrAU zbw4(Xg#oFy(O2sN4mh2knLi?dyOp=ylx;PMI_ARtP+h|=lJqTay)zr|RB~5H6hq=+ zPYtcB`|P2Iu(n_=isW7tE`y@94aa!siSH2{O0Xsc8* zGuFnk-0)Q5%u)~BZsHe!vfiU4_$7JflIPBS^l1KUF~)UQ95{y9uKgyyZXx|PelGSC z#j16Lr%=rtjOQ><^?zt2T1ySF6-(nRrF8@m_Ppm=S8(L%$Tr%Nx?TD0={U z&hC|E17IT)c~LZZm@W91VJ~Vfuaqc`yZn;=_25VP^wrC`Ezj1Ty><2aVoR2dnmK*k z*cmeQ%48&cB?vQ2Mipxc#6k8Y;h;s_(kON|HImQz(-Ma{o`|IW_OcNl8{bKNTUY` zAv9^yTd2|@)X+;nY6!h|K|q=)N-vucQ9)`#SBVN1L;(>I0Z|r|lD+wT&z+sk4Dvkh z`~E+FG_!s8o^#JVz5MiB@1Dalu6>ChyWioX{OQ??IbW-FHmqztF*0;s&$XM`=iD`K z{=O+QRFZphp-Qy{T-JO_!!PYsW^oWq<{+^!p+&ak6&}iGl?i!^@D zjjX_OQ$eksuE6408sBe-E%TQ<;qFE@KQuTX2f;eWQCe&QwbFvAs})COwW2c$16pGsB=6ej+kDIil8IX z(6>-jiKvQEb)!@oU`i=T!e9QPm1b?ZrZ~H%9C~;mSBkZm9MkB~n^~RS8rryfr&m*&?U&~?Zk1AR`A6>&P0zzwEry-7T5AU2g54Mo zESp(itvP7_G+%LpMSHC^CGz1z-t8f}g`peySMDl^X;qzgSors?(B%$_1)l z`inw~0K-O3F=atz!1fP6@U{IWDnbstYVU*_Ey!+7_r;hzva7Hvj||d{eD_qBYdEHk zRN|A_4zvR*C>?+1{tPRO!U}`2LVvO+O|=$+MHy=|^m?Iu8w5g%q)8!oA=?z|(CtY$ zGf@VKL#YP4_!q%JyPd6B_4Y0O@T1R9D--y9{^$MkwnBSvu`fIrvyB!Q>#k$W!Z-z9 zj8z$eOsvW!Ny!C99H?~uH%7dN2VZEC4o(#{ zEM)Qs8G%-ox%?d$f1d)+dGnRYCm>Bb^Hdv6z@hG~3DD~tRjYxfiQB{G#LM8QF6N{XaRc~cJ*^uVDe+m*4@Ad&J3M8lO-EYL`%up=8ZK34bl z!ip6!f4!Kz^OuL))PLCUwL@QD`lwUWhp(jU|I$}Y9zU#dY*gaayh9^~p*W_(kTEjs zW0qVJHg1(jWL;c$@n1{x*Vwk=Ou1R_;F%)sI)1G6yF2~lbABgiFZnU`eL;V)_I>m) zvE_VPxQbVHn)VX1)9V%%cIG@6+vZvo{A1o%2nH`uCP56kStI;bU+nC4wJxmvmKOh# zMFSW$n+ z-6GILP_wRU_=b@$EMNl(21nMU!bThlZhDhbjaaqW$cMDH`sWeQ6{;h&r6dHM5CKIJ zu-xj8xu?_aVcp|u#Fi|l)RJ^#Kp=QYfpuTh1CB-S{g-uLuy~JkuP>j?GM4XBo4Z`kKe0eS z(g#x700Q_XSzT5@L9?!limo9@0&*Iby=C>rf;Gv{Sfj}dCRu2;>`iMl&}&-X3o9#F z6{F4tm}b%r84NSD08R0EUhF8O|?R} zWR)n;%!m(~Lb2#XqG$Mq1c!*uyfB}1uex5`Yx1ZgNfw5_%(_gtHU511RaR*7)s<7v zj=DABwZWN%p`vywnyD-vnlf5>^jPH4^RVLvZ_p8A{-HO2{Q}{ zOWzQdSQ9q9cQjYL(5k@db!RRLS?7FKg85xAMs$6}T_NWohCf;K_kyL9&;@J$#V5v? zF7*LGi`>) zFwLQesS!BurYO9wi$yEZL3;9~;x*r|J-m54FMr_hJEuP#l=9$u_hED_*it@@eOO`R zptUDtzNJYQR;vfJp!^>&#fIhfiFm5Whxt(80G$?=O9#Jg()SRQ{NJU(88V_wL@YgET)>7AQ|a znWdy@p825q{#bJH3|?S4dJ$p(t8P7&iAac$$C^(X`lwP@!UL)jjFG-ni({Gsss%-e zuGA%|A6Ow=P=15ll$XwJMH)mgxjs!OQ6`-q{|jt;pI?qY79&bVP4i z$l{qy!&`qonELwar91dNP$5~m>p_K~ICs-DU;W(q7I!RvA>;M zy<>B~VA*xQ+mL~;LFb*XeERm56S6QT9{#~#Pwc>E3)d%_H621HqMxP=2%!eU zocDqaTLc*oI=s=)P~LYLAVa&C*WK?icwkTYZMMOHmHo)0!BhZZkJimP<9z-Bo8H;fMoZW?_fJh@9wf!X-ha zAI5@w2;&Sc<~K}}Nf$46u`=q1f0@UVsvqup=N+N%t`~NCA(~$PGJV&ggBcuA!1FQ{ zkjP6{{>W?sYPY%ghc2$^YS;7A6t)LtFlj;lT{~q&P=^e=ske?e2Zk?INEzdSs53Dh z80MZ>h;dxd8<6-}mdQy4)YcqbyKQ(#0M7iKk!P8aejPMVC9R&(L(nMh@sV z3gMvqZ~l1y{Rfv-_B`+PSu-c9%JaPOv!_p172;N7^6%R21Cp1n-=J-a<|x*0yz(E` zAMzj8FWQd0uzpO=g@TXu+wQX~N6w@UzVPv7SLbG(20!YR(yAxOwcUxU%a;9^E$?@C zNFOmGz2FuaZ~k>?U{UsPaYn~$A^I7G9-w%nqTo7)ci7lzt(d=vH9`=YviCJvf0pXn zMGZPa10vL9Ig$v#{Od>>2NGmU27_@VWr*LexjT3> znDyA7@=46{Uw1EBCWJ3qeo>4-Mub4Jg^4(5QA}znU=+sgdp#%r@*x{my;el8S}*di ze4nD?1;9c377dRv<@R7w;q1&Cr!P|g85p5sBo?vBYC80LO z8VgAQmX#TM@xNJ_EKDJ+%oI<8N{fawp6nKVWi;;re@eVV{-t+z&msS?owwQUe|WDa z10y0>7@FnaPbi`}%sy}+*}{8}0Z9fn!9+No+?cYzG^{DhdoQ%ltVV_+H0AU7$WbD}tzy^^8UtTFGLn|RBQ{x1BtvbD1I*t7Wg?Bq1VjLqW*Jhcpa<9x zKsif3^%=j5_S3(gJ}SFdpY#Owu=3x}p zl#Y{9FW|)3cT3pf|BRDDUYwX{;YFP2Z~7NblF)DsWI&3{eg4XT0Gj@R9?_+O+jroQ<*(GMsUgBN_+8q#-T`Qsc(N!JA|9*=vQP%9i)3;D%`3t7PGRTug1 zYtj8pjp{S(%*7AKY^~IlpZZN1Fuc)QeIwvPqYVhX58E3DHkTCbR}_HTrh7b6_2 zl9J@XmY!%p0b45X_hO5zb;4G4#fz=+p>0kvxTEewU%|oxSph8S<9LoL~J~zOf zA24V5u(<)U6fjqgM+Z@U`}f+O-l2Te8w(e#l7D=2)~q+7`v*X~blFYaLmQ1y2VUB3K1KMPi+^x{%6lx_N|=pd^H3ZRzi7G;lOVot;Cc+ZIRO!|Bq4Nsxh1B1%eZ1a&&<%uo7wv zoRXnH|iFW@2to5L&(*PYY9@dpYkcIWi(tcZKoq|=k<&Y608@(fwS z+=Io@nVHdS94plYnb=dJ$EHW~dHj3(7muS;`$a##*yfBoHnnf`-xmQ_O8x>+@d3aU zEA`M6Jv$Eq+|Y_F4je-fCkYSG0)(`GAtVb;6`+}!6gg2a=1~BoHy|RlM^2*lN?zy$ za4A0?`S{q6*N%OB_(!>Y#~wY}f1J|owUm$L_$w@~%&3uN_;)`B{=mO4HGD)F7WX6H zUi!nev$Qg)sZrXSv-d@zRz?w?p${Dm(AoPH_@s_%RhzdEB1GgORSj@9_3#`a+YJ)7 z7$p|3o1yw4?_W67z<*74DR~M_+p3i=B=q=bR2)$R+zD4k&0J^A1R8u z+|_ewQf6)IlZ|tojT4;Ba-Gdc;(2?&llKOV(Q9H#xg8@&b=QranhhTgSbb24-r+I? zxUE40#F?m(grin{^u=3#)EUMF@V^xEP%)1-g z?A&NxzLByswJxm9AKkzI_B*284SZ_#LwMhttD$;m(x4yL9^z6Y0nybC+ ztIcnH_T;nMD_34W#gxx)XTP5DIOFv(!_J={Hb&fKx93H-@fApKSAx$qkdArN1%EuL*-V+JkEge==O2Wb)g6L}9TtR!dw9HVmB7Xm)}44P1LDoJI@@&&TFM zN5Ql)XnI+ZCgP2wA9F?%+5UHrX>3vyn)`#Nz07l(S6k`A__h!Kn+Nswlqu7vgLerp zsx3>vyx0VDtQlrQ@>(3IrQxC~ZM@>uILJaWPy@V>Up>pDf)FCS@Rz|TTPyq+B~upg z6Cc+bJ^v?2T;=9qPSAsZIS zqOX?Rkhd`H@PwXIj_fG$&S4qr90{#sm90MHVUUETNy7hR5C|}0bHpMz8=Gtse~Ahn z%RO0C3;yGW`)}}T%~|Yu{!>d9{mGtRSZo`9ot@Y-V;4WcmhGOgn+@4LYd2aT4BtI{ z7rN{YR^QF9ZF3_t3*BJhD^cv;2F>`Pkoe@N_^=RnSQeAZvMgIUWCrD0m+na50)rA{ZwUxDe)ocq&}*JifKM5D%FDjYisVqVnl~EErak|Gc<- z{A3nx54l=4V8}!^k724Ob5FH}@^pJD@`?+Kez-R3YmDzmN_2!d+*6ORx*cs-=d2mi z1n+$x@7-v;*BI1|_r@p3Cx*EH=~$N?v4u7nvu2Lu^DG9rjmK>J@vX(7OC?E-r4*@` zG)$TebsaGPufah?pO65d%it$Ew+wq={@Slt~ zCgBc!wsNBC>O5pf=PJobRr%e{L(;lbtATsIUDAehu2v(tiu)}mE<567UWQP`$FhaQKv92a;YM$ zE#LP1^&rOG(@JXK>c%4HIHYizu3OVr~cGGC9TbO(WjL8FMNtbi};i`R@Ps{ z$|k-fR%VgR!k#f$w-GoaUx_7RRz$?nyb-g)RKH-+OaF&(8D(1hPvJ6~&CT_2na#Fa z)ACjr!7_a!=l3wuiXuXfTx31^XR>&O&0qjw8UZ+slG=NJInA#4s?DVr0}&2LdRQfGVt}T6NU0(L=hA!92U` ze#1XLedFQGNi@)|S@mI~x|fxUJz6_uz@(a(YiGMMY8L;VKeNAkl*ZacO&yrl4Kj=J z;c7Sna87HQrUwYnEM~hQ$NrT*%%U-Ci9+s)Y7HNEs$14v>V02ovCUHP{h3hed)mf9 zQkgq;8#waDo8YG~rL=*edcITX$wOW2?n6B(nbpQOZs4cYO`ss^{pRoK{pKl-75;UD zc)ZR=>iAY5?lmPyoB&7~P`aPVR|0Y}WsZq8&|IGYNi&(}Q>nfzzv2e<)3(95!r$nKOduUz2Q(gVut z+Iyijg%T_(Ix(`+q;1jKLP)QonkTfe1WAa+Db0ArmW;8;KD)EpH;Qa|JUm3mgVHJpAR-iuq&{l+Ju)X{Q|GLrE z?rwMYt;wte9R5A?9?K)|+;OkM{q>y7gR$0SI0O3t1FDwvwOY=xWt)uUi48PdHJ-9U zsC?$2mBxH^PZIhGdY7rZu*@KprZ>OFFMOA_R{D3H*WpfbXIbcy#^)+*6S{`XoNPJPIS$qPoX3am1pgo2n;R1kw~ zW-(0hsrz#w#n;<@5cF@*^AbVN^FYs+1wB9V&@*z%02_KhIA-JhMWlr~_5}n6!Y%YH zpe7HVg`Sx#8X_Q->}`e!$7lusuhGSyv2=QFtkwXR0|~4FLxo5N-b9kw1g{gBXQpEy z7!(p11oNdJ?n-&FlIDIB(=pN7v7_84`+1Ozp_o`S)(<{xqx`2L(h$@7jqMQYHy-?l z^^2*Jdb+KiXZwF#-dOg&+@5b0bu98kmYsw7Q=*mytJ}bH91pP2P-(cQM~wu-L?F(lA&)s{2Js1%_vt4NxE4ru!<Wfu%4sBGi$jo;$Q^%8Fb3(hs{Dwq8FicKN~8YmUrp+l?2U zG~ktL)$10kS(`OxYU7Kn@OgFh)Rx;mli&P#%HiMhKcD#Z>Oi)9*N6c-rw*S#X&AU> zd1wJ;1=pwYT^R*jffGQn!&f>4uIiwU5AJUtsY)RAt3UG`R@6m(oRGaR&608+d=X z)Lyfhe03I;N|2<1Rs)loP0X#Op;A6@!C34i1X6*-Ks0W~SPFEi#03>xB8Y@=0h=bL zB#qiSVxZfdI%ezN&lpUkO6zLJ+gL?@)h|Ncjt(yM!u{^vvj2XKeNGZ)JT(2sSbZq^ zWZOlpgDz++&Qpt^@tSx3Opnj{qo&N%OV|ldlt;shGA#|E3uDE~V^`B^TMr&(5814( zb?c(Ytf+0#;-K6t)}A}}lwpPV1A9?XZ8iruyagOmM9^l62#PfrXfw{Mpi{$wMy~{s znt=&2CKgAP_*5_$sd}=GV3W;pZ_${Y`N?jUxrVyjxKEb9eT!FP7t!O!-2;3jH2)5I zMdlzwFb2KNLSscX130h2SH!-(xEigrJ|MSrg2jsowW+M_u#gZ{&qfw=z z-Z~84*f5$@Nw=lkue=;ZO{I|_cm8+tRDD(009}(-!zkta0V&SW$N$HXL zr1Z!DZ%ojlS{wM*N=hP-P!grHB`=nw(%F)uOQLkPOvwb?Xv<3?9ppjDeB93pG|6|M z00~Mu3QY1w3)8<2LMhV9IAhT7pZbG^QPmG5ipc)-;2{ok>*Vh)@n3_LS@U)dDZ?Cx zfBeh+N}){?ccjf}bcK&(7k$#tPYHhhW%W`gR=rvE6zVxS?>}$j{AvA$%WH~FnYIL{ zUE(+mkZcnM5;3n@9K zIue{=kJ&@HKT-&ptC4{+MpM1%<93f_8mrFB-eD5|^AU!9%6uPxB9C=12XVe9_i`^P z!@qsXUHt2lY&4sKx&l~3{CD+LaLG`_bc=W(@aSTeh`Hrl8=O-M4xvE4NVPc7Ft~j% z@)1m@Mw5ccCc>IU0yY`16r}b7Ab-C9(>}Y#b-6AtUA*rhrlF(fzud3<&Aq9I7L?rE z{QQKuzQ#~=@X^Zf#OXRbNDm%{05voJG@g%wukDtQMi2Gpl?Cvzk(SvSGr7nnouK)d zy2e}hsKo(gsAvU(3l1H0S)x$wOQv(ZP!ULS%xbj@X=SQlBIO-@FQ>bY?rd7GXiR;Y zAda1xEl=}|5I-cZbFamcv3i;w?hB}bQG03bc`sIPs2>(__8(R+ipTWTHUjn_X3km2z9&LOLHoRuHr&EkHaZ-P5dWB2PY|fSCd`C{!3mfuyg0w^5Yy-ESx>8J~Cd3cvOH z;-53~<`0>?;;ofS(BO0{PYM71dzN?rx(GuBa4^*&>; zGmHXoi+195H%Ux1 zHTbs~Z!DgyY>-2qwI6?J!Do4jJ3+QDe0}^%)Edd~6-ag`))$U?(Egf3m*%MTj_}w) z7AfYH<_;gBFp`U??=I$DxnoDq}^Rkd1F*%(?XoK9%2RWrF}Ts$i0sHsz| zv3NQC9{*XaJg;@15jHuh)Z;R7vg-PbXYp^Qtynh2{q+~m6glL1ae2|SR^PYj^I6kN zdCGa&zG(dGZ%~C`pZweQ>8L)UDN^&a2)%10ex7_=Qid7n1iipFM8+lnG0l+R=LglgK8?ji0Ba z43$6HdGvDkk7n<9I#>PZ17`b)br^Eu^Ub@`n~+?omw(%a00_Ff4%VVg-Cc$LYw(-0 zMvW4yAiU@BRhTU_c+yRJJ4W7!s^zj4FMw9OtnRK0LiiXSj3}_1P^98ILW4rQkYl?6 zxt#`r#6|zQebpT3ChxO>&zL>`ZyT$ZI4B)(KQ3NTmZw}&0{ z&wd@)fb+JsTgnHw9=+WEeA><@bJgv;>W|6ePJGPd6v2rHA%YRo`l?P1u<)-raz#g+>YWZxfr(}+o zdu6k6qwi)bSI=D=+?@YCZ_j}5TDPq|v1PTaIb+d^3^pX+sTRjB5j=JeJQ!w72UEaf zY?Sx-F<((5NAQ?-utR0lV?l@B#dW z$|7%08n!n}K0Kt$?6_L?Z53HI+wj4%R-H}^?Ec2)lzQ1Qdj?%;-u9Kul+v?1Zv!Na z^Y1z+UQ!mb>Ld>&1ct@8RtBf4jNuOSx?Q1in9mlR-X^cFvy5d|w6HQ{Ig#+Dmhwo= zKo_PU6fEi4W;F!^#fd^+7S+M~g%|Dj$;MXYrVQ~fsVr;LVAOO~ZS;4^^0GYr`jYI^ z>P`1`{x|=6k$hh9y~aw)*Pegcao~aNhrb(qX7Kjsb50zgJ!*(-u^!l?C_tv$clt@P znD!z`%7DyU5@KyDPW}iJ*MK#fWioS6iBC=e9WrmNc$DZU3(;UONu)L^Vf>$^N}CTOXcnpOvxU*Nmf| z5L~PCZ#&Rh9+?VhTCm8+*5R^P2bOXsZ=)gFSR>qMb{rw|lR7rSycLZbOD34TqJWHc ziR7FC4DmhEt*K?L&980!F`2YK#N{)7UUoR!*F6ixW|>HD`}DO``GakTzaysp&wTZG zc0O4NWH%5b6Z3D1-gYIWDOw59sZfVR7@)=x5~AC9AXGAAyM#ZJbYzF9=+mvyT*q}- zqX!x~+T+GTpKwo_ln$+!Q3hxLf+45|lsb1;6{R?DkMc67OYYdL&p<_Oc$YZ(m@}Dt zQ4l-Cp*QkZ*%5c;j)UcGEBAlV`&d8hk`J2$*m}b=dlWJA*P}~zgvZ13h5@e~Hr=H&}vSdvgBOl(w?{1LSv#`C_7Bram?xs^Yo2SiI zl%&V;K}??cd&ou>zL)v$XQeiG|G3HRPxGaNcjUR_-~24ATeroZkD@*4l7CmZ4Zlcf zshJjJsyY`D4PzS&t=^1Z%~nXvUO*Jv(o7*1>#Kp|4fTk|&*<4}p5>Dv(xXFDIp}UF zS^et6+x+K@pWir~T4lhZ^u??9%da+{n6{+j7ALFrr1aWniyI%QbL73q^~bQvDChz` za0Vifn_)9%-GR6*Zd&hbO{?ue-a2?{(Ma1maFZ+%I>o|U>m_)vpl3IEeRT$}@1gd` zB`OM?2Gp+J{QUF0`JGcC&JTYFaZc*Xaq4>3B>$#*0URidR%x2yYZw+nUU)+-FI^=< zL6%j9{Kr)Sy{f_%^25qvzPDr3J_CqcD@8E%uakUXkZ|t)*?FKmXZ)(o8JWqSJtQ*4 z8fSt|^FgQO;A@BzGhg*HXc;Z+-EVU-R7@x(EoWdbQMfiP2BE@KT+Hzk&92bAPc0x; zB%=V~rY>h%i5Tj)0)z_*Ky6V~^x{sc5i87qC~5)aNpK9N;eBXvGyS8t=IzT{%H3O_ zY#n1oC(L3A%ol{=-kPyu7fU>NY|~b@ZcygTKC_Mvy_4haT5VhZQ$GwnG_m{Z;|6?x zc8u`Ew6?)<0b5>pV&u*$h+cSNTHEZ{sW?e^V&u-sBw_cYsiWm@Y~Lb3un4LRDHROF zL*qnpBM>wRho!2gCOR1^SSfKZC@}<6>JvjkLE%Yk)tIk;?aeP`DQ((2)pe{v2(lzQ z_nq3TMUSKOtIcI!xwHH^rjQ4URlN9kvpiB=r&L4B*ZtR}HlH`7&&-lX+H&>h=1PMH zHzeyv;i<}7Sg!&vgpZ`jHl?CY$Vnh(j0PX3tf4=ix+!fV?fC#TlC7~9$9qd@KBhUi zf*p}fA%MhJgb6o02J#lq=&4+0Y4+l6=dM_{DD_MFmPA!zgW!pK@mmD<7M2NRxRd(g z$yC_#M)8dO zacqt+JUDC)>n`@8A&mUv_|`Z!w+KBym-Q6XP@SKYcS-rkK`JJdMK2sNj0GfMgH0A> zSv3q}FW`2EJWMdoa^{kmfXliX*_cE4Fe^@(TfWK6><)nNdhXBZNw;>o7$acAj@&nFnn5GmZ zKdAm#(S-uvk)>lA2 z*0>%m8Z~awqH&`Z^6*-%TGdK!*%IyysTF#P_l3VO1e_-xSv8HMcG9O>xs>+!K4-ZA zr<_Yyj1I&#PF$13wK=FU#+$HeQ3Sn?Rs{u23GfMsQXY|9O(iMFmM@y11r!rr8~7R` zbDfb$fkjwX_MS&_|mFuh?;B0_52V^xhy@bm5HU$qsfdn!o2U74CaErQ= zBu@&lcm^ypo`q3EFqCe>Yc zrvfAdE;ZRf#jQ|50BKM(1^_3rdjD)kdVJ(|If9R6^MV-216{oExe~_b&_hMh3&<5> zl^FeuJmTjpl3(Bc&p*meJ|&1xVawSHe6F%IQWA!=FH%5ZB!ppvnr3`B!B^r%Dm|{E zaOa?ZbeZUcXa$Oj>4PZMji%5a<+50GlxUBR5)~Hgh;l>;hY*dU7k@QsMtXG=6#Jmy zN2!iw_BQ>HQ=XhAdE{@=65-N-gk7*;=BH1JL_JFxsu6(;lCC)OxKcek zaaqgkNDI;g12=4@f?%<)$n-$K9auxM7qYv0XDJ0+=p>`p^e`5`2KGR~c}z8+B>-v2 zEi~4pA#TkiCy9bl^k|6;3L$UgGeeE0#3H+i%n;Qw@e!go zKcNx$h~o+EAQbwy7&j}6qCMOPI&mL#i1$EZ;XPO*?t=nxAEyNO%jIen#BXbr^d;b1 zE9Ytn-Up(a06NYnAXEqb6wiP9QCIK^y3r&igIJUCItUhblcSZBU)RrTzhFnyx!)%| z?URxEbmGnIsCQ?kJa6#*D>wKn>BYCn?M4=V=YEUgLHuEv26Bf|b=3-N9-p%1-7W5q zZd0N5l&zzardK6(WLU@H8E-UNG!Qd0nWmLZ5I{SAZU#V2mOP}1SJH4y9by)2*cCl`=+vG z5K90N3qXaBTV$9-M@ou^HPLVw-9h01R}YC&hN13j>$*{WOHP`RFYzlrd-m+vTZC0| z51U(Z@SwHt`mzChBO56D@7ojgSt%CDUtA%ba5cEO*g=Co8R!yqX#ATlztYGlL6_K~TPVNu>iyN0zQyx?yQs@2oh}fx}6TPiW|0HNIhd`}oxOG4V6v zSHwH4p0l_BClp2`@z_bnLx-cRVy17v`GY`kh?gKD@HPlid_+v_1^pNd9~CcQI9XwI zyg*1S9;l54Yep}akf`eH8~4ZfCs*FfSiN74$n4V_HJ=?}A&UE`{Hps1`?Fa(Kg%BR z!mQdY-zED`7W5bC9dq2X>-}haxH$VEAzU^ptf>Ad0}v};;4iPHPvBSbYU-t zf|_b_Sdln{>I)Ep5w^q?&Ull@9vBGrD9TyB1kY%g25+;wFrHsmOU28#sLJJK_pmM>m1e$2u+X5eiG*axK7(qOP znHCaq9Zow_g%XKyoUJ0F#2=y^m8m#nPzWr207NewKpcXp5K!g=9ZAVa_zO%7Ne*$) zA58&IQai|N_>jVHhRS3G++a&Eew>AJA%*{-baw>{4GSlHuiclUES z?@3$iVG2%+zwn)5_>;+ZW=T^v1dCvF7Ov;TM~E2fSkO+*9EKLH&_kslRAivP4oH=f zRHKuMQC|wB+;yg?E;cjWHFlOuVd^!PGORp*gWo&$@u>$D+{5HIE4a>N9b&<3Ohx!I zWTHA-BKC>;*G|>K_5QWtqz&jAn#GV7!jgQ~c3lM7Ewkx#O_mfcKWJut*g+GwCfbBx z!yZA1VUrQ8Fb$qWOpof$O@~fQ@&z&K<7?xiltg|s?{oZ#`Z$hxZ`-z|&1dc0zR=dBYp2%2e0(GfGg)c_zxoQ>Q}}|TFms6M8467A59FCS zWOz+9dts56Kr~UnVw{k05ZmUQA5cN~zxUsU7@9pg3F8dWkw)0sGG@PrCMt9sDT&e{ z|8V1eztn!Iine~io^Q1wBQCY=S~2o9CFJg{pu^$p&arO}Y>3;-cc!&nHbrjI`m@31 z8#KglcB++foU|Q>1?rFJQ@pg(Gq;C$KG-I%Q0s6tG?X}OcbsMKjR(SzGaf|YZBd4n z?F}^oI4~gOJ;~7Tz{g~q0#>lil*u1tdI-a>c)>$0GA4>Nn%FXwxk_R_DGHkpdkubu zf9*XI5^?ycTGX-D{kGiCJ>e*;b?gv7d1QaD4k6#E1bM`i>mg|M^zc@A=<|db!jh;0Gg>)oNGpgTly}8KtuT3)q&x zw^0#@8WE%X1|#nX1Vs$e0cJHHeT*fD227jKWB9Yk_5zPa-v-!ZRApc@U=yKSK?83DCb#0g}weGEku74tftU{H&G@6 z9APxClxP+KZUj)FmoYdGq5|X8;ExWkd^CT>lR0y5&QaXb#PvCQ_=>ets6s5uxewURVOSVq;{KvKnwzn|h?{xn6@;uzGmY&1i;E-b!=wwoW zbziDh!)H3@Kei9C9YEGp8L5=Knm)sJcs_Hq;4?;mTzsY{KGVy#1D}bK3d^g+XHcgl zen%_#jFBcKKGO}KDQ??}&qPaQ5t+sIOXL~CBPJr+E;j~1kN zK!HLViGA^8tr=#jCh74wc$j4U9@Iyxp=y@rP>Zu73zSRHQ1!4lVixFqo~Y*%VuvVx zSFB)g9mJ+wYv07;r{-iVE>&;Tax}S$pK^2cO~hv&O`S`#>p~Z+>a>%$l0J&4#M|-o zB$PRBXEFa2xyFi}-2UErXYQ#kZ7}48N!#*EsE=@39FU_T!UCU$_qf2y1AoC0Hw}?N zeF@Giom=(MBllbAyK&2Q=~*TFoAhlBxl2qFFDxYs*RREm4(Ve09(eCzA)1?LL6-&i zq?INyM$tf~EO-B*Gr z$&J{g?|Im%@4jSdtfD#>RS%6$QADT@yvWyZl8d8LQEh3w=BQH_zvgrZD;d}7o)c|& z>BY|)mKd4Z&=EruG)JgtcM4^&Sb`R;Kd=V$@ZPNW0s?YY5uzZOP>Jeaa~r;R|yJd4wnvsyoQ%t9yHslau1&L3?(`wHd#zSbC?+E`DwKt(h}_S;k^^?qo5` zewoQ0yfNjyQRV#os|_o{A3ta--gx{DvI3&b?ymc7AYK{#zmIP&&>( zVa=Dj*Q)DJpKH}PrfHY#?00ROvOic+Jf?B0bJ^IB{QbPOyaWA4LZk+o?5Tbxni0mc zCCd&W4ol<$fecHsj?J4jolLE)bVD+PtvI-=af4!&Vi(EBwFccIDpcQ^;yf61q{t@q z(FfTfpA`C-G{`}WsUgZUtfC@%)CTA|e=t-_Vviz(HG(Jxly~?~;CX{~2)Z?;G~h{d zA{5C3D2Xs;VD6%=5$;6No|qd|;z@IkA9+D5V#k7@In)fJElCc+)}a`|fz3c$wwF9R zsOp@?9ouYd)?v<|!P7f5TbI(g@%*Y)XEg4dvaVT&`GW?}?$B&wn~sg=R8^K$uO*jl z7n%9}u*fzsa^rdz@HnQ8c-*cm9^+iBlx8Ro)NFXIaUc*<*_{5A0YdQz~mR2?p*Ns-{diyKO{*CYP7KYz1(J6@fESFej#%UWLj*}WaR7lY$= z7iZcA|9maY?k8G$K}=hws2Dnr_h8#4Qh+rdaf8JtGW6_YN>C7cFNe+MGfweLcH{oT zd&ruJcbAvHW#8}$ygd7sq}w~vZuY6o0ZptJ&i6rzKuN__2i7t`o-Wr{YvcSoq#By! zBlKqqQxZ`Nd2RTL1dqi7yopi^D6pUs5~)y^{-jNtSp7|#vhdn_i({9FtT<98!}FxU{J=YEv1Z>^d@2BlQId`h59N}0Sbu_*cqnMRjQ35 zL#e(Dox?~;fp;P>3XmUp|9v*>+XK zH-_jgb+LY&%jz~XS+6~~RnFC`E-d`poGGk{%hsUi$33w~th(Pv*;SvF0M4;i`zoxp z9I6~X)`BZnp-qQN!^kW42GG4LGXSUvHNB#-0YYat9$3n4Ec;XrpDAak_OW;nYdolc z2Nmc+lr`xX{#xwQ3yckt#@bz4fL*{|A*uhhhol_PK7RH7`)3a{Y+)~#w2zJV>}cGA zakQf@^cMRf(WhcmpRfcp%JU!o7)*&C0}!s6iOPgc`C0fWfjL5}mXyIP%a5#pDd~i3+shM<*Pe zj`$J~j)57SfC?xoZwl!#m0>4*32KUwrfET@Y-N$hYrrS~Hjv~cc7o9xUqK45*#`%2 z6Nuo2zzUkRgY6@&)Ff|-=ZkK1*5C=wG{Fevkd8OufGRQ73&*N?Vker`bi`I1l#|Xu zf2ALzs7d`~Pt@eVlgLk=rvdW*2eWHc zHN=C42FRk!%y`fmue3HESWbQ=yi$pFzjc5`)cxDl4O19MX%07Cfk!|q)vyws-iFZJ zP0z+vYKUjGYT^bxIJ0V)UaTuVBKUi-N4k^ei?9TYq3)uz>`l80RYma|R5z3&lAD~$ z`JtEzgof!iFsKvllaGz1kktGYY|oz5ZYkYLMU{LsqMsVf~{8K>DtC<#q?U^&i@~X`P{iC%0|ZyH)*K^&51Z()EoG zQ+M=xt#`}1jhfUb*KPZfPs2u}%zXd+DYynhrS3f4anH67(UIcdWy$F2@v77kUc=X< z9#S9lav6+nYonwI(iG&e%)-2-Mba{MRts3Zf-nZ1N)jOeWKZCJRdKB+uJy&WnYgwR z*Y@JtRa|?DYd>)vAg*cRI#OK6i0edgohq)=#dVIjE)drx5W=tMuCZp={bo=Iv}sM0 zidx2GTqe!LWkzjW{F=Q&D@6+);V7fsN2fk`{DkVGjTUJw8P3tDL~dWmWL>u(Ek1@g zcEPix7{;0&;?R`OMGJDs7E;rzBxKbvz4}Ukb9k;3Z~?&jxmgVgYuHTo8AEY08zkOvjAsmuCrIJvs7i3n@KPa1+gm7snWBIc3@k+J9b6NaDXeh|Hi)5k-dkMulY1* zY>utmnbA*1vZNf_JyvscUa!ow{ufxNyC(~A_d-)|zk>Up_MT9uUhllW#)^NAvZKX6 zcB%j5Nqt`Ln=z>m|3Hpy*|u|Jk!F=Quog|*wQD+tE-Z!5YuCF&r=B${<(*@7&yO8@ zo}aRpVWr0J+&LZPog7|VnbhhD%YTGb0EuH-%EJn!;m#V;aK`p!4`NO*63TWQLi>V zce&fBew&dl2pwONH6)#cNj)_mQ)EH#?BZa?B$*6fY*whrsVIk%UshB6g?ko+>tlMc zutbSw8hTa=z)&^BJWzq5>QMR1QL^UV$V!c^S))$lLp$d$-#D!Q5;krcv!%USA*pfA z@o&u-H+*o{u~_8{tWs8j5SRDNG!}j!V|l_!K!U?2e6A`wYZfd8xpUEo#>6Jd@+arm z=peaH)i==XQidFREbjv3$cvL&Xo@};RSEF=YlS#Ae!{R&NDK!Qd^z!wkb;NCS){yf z+PU}|a2bV=XpwhqUh&4;n^&&ba`^RhW#VX+)qi`}uFdNY3x{L?d1wa7o$*p;DFh?1{H_J4E!=%0N&j+@dgJN6bNPD3K=B2rIQi zlzG+}5+K=df@{`{r(VY%*%vCG7`XC-^AC@I@f$zRPsN0vIKH>Z!0=(+yA2)6F8926 zP`k%(HR9R)sCt4exnHM3_mP=X`}BpgPLaCh|EXjHk}y=#RK~1m-zo{90~j1+bmJQ5 zfkwvVZxB~Bs z*cOK&z5#PGWmVG-DFRl(9re55l+|l0+j@2AGvbdkr~e$$r$euwJ~?##o8bg?k8Zo--ZJpZ0x_hT>Pvq>-O(+4`}sTmo;6w$c-DPv~BWghYq0J z+CWFHx)hXKM`I4^7^Lr|AYcP>69HQ$Y)A@)FR^_1M`^cf{ z`ZJq0jl87ozwz{^7A%>ySKYf_sa$3Jym8%o4ksNt4bXfEe3e3$M{!d;lr$}hu>_~WZ1yoBjdw} zzf{G}-vcCm=&aUNE9xJB-yt2t*RwBz+zp6?fdq(9>L*-_D2+24zjnks?4lfwky{6W zLS0~pY%+Lv;^8TY3UnwP*ROZEfAEtlJaK=;>ISnfF;sW{us-e=1;j%xnepq@Lg9DcXs`hh*ss)&tu}H&$-w@KU;32VH zs$qm?ijH8#!r}v>%PN5ZiPeLY?3-VHc3b)Rt1o|Hk4_%3S9sPKa=j&V5?=~WB4&<~AQbtw#$t?so7racG$wE&LSPE=XudY$1R^N_ z3JEEHtFQeWM`9M}ko+1ai9ef0PN4`fp8iHSFgwm85oK)L!iow@-CwdIi|!$kON`}tiGg-TI8gQF2~Vy~G(FJ##0>~SOtGVyAOvI8 zR{QVqU+;eR!(IOCy{zxPVHK7B&&Mf^@{XxX^EN62u_j+&Dc!se;>V0%w(NrtsDgtU zLNkm;uF?3J3PHut8j$g3fAi+(vs$ZdYo+V+arj#N0_2Ta8h8ECNw=^m^V+R2 z4L)Rj0;IrT6_r}Z2PBr19EfU?eBLTw`YUt9Emd9WW=F68cmj<$q;W$oA-SEAXIu@% zFFL5NwijmxOa?Vme9!9d$4`j>Us9AB$}?Sjw(?LpmzPXR0KSzt`vY*ZSo8z%6&b$z zms_Naw*XryDN-DZ=yljoo&pMv?F*+^ixTgpI&9NSFD#};T#TVc6*x)43@wZLH6h1F zk%#b$&Q5=4V+Q-AL48M!UH;?oC!a04mHCKC{YJCujZ@n6pWW+?!)Ld2=-swqoi@=m zwPkPZ>%D92aX80icq}BfHgHu0K8^vJO?Z=uz4?1&sMwf)p^lJhs0dn7SqM~gc{QO^ zgHR^XhA5q>v=l&jvW!ty&JgMJNT6uB#Wfc@d*#m?tQgPZH7<`BF?950wv%^NmwvSC zZciRtNFLs|U58YxxJ`albp-Yy7@BVj%}3Ob!%OKb<5{ur4jU^LLw_*wqQ=cD7<2NRv=vJZMNT?v@>gC2jxqoi>lSMvUvtZu3x8^TcEmz`C_-Dw!FI;60b1X*s%VQw#_(w_F86Xd*kLVQ3 z2fu2nFU_LfTX>0AN)weDyc1Q5LZxts8!}l5XEbwMO?&8mgaLqsA1WpCz@GDdQA&D# z(Y74FSQM5b6S}4T!xn8{FcX8=3??Rcj+gRi6^2oOiScw+G>3lUCvHTaW-KNMtTYI> zXie6EwPW=kM(`~=_&X5~v8E4|*3ZYWx^kVo#mX4>1%3(()3JiApQZ4dl-C#c(&K6- z9bGg7@S$?Wyq{esPvX-$c8B0zVX5^?zQ=-HFZ(381bvW5Ku#u|b~`0bKo*gx#960G zoWwv968Qhm`7YNemclBbhW%^)6Z>H{-vRFJo`+PRf7}^ZMGQFXK&+yu6s_4z7&iou zIWUE9xBA-Tv96oi`5TWuY7L%|cYWW*5Xrt1{xFbt%_7iht0BQ2C)byMk1T| z5ZE^8jWAM3ny-IK$X#QnE!ug~_F{>QWxa9?+^g<2<*( zc`k|WQpHUj4lL{S9*Lrg=%EgWi`k8V<7`&NOXzPZkzj3#73;7^(IF2&^141XIuiZA zLL-xr3L2%h2wTH1KjBwd=%a6+Mcgkoc?ADu3oF;`DZ9HWd@-xSd>b)eRw?`VK+Ldd z_|c)-XF;QM#+A*$xi2h-0%RIEHwaJj70m#FSuY)FR+7?%cBNf>*+4@7r?ZrbWWf zI}q^w)sW*$3#9zKJ%KlF1f5wALC=ssE1`qTCgci_)k=t5VI5kLzGa};IA~g#Ad%IL zX-wvd2xMfNd;ydi;ggFTQNjoYH5H{y35#1GtPD8-a8jc}jcT@dQOOnu!C=4~hBCJ1 ze6GE{ypH7!@+)6aVOusR_V13J{NB~IecP@!mCazcv@zM)g;q^-tQz{oAD2GuH!O29 zjl-(;fTY5MRoK3{T6VWA=%%_(Szj0=X%uQMr;TAk0N?Ggv z%B4h+wu*0(y;{8@GQMmgJ#zh}EKsuv-?`bX0ImZa4MVV%C^h~0TK37`LyHCd z#BPG)oNL|IJsJnrt3yyGTBI~tbLjpt2Z;ihhe+ts;jvi#!muF0q4*QTVjB*mDq|G% zQmLvm#Em8?Y^)hpirkpfD`0t=CL$6hlA>(?P4!S}@g8g&OMa;W!medc_-`tR*;1;d zIZRy)UW&^C_C-KCM5sbXCMX0m#r=3bkck|ZeEw6hAayCMve#O-1vbfN(gk}+nKXr@ zewr%kxv}jO{=n7nR9iT@A%V;SLv#3vumN0k<}xNQds87iEeT#v`~}f%m%RH*P)m2F zJgX($%Z6K#`Q_-GS?!NiIV5;TM0St9rxu}Th&}-rW%9Fm#aNWu|B?H-OS$Kv zS#&P}Kql`fQM1V~!naaZObIe~sfm~>B!KAp2c{z10#zIO z!3M@CE&pV}{LX#XpEMBofy@0=W#O1y9kF5-E2f6{|9w99ng7TU{!ep1|EI_#_eb0v zB3B4`K6j>{`x`h!n1D_JbjU8Sa7>XqWI*HPn4yJ5#Xbg8f%=Mx@(+aBkRD@B0~D>k z5mJL{g5DFP^To-Eu-1_Tp+rv~Ls#CL!*AY?SbD9M@EF9Pf~6yH9f6Lep)Ym~GT<=k zB?TPbUWzYk&ZGIr!lXtzCY-X&1;gd=U)%TBzHel{HvgYP-TAO5J-`p1B+TmG=6C!EE~OuVm#M-}wK?d+)%iimh*W&ps^) z38^IXK!5}g3=l#9>0Me7l#W4~5>%R@C`eI2k)jj}2%)Rga70urAYelfY+%816?;KB z3IaL%e8073&zyY%-uvA5eZGIb@0O6W&&t{}Yu2n;ea>2;qGP77_*e59>-H0|){=4k zZWw8MetZ=%(_~1w!+s&99JCvCOL2r|7;(r&l)-5?kdeS@uK|O{*l&=*W6(m=5BxUC zUqC-X^bu_^VfEm0Lb7^{vw+=`4p$xiq?lmbaU8;@RL_X?Nz#3s5)|R4GF%U#0~&Dz z1H>a^3HH6Dml!L8fpd%AA0K+(O1E6F_kU+PnQO&a>-K_e6Ly`D#zF9Dn?&qKoxkLZpWT7G?y@)O zubIc+u^tzLXNuUYzxzY;(f#lHNt@XaRLpRLd zbFX-Maf7wXOmTTjt(}m+m617{j?q>yiv7aDeG&f6h_GS=wN;#eS2;pZsSa@d zSQnosv4}p#Vp|r(V)GrxZ+mvd?vKT$TpC*(YfgeI*r{zH&&D4W&t9x#A#+dIr(26d zAt(UDe`RdsuY$nvQtJaT6a}BvFWeZo@q(ys9TYd?(MQ%b^nY|CYhl$8U(x@ACxRO> z>kjY%eUDQo%UJBsbY#9p%uOeCf|DE;DU#r+&8$C@|G^cS{^OY>eeh$?uFUQVM}A^XDhnAhZJU{2PZ&33 zOdxRR<3H~lHvUe_Iz4Px+Zlm!N2cEO)rwVbx;l2r@6u|;vrj!bWMNkAM}`zX*D!lX z@$v!Pwzx{%wl(O!6RRFg`Wff+OD#CU>lmRDE2BMPq5W zdv+0Skc7fGNjPiPLCsC?4 zMogAc0(sLDNl(2BrL-ABB2lHk(7EDZ8#r~GVJECLZCg5eefH7ozA0dBijN(zbx})^ zHKKbgf=eLY+g9*bn+XHSXYGY8*!E@p=e*;SH{Je zhMj~iwYbm{wTU&&ddUG;juV;>zIX5#F<^L(H3Z-G$TO#x-h1!Z=AHxYTU@hfEg|P> zV-)3#j{a!f4~+REBA^$srxY7S90Ud_$$WQ@eGrT|nDTjPM;ruy9wK?AP6Cb~bIgtI zk#Rfjk%{o8MVL%#Y5Z}r&NMEg|~*os>N6Ezk4r;P=_{rge`vuhnZK&S$uGV4<;j4TBY;ozFi$`{?}hmcMjc zKokVTaBLeo`{en@l=f0CO}x>;U(Q}{oMZIT2ukZy)^YApxp6dmNN|!+|2`+7`K|FF z_N7PVi=FgVr*8}_)hc*aXICH|H%>mt0G*cDPG zMFJ~yEPecXs<0lq!R-6T=*lYQl9nBzu$=k?(g&EdooTw1@~*Mv)Skom{yL|Axc)T1H3YQA1sjdZ8Y)yDn*g ztPbiS??Mh4DT3CAYQ*5ER=+}x-<|1f)` zzuyiE=r{m-{+&K-G9(aH|%=ucR#YK^^2GwfMuDJbi5dP!iX2s#!qEH+i z!f`MHmc|vw;Znx}{O&(aPut?hp7gkEK+}Us5%ef?D=_%g(L4=wVdtN^G>5wClnvo} z+r#(wh4ppv%jeF$oFb~7zDfu(;cE-o3RcA*MSR&Pq=}q=!<}UBxS@Wp+;I>Xsslm9 zj#I~tKQWrq;}WwY{8J)^x*ypQ1#HR)1u{o){v_$(wOq-Ne>lH0yd6dA$=^Q^M+1L+ zckM0XKK~r|E5-DGZ{v#y4Zdu3a%FTMB)ShENeIc{3SpdaMqlKZk(PE9Uz$3=;htm# zoIHgNrvkjU(8nDNNY30?$SJArA*CJmc4ChC)XBhyAG&G=-#;n7vnq@4wu<0k3)gr+ zgSB>f@4zT3p(k3D?f4>aukUQK+xOPnviDa!zX$k|7PbS&sd*Y+ub7Svx&3b zIZ&m_0rT|!H$E2|3x)<)yt+a38}e*Ii#DQ_sMyjH*B;%oJM*1m=2lU?!A~O2D*dIK z71~h_y{`ToKP`~E?qTbF=o~7~FjFAS6E_lN8rO03w*vO%Ls6nXH)0lF$WlWZ^lqnIJg3*VMFDsiWXien}r_u$9MLUY?laDLkC=MQJB%lWT*u z-SnPgU7x(frI*O>6V~?A;CFI6YFZ*h8e8d#j!NzJC&=h)Lbg*_(_T-YAgfgS)n%Kc z`KRUPRG?;nQ2u-N-+1)68Mv|k#_j{5{-?$qeJk$0{i4t+9yc~Na4xzc%m68+dVp$W zEs=<2jc{S=m2@X?1~|%F9PtfguG58<2v|dAk{1AO^eKV)vwuAsC?dxk#AE-=lkZt; z#VzEQ`-(hGF`?gaqTIw&!;qIarlz4dnXj`%(~CU1#gR$}=t4`Fte30t0_!dtH>-b0vuIQ@PJy8a5?%*_nH-Z>f=XcAcAsXYY<-A)C*SS!$JLH_0EzlKv#or^uzqSSuop5w0@F#P?W(Im;RNON>c|tNyutv%4@Q?rZ zWL&vmB#Vh;CpzQsNc#A9>3@QFFEi6ciTphkua!=qip2d!wqUqMLXx?u~~^i&6_zSqy6z zosP>K+9DmzjagAIVl!N11{c6?pW`E{b7H~=8*FvBL`ji^;}^O=GDC3wrE+FSMN}dt zIGbkrYu4tRlh~SK7_B+FHdDq3NW$Hn(KjR~_sWzQL9ICEy2L%!ZZACppDUwUGl>TYZR7Z)@V4|I|qislN$k4HG?L|Bt)L)q$wEvYVVLBSh9H z4kn6p`5p+qZeAB$xL-6o@Tzs>wOxbz^%-am%kMqR+M;q?-&!foJ^JDcPj-9sg%?(P z28v@XH?hjN6Ny{U*Uo_yd?^F}$s?`y$NzGW{kI4pZ1l{e8>iqMI*1@x&8*dp&Qq{J`3C%+pJ_Z@sDiXxb}z zs8f9gb*iot`cm0s`^yb^#vVH$)=itlHH1X`WV9z#N0a@tqRu}u3;e2Zf|(g zAP1gZR5-Y6JDw)h=ENN8{qA_G@28Q)-_Vy;tl^51EVP4;z2W|5Z5p-Tw|VZ;jSE)p zUbt;iz-Lcx@w7BPOi@^C@6TzS`{tsc*S~XUwP5;v5gz2i;GsB zbE%+pK4`rYW2lVk!#GE;T(+*%K$O`J!RjnDhND7NA65*H3s)Vn2^QI)(Vv)17f|{u zb28>ts}F5L`$jYszgXK-+H}2d(Z)q{c9<`ECKYw;&@mS^caJ{0qRRZcr#*51$i6e1 zcd82-_QLpIbY;LtRvpoOo1`eY2xKz@LckwE{n%jkm>Y0rs_ukdMiCCBj03@V}Y$5zU~KX}(}CrMHDksgKx zNpYE>UEkO=&#q90SD0@8NJ&j@n!&}%=3^7@U%zD0ru!$Zo-=o~btbo6`@(kZ9y^>f zys_E-uDsT>3-6vjZg%UuyT(qL68tJ>aHB?pbHuj({j(Z1BHh1B=z_Ts(j*zVwj=#1 zoNG(DT`F^r)nP>gWrz4p&Ku*~r*S6kP7IHlHi2_au;n7zgrX#}*qXFtUoE8jWHnKz zLrSXnzQf>q7i_)%p$+EguacKeAKANBho(90TZv{Z7S5cuY0G2Ro%%#9@4LNe=WgwL zx5(>6obs@_z}3*%U9ha?>S`DqX|6FBY$Xl!QFk|U0~#UH=~@LDW)w_E+h_FU^hN7d zAtfgCBsjic$7HvQj|XwGjkai@QeccXe8WhEa^aFI%EqOQ9R52j8X8>t?3M;J*E|Xb z*}V?L%isOAa_*hId)_`a|AuiRo7Sn_s8Q`YO}#4*e$ji>=w97Nj_6URapO8!IXRdW zv;fz0IMvD-H%R`EILx*5E9d1}hB75LNIr4oG2(=BI@(BqVRtAS(5?b2#_U=8nyXdt zd2wF+Z5=_C#bN6l(JIF3?yiFVi1n}=I~wc!QJp#y-y&+$h_ee@+B+o^szs)1;=@WC zU3Qd7E!#`#A}u@I5FLRZ3S1+f*&m;3Cm-(44eP7^D!Js0@TW&H7y{dk!e5e$x25A_ zX2S0F;g6VBEpA~8vS*=LPa3QgHpJ*YV%k4c=FhB`R#TvbhP^OZbt10RaaUQ`a`L;g zZ|GcS^1(w#mR?%*iS^gsCPjlsmA-jX4>2*=sCMHACe7cz-1@2A-Gc^BEqbBIweF^| zsiU4LNUD|H-6d8n8MtM{>gk&YHR(3>hQ@0PQa2n)x%_Q%w?-X$-P~?mv}r!uW6Z6+ zle&)?(~I_BfAlOZ0EH8fxi-OHg)`TvT0f#HTv3)KwECbWAbDr?&NQMa9m!vg|KZIT zCJ=>=BMSGag3Qd#O@N0hEicD~dwG~9XfT-x|GjJEo#)>C?uR#joLKeq+b4e>d~?QK z!)K%=&Kx;;*5OY?jX&e*a^h2mf?uOI?!|T6w{6MZ{Nn30r`1qm&v~fVcXh~=it^N{ zm{8)jxrJ3Q)$32WTG&ga(j@)3R+qRWEuw0|zKD}dy2xB=Xq0G}DK8;TfypATv8_{+ z_Fc`I;;~Y*-d~{$-+H=QrPsOdnShjzl!xcQcKgQcAKdCG`Q+F)qdQyYCqL7=a^n}K z-!=Ql#5=wtxq^9k8etx~0+u(O8aPWv^&w(0l=-dejI#adNGfo8wXdE=V0PSITKW(g zFE+Dc%^J8ZXF~8{v^IRw92M*ujK+xOfU>!`do33CBr(BwM5@UakLarGp+_Vy14X!Z zwZ?UT`ZQO^!gJk%WITADb`wv!RHixl0YAQ7hA-_ z@`0)76S2x+*E+8mea|oN=%lOlE~9i)eHPuWsoWF8FW);76++Z5l%dWtD3mc=$Y<) zbS380NX|{DGez99Nw^+1ms%H}zHhGR>b2f>FJ5@Lxb;TsA>^fB6hk|T;ZdN8%7w-0 z=67-5NXaZ$^{HGv7nb&zqiD!q<)1TIxsnP;+*R{ftzYLaxLOM9l5Q_PhZI&9Q!fV8 zD-UUrZan3$E;SJJNyiC{pNY22xGEL>X+jz&h}p`>+W53tX%w@iuvLC}PdiR=IHyY~DxTwTSdRpU;KXW0Saq~Kbhg+{WUxXcYO>_j+USKfMeO?G z=Rre;Px$)tug{Dbwdc}@Lq^^@`VIHu%}?gvS1WDl$c@{?W$VqG+OPF&HM@1r?*cxe z+IFgr^nsVVI(D~Wod^-bj7{3;Rm;o#FMN<_jq zQcOtgDxY3HyL{X7`Q;1Bk1r3k3$f1xs9g!!6(Or48h_NuNvq{Dc0Ias_wJt8ckOAd zcz)7s&+c1h+&!{st5!{q9Wcz&HSU+Klio$pbko3f(A`Sp*y6%tEAV=0bd=~qRS?*d zQQ=`?)CrD{@GnTyKNV$Jso53VMaQS6r)HQ`&M!68F<1|B4HDlIiyX{mTW=3s(QCN7_qsdboZB`ie6@a#|z=YxyG zm+s5RmP`(H5%xKo58bhIKgoS3GIeGPy>Q65SJeE3rz{uG)eVh0Z23mr-r zOC1c!mE#)vQE~ByqSktA!J1Q_yFXpBS)!K|5)FZFG?>f0lz6a&r!)PlTOaJT9dSnI^ zQK?;Y+nD^Af|&6!SaaS(*fepJkkU3TKdvBdd>l3n{8C8!O~@uoB_%u?Jl~z>_z0V? zGPyP-wexs)MRKY=!TQVh4Qk!^p4wIF4Ib30{;bT(4cy-^S@P?bZ3;Rxs8X@r@b(R> zLY|QI@)+*t^r24B^-nceI+X%j&0(%!6qL*crOw>`i;bK8v>kt0#WQ?T7 zKgk%lcoV^liC4-P+Nlm1GkWhcvppqB#JqbzuIxwVd0!OdUsZdDGD|`J@jh$o%lBO= z|KM?u@(;i8sHFJt$CszXgWVOv9VGo#I6FoO5>(tu5faIW=J{QZEZeoCPp8hkzKrVD zcA0N&aqmvTQ!)aVw>E9jGN;lMGp%y7;e9$~w~T5@nQlGI#pYJe3of5PB8>SsBqZyW ztfr9ymjJ#XxLosb+@86^zCBaJu_{Gye0wI~@_}zB!_{Pb2@)S4;eg}&9eM%gMbAFq z%5c7u^&`TSV!yK)-YId_a^hmnQMmN@P2@>n{FUVU*nLB|VBO>7Ajdt;qKmoI+(@%E z>p0)6)p^vL~~Pb-Lt$_3jR{Sk!v{Z*fif#cQn}w|?OI;pI`Q z)2+|V^p^`J`9-()8h;}4_x}D3E?xZg$DCiRFB{bsRSsZRldYA;wnPQ$?ICm3a6G8a zE{b`W>i?A%odhk~bR%yRJ*Qy)o3&PpSKkut{jL)0z|q4yPyhYLr=F7C)}gnWAGP-T zUG6iNzZ2zu&-_Wmf<2fECV>kQQDN21QDLQG!Xfsp0z&RYCa|EirYli(*hIN z`o);%#_#;ZeB+4q%7_Ci{)&5E^m^yo6Qb`+&s)3RMbB5T%Gqjt|8v0?*s_u>DG1YyOk_U;CVuT?@eO|&D1 z9+Bp!kUPoYH!E_T0;&~AV}QTb5ef1m*9%YmX^8|wDcR_8YCHZYIzkE`nGB+)yFux2 zak)#2v4d~DCouigydysixYOEgUSBNswH?{6+u&)#C%;_y{F8%=ahp4hH{ROlItR<3 zk}*`r{Owz@*eC!!BFuTW!9E~QEd4_YB7~E(&J-^5G>1p7QkrddVwE_dk4O-?9pZS zV9(@Qhmozch$A=sB(w1uq1NrHmW*=-WC^gIhAxL};29LMKnRB>L*BfSUGkLhblHF$n?V@QDD6`VZ zIDVhtN+UQ)oU+m53f_&pg-t{#TZ6TQaTRm@1;kmG-{1wTWq37tTB$m|Nr@`Rs}X8*;D1=sSX% z3A_g~Sy!ZC^YKT^?%;Q{Ah6ew>^-zKNeJ!@m~DbhT*utrORsux!6<7_C(H}p(?%@h z8Sb#xR?rm|TC_tq#Bv~h4wl0&QhVYMdb>DzqiEaY{N#dUm`>20Qcu7b;?^1tTM0s? zn&V?-5<;xR;Ic7Whe^JQ!f9S8vA!tT)w4^t?hhAqZ9b=ddi~pO>74yQgX#@DC8O`T zXLN4MRyiWR$GEOd>Qw1C3xvCqF`ctK+GL@2UCEG@%R7SW@lz3n+(hQCK6o(tfpI~rGIkLx`+Yi14 z2M3Xl)_GPyEq4fD5+fefp31}idP;ipS5kNZaVI@H(KHv0S1WgHCMQ7Ar$8$OBESdE~a zi@QFZKEp~W6h5DUxJku`06Jt%M6WUr^c!|F%R+Uu-Qf!{^HP(YQJ)_lf2Go-t?ym9 z=hFp$h?GaHfPd}%*)!iO>HEN}I~FW_@U2$|i==O#_;ohYWY53*t>yQ(+M6wYoVI=Q zp54n9mf((}603(;VjP3!jXUG*9CCY)^Jbx&i6YWsY^590Br(OOc4}^-STbSA$}Y{z z*KR%In04W;5&4O4#QKE?dSeS~qr0kmvGAHb>+8GGl>B9#wh(eA!@UR)z1UJE=u{?>TZ;ly}G0#75xlV3waD51a*xri$abgu)B z(MTnAYV@?0iMB309u;7XJ4g1vE%?KZ*K~+w40f<*ttCGOe#v)H)HVLAXf6 zVUYS`(kp=3VR6Hj*c}xHPr0@dG2}E zH-WW*xgz~s@wqwYR$J$vST%p%W2V{0`q4VPM8v)+Qbn63R^X`@-h69cgXfODz6UgK ziIx2cdVHkDrqoQ{3`Ae3Aphp*a{Uzi@K3Wru#>CAQ}5C#_pgUA{~6$~E{J`^WBz0O z$#VYivy-fXw@%9{wqbgs18B4jW3tA(!zF%f8QNr_o9p_bOz z-6inmfjbi44~+F6{XOu|J*zr(ed6wgX5ydL@g;YAjo|w;tdrKIs4Xwev+@?aOy^P` zjNd?{uNpeWq&e(K4&A9iD?L0y)VjDai$6s+Yl*BY0?R?b=Ai5{gRUQx%*NIwFQnOF z@ZH`s?z@lN^__L@?teXg=uht%-?X9A=gr$WHh)aXC~=KdY3=0&XZ93KdDdTLM+u*a zzu?{}0~SwJ&{_6nqnm3fdS~aHvz?=ctkYkJ&N{5(b(8zdUylE|iaiuz9Vhd+(SIee zRu)7t!n7PU&!f9|y!jK3&D~xY_#pB2m+pP%uE64ZR&?pIZ02G!vpBeDs#))nQS{)V z;A*kv;2d#k=4*7S# z>BZ}F=RUmTzQUcej!p_Jowl+|=M^(B>vfkG&!1FU^}KaHyJ1PsCs(IVT~xZJ_`n?T z$vv-O?mfX}H(~Dakieb&ZnkbP8Z9Lv=FZc_c682*hVJO72bESH+|*$P!!_t;V9saA z@b;Yh^P}I18nb?V{5$Jk=T??3f9&s-W>#_VA?sLa+?vwGqOldd?(pH~ieG<&_OCVE z-H9XJRS@Z}B=0k;M8~j{%FIVwh#ott5T(B6~pUdj}u6GYXov(~;oMf^GW z4caOFz%Au44~o>!@<&Pi7d`@YrbI-PmCEa{62(SYmFzwHs~oR_I9frK@l}xal#T)( z1mBicK~nD7Dk1#7J2sq(Lov<-#}dJ{5W#0qb8P{ZR6(`Iaa04CQ_fk(XMX+A=YIs- z#6Pq6re%#=wteNPF*6dxiODIV?uD9bFFs`bdEuUUYqD!>Y0|<~ym&_C32544+yH6d z#vCg_$DQxbkUB21fR&EV=Y(Rlh=4IG{A#UqMy1Ea5kjc?j)T=7pGZ&r737!5mUK9L za7ypa2+y3hl+3ueLKv8i)x6>RL$8cWJYik>b-=E?+@kxI%q`sc>QCq9On$6mrxjCX zo0&mz4J1LrIm^1XW=Y#eS7b~P+2@~rex7)5)^pH?hG2pHp6759S9q3kd4`3~y6HBN zYLJReP(9EfrzuIb1{}x2Pt}o_nNpeRYRy?7>eAtqIIa_hRbE~7BJ|hfXa4%~D@4Z{ zsjsNEic-Gv^6cBOC{x3i)(obfFRf;l7wKOvAEo0U4<{|p_Lzvv`_A;8)TG_eS64jo z(wc5RwES@P+t#1mSMR@TV6bzCp=mAZx4phYUQXM@t@Cc0-#er8*vZpZt-EL0?M;3k zIsf#!<2{ERYAD=|8g*;i-8@vUd;M#2+c#)QcWAmminZ{i#2IZOYE2`wCnaMdbU<$m z%bQsK2NDH$FT)h`<)YdrG0(i?5_PDZIWus{T+`ET%IY;Sp#}v8A5+RNu@q! z@vuo3`aQ{9QHTzQD!P5uw=>O^LV%0z>Gd;kvu8!8azK*Ksi`J17{V8kB~%gF2L2>9 z2Q<>FfQEXg_QJo(HE|1p=W(lZtXa7<7-P-7&nm^ey;Fnd%ZY=f?L=ydu&jdz;JQWR z_%-X#i)d1e{Y8qk6VdTtR;RGwRAA;nKR5yh0IF>XeRm%ri0Qin5*tYU{H zjV*(_sigl5^^?hVpQNlQft9C%=TAKu2s|-v@|2NizkOos&LiR*SCX}&;HJLFBc_Uz zx6w6vALkd;H46RSdi9C#;mWAWsoQnz^CzN8Uh9jMk|NONdSx1P22WeI!C5^7A^j4_ZG)vI=lj}OOz>QMxwRiX2wBTWZ zUeg=z>9(o#Y$*%5%=y!_`-b(D?*X(#Yi}2zZJMm^~%zw8)-SLeyZisjtu%n(;!(o5>Yb| zPgN2|c^wq@SCC&-i0Fm^Nl#&2=;ny!;wOSyc#=vkANJef6sDpHw5!Aa(Le>hs#Al{ z1p|S>g9B#VxxuARnQ_5@E5CF%HMw%-Z(V`^Seti~7Cpu4!JV~};kz|C*G6g&5p#+t zgJk|wQk*v$86Y@}B5s1YjaWG`03IzZ8; ziufjO9JS<+NrqkoMx?8r@SFdom&DH3#5J2XTgP8-`|ZYeK6>}??r+USTg|(-TZfNc zf82UwtGUQ(6a2l)ua~U8|38?a2z`h(Oodfc(U^klZ94u8+_1$fcBQh%42B(~(;2fX z&}z|-it+48sE7o-@VcRrnUY>fmdA$m1S}=x!ys`oS#BL%HJ^;VBaKkChjPq`-%s5u z273ax+`Oz?;LxGK_wF~X;6ke?*xKATVc6W#%kDR~E?tVf(-3ngVLt5baBnKxTx}^ou#q(;n7kk>7B1{f@X5mAzd=Mx?^Rbz9~JRE!1B(@r2iRK^KPpl~_v z{v_#x;Ea=&fQPuRnvKEYnnPTkhY$iq&*=*98i_2AXaZu#{{0_++_-K1Yx?!Fet7?V zzu(i|y0~ZaRx4oNfhtnBZr&q&w9k_ev+Uq&hAM4DlJTe$9PiDPe~h_;Xd8Q4#+}PT z7m0&mXe|QbBB1j$?zP6DFto-U9|7G!LvJ=-7kS7iBmZ}51dOR+3XR`@F;#Mc#+Y=P z+q&x6t1&PW^(4)VA4F?KGd^*MW{L`Bp^L#Jbv$zHBUakYdl6dvHVS5f26JYAB_t7y!G#rq1+6F2eko=psog^hC> zd6JF2EOe150W|g%uJ%lhfT7t*jBAbESHY<0EHTn}C3GQXr^l<`U-5Jab^OKJ`LJJ3 z@_1YGcxeY{8KaXT%GqNXjOy^qUIW-W%h?$_u={Mq8$`ee z8>81)+v`ev*E(T6=QZr+d~wCQ8%Dq$)v$%r=-y&JPxUw(pDt-BsUYWdv!2&R<7=bo z74X@oOY)rXGbB7oCF6(C_n>CJ)ynmkyEmS9wWU&PmP+ggVY*S1d+cpveVA_A36gG) zy)1N*Xea50L`JnkBn;7wFoJIFHQh8m#lsBiAzDb-!rL`1G;E#IxQv+P&>rAhN-p zu8}-A6?^dl&u5T5AG8Zs^U*#*C27h+7m40-KHjjTkuWqL!if3gUx`o28HVN2O4!1? z^;q@0!%`;J>ft)cV;#n0EfT5n?Z~86BV-uHNHAWbu!F|PdM@!@+G&pLW2dpU<2{41 zO5JFJ@v3O4X0Npkr5o}7_Ur^S$t|xjp_|*5L-lRyOYNzu#<$)$FSH!g_>?9f@qK6G z^Iv$G@!>0`l z&D}mnfDl>5oYU@y+niK%X4-Mcop zw!-y83+`GuIe2u6h21|wZ27=ba;Z=An;UmZThidpA&W#ZH0lG5=ex(JxYV5_g| zGW?zm(S>5GKdnVeetiNC!ZKo(#$FEveD#!(%;Z!yOm%!yoj<^~%*I`p((BIu;6^3V zyq)zEN!&}QU36?x)uaYVt&)(KTo17j2Ss4yTo7T?BA$wGo zmcw-d#}aD0%8-c~b>tEZ8molL#ohJv#&?A{yYlAY{R#t9-kJI8*RFc?!^E4?ZB~u@ z7CbP_SE=*#eGl$frl5;Z9oiKs5u9lg1Kmww=6Xh!azeLcWS z&}1Q(io^lYgSZp@*z1t!&*VC9ch^NGj3fiFqNjrowd%mzS`-}~U0Y@unjk}J$@5+w4*!Wn_ zUY%cM$6AcDPR?CvfTWqf&-yKHl>~i?ja#9y0tWJ>a#+54hImaQ)96MpVxL>MOX0mP zsScwJ>%iK*_h*b7v;~d{#(ko_8n@QhmE|DMRFWw2Or6k*B??aH>h}AjwNOjrTW`D+ z*10u4WnoEtBvB;3^>^v@&}X!Bwmp4SH0BNBPSNFxk*3(=keF#b2s5omR(L%$Za#%z z!>_RVx_d%aRWa`OCpv1oB4iY=;#GIEl3xDGQXEyzc2*@)j*r8cT!JDBTu~9@*u1F6 zjf{{}A#s^KnP(RlyS@)@HD4MZ-hK;$%U!#JJmpXQzwYt6=o7TGE@D*$b;r}AvvouGXLq%*Uxu6rHN(QmUrS;^bEJsiZXH}C5-GeP zD`YG_DN%;9^0=`O>S|q=uAdmDNT1`@Wv>iswJx`2>QiH_*GJuxD2_)>dZ92!6KG7| zIFWp+<%ZA_>pRODG5^U0X8%PqlkT3Wb_MS&xhsl|;;V3RqLbIWVXd@yAn#P{3bmID zA?<$mk(8nq(a-{MZfhyU-g~EI*B7z1SjN`_(2!E?^sdnQWtRPJiMg@Hyx#cZDwx$c z7_*1=o5Z|+@|BqDMqnoSC&#=&YzoUiJ!UlyiJA5rVW$0-8QyPt)XGjN3zmdYQ&8nW+Z zK*I`S%BP88gb}4Xwyha@7MtW@Uk+fT<-a-9LbOqmMSB%|^8KM=K+o`;1vFU-K4WvJ zPUsS0L}vqAu7!>f3`VjG z+h_DP%klGu*fHCDWdC0)*A3b3S~F#DKCQAX?J3jE9LKXnrW>ttsBY*BMM0KmB;M41 zU14?dRODG^+uXOt`kcp-?QX{U2223v%7)fkcur?pCVLCe+ymH)jC$d*Xdk0(!x;-f z@e#g9F!DY&bY<~~8Vhq1)lu8_Ax<8b8*~9QaPvFwu%5&_Z?ars7LNs3=(&-=&v`5! z@(l7=#6B~|z0F9F$|uKRKo>sQ7hxRV^U2TSz#dzSli!Bq6AET~{Pb+WFieQN9 zR3t{G!fESqylJ}Sa{bMo1^OG36YUihFaqKZIf7#03Xk9lEkllg_O1QmK54rR=SwPovU$g5hH=y_9keb zU(M5;KniM0>>-VPivd}q;5GJY+KWcK_yzMpGagNmmn1 za`j>4C>c$A;0s=T;NexilUF(4a+!O0_3^j72V&IfAhHUWaeOGc&oC!yMvYsdGvYq=$A+=NPATFaSvD^N0@g9*f7Y%Bs_po=aIxiqmg*l z8cTu4UK8zmjKoO1NEmUpqjuE^OL9tMw%WTF@+k?-*niUnWG>dJht=G5gy{kt{Jc}> z0-krrO>ho)?3M?uWdt8Q>x9SiM%Dua&lCucyx7>HDWE)+k^MkhVQ4KSJ(jAb+8659XG(R#;uVn{y^ z`CJF_Ds8?&`&@b7&?+fQgE6y2keDYJ+aVD^BY3mbi6ilm-6Zi%=y0zv`D%VrZBj>KGN{^pegb zac(ruz%Ps#6})C)D2a=ti^R2sV;i<~(G*3bhptM+3IMD7S`SGViFJ$dF-9t}0*ce( z1@wUj-LF7TNJrL7RmUB*oHj~JM1B>%mQ1-)xQ}i{G;MHmx3}HV4#Nt4NgbIb6!hgr zlZhx8D`Q*KFmjP`gxDp*mhM;P-P<}}`G%T>4C}Mf6%UKr@B%e?{7mb5gYu+r=%&sh z_Gj0z(k9DRiKebMU`I}ccjy8#>eG=MU)eEw?Hr@gb4rXFeMF)>pWo!`@;K&z6kD_! zh06nJGLHOSxQICEpk|qgNIwtF1#U0=(=34Q!1OIqZ{F?39r1KCb+>RvwV z=97%OwS-1AQA^7!A}`W|O43JqN7trl-qt=9#oH1y#egJciahGmPHQEIu~w4&2(8Gy zh~`G!~wc{B# z`;H{;4TAcXl-EpdPpup+(Ty&<)7UG{Kt!9o$2yp*xK(hCiKJ(u}7c zx;f2WYFcwV&4r5w>4t~`{MbtmzD#rBZV|dK;>)zN)Tc(at0CF+|2BT)$Rf{$#xEfA zP(75{u4q$3fE}7!4vd~3hOc+|#E9q#z zgig0{L*uyhO3Z36OIYwNkHc$h9-B_941G@JJ1~UjCq2?aV;SCUOMN}kaK3}YI77p4 zG)kd`mxaf!q`NmXC7zJ*ocUlHZamM2E`*y%@f>-0tn-Xx?O+P_<#S9q=>zR#JQ}72 z^D$9QX$NJYi$rc1ns+flt6e0aiE@M+lxwCbr!gxXmSLGL61MObO%)9r&Uyepi+gF* z;v3>;WIYsx$HBHUjYGv7v7gK#8ro|VCF^mpJxQ=C-uMh)2YNAP8}r%__3yPgNn=*= zM(C5~NE)>bT^MQyXnS=ubU5CKS-NlafXS8|NBg;)oAFV2 z1biOQc$JM(7P?5>1!!P|rmj5k5{BkP7_kmTdPW+b@)Ao}8li;UJe9`>Y|5|25Iko& zzCvSNc+MJM?b9U zFoOEqvbLpnb)@`p+pwf_t_ow;(7cP_eU652ojPz)9B}eg;(!sE%*vC_Fa96K0TBe` z%89Z;K;xf+fLYYP3qe40zxb$GSUXxXNcQq-g*IN)#7nD4%WnA@x!J)waKexz0zY&l zx{@T~lWQbqFFDr9)g5UUG7Q#rlwMcOVPPKu@Oer#z73+gk)>#_p=;ZxtbBVIbMZau z-3lL^8}9o2ZaOP*_W#WghzV7+aY$K3Yb|BfgvNTUwXBMin6%ascKujA6Ai0Q4~ds% zLa^j7sv4e&hGu?-_AmvyGIe6TYRrXLlXT_(LCmE}D#cDqsvxm498~yUj=69%*PFwv zvLJZb(b!cp%KQS^;}u-OwBS{cbSv(4CEb+FxDg!Hjb-+?ykcu_)3CaPLCH4>i}er$ zOM7p1c<*Uw#S08gyk$dgK>mZ~Ee)-rk~hMC(2dSmf@Vv;kS%tS`nU1;&dBkB=bYLp zEtY)_h91F*awne<@?_f_N|Psq!#lnWFoP-Q!9H~+gc5K^LD@4(`MX~MbVo1ESYlis z>K2}{KBJTey8?XGi&@hGH2JE3p*;&Pl$gtOARJy}!(~kcjX_Db+E|S|p6%C6;kCw} z;rD4_r978YEYODE_b;1fN!Ang`m;tSzT!6)#b}yo4NZBBr&z)T35gj3mg$~gtx`DL z56kBiyA>;43zb(=tsZMl#BB}?*m;wJT_gsa*Q962^SVgP3(rNL8EWQJu>QC5H~`J# zn5NhNR;Gx=QVid}o{iquN(#vHl~z}tz!N59*wQhaK|~q?t*AVK*BeiV@o9;tc$o2d zt7+&hh6QN&4Ol}^8GXc~EqJD%@~RbyG(bt+wUUy1mY|zifeA1?zlw^^_**)K>2D!1 zv(;ZJFKrv#q;Zz9e_~^AFR{Z9sPqa6Ls$p~GQWeS5qoWDmnbh_8>_wREblbHvNnr| z1JDocH3J#h_`I+aZ0L=~ zPp}iP%D`CRG&j++N7$5kLE_wEd?9%OP_Q|;y_qm>wdSHT&0>5r(qDzEFQp>GUKn+< z=$%Z*3_JDUL*g?>`Dbv4E$bC%JLPOn05^=I_JcCtz3?VQWEo{TZme}z7c-~MQqH7y z$Ih95+vZFwT4wQOroj)FL{&eV8?iko%^w0tnabwd-BtI zypMKTHEADNzf+8upCd+${k7Ao&(8yRPqDWnupnm1&l{r7CKP%KdD>09?F~d>Qsnc0 zF+6^HTJrPOqvUgVeE{#rd;j3)`)v4M`1yj+pXR6DRlt9UskJLqBlI(BGMKM)91q@9h-bAt!nOLN28Q(R>uf6mmOk)49?F-v^U9w7eQMt#rmOfN;m@%=`cC4Y}u zN__VBhO~Yr78zJuCrI?ib-u)qe-;_TCD|_(0W_oLH2>N!k)nj-{NgxuQ2#9{%3j<Z@*6dT8#TJRev%8@ukc)S$n!{=qFj8Et9an9j$ zDt6_k{CO}&D2z7N_3qK`?s!k3KiN_JqhCluj`a?D`xS~cTV3!wqvab{Od&nVap$W) zSw2M#TtxMMGLi?AlXFCJYFb*J&zqH*mE}TjTUTZ+Uski%#w>0!>0f_Y#c$;HtXH>g z{bs9%RJr%>zr~C#9VT=%JGi^wc9&&|cY}4UHuppc*Bc2Q-=5}ZRA;zi#l4Sv_9cUF zj)K<9JkRUhR2UkM-DJxuy_-@_vmYE(Vaj{^2J za<$>@Qksff%ceF);+nWnz+P|V8^vz%HuN?D%)1hg*#MQvk|$L41?}jbTw8*@fp`6f zsu=F|pT~7ByBWSn%nj#wu!T+jW7Z$B`W(kYSJ*6c7D;s6ho*x?wBUR>8lnz$$InpvBw&e3!e4R~tFeA1LuH+AEo7L2O@ zm}~I@Yu#;ciu7+E|LuX&rSoPyxI*0c?r`hJvumvj%Y&PqxPP9QI{dc6_A}ouIWyt- zy_4n?7W5f6dd^c5_MR$w_rVztjKivyk!Jt3G(jeb>kLaHWRd{iYRL3P^x2fs5^IBe zBFbifbi~>eA+J!!0d#hU+TAU%yFcgb8>tJgH$Dlk3~NcWGD@rYPm&#Dap&>au+A78 z^9g^JlAgvE$8SR{6k}U2s)gUir!T!to$P?VLatMui#iv|%5|bd3V**>zuhV40ngQD zUT_wTuB_ZIBj*jy4`->>nc+DHlRsb2pXXz?NuEzYX`IhA8{;wTCfn}g*^qQnvneYB zsnba##&#piJbX`8Cy;r)A}Stv5tNu?tl)a5ZvdT>Ny_4K>DujhwN z+qN^SWCziWqgX7hhcO+}A-#X#&qKp>;u-+Z2`9$CtyD4Q;jepn0-z#v4&qniV3>+5 z8))*%w&eIdQ*8jhYbO4Na=E;eZv`1kw)lThal$>t=JDV6W_(MrrlURQFq%K)Xhf&b zIkl#GG-~(AY6pr|gA$nO`tDc8^VHsH#8F7vm)9}oPG(eiU$T`*ysv!q(EAXDq#CUu z5*l%Bjhmr~Dhj$R?jrP4)qDOQBAj8|MP_suw?2o|93^gwL&|X%iVuL>KDG3?!>1OH zoL66tybv)7opqx}9)_3N5zz(O7S_KLYCZMXwM>YyZC)8crLjwjcbclHqBn~2L-lo) zigI4V<;C42dIY?Nl(A;_dIUd@cKvC#V>mD18c6My4;zA`!$$uRI#!^#8F>TYzg`+T z=Xwhf{!F@;8M>o~I!1#NcO!>yvLlDDIMfz7eDWMsBg}CdVl-CS3qJ zTDmISov>eqvh076DVc9&cOq9?!?1PTo-|4fvCxO?aMfwQ<%e z;(2#|Ue<=GVHE(5dMCcZ?=Pc;U}lDIHJ{AnGs0PBI!3A zwb2wl|C3-pkeWx!lEhpEVU z1nxeds$5mMdujn&J$q*VTh$^rpwE1D?`Po0fsDD(I4I&3Ke9DIk|A8dJP6vv-|l>j zYz>c;KYNa0WcVy_CXW>ne@)yR{%gq%R68!Yq0p>=eQnEp%?-$$%#^l=OjJJmC++UmyC_JBpwQTeEdR1+O1QGO7iM#q&lKzm8a)nc8@diE&*~6% z)b%!(qrob8;y7DfN~H7c=z0Kfqr^>gkut2%SQ@5_mSJId)@Xgiz3@~JUH(IzI_s277ishTr)tvz_=VEQ z69PXLNg=J1ii-T1zxN@ZgTCkYCnNXIrnJ^0!hE9NPk&GAGFh*S)|GTkGP>md;{L-| z5wrVVVmH1)ExC=IbKr=FvrgmMK&IXeSoI%xb|Hy-bG|+&!cl37k>rQO`0wjoCH7Vt z`xac-t@oA2j-13KIVZw?o^uQdyU*BSti!2bQ%vhSVR)Q5?%$X{|0OAmi`g`Oj<67g zE1fRMkoXIrdw<0qhKrKIx9VNX`VZ}^F!X9j{eiw1Bh&Z}3RRZT(2cW}x#XZ|qei7Q z!WySb>dCi2r}n(<*^j@oF#0TGvAe7NcXx$%gVqewPvbsFUVq{)7imXS>qoNPdMrYh z+2M(^b(z%>y37uDJJx07S@065Ezg2&9*<~6908Ar+R-fkz4HLmna8xvy%gS9z#tP$ z`OyeZcc0z81ld}7#(q)LNRxV^vZ!~LkN#6S>@iYcb$>|Se|f5ydDs7<=%Z678=fx7 zk@JLZf(|jnl{`+fxp7!rqww=S!m2TTYViX~gjQi^T=q`a_?!1We>MC~Bz{SUFEsuZ z=n|8x;1zz@5{m0+^aH$9wL~=Wnk{;Lp`i8XuhIKXVn3;|x5Vf@3SLnMxn8OljHEcp zTZz48?-L5ziM^>kc3N}JuLNztJKW(0JjZLV`J2(Lmb(szrrxn&(s%^!{pkrY@cevj% zsIPZFYXwz>;yJ7feh#^V=Tav-DB&@tZSEtYBkgclaVpLuEfbOg(lXiRrW?xub44XE zRn+z5! zs;@fXe2ZlXjm>N3wbrskpMt1)k)HPuqHD063!jISicO#=!0rnm&OCNsV4P2Yni(>?+xgQ zyf@Z_EWAzXn{+|~p2q2^3aMawFtxrH&ReA3E3`|1g?G~9a+g3ts>}kycnhDEj1l~O z5@%&u4HDUiwr)UY{O8QwgcID|S93SZ3(`OkU8*hDJB#~Ik&Kqun;W0uq_nYXy9C`; z?A?GWO^leNwZz_hfaYx{b~}P1sX^zY#NGlOsPs9hsR6yl-U~GL;mq3-dyD>>w>9>v zTGx`;>7102myTV<$}#oft(K6(Xi6)}~#N%G2ZZ}X%gPsARH9(S(2J0))7 zbvf>B?$zM+vg1~Cl(_3@+?&DmO%=4pt@KI8&0T~DH%^Hmx*AJUMD-IIh#kGvd#%LG zJKAgS=!e`dxu2Hf;~l+d@5Xjp^KfAlmJFgUC%GT z2%7>Ji^PD?FN$7l3DUk-wU1?ScM{FRxY-v+R*kCAEQ`C7@j@6kuLa>&Bayh@#7N}0 zw|OdyZfYcY+*vkPFmCTeJ??GpRpID>9=GC8iMzSRy%~G5;}y8oEakXK<06`XD+d3n zU^Qm72WMIjdF8&vsF^D&@#}Wg@x&t0Re|A;$Oiiy! z4R7%qYZ48uRnDqR^9HjQQ=*o&%5{Qge4b_uf9ZGpdk${l9dqL%)`L3cq76QRO1SC3 zarG3@Tz0_b2J$)P;`623wz-}R4nJ=e2X8Tpx0<&ewML^;F0y)wWe7xo|P@$+bZ@SzKjhU`UVzFC(J&D?#rfEM$A5fH5Dls zjD;G}rM#p|`G_V%e%Q0by|`Y_?O;qFcMHc&CAjD>i7(&_6N=h|Q_o&eQ;3@8fbFG6 zkCuM)(L-XpXlBKlHLMfngy6&Owp-tPb8EmH73>;}#^`rq?XHJxi5$I(`|;?@lCsR` z6L_3gj6Ok)KD_rz{`2T@?;MR@oF9Mg`|r;kK0Foc7Di=9n=q1n9{5g;lplkc$EH_ax#_m1P{?;ZxxY4`CB=z;im)dZm?du9Qft3_3|!Z4HxUS zx*iD%GgLYrql5KF`F!ZRK1vcRf<9b-U3T;GNZ8Fp9Gh5<*BsX;I3=rFKE++m=tUKc zizxXs5m1s2yGhaTvjt+M^@;VS^@jDScy!KWYp$p*n&7{+t^1~%D}&?A)ZkC%%B|+a z;5oB=kW58X9dMRKnNcfZ@~G8^hi0PSjaA*E%a=kGw0&TfZ$5r>uHnlC?yf+A3bOo)n|3u5{*cjv`{2USmI>fe`(; zM~==+WUu0^%M4Aj#sY4xd57nOtFh5V?3!nc8Pksb77=ZCJI>ieEDE1VXRg4XW#pnM zz8?r&Ui4b0@8{tAmi)OAe|`|3dw8CQMp8M2zNdS^njsF0d*}G`u+Sys?$GCI3=dt+ z{yv4jFU05Hxu4%PAU==e`6^;h!vB#Z8?@GomK(lg; zAD$9|cl{N{uYDx&_DFl%f(B+*XZ-MP5WM+Q7{AVT@SMhGBK#IkIy1&|2IE<5!=FS% z)*e5@(+1G+ws(W*1Dlw~za&f_r^Ri17g90));xY#!wer29>2ECVA0dPq@ZUL(B}&u z;csTcXNK`>`XhcqmnlHkyA$Kz!}$A4c-M$9ejRHJ`_3j}Jg+eR7B>FTVf;E75_JN$ zH!L3Odx7x}vf*7AEww|BU4!klS61x;$E0yCk6D!$_ z^H}nkaz$bs;%1RBE37Y_UA91#AB`U1f|=G4XO}HJpKdh6bE1l~3zecG`R*QKs`QcL zw|#ej=WNGJqp>}&lQz}YdZ)AR4lLS?g4Hdho2s7Y97fLt;Jnd>S%Z5_ z0E4cl%2p*hIJzN$KKpn)ldQGQZb=~AgXc4t`x;u0 zIW0XrKVf#0gXbw)(!b&(I`FPCo4HSe4v4A1 zuwo*(;iWzq_}+uh z&G_>N{J8=?cjwO^(&x|`oDly_{{qJMKdyZOwOr2;v|aoBZ+kj$#qVQWDfJWYfFe-~ zl~PhBs%%`Y@8kJ@!t*IZ&;NQps8w~1VtU}Lx9NEsk3r@$IIAF)>`uoi*@CJsO~Jcx z{oT6iiZdjrKu+c0GM^Lvf5ShI@%u4n`ThugB7VD*D?O3P01aWivF|F?V=4*_=qtF# z$$eMjo)P@^v+C_`CpQs1^$hV+6+T8+mb)Uj2^K>KtA*=(_ZrmRJ>}2h`^-2{P3D%W zwq?79`{k0k1-Fm$zFlt&-&&tliTy}f+5UnoG@xml1wY$y`VpcZ@p!gpeAcWi^v2zl zwLgp6iB9b(^jCh**Gaqllro)qqsKo8_HJxne@@%n9fl zrz&_=?l?;dg0UsVgboY=SE!2hLL>9^;vM)0@348H-#x>@C0whI^YUv^O3&p@jikew zlli>j#CO%v49sJF?=U6o#D$qFET5J zpR>G|@P(oiKj(9R@!(v+`Q6D_!q1ben<6r%Hi@}*#?+vQjHyk=b29te8XA#Xu}QRJ zeWD+Fo;Y(WHVHS+*Y1Ua`QkjrdpF^XV7X}xX1U2H4LwI*XcXwcx-aV`u;B2Vvk}Jj4&Nbe(!S$BwCv<_Y z>u!s_EC*2m^Z+U+a=cG^-}ZI$J?q==d&l=vR8myqsK=wbL@$UA#B_~$B<6#1G3CaU zd#2psa(~CRjU5^La_rx6{o;zt8|4R=f4lsj@j3CM;-8JT60#F+OIVz+Dd9-MABnMv z4HI`J#V3tQI+&c7JSRDr(mG{o%ApD|73x%Yq{64E^-`Clo=t0*_GH=*6|*ZARNPVV ze5Jff6DmDh=}6_+$~RPATKQa+0aaeCTBT~ss;8<&SKD5_LG|X<`=>{xXQYov-;#c* z#vL^l*Z8t#xta}Xj;Q%a&C@l1$S^aiW;DxKSIb+gQ>_EFlWNbZ)2_~onTeVAXP(I# z4srWw-K@G3>+Y)Ot2eUVkM-x*fBu@P*W7>2xdsCoY;Wjm*u3GB4L@jj`C8YtO|HG^ z+9lV%bZscRY4-H&C$eA6KA0V7RHsqS)Rc22FF z#ZAqolbdeIt(v7Gt!I``>3weztqExYXN66o5g>!PkFyEW=o(Cwk?Q?KuN{gUgCc2Dd+ru*AHQhLnq z5$ZXp=Lfwy^m?Ipz1~ZDAIYzuzc4@0XL_H@H*~wAHP3-J9*8SIWu$SoOe6#(i#7pV)e0=){|o-kfBe{M6)aQ!G=4OxZEz=F}Ne=S}@|>TmyHJ2v(7 z)C*HHrv4sMFQik*h>-U~J`VXZPH&e?Z^-`(_Ht@rlKx@*?<_x;|V^!}pvH@yGr`-!ukn>}In z#@YL47tEPA=h)n?a~I70a_*+NDRXl_sPn<2A3XKJ`VUIyeLlaz{N*1G{cz}l>I-Hr zh*?m)u+zegiymC`@}k2ZH~o0>$Hj|-7tdOJa*48}&64+*9A9#E>7z@BEj|88qfcgi z67}i5pAPtR^QR|2tNPiKpY{K2=d!BHo?mug`TffmEe~66|9sfzpM8FDMU@qORvh{w z;EU;BBz)QX%cCm?uH3vb`>UtE`r_;AUw`&>%r{NGne@%>Z?1$s5;`XIyU;(s_5F6x zx2IQ)SQWOac=g+>Q`S7UX7k!7);_oP%e9&7TCEFNm$|w#(bQ zZvSNaxgE`StlAO1^X{D?I~VK>-MM||;hph29XtQr)pu9;uCgD8|9I%f+})jbhwjb{ zdp2xbSW;NQp2>R>!nN?);SYqj4-XC>7Ct$AUig>c+rsyR9|%7Z9uuAzp88XlpT7R- z*xsgl-`RU{-{5`U{p|nqWeq@`-u93qc--`Swa#iG>$fJ>$B0WcjA2E*BJlgbV$D{p@jz2p0=;uc_9{uU) zv7^aH^N*>=>K=RaSodT5qWVOQjhY$tS=4t?zedGJIikvrS2^C`c-!OMj*mD#<@mzm zYmWbP{M7La$1{)renLADaN@xePoD@r@zRM&Cq6o{{KV=LVJD8CICmoLL`k$ybgk${ z(N9Eoj~*U9E&8MARndE*W1?-*#V0K%Yo2U)^3ju>PYyUa=H&Y)SDajXa@)y$Cy$&w ze=_@|=j4qTznI!FkHoZ#=@T<1W@gN%G2g}f8WS6n8S}@fs;AyO6?-cC)Sstooo;@* z^XZpQPdUBt^qSK@o{l(eJ)L{nI8*0L_cJrkEIPCH%-%Dn&ZL|vj;$JdPi)iJjP{Ios*%ptBRtet0(YY}nc8vuS5b;zqwd)cA$*tK-AtW8yEw=f~eXSNq%}=bk**>)hybQ_n3tx8mHEbK&P=&Rsa? zJa;ppc0#j+4hh{71}98Nn47RPVRga}35OD55;77z=Y7xDJKysB^7C8HA3T5ld|slG zcu!)J#8!zN6T2r4NPH#n&BTR?pCx{kxHj?o#9fK|5)UOFPmE2xkeHU3lUR^gdZGG- z2QIX`5PV_yg-I6{U08PE>kI2HY_Zm~K4k4|9bg?}ooQWW{XVI7QiG(&lR71JO&Xpw zEooNLhe=D5zDU}Zv?u96(vhT?q;pA^l59y=k_wZqT@1Wf_hQh+w=d4QxbWhdi$7ib z?c%YEDHktaynZS0QnO3XTzcu!q)Q7ft-18mrI<@;mu@8cCD%)ClH4J=XY$L*qmySP zuSnjS9FZKI9GC1!Hd3ml+>_EY<sFaZtB;mKc*f}O-(IItCH3vtz+7Nv|(wlr_D-Rk+v-@GA$wPa@wEi zwbCC>e?EO!`djIrrEg2$lYS^YCOsv+Aj2o4VaAggy)(vU%+6Suu_NO|MrKBFrX{m( z=EIp!XZFn;pE)=4+swV0k(p;Q(=$Cb)pnPyg{_Nigl)R*6Wb=+ueNhGXO>S^-K>YR zp3Hhat54Rjthcg0%37Tjo^>kAmUZoN;N|9*pS?Wn@{G%$T;6o~z~%VMj>~1)cV|D6 z-7dRJcHivb*^{$B$o@KeS9Vl(N_J6>e@;-&lR3}l^vM~PGcM=doTWKya(3lJ=bX>U z$jQ(7-LBaK?Dg%Nw%>Gga?Ez5oEXS#f3Ul^?F`zVgeJh%3=o z;;tlJ$++UUaxJfFUX#2IdHwU=$eW+{ZQh=|m^@qFwfvg-59L3XKQw=A{#*HT@|Wa) zmA^56Z+>L{nf&y8PyP+3pR=|z$l1c#-r3dJ&pE+4%lWA@)VbLi<~-;;=}dGwoYxAf z6x1nbQqZcPV?pUO`d8b(iX@ z;;QAU=W62W=<4s9;F{(7(6!XH$+g{e(3Rjyc4fKpTt#kwcTM-b?#Awi+>g0CxSw+e zyOTV@o*kZ_JQ1FAo-9wE=W3x^Sfj9B;e&;33ZE(LQ8>8p^};EIvkDg#eo?p%Ur~CD zYSUT|l?A#+1#~JE_xec>Qbc)bwvyomWq|Yg{WmZ8kMNV;x<8Jk`O`iXW4uQsAzKZA zwVTV;P%&1|60ho9w`7XZ%%L2t3=;#?Q(~T)Aew7SM1B2j{*UWX(Lmk6|8edlKGq%< zD)+wuwu1hkFL)fx2XBLJU;-Ga-X}g(9HNz8Q#`BB5-aqF#XK!rbnWjcw&ZmhtExW}^^?C7~{+4KIsY?7HvC^0S^<()FEaLu#X85XJ#wpc4CgDrKrWKjnQv>~E4VKr@{ctTqvGC3oRKKF^}tHu*H7CqH% z!cU(j-qA`$9ZQjDtow-jv>Z{5aDcj7v{zN}BVm0@f7ZD+5{vXXVvN2|ykc3)>dDT0 zOZJ9Xtp6c~X$!?jpRVF%ZH4$u%Mvf^lf?(-d5T!ARS_TXosd!LRnZBwQ&)=+{WtN2 zRv@}Tt7U4Sm`waU?FaIa#p=s=G1TV~(a+~)F&>&f@4L{rs&^H=Eeqh?Dd^@9i+zrW z7xf>+czuQFqA#Xg--r+O0@0Td6 z@sM^=%+l@?!RjR1uj0lsA#T1xh}_l?*%b-UQ6zbWeI zcfo^V_`O)n)7!!G&p}J%^i?qr+I6+Wh`v56#W?*ZF;jovu=%_t+FHs)6U$IB!Edo> z>9<_0_8Bf73DOwmZMBYMM!Mfw!@u}U=d z(FnicoH*pIspULnJkR+!G0JC|Smfgrb1i(i&u1$4O#q!lf1d^DUWjO>zh$IwKFH^s zXylX4GfNDYOvg7ES?G&AjW;sU%}^gjEJ8OMK#MBA&x@TtPl~lZ1<=JVKJ;%RzM~Aw z^&sqn2b#mCs+T(fmYuKBI^zPz5-^bujhX+0wvYc0q><2rtfP*;TN0qi)#2FeKHKH#`D$D4pO_UF77U)_k^ z29}55VQVp8+CL>yj8+bcw#r%2M%gNQXtl*8t-k22`H43ANYPpUR*XUcDVM{2HepLvh}SJ&i!J&k^74^b zr-zHD{JM!()Rx9>?C)5!E~zOx$@)aEChk{^8uH$TqJ!Q5d(jYDHx#YGGvImfH5khKorhJxA3+3Gxx#wPFVXf$A86^7Y*~sb&-ZxM5^&N!Gd0EV~ ze8YJX_r1mOQ0zuWQ4bk^o_lASc{|c={>42Ho#436M*W~+TzGYc{7v24yB&~eW z9QwACvI6>o-rz~l6LhD%pHm-vtz~lWQ>62o7;Nc^U2Kk?A*<->YHad+_$~9-7kf05 zdd6S1P_Jy{`zhkaTD_{6rB^k25xy-UX(+Erl)vYW@5W9|v}^*4cwZ)A7IjB`@s0kW zn4-Td0`$Y;34J0qtCi>?(^7sCBYZ;1!(Qr;KqHK@cK~mK7GNA04W0l~!B8N5pv$r@ zG*KUatG@B4N@$iNU%alj6&<1XZ2f8YD%1WFY(#!m61SdXd=mVT{>a8~izY^DQGDW~ zyHTk7LkDP${yZjS%=G8*&n!cX-zD_rMxykI)NMwt>1Rn_>rTM;GW{;;dzC|d-Z)8p z`yJtF5OK@rl73fZNPnvmOn*!IRR0F)XUXzZf?ChmEBy?@U%-xA{+0BxDg)n2`d1Z@ zzLkV5lYHMlLRl`}CwBjexFOpt=52b%>`l#NpZ~1w9(`UwS{jZ@ZL-@9p!*jw<@?G-ce;Q(ITAO|`KDp@^ zm;2_@KbAgvGPanIHWuCgS70Bc9jOfEhOq^E`w`(buvY4dX``f_ zstjqvD?{4&%I_j=)BaS1v&5T~-&1j{W7Gcv>^$#X0d{~x zdHj1mWxY}XSzlC!q)P=Vm!Z!v+hbBjqz$mp_L6qPv6&=K1yAX z?E|x|DD43GlX*j*hMBr9eQNo>@-|?3d+k1X&Gf%zyH2(Nu}fw<5dT}=Qz6TL4`q4f zb+i2}+tod=YY!TGq@N}8B-@WBz2*DM^IDM)v+Y8A1Dk{oZ4}aa@9Qt?9@-Mp|F5uP zCQb3NB|T(Y!-w{PJg!K$BCOD%3cF_dr?ki5|Imu(Zlw*);K?FMJ2R}XB^7>sMMyh^ z=le^#%X0oBqQZ4cVi75TYC7^n|2R^1`~&i4FY?0iM}{yj7<=sk^P|90J! zD_Q?MZ`$5>|874koj22$8Q)bp|Es;NX#f7V@b7lByuEDsFJXmj{Uh9j{36$8JFuMo zQfH+6R!%cJVh`bFeg!Jb@I>y>4d)2t44PaFCQrch_RMcomgXO!(U zZ43Hm_WwwEk^Mk2EU!;1>c)z&yl#B=mOfRyTk1M}LZ&Wz>$+*T>bCN_%k)*~A2R!B z%EJmBuBd}7XUZCwcE5q~u%({SPHk^=&`%j(>hG52>Mmomc?};_+I!Q6n)V%k9^YR2 z`M2!0^#7$zE^jNCbq~j8Jt5ly*l+5Ag)+XPE|C8Dt@LG`Am4%iZl){S3$nhFZ2_6K ztcPU!v_nk4TV9uOyuhb%~+Q9jRmWnEAK^Zs((x}_u3d-C1*W$(&{ zr0=9M`eOViUcbNWIsXwdrt*)`Y)ubUO5}Vm%PI?Fb;T6Bv>(xltZ7{2g0B1e`s#jM^f4duk#~|4e?QfSK%VOSWR7&1V}3GSnHQN|-tPVJV(&4@ zSuDQ3K2@r4)z@F%$(g^uFL{=k)qS|m9URFU{p6{P@#FP=zWh--Uq9a}{@f{ZWu6gw z-_2Ds4e}>n?#oxs{e8U0G9|9co%fPVO1_VhnTP(q{_=78Wpv%*Z}Hb8rBp=`n&#uH z!vLs3F*Myr^{2S-!Pl<}r6G4bqx-WXA^Cm_?KJt*G?g^J8w6ZoU;KMbEpz zT}dk_F6GjDL&cLCl2(~^Qozb@@!km4CE?`HB%1e7?oHX0rTgbkUh+OAGp@peit~Tm z>tz-1l0`RZ!d~haL^3mG?ocG2l=s5va)Oz6-QtPKVq^-Pkuof^t!kWmIZa~7UzMto z@}%MKYnD;UJ?H)=Lv9n!2OhCWm8cZ?m*c+|LsI(R?lt3Y@s)haP3nfJg%m&@n)&k5 z!aR`=kx`i_S^ev~%`(Y?OPWA3GD}uXT_HxME16xMn6x2~!Q=*VbL$m6DmBt;F}#`u z#n5yKX>Dq=$3_zBKCHapzn;t!K4Jz(s;DmJiVb3?*u~1E6CzI7`PyO`OYLeZ4=Bx) z*2+L-lrmnKt9+m=SH4g-Dq)I8G1U5MJ9VgfP>oh&)HJnNQ;>ImEkJ9kE!95NzR)&m zVcG#LMmwt|u&Vk2{ZYM}-dBHJpQ^v7FVffR+w?v9uliX%Nw?{x7R3^1xvP=V=-EaK z8x=MxZglOw-a-CBRfB2<-5t~@s7X+>pw>ZcgE|Iv3hEj(Bj|&m%|Y9Pb_VSU`ZefK zV@u<^8rNyur167|TQz=#IYK^70-DrnQm;v)CXY0EvB|4V#y)V$rCvSur_M({ZZhvm z{$#0xDD_54{iBE!(ITGjuz7@`1S+-o&Tn(&F=deQD*u<=-^*U zJ-ocsH7k{R45d~mwL+2IkDocI3>?nPZ#AsPa@HY?EvgZEx6KyS&Xd z^zueqKU-rP^BOaYKtZNGGbeLh=8DYanF}&jX1#Ycsyi_%h?;jENb8 zGWw?!-1SS9-+g{lk15fnyvxkp{$+DWiOlcwwIQs4)PqVtWri}h(&=9gl}YlHJ3eQ> z)q6>aM4%XEm3`~#0Ps3sv`SqC4ui8G%X{x%_l%(4;in$ue(-PPAV zM$+C_m#d$vE7UL4FV&UeL-CRNmHM^%jT)+cD;9`_>MC`$x<*~Au2a{GMdD+1yShW& zsqRwcSma`{L=98-sNw2Q>Rz!_oh3dIpQ^LPXX<4&Tg_4Jszc3XRPlL zZq>uw#n073wMZ>iOVm>Js`|TFA-+)mP_L=i)j!o6>P^8IfL2Ybt_5l}wOZO;>Kt{h zx=`JqJ)*VHT57GdDq2;oh8BQk?$#Eug79N)G2_g8M7Z|3wnFwpYv1_K76zXK_*cMcXef(G!y_QbZ~m z{*y@4e$x(WhqS|5gchkC5$PgBJE|QMc_N?h92C$K>%x+H=)o@(MWR@g@Lz;WwP@|6 zxT?j7-?0&P?X;q5XE2?y*vU98UeWo+r4P2lp(U{9%TMvw=WDsz73ChKE^CbIX?a?{ zR-ietPY)>%E05^&lom=$r4?UJY^{H&e}vU)qqOD!6}vRI@&wxd~{;~c6D=&NNbM(1diB_szRr;`cxu4Qs8GyAPq`ZWUo}~;{URH)MmsH>t z}Y`wnTKpC!#P(~`RVAYl=qx6RQQe~3A9P$SHK|>mcbsk#(*5TNp zLp!z%(`7O|!QD%TlE9ng2@sl5kKSw-;n-8!sd0YAJ)0+(E5!V2XzaJ z>^CGVvTOauLx;8u^SPBr@;c{@y58LSwg~fixMi51H-&*i!aCOv6GN9SmFW!}(zJ2d zyroO)FQo((=aGLs->-&u9yjhi+mheNxz4D)Z%bKsC} zWU(<>tkNQ^MYon=Ra=C$B&gP6Pcvn4(9(fJ4s;f}7`@+5EFLuEfM}uRh77GA)|AW! zE#4mp-OA%+DXX^#>%4e>kQg>(PfO9Y{sFoRa=Q{=?U69?hzM6|sE;V&0SfzH0y?&S zpnjn6dqn)Av~P8Py_y{UqW0`CNPF?AR*h@RBmGvKrFHZB>eP^NKBN0T+p@kq z^6mV1`Llk*dvtv0UK!{A(U?9Ro64gq@4VjY$;R@i>WX(pc;l+ge6{D34P{*Q7WX%( zT}>X<=+f?y`)Y9{!UJpgl36d0!spdiriX{gUBBAO40$>u{MiPaj*`0%pLkn7@wR;8?eKsg9-A5@PvwzY?(PfX=23FDT<+rJ&XBuj z<$aGfkx$6oqYtxtmYra=_ZjTrI^Y!3_%Fe8#9%Rsx!$j$Xxl{;Hotb+tIS1N%{OT} zi0WnE&>zuQRMk6*+WKcAkT!2^pSp~%MT$ViG;8_NdhtoWRo4ZGU!|wV=#LQ8v*pK% zsK=Q%UKKTHwR?}XO7VK7V~co@+Q54cYsT>ny~n`y-?pNTi$RaMkhYLy>X zV|~Db<;T^<2)^**y{Cp4M1AQ!4&>Pz<;OKeHR{eTVyXz?J1^738)CeefM*&c+VH>1 zA7cv=FK~4#@sq?@&Vt1hFhj7ru1bhp)JG<7~FFuv^Er>C^VheQ`fVvqts*HKxx1}NNK|Ue&qpXV^G&~OwG>z zN##lQPw@>ZMR`u?%KioAMfN=vKI}>z-k<#-We9s}XZEivud^ScjA1`PnZW){yjX?* zgfoSGh!Vp7S6Tv^@|$u*s7iujCnitHV_&QkvoBRj+23FcSz#Rtd6r!f?0r>V_Wq24 zC~8f$Ci^<9%~8~P^b9I$bNE1=p^|g8rP`8xYqc%=QR-;+v+#%&HAnkIDB5TEWs0^1 zze~aAI>bIgOJ<+0K|?K5bFjan<+FEbMeIxT+1Q;An9rg}uZR6Y`X&`T5mM8?(&O1D z=!NWyng65cH*}~?8@f6)mvSP%2rK`$g@VLvW!uKKooxr(PO($coHk%Hu3M0s&B($o z*;3+Mh$`ZK>4CW%(#r;VK`T1|Rw#uVgP9UU?~MAV%)cG?uthv|gT&sM4Lb%eV?U@@RG&5tSM_Mna}a3`YSiGqdO;0>${KIzo!NLx zuQm67`rzDVYnuJs>|nE_4Y;Er~tIgwW(}N#>va@`D@Jcf` zv)a{ZAH`WwhaWr6c*^Ig37z@`=XS~rUiS3QPiH>+Yp!)-t*_mTX4E}leDYY zdS~{|B*mXA5ZuJONv;H^{~e^i=JuORevN@=l4~;Fi#0rJuRyPKbMy3K>wUdPdhblh zO>>(-Ij2z4%A1VyZr<1lefkC0@^0nvQ^@H>nTI}YC?68k^J1U6W}cw1S9W@NdcezG zvwEb_xzdiB2&HLFm%d=GS}rM^X%mk zDXcN+1Z5lz`zlfWKgQjG50&}iE%j}RS5Uqp z>}mH>T~eOhdZbkLS`%CZMe0DITV;~8Bd_Vbs!2UU2PHqH+}`4@)Jv&zre0=B3iQ4X zC)?bT*eT`GJ4L8H7D;XI^Wfktyf6AmD*b9z92=L7?22Pi^VX5~K%U=>BC^1T9Ja?Qm+{R+#P4g~kQ-xd& zYAgnqj3^`8%n!LYeloAc7#U`mCij2+$>%D5@*8sApRx6Jj!ETKe8u(ud1!?F_j|qc zFbWJ-oXekKXUBZXJN^DWFBMd+{8Z)A-Hxw#&+VB1e&nUR_rBYyA{UkESM=Z0p>)M} zx{EJ({@0)PN$(pgzxQts2HZY3h8jt?&+c^eS6Qpf^*i1D&p5+w*eFwuSB?#b!8a!U zr$2+avBq~5cN-B<^CJ68#+$|w)Q@7$B{|t>hgPuwGTUw8c9|Gf14-^k;? zy`$VdrONB_OP5mO?(hq|9cK)@eO&pdT!W?T_AxC=xvm=XymuL~TruBMem=Eql0kod z<)86l<+J~MT-j><`<-5CGru7$j3Q&9k!!RqyCUwYctUDOStdFA&iKVRbSt(^#gPo9 zoiUCBr|~`e#m3pcT-|*8%t*O?{BK9)yt{)&{@WA0^NyCsD~Z&s1=PD4QcC$rsXP5| zDH(57Trr|5j{Ysg?#2AuwTgHn_tp`7mp>!Xv|b6=mVHJ7_Rg%;iDxwE&Od{d1>T=A zy5i`c;s4%x|1*t>7-L_>QRUEh@890>pLNt&Z|tg^4rPt}JLQa%@(!;Z`Rl#^`K;`J zU>%H?j6+}?wz;h_inqK*n!aqpm}o4zZGG^lEb?u}7iCWxQa8DytP$s?k0r1CuYUgv zJ9;Odq*9Hr*ZA$uZiSAqi(geI$B+=p4LEXqy=eBwEMLOv}W2vjKQ_k7I{b97|H*{ z9MxX#9e0zXZgR{``$h|uqix!1bDV9L_G87knf4R?H~X}o1W_wT!VQVIaiIGUpD7F@2DO*eW z{=PlYh^HuQ&eXDSUtNyS;#!W zkCi3jW96c9k+oRKN{aYINmJ6qXG*4G6U&r*r9gbH7>XgjRBNyfbR~Upt;IKLTYBU| z)oyAx@vZuT`l47RC049KX6J}?$n8q8Uj0_xAoi)7)bGV1#)o%{BkErDpom5;4~uwY zGg6#GK97n7WHd^gM^58JqLfXMj&!C98xoo>vXIhD*1BZT@0X3FIz$fAdPUfg*nHtY zY70azlIs>%klvdjPc>A7QBnve{*aTY#5E+hhPaOO28utC;9BAak{cv$BE1iYGNiW| z9g`1f4>6wlsMcQ5wJzEVN)4^I)>pYp8>9_V>S$xNH{hwIfko@M*cm@SjJ=j zRK{s#nxTX+(yA-d__9znWu_je2P*ICwe-5mdwL_iyE0FIQGZd{#`p1FR<`S-^ij%w zW|X|G9AH%U9VJ4J$tsbuM^rhg&(}Xxjxp-HKsnA>?;_=d{*As`iPqQZYn3z1yxE|{ z>YMaUN}Rq$->SqjBW1gCUf-$zs3huPdYF>L43%)@qP|!ERk@@e)DJ3|dW0UK*z}|N z2_;KEsh?CF`YHXilFK}nI3IxQ;#pECNRvNs9s}NSFUL zV)$Xt1It|eM?IH#!00Sm8LiR#+M*l(q4)*-j2A&L=m`dbm%w0JEJJCt4#TS&304yK zEm%!lIBDz!zj1zuYbOa~z-g`}kggT{ZsaQ(sBUyt>Jrv7a+OAeO+W`m)1D{nM))e> zYhWT+ROV8af@NSi$TuRD0wYpw3qCM9tMkBo@FDmJEC36^DkEK84c36QU>#TwHh_&r zzPbr~2R4K6!49w!>;gZ6-5|`!Q}=*y@Dtc;Fg&wK=>=+Z-j?%2(jePXIHrierU9YX(ZRBb9fO|oG&=A}Q zni{!U8zWMK4%*}33GgIn2ik+)pbzK^78#wjWndj?Z{k_XtZm`>Z5(gscn4v)(ORQi z%;>1C9W&CkDB@25_^8EjFZtIJ2$Mk;_g&^5J8}7(I|&OxF~@&!?Hbpv6aGnP82Nl- zI$u{oHBbWtg4#x|UI)|#4LNUQbk^@P()A$1#)M4>n-bnn_yFO9gv|(>6Fx-vFySMF zEeKl@wjz9#ur=Xhgl!1h5Qpx{HI+Vnb(nd z{beu|3ZRF`=!FccnmzJ}*EfX&>qjS!jB zBe;Hy`;LRtMx>qq@@Nyb#b#uPj`Vy!g(v#7k%s-qzQpteZeQ(y9%rYM@Zv5 zXCjch#%oZyR`r$r4t0p!u*Ye;F%Qknuv z5>H9aQj)Wjq=1r~r6gy`eLT58OYYB-`?KUco}9;%qj+)@Pmbcr(OGg4Pc90`MLfBP zCl~SLBA#5FB^UAJ;w-r+AQuJnakQfK+`(|EUxBZ|Hy{+`7%r_gr~~SPdY}Pl1j1SS z|2h~0j_`bMys|)Scp%TMaLPclj<%C{N z=;ef7PUz)?R!(T;gjP;y<%CvFXyt@fPH5$XR!(T;gjP;y<%CvF=;DMfPUzx7s; zgf33#;)E_v=;DMfPUzx7s;gf33#;)E_v=;DMfPUzxb=Qi@_43i2Aaa`m&h1s+jtwn0lm`I;5C7q*zNainLUa0rEL7Hj1eeim4NdsS}E^ zp~cwHVr*zJ_OTfISWKIz8T&^-Tkr(<2z&`b!A~Fx9X(HO{J=e+K4=K;1C7CR;7xMX z4Ri-Df?%)+d=6HCF92z(E5Si<7({}jARbTxl|CfZ3jP4s!3|2#5-bJZ04SsJj8*_V zvE(|IT*s2*SaKXoj$_GjEIE!P$Fbx%mK?{D<5+SWOO9j7 zaV$BErR@+!eSQF&o{CLR#iplX(^Ex%X4ek@0}ZPfMEDYML-AUM(FZ&dtmOP#u$s8v z2oDh#0Z)%`?@`(b$LI$;&bZ16`XEnoJqDbjU-LBgpTS>Hdas?_?*WCN7?glg z@H_YaL3kbf3CfHkOf@<}eUhqJ2Dx)!;CcA4QaF+(r7oN zVFzv4K^t{dDs@#VbyX^LRVsB=Ds@#VbyX^LRjL{b;=nm@9$WxPposTSPW3AI9bDrb ze}bEamG(gz?SnMh2Whkq(r6!~(LPAS=Gv&^Qnd}n5q$nMY89)tg}7}TZ|8UiVHDSo zbNvM8(S*<)8*QULOx0K|p`8N>oF@Yt$9Cc!g!!C1IW7dn92-U|^<}EA0zc!3UIkP$ ztkj*UdLUtK!-ieA>2*N^t~DgS9q0g_0#Acy!E>Ms=mvW7TyM}9^asclHJ+6k&q`gJ zN?n^uU7JenXQjSP#UD?@A5X&{Ps1Ni()SZq z0rnC93pmDo$C1z3)Og9%c*#h+2Wj^p?H;7vgLHe4Za31Migc$U-5#XdgLJ!*ZV%F( zic~w%l>j6<4~h05(HW#z$piua=yT?f>4Kv4%2bwE)E6m>vR2NZQcQ3n)tKv4%2bwE)E6m>vR2b6L^B?nY; z;9;IZmJ4VTU1j8>jMiI6JoCxOxk5}N`%`#{hqHZK-OsrK&0CMRmmr zF49tFtWQZH$7$po&sWJeA`~ab1%xGBFXi|TTGD}_rct2Q64lk(9M|LcFOSy)Y`0gDGGtm~ND6 zGr-$mCU}?Lf%&BOA&_lK`Y^ODIOd!N|(%!5_d$Stt&1%SI zF7sKdBA?ziXEDc>+nv?u7m;nxK*HM0{;vb-g8xZdv?tH>27N()fSgfZ+8v`Z3~;gVTJiC62y?cu}2xnFu|d&<652E+oDLl+wd- zjqo}x52*!Nct_AvyaHSRy-*%UuHcPAoc17S4ju-lz*U~rK@9h@a)8#=E5L=v_c-VZ zIOnM%o+{$0BAzPZsUn^#;;AA=dWIP#=941{X*E#$l$a^hGfFC_Lak6jtx$p$FCoQZ zQY8PqbG-M>PkDgvx@|-TLEtp>6F|?ZENR#7x!FcY`V-ir}!H= z^bjRdKiZWb<1)UzERCBUqXg_u6n=gQ^l{UBl%TwTXBP~h0Q7Pzuk+k!w0#WLcakv% z8x=!cdWyRA6m{t->e5rxrKeQc4%kcUWgo4S8u;^ppcatrfu_XCHi2vpc-sOcnrsVf zA$}j6JHmZOp->Fxr>FyI6=|}4kW83joTA=4MZI^5-l{~XX*b&p#e}6MRo%21O5mYe zzYFfyriF7iVI9JI22ybyL+X>SNWn1BG!gpwIyvKFfZiwLcDBm?ZMtl_E zap*e@PDH|qNI0<;PDH~A51iP6pPqo9o`8>@fbY#n9@i6@17Rgh=AJZ;)3MPu<|Enh zqn+I20fnF#lmJ>ja48mlI{|+?0e?FIjwRr4C*W%*z_n=n>jXIF!N*R3V{75q8HJV` zzP4kyY55fa@`=BkfWMpoXV=2nwQzPVoZSIecfi#haCHYQJP%ymfuEcJ zSM%WNTDZ#kMb4wa8E_6<;+_=x(o;bOuo33~c}6sx-2qoUaMXkUn}GkD09W(iXdc|m zgOfS4Fu?!YHa!2e9Z|4e|RYvE`#+>B-wi;uAfPVRw| z>)_;ZI9UWIx5CLdI2i{g<4oN;LYTlkR*=j+>D0?!y*dgP<4oQ15Eg=BPy$N9RpSJl zjDwSLa54@~#=*%rI2i{gkHg70I4SLs8*awI&2?}y5^l!9%{aIz?UdBRb+`0Ta&#cq zUIH(J5n!Zg%cO3`!qHr~xgKtwfRlURSqJuLX53&bt_!m3Rl;e zdO4f;MVyBc?gPJo2(BG9j>FaCtOROr9EO87IB0`|HY}vHj&7`@8@;r_$-{8+Fgh3m z_iX5(4bIu%oDI&|;G7N4WxzQboU_3>8=SMjIUAg_VLc15oNg>xK*6$c4IgsB1+x3ZbwS3R|JD6$)FSuoVi&Lt!fvwnAYm6t+TPD-^b( z8xAOIE!T}gC~Sqob|`!n3R|JDwOls}p|l;{$i>>_LTxM5wnA+y)V7xEMIn0OKraf= z3p;udj}AECfECJHp}ZB!TcNxa%G;s59m?C$0S7wZKnEP?fTLUo9B{*m4!Gcky<7)m z3*U+k6v7cJ9I>JU4!B{38&>4rf!sUF<-V|7?hE0L74BG(dlz!=LhfD2eJ&ibBKHpD z-htdZ;E)};&xK1?Q-r>tWn$Gb z#7CTe2|__Q_x?n_#ZPhZhN@2hoGDH(@`X?GJ{aAFrb`V~liV9OrNH z%p}gIV9lm-9>RDHVIcNl1M|U$;3L2|kIFcYx`^wagB9QlKw0T$&4t?; z>d#!?4}Jv)i8~A;!BG$o7=Kk0fff7#u7ex+(lv}sBq9@u$V4JCkcbSeCGm?G<5^1h z3Fpf={)XdK9IpkuOQUT_1yRVw6QVwKUwibqBfh2A|MVa|h2>sO zA^KlZ?&TDt|Ak0LiRoK*;8^;U-3X;OB7Mo%;qn;Z^*-RZQ3|I#P`nU|7eetuDDHvc z9;obrx*l{;`iUO&uNcY}nm*zs&hv;bBE=lVsxq)ohqiJ9!lz=q#jCINC`cZ zs*qBZP^uC!5xog0w{mu7W7R-c?C3`Tx)DI>?C3=e)4E+k50W`f1L?G*y?z|C_eiCH zo{b{zzltVaLzl|1IJyWxPXeHVU15Z{+=4ljE*yKURt~x$Emsb@kxPA%jc(LHHv*KA zgs=157%+}wujO)}7d6m}0Q4XLJqSP#0?>m1^dJB|2tW@4prRdl4?x}nkoSPUSvBc- z+mYV@sA@-c1E8#(uUyoCvUuOL8+q1i-H5wHTo&hfghhZh8FCwd+y?N~u^ReawB_yw z_WyP(`IsJ5%zCt44+cH#R(;QK`2`$Rys7^t=jd93W)*eUTYO1z5_@1n%(DY2c> z?xLjYk;@oL8pHh4_Vm$rqOI^WqZbJrCvt2hykw5b;(q2c~a?r9IN|}w8#3ypoR*)?}H}dSp7jokZxoIPK z@O|9)K5l#;H@=UXwz>zIcH`$r57UjG_IkZsC4 zjbWGK|CZw8mg3)*;?tJm%a-EHmZC+K$FNJaVxts4wiG|M6hF2UAGQ=9wiF+>6koLz zU$vC_sXsIv00sd$zr+a&@Mb)Og`gOefKov35cGCotqY*J3!1yc4Sdov!v)=4(A@>S zUC`P^DGH#q3tGFd*acYY0_g36-Y)3vg5EAlSpcnF(Aov9`9K)>9Y1jcI1Zx08E_8R zfCGQe4^#p4fUh*{Yf}EX6vNVx3B{PNi5UNvBe*O)1u< zl&@3#eJo`ltz0>tQZa@i$4@H8ODe`l>e8do01f!Jagr3Sr2$sL(W9HMl%gp$K|Syo z*vPoX-^WaL7}4rZunYVMc7sFQ_kS2KQLpjrpWr6F0ca|{vHA4I=4)syy|MZ9#^%%e znosX*KE1E`^uFe6!vM3&v=M-rW%R!0)BBoF?`ys$$5=L#)^;GrR!$Jg@fA6?BF9lG z#!xDcpH!pAK#rjV5;i3KZ(}Dtxvw|q3;F~2puYrO29?KEMsY63R^*t;297JnOqj_? zFK9l!p!xKG=Ce}3haSwTfRP!v9Le}eB;zNM%ulaQ-+!c>1p}ueEA{`zHfTUMk`y_MMveh6O9g3&RD$T^Z$kJy=+E_6310)F zxo0ex$Z-hq3m5}f2tEdj!4mNQ_1FO3pGtocb51=f9-s;rnRDtOs;Yk@PGiJ+gcpAyT_Nj!>O(gzAhVRM#$>S`b-2mJrEUSEMG# z60Q;dCoq^psQ?wo8B{e0l0TNgU( z!aBRqRTsMILPuTbsI0YQd!hz!xr~m=TIw>j(|+D|8GUu}wi@WD3;mR})34~LteGzJ z=1aV}2D<7(S6%3+teGy8iyG*O3q5h6Coc5Fg`T*Oe;4xaLjGOIzYFB4flkQWy+;zCAT$cPL1khR-otfmVd zyWp`49=qVN3m&`Ru?rr%;IRwK=z@D$6 zHR+x8=lm5Z4a{h(JXUmjd`OP$|9yPO>xKQ7@gdVgu;L+D@v`K+P%9pS6%WCRhhWuG zKq|a$Yi;Of#U#U_GJe9yx3BsSJ-sJr!{_@1% z?l#MoMA@iiz(VjbSPYgJNt7*#vL#WrBu2$D^$E})D^?216OkV zE%*(b1To-uBNTlKrS1zspF+{6P;@C2{Ru^XLeZa4<%m%i-3di^LeZU2bSD(u38jt+ z!OIE3%L&2D3Bk(=L7zg=r%?1M6nzRspF+{6Q1mGjeF{aNLeZyC^eGg53Pqnn(Wg-A zlMwY!aMK9UEJj_;7x;s!pgLG*gksY}@LEF9yHIR-2)Y-F?uBZxT(=WCK_M`VQ1mYp z{R_oLhoFO@*ys>+FccjOMF&Imr@+(TS@0a_0=j{o+}9iQ1^odu#B&M3a|uB|L($Jr z^fMGY8-k98qMxDI)ewCf;SO*ZTHmE$DU=I%s8-r67m=J2@CUP213@h;NNv3PURa8E z3FqOx&gb|;^eY~u0~=tRK$m{GOf84h3P~-W)UJ@)6;iuGYF9`tht#f+S`MjQA+z$YV#)18wSs-t}f|X9zt&uj2`g!Hb$q zob1(1A?G>tJwL`Xe3gryvs&U3-(Ij2=96{-eHm`_^d`Na3cBNq_BEi-yOoiqw8lH5 zH&^KldXP?U<}8ik|GT`wy_3kBoE0wH2ay~V7!!C-wjJ!oG2-ROQzAJ$OV5YnZ#^Fy z$lW>CC=NE}is6h@PA)4Hv&%}v{IVTlk@1RHV)TRt(?kU8hL0Il#0ld$yh&eZF#%cx zK#LmCq8jBGO*t&k;}B&D76oNxl;tkoK7sPoAO{nKVcbQzd=*u+rffCP{F+LGG6&XY zCuO{wGIpYjHOT3klrxxeRwK91D!t1Jp=YErs?3coc?@ebigH(>+!k`*8roi99Yhs; zV7ZQ<2IZA&33@QrI+)e-LkQ&>g2_;NHoZdg`ToWt#$T5}tNpb24stz$k-A8pJ_=`! z!GRO>;YAz2Qpzio@-(GPrj*gjXK*;hI1Lw1!^K#76PmIzc?i~jI6dsEp!t4e;{X)( z%F9V)Gd?$lvGYm%-=N9HNyW-oUJf$q z$CF+!!yg?8q8{l;IGS)Qn2Zc*l+?xh^LWOCUG>f++Rt-YP}svWg*?L=LF9uOMaanK zNPbN`czPAd-Ee5JpB$cIoqisc^r~@yoL=GCaHJ@K=TGwd268N=I-7LP@a{9DbsEaM zp@)N<+M!1-{2$I0tFfQhB4P`PEv5{YsCx?dPej2y-vfP?eVwo4my98g^h@1Z)3BJ8 z=|k{)=9eW2>>NCZMzU%l(F2h7xWCMkl4C}<=Sk_bg=+I`8#KA5T=&7+ZJmC=!Sodl zhaZ!TNpOD(-0uSScf$RRlzKm$p9{y|hvTbx+j~$U9FEV0<8$EnY~H>bxekCb*~s+< zaxw$Co(Gk}k?T3+Aso4$hg`phTsKFq=Rmm+pxihpmyBG`N3KJW>p4*IC*)dEayArL z1@9+8Q9C7@2vxs;s=JWu_mS%WsJo)vW*mTy2cY8tXyaWcQ@EWvwObsfywYq{#Wp}F3=k-6TQxj1Z1#R$H?c)z*Q zx|zAsx;bmC_mYxaSKX3T)kj!+Ex)ArDE;;quxm+dZRod8X5CjR{q~QuuG-Fueka>A zd{NoO+K1oSI#CRX{nBO3Qyguss~&5vs2*pos2*>w zsD8s-Q9a3AQ9apQQ5|BgsD8^_Q9aFEQ9a#UQT?{LqWT?HRDUntHCIuuXBBmn_}*MC z{j<4RI?`M%eau`f9c8YTK5nj-KEcZV9M)UQHPa`}HPbQXn(0&Kn(5Q#n&~s5!<%;duJY}MRDfwuBzwl zcV-ws?qRrwOHq+aKmb*RSe%y889g-TeCL?tcAr z55Imo*RP-M=hshP!}{qHW`JKg{WHIEdW>H=J;AS>zRs_lp5#|fPxC9M7y6aci~P#z z=Vh#nHF+{##Sr zih8YIMZM0iqF(P;QE%|8s5kpn)LZ;2>aBhi_1k_G^*geTRn+gYih3KXt9AYK@BRAe z&;0u7&;9!8FZ}xH-G2S_9>0EiuU|in+%+@(_gT|_pEdpWS>J!3_4z&<^R1rCUK>3J z=h0F$VK;nh=OgEvv1jm=HRsM2>{)zYXyfGjYE7?6btzO}B>Ymsv}dows_PDD+~~+& znbp^qA&WY(SK^!RP6~Rk$NBoNL_3Fm|7rgF7ry`g$cO>#4$TV#38T6fs(D$Ejw~2~ z%pb{~j#L;$>Qw_n+{*=nTzHXx#+TSFx;b7ZZLhFfFDcT!q!2GD;$BkZco|U{3Go^4 zy^lQyIk6wAk|Ntn3f5V($GxPehom?a)I(aF<}Xv_MLjPs>Unu#kr!F?Hfx%iK^>FL zxLP{0qZuKao900S(*ik?jU2f+sBBuB)~w8KgRX{hUZ#}uGNl~S)mm>x_ zGJ?BCf(}+TqtMk*)$3}gY34Eyy1v)gP}9sq%GCE#roNXl4ZV~pkCgc$Xk>Pq-9cSs z%|Tk2L*@|uQ|9Qxt0srKUhdTOa;LnPI~Bd$sp931YFND;4Xdijp#pN}ZY(3swR3|i zXdjsu#IOpm0N=;#!XN_;4~v+K{RDa*Li@CR8cUR_?I8_0RegB;vQsy5k8Ja@C*48O&0f!}Jk!f&(N;J4fD@H^}d_&2et5VLQgYa-3= zvb#v9O34f_CChm!ndPNqmY0%QUP@+Rl>nWp*d;guRm%i|j+F0s`Z0TqcRX&<7K@F7 z6YwYPN%-&W_wcHTqMDbg)x1=#=B29YQDJ_xm#P)KRL$^GHN#8Q3@=qPyj0EbQZ>U% z)mli^2Fw6%=o(^0s}WiZGSR%!IH=^#b<7%2Ef(-i9J2>hmj(Onm9ekz zYxu=(aUd$?F@%D~4BiNh8SurfnAl#xra}Yvl6xu0P|YyJxdgaMxL_CZNj4BX16)0 zf&PsxJbkO%%BuZsj@sh3yX`?XEec~7=$&~tsHN=+6ulhKU2YfY|CpKC>FyK8Q)|-3 zd=B01cJoGiK+&z+>-O@*eQqCVLw5&pe&xOj>R^N6Aa|j`gA{)2zUAqM(KM0iSi6b) zUqRg~yQA(XDftfc-MVA$81Wnjg}3g6I{|;nog&WD?lhKDjiALE3kwcQwQQ_{FDLZ9 zBon>BPTMnRWvLv*(UnsrXe?D3y-k;D;P{QDIy1x5r3QF@W66>%_`2ZvR_aN;Ae0<% zeJk~)K70f4eJc&6A$%inev3_>#_;EY_ggtn&Vz3P?r-IMIUoK4@P8{8%7ySvnFnCy zBDn~@8S?=w)_j`7w_sj?m6p;HzLm6szgRAYZ!N9i+ejPuw$c`hqL;`eykk3Q2Y;zt z3g2GZ!*`Gl@Et|1;9Vw{!FR&yc}y;s%i%jqXZSAC1-`3vg}*|sfbS;V;JZtA_*}^) zuX;*Pa;cZ}g6}Q8;rmD*_`cE?zMu4izecWs?=St~2M8^c43vTJgJcl=U>OWQM25hl zUj<4p0ejQ*5}?!3B9bXLF;1N>H=|7?Q)Vz{K3!&_RU}hxVeWjo_92*Ial70eG{8c} z9oWW3A4?!|r`#Fj$Q+r2J6c(od4X0I?pi1d@m<862`f(s{cd?up5(p$1G~-!OF@fC z1A1Dp-}jt67u1&L<$3Brp5)<+&K78~6cfV>@&a+9xdn>m7VMfYktMh%rsK%)zBqS1xau9J1V-RtN?sUvSN%APLk(T!3^Hpm9}jj}PQrTSiiO0q?^ zFfzVXwi4SmG#XUYzKgB~K(j$b?Yp4I8r?5JL+!i3qx%JSbiWV=-7owJ7~b%&wExL1 z#bwhjXD6M1^sIj-HW4B^H!KOS*LtXif(&{v)#WG_? z`_wJDl_x}5&4O9}o`CzKt#&f#SyibFde$rWQFY(aOWz$mPRfe&NT9vAS zWxW4s7h2lMO~+GG`Mxj?!UL^*WjU(&vVyFUs)AZ%t)^e!2n^NcNlwRpwx~YVYY-vQYQ14J(%Gw?z>z{^8 z*0)IhMlMlZiE4<(q3;#Y(xjIhk&M>%I2qS}_nds*+0RiLlJ)b<=hEMe)@4&tFQbys zyiXlb-PHQ2d5~&jqMVAJm!zdNxR#a@Nq;>jb6d+^b2rML=!xEm^hEC$U8QI;XOk}a zwP|{^Ka}ndEn)qNQu0z|Z|VxR%%*ZVD5oM`rguz;E_a+;?z{Cy_8o2@%B>o zczY>(yuFk?-d@W7AZuK!8RLTEU!G9 z(-u>v-g2X9H)YzmDW;RB-8{|3`DW8^n)(i&u5`qV8^=vIV`t20(b7zUwlY(p7n_;T z)@By8jhPK?Yvw{PG562ZyXG@u6PgNG;%UODPdn4uT!m%4p+;?iPB*ui+2&sJuvusp zn>;L`E;DP*X0wx#n*s`CXAti)tb2}~dqw|&&>sD}y#k5(*2?JL3s_S-*Y!TT8Wnqb^z-bgW;gN*%*D?`xRZAD!JtKISl16k8qJ8QT*(5{8iv zXRVzkJSTBID{;Opah{X#pZ#}d|Ag?DCE=p$*@^34B+kj}If?6qiStv5^XrN8f#@8s zkT|!F@*>_UagDVkO=rAM0Q{548%sWG&SsJu7088ic>BuaSW@q>hK%wr3K{|sRm3-h`8j8V!@%_nG@-9_F1(0pLtH}9Euu^n~5 zd}(LcTkK4GtG(I&-1f(k)Ir8B|78AbzQ%^`x8^WbrH)`z>Ztjf`OX|O$IS_BOMP!n z(FHPc3aif!L+n_&#=cY<)}=CRIorXOXSB1TJx9L7(o_{&)mFpSR1NI$*2EHVZClsY z(GfDn$6C3IU2E6IwRM-ccJ5Nw-gR&t-DR$myWDklU0hej&APenu7|tQUFEKJxvr<{ z#VA@I*VpxP*SP*}0HbMx++a7v4RzPLVQ#n^;YPYq?q_ba8{@{hadO&?cN5%1cb%K$ zu6L8&4Q>kKZByMeH{IRjZgw-=Om_=L!EbdxcmKi|+$?vyyTkpf`-S^AH{0Fm=D54u zFWudA+4Ns}yX?RY@>{Y~-sW%mU3pL5mk+Ru{E_UEkL45jl>h3_o7{4t%-~lj1X$eCe8U##&H=8gT1RF84YAlal{mm;2wB`bU(nmU2|OQ62com*%%x zj(iVCOP9Hpc-eBbStTV~y`*fl7hpohyuz7Y>S>=`UAl3ME#uNI3Wskp3|7&?PG!R6 z;DT$ZFtiR~cEZJmS&aFQ!K&L>JI;<)8*Alrg0^Axuu<3~Y#O!-JBK~O+;C_(I-C?v z4QGY(!Uw~J;o|VQ@YV3OaDBKl{2<&H9taPGhxz3Zti5N(tHx``8^oK%JH|W5bL0Kv zL*oE)U3q@{l|%kJyPaUmhh~A42xzt_eZ59YFY8Lgx_o zaAHfvel0N%WAvi5o_y7kUzN~xiTe?mpNgHhAN9S8EP5BU2$N07+KdG*;N7##ozwx& z9L==0aAXln?I6Q!G8@eXThHd$`nG{>Xd79O#<&G-=@iVu%H&LYx$SJb*sk^p+l@Q_ zUwi+e|AX55)wX2@=x`5E;j6%ebAz6s!o9Kf**ECtk>LR#!h^trhcNHwS}@_^;K3uo zf$t8mJ&q;LdBJ_b1Hr?={9r+_GI$N-cXhBPSPS<1I-2*_2OEMi*1Llp=-^LS+XnB& z)^?Dvv>mXr3FP;?;23pLweWu*Fi!+gV2nkfJm@cd{5bvl)>zYQXWE;NrYp#850h(p zn_>12?2(tUjBXc$iaukXwZE~ywa?k-ZJx~s8!fSvp0bi|mw=5f2M1jR=D7}ha|4Lx z7SPKbAeC?1ckTQ3L(s}k?CkAO)Y1AqM9o(64nF6QDc z9b4$-T}3RVS8>%`4Oi3Ea&=riS04oNTyVk*u$kVh#Mb$#mk$bg3kcaZkgz6Q(s3usiDQ~+c81?a_Hau3opLuDxC zs1!^ERCKpe_ok;imCDsBe}lZB2*J_FoX6jY{)_s+Q#;9-Pw`P2x=%p`4`8|C2soYr@52r$en%bYcPGUr@{M*8tj>VZ z8L&AADpwb@&4Ir)@yJ1_j_}_GY@B4$AcaEfzgk_Q-FL9fjo4zgBHoOWIjlPg+^-EY-eh z>&o4Vl|&=&wP^tj_w=PM4xpt?qun0GOos`yyj6%(|DgG!GYiZD>h=b+iO}_E|DsW$ z==rk=k*~OGP|Qkiz5~gD7P8XY1_c7X7Zicusqbshe6TxfR)FKF%e~&Et>^E7=7EuN zFX*V=`y{kT9)srF5Byz4@Wu9ib&;z)e;+hY_#SxT$It@#H57Xe(0t~M>9c+ZUu?hh zE(g7f&ax?XU#N?`;(fpMzJKw)ud0ju3Yuphf)-GlTu?xKk7y|Bt;WZ^RG!=mT4*1I z7Jyvplb1vD?J1>nJoWi2;fv*I^>vza`7Xn|uTpok{A-vsKFqqra~{`Fd=dH_eShX_ zK?|9?rq6j6S_D$5vA(V@Ap07Aqq>0gYq~YxncwAIcBu=vt>)}ZXaO@W)ORzq7+dSg z*Yo@n&^!=xeJ9Pw0#TPDuvZPK>CKlypWb}$qAAG--B#acpn0I%`jigP0(lBr#CNJ; zilN0`QWnd9s*63~<9Sit?N{FAkau}mU6_ZUcP;V0hrRC+@4Hl8R08mh)TE%mYMsaj z0oQm)8)@hbEwow#3S=>~2;^6vcv`9B8zjHAE*2v@@O3foUe3EC3we4)&#zV9Y6|mZ zoaePBqb?&Mf98IGOtgqm^Y{A^Z@hv X76wnEi(n^v8!x?bp_;lO!DIggn3ZN8 literal 0 HcmV?d00001 diff --git a/core/ui/src/main/res/font/roboto_regular.ttf b/core/ui/src/main/res/font/roboto_regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ddf4bfacb396e97546364ccfeeb9c31dfaea4c25 GIT binary patch literal 168260 zcmbTf2YeJ&+c!LCW_C9{yQ%b)g#>8<(iEkL(v>1zZlrgRDjlU0dJmx&=^$)IKoSrV zsZxU|AR>z5Z9}l20?D3y|Le?7GJ`(v^M0@XnBCdk%v|T{^^C+MNeaV3m13K{+@$G& z#-8btTz;k`$-SGkZPWhzu!d=pT=54<>VBbF`;Lt#PMbAOk|!OIq{t<0+9%arH9dQ$ zB>NA=ReJUr)@#J+`|XBFa>!jtvQO_bc1&#bosRXATxJBm@6dn5fMMev_1q)Lkpm@( z9UahX^a#mM3djA%6E01XNL~&(`)Lu;v*9K>98aP zR2tT6{0K(_#UJNc_{!c!Z zHiyUi0&y-VDU@(;Ue%q|1a+I5&)Nmf$Q>PAJ_;}cl79l;-c zoIdo~XNRV&S8Ya8##8v)MS;?a$X>x!Mto9awqs zs!N0P_4{LC{>GByaS~6fl;iyg!TwH9PyrpCbj%KCrRxO)l{KBlJ3TQ49vlNCWazs>e-87}kwAG)TIKE@$ z&Lf9sj~e&(ELLYvyYnBc$i14gZ1#*yHts)fC%<@Q^VUxyzPJ^A@8ZJkliut1o>tvfy;HCik+H8mvxXkaO6vErLp^B065TOx}dv}4AsZ9Aq--#xEO%VwQBt>`2_ zzk}I#?%+lAN%KyfTQuv+9fRaEgVd}UyZ2-?o4I4hd`Ihky*svO-M{~9MOS9*+Bv`3 zj9okC+uQW()3IfnzI{6U(O4bT7+R-a@jdkq+exXClqe-jbN+=NDgZwf3=t@UlQP5{ z@fCoiwLCN6Gl&fN}^1L;6Nwe)o_s{CG^0hX6%JhxJ zJ0Fj3+~k{9BiODolctYdq zi(foFIrqR6<@)QZMzAjY-8Zwk@!#HHvHbgP1bJ&|nVO;=k^-S~aWS%LAh^Ah;2uS2 zzQ{P2+XcPnN|raUOg=c54`!LUO7MQ3!Y=G*yXaaK`E8aWeE}<9hOU*ZmKqhhu0)7V z6iOz-K6}s`>cKwzcJmqYcP#C94u4%mj*)}qL*V-`36>+9mBK)(H#JTU=4IFqa?C2a z*AiH^vCq2e9J+_h-wccdcC~o$MF5G(KU;bEBSre$;clYBy?ByHUsU10k~&?p{s=AB3TS@ zX1hvZhw92MQ+kS}IAwRdtfV@_lIwDw$v)g^5?mHz8qFjy)t*_8C<(NY;rQz9WAxduWd2H z#>m4!lKEKW@>YRVps=s0im zywy2O`TYDnxH}W&FJ{TL-`Uu4)Ux#pK7RCB_H}-pcLjWJ6yH-G1HJ@lk`7-m)*fuE zy(~`3l2Vj{g^rVww969fu5FaqNG*xp^^n*oPq3BegPjmA82{{qQsA}l1aja!Wu2Z1 z1vr{@C8(N=l{m>NxOGzk%}CZ$jjimnoX~`cZZ>=VjLhQki*vjuF8wrV@c0?U67SE8 zb2Hzby=dL?`AS`R_9!OJ9r@mOH$Up3)kyHXbMn8p4~?F;V8%NcGI3!lsL>WY8vwn~ zQeUsdLl8=W*30}=f|ey^%cX1Zz+GkJ|7d>pKzywQi(e7=k!~U2ESbf*9Lnr-=W@M+ zEXqVzkDgN!=#MtEFgoB|si78wEYNk~kNB5y=k7l-3g zOZg}7`!$ASocZaGoB0o2`&~=MPFucl=7c77dPYcf+R!*o6{ojl270nbCX_G zt9ZA4BzG;kr`)hLe{$GXCJQ=v1aK1~q&^P5sE@{xpmC&u9l>_QX^H-kM7~5wRwC)3b|ndXH0mdb<=>ld!u`gnpIrz ziFewlUL)@1=l!y3?UPl@XG~wge;PJt*6msI)RbYnYu7nC?!&L|936YCPVL=858t>^ zw0Yv1tVfF$tL5g589sOJ?FHb1zQx7LBeBxTQa2roA}li28IDDV(>j%K5*Z3_Bt^Un zx3a2L(Ic2JuNM43?vYp%@q{bVDcRhq&>B_h!Xz3Vx6+{A=ALgK=|B8J#*N3^!{4i% z_}yRpe)sj2H%yqgVzE56Nr%aIGM4=`nSaQCOyiyT1lv0G`zND1v^;e8$m*5(#l_NW zSjJ)M%g~2me@V;%EBCiDT7qXp=1mA@xdvTp*TFBJfxYgCUnb%=Un!%RU2+CV#xI3A z6TbwXHJ45(6V;aBvnUgv;ajMB*lH}!776nd$^7I|MVFw(W_nMuNz2$o3bmyywph8T zTn1M;a4$$ddt{=zz_YP4y744SiG36May^PPw12nCQ|5V0;-en;5?e*1IELtq+9SeGA zmoIfBG^sq9EKPL^$^Un&Ch1lUCM`YP=l4ds(?D#P0S8>-(pb8mT=&%(9o`(&e{zoe z?V%5^ZW-1h-xpf188@%PoF2mljT_o+%bD}p`*#m*m&H$%#@d7V^Y&}DRj>n%rJ<6i zuI{z?0cJmvbfrKGt?Nf@8k(fp{6guSpELV8xio5uEb!EIW|ud8f`GSLfu~whw%hb! zs584!=_#=<^saF66VlVdXjRdQ9V$3IOp1$FWrsaXrL$-e1jylGVKC=v7_&#wr|IDo z1=!C8-8gt8HEn*&Ma#lNCmbKtZfe_<@Z}>H*u!}a*FNTF4+I7+VTo5>KlnnG1{ViC z;aTqo1>I(oA3SD#_Z9vg(yq%3!z;5|&o+8%HT&y#{=?3W?SHtqjVUXtH}qcn{_6v5 z7Rx%rGyZzSm*>}Tk4~(6hwWhHSvdRP!PoqCzGP8W{~rGA?~3<{D=Q!jtq9%efGzEy z1q22Wt^%A$6zEJ*>TVluAt9KA$PR4VNhA2Flxy(#Sy)*M5T6nYD{vu6$12K2?}oXj zuXZDwd*9i;`EqJ#Px25Q#dVgRpW-CMsVT%qQnWh(3?w5yhtr&vuHGom z@7(8{f4r0h?Eit4iOw&(BlGZ;)7qvz71*Wk3)v`^w%|NV*~Y!!?OVrxEnN5u|6%C? zP@OP+8ki20A`LJ8U-3-13o=0o%m$a9>Znx1qT!9G4#fq9j%9)!R@A^Dtwzr<#N1oxGLbnUSiYJ0kZh=o?NOzGa z{V#m-KgUs8CEW&BN;+`7(&b8W_XDAoV(6t|r8aoUu4qO^6);nLWjPTZSX^B-+AYT+ z0Q2z@85#9fOa8Y<sEeGf;v(VBKC>o+%if*A;M9ATvq&@Iw-49&$|H@w; zsV(-WCi;M(Bo2yOM2w`QG@vJo$D$sN2Kl@h*}_5p_SnVH}`R;HQh* z{cCDkTq~K4%ge)0@mHycs4n1bsFbAtmBlL-E+#>Y2nmj*Nl3r|$u2#ErY8&2mB9SM zE1&2cNO8hAqtjEuaUFXB$?vYMy{69 z>(XFpqBKuhgFrY}^6RcWM}eK)M%uYic$&Sby_3DaeXM=9J=4D3e#q|M9iTb{@<4Cq zmdk5E-kcx2C*;BZmAB>a2%xaGT;QEjbXA8Gae@a~%V%^*|5ZlJl2N-(6%vDFHdxk* z7Ur*qyy@4mzlL`qQrCaMtA#X%@C%}qSa*^bkq;;1!z2<(&7r>ph?m-R{N-exA`yOk34(%U(4lXEO76B7P#bi z!I48(l&d+p7ZiEdHJ-n77klo~pifxiJ-hhv&t#^sNdEI*LkjsF7V0IBfounfNC2u> zZM1+05%$1i2=aLh0tp6sjNnTPRD{8PN`1rXnT#OV5om&LLc+l9GslT>Y*3zD_5lm! zfB(&Qv94>jZe7gR$@RRjUk^Y2^t<&-=T2Xz0Ip%h0X92u7%9aAE-q@WqokD z;IFt0xC~~}6hD#Pby>|XoW)qP>O>aPVRKYL=tBDQpSX<$YT4`wOr60mHg8*kUk~t` zck$T4E6No%hVXlpU+#2a!o#o<9Pj4&pE3LwO*nqSzxLsHCvZ$G8G?LMAI(-qByDU? zPt^bFl^Hn)&8d53PK&M50)>Ehz&BBr^$C+jh_^csu`}HjN{o|_^WFLEo4=U<@)@kt zCGVRoaq+IrS^TE_s`q`H=j&@3=jwVhgXEu9OrEm@6;&p+g>4%JDkMmKH7T)bi3C{; zfl;RN*eMHxV|GX>G+IJAVd)dBab-DCx+(W`v`nESrOckL*N_+()tZz9xzpcwSop2X zpQq*TT)k-HDmLU|AAaxqOb)el;@zw*neyCbm$UZX8FOL6%vDo{cb(LK($?YGpN&5I z&dk-5uf2tJ)d59Tfg%pW8dw%oqMET3i)$dV#>CVxud8^C`>@Q4y@Sxk*3vt`&FGsZ}6?2^L~FD1ed>UkBHx|{LhTgeajUHRC)&F{Wv z^AyEj;!m71lfO~EE=t(2f8Pe>3&4N~K=lF!yY#FkIVft(@tJ{1>rCpT4&!2#Yech^X)ugiio{9}3|O75ZKY zz%4bq{t_%+u>R;4UD3D@uPH9YHEc7rG1 zQKrkaytTaX^0VHv@@@GO!f7ZVJpxGmz?Z@}T8L%w8VpE%!0GoRqnIrBW0P<4fIJ>> zOa4s$qG-7HjvS*brR#UX^(W%`{!&x@`j$%?+-_!dO_f9xhzy3!B+LFbhgc*z0;t=k z#znH{lotzcDV2&ID1WbCzeJtBVIkdd89yrr+NVOkDoaSsQ*zWINS53k76Efg9=05K z{5YS(CfI&>JU+{TmIo$PMLpwLz^=ePQSF^5WXKazsNj&Q9=WH-=6OtBjXyujW{CSD zCxc(JBx*V^ErCKHi+dlA+or<3@MjbG?EHND)JM&;>=|_DM)Kzhd?rXzqD7KQ8NNVc zh?8KKa2p%x248Hv``BJq{T)_qk9vexlCOK8!PV5_K??P3C`N6^5IZwsYS*z*dMK-C zsIp=exl(Ft8JL#n|B)vtZ>Od%}OftEDBq%pGa{d+mEP<^1 zFnGN`sjX3Mttw5{qMxCvsVCa$iS=2YXb567C7B4V25*((m_$^L7A{$!ctLD~Ket5b zVSyq_hYd1?e!{;ne(dyVeftlg?EN4D~im0g?*UvGZ< zOy}OTX41m3z*z|THu`H}<;v5V!<-%kYxdI_Ncfw^vJFCrWeYn%%eMIuWwn4HLEs>Z zXG7&LQ)vi@r~G}Qg94Yd*f5uq%~B~oMW=3N}&zdL6Hn|CK?+1wA>c04d^h3tC7 zuP&Wpm%JzD^K0B|`|#3kUSszqQ2alj*ga6JqSQ)rR*C@(y2y%jo&mDq@0fXqoFk+l zQH?^Q2a~$T`At55V~=upEkBhyGfb@>G`hl+m$l*Rd=R zYk+LH_yWrY{F+Un43!ojUeJ1E>GrVZo+0ch@Oq8SlG+j=4B8|ylDUTe73pTLdRzu^;Qg=ZA2e2FoJP+0U z1fB_jhDRm6 zdJoczr~x?Q(2pX&dW+wi^yRdxKY88i`}2BdB#+GCpO452lPmdUM6kHu<2QR3^Pjl) z)lH|`HtupoIrr}JkcDeWTfKl~owG+`Mg6qUC=yAXZ^TMseG+b=h%nDjuaQ{WR2HH< zt0_eU?db_G0E1Dk2#J2I1Qc-)1tKG<+V=gPJ-NFZH4I2feZBYh-z$3-58rppmFYjI z_o&519f9|ryp!@f@Lm>nVYU`uC4smG4LpH9ePjVp$f5zDh>#kw*7NU1_A)k331 z?E*^2lw8pw#h0Y7Oof-FU^FkQzF>Ue*Pr~}xAXAjS@XJ2Wp)4f;L1jJf9)rr z%>pR!uOKTfsihVW7A|Px)MZ2%Ut^7iHz;Hz1gbfN)~Kfh$c_b=H7ZL>j-_yzl8AN@ z_p>IGPO;8P4jVN5^^Am^9OZ*me2OBHLH;oaD^&)J_7_)NQ0 z)MFg$%U|%$0~f6WAR;`4RtU667htxE7kl15`K(F2)Os1~%;E*G zWT_i`j}$-^ihi0VT2O_G#Oq++a38M=1~YJLm_&=wgCAw89FWl?b1hL9A9RvrwDAcn zcAN6m;xCzN!kuNe_=DUX3l?tQwP5Z}IdLPO$1m~V4TTF>-6H=3H@`fieR&hmE#N)X zN&>oa(g-bFx7p#PxgLuoia6B(Rp8Fhz5>NU`wHjCF(_d5LoD=odKo3=!tEj(VR1r!I+Zuv53XMB$scpp&)U|x z%a++2oiy(zEb zZ_4Xfh;B4uYKrKnq?X)Z(Me|(aNx(B!mQx*#1&A}Wo3&rr6g1~Iv<|y#1;JmdgqHG zkL2HPYjbD+;qP*%_3k%nFpJ#V{)e3DXGiAP=8qcm4vT5k{)G->+Ri$BY{e^Yc4_v~ z%MChB=)83Qf424PKCC0H%fI-Z+{xAmUQjPB#N-8ufZD*RXnrtGj0_vOHlm-8B1BUs z8TIa%icoMLsG%o})EZ(|x5&?=M}id+QpqE7u{r0?rM(#YY>Ot7-#&H9)`&k@?Ctg9 zi$R$Yne*h0i_wq3qzqvH7W9P^x(oS_63SZ`)#z#v>dIn%L?|FUgJ2P)KkXS%VlzSH zj>vt1qo!0HdgZ-?Ea&W}O>;a$-ud{Hoab%w*9IlL@HC)_gGtE+H2<10GSDPg&p0Vj z0Fr1*Ey)<6<1^?(K6xP@|6!rhu<*35sjH(VeHCwmq@J2h_!~N(TWDh8bBhERHxqa; zbhsu3itx;)zXXUEz#%e56b6TfC#x+Ba`>rC{+rOcl693OMfr;;7;=Bm-v6recSc*?=JCQ8Uup;Xi9t8 z$Tj_=cb1Y=?B$g!`S12)1aCOt9p!`9=7SgMkuph|D^U2jt|TqS1$e_u@Y=$NtZ2kd zLko2}V0I$nh(gIdIWnGXyd(U)X7Ubvq5_g7RTSs$b^1vvU7w!%x51!hacke8j%#rsN-m|@8 z#1jlt7J=xEO@Q9&ph@v=!6#(%g?DN&Xi2)+QDEj#>V-j)Btj^095DwIfxaQLtrDpc zyFMTygQvpu0TR7iL(iAA?2CMf{q&NY_s^co&dJQP>*`{Qyy{uIwD+;V@) zD#m^DRrIHsM$&|#6Hihp_KK6<(JDL*xlzk9jJy^TK_cymNz!`6uut#+HB6F2!AqTiJ(UAyINl8yk7miJO zG(;Q284eZ^6;)R>TPJ{R?P{BiS1xayJ$?Sb5zD79-*DpO#+5Tyz1e^9%%Yy7PkwW9 zFT73S0{}Bl;oST z@|B?tqA(#RiKx|Nw+w0-@evFXRYWxh6H!n}JD{z!-Hh4+{Y|GJ5gLKfJA_IgTnacA zNUgvNi6mi!o<@$H{)fkmoG|^59DjM1@)=*sZ2TyDnIFyPAF&4b=ip0kC}rhU-r7^P zP3Ff~#jhnH++dnWh zXXpGyo1dM-Vs?$J=e_fKtG2DuX0Zx2T6dVw_J7#1PDbCIXP$j-@HrO^igNe83= zX8=A35z~*^E)xS&XjFQtl^4}JPnt73wsbPhQw#E3dg?PXWUDD(W01<%Jzgau45I~M zXgaIxruIuz=3~+H;Ol}=d%U+{{fEcbZrZ!7N4GbI4t?W4-MtuJ3TKU2*rpBqm(82_ zy^W)fuvTm;YkA}VKY02SKX^#)xO(%|LvMPnZe7`@etYncBb#$RrqE||Y zrRBjv_E)Bko4#Z3(8*2OY~DL})|zsBYxOP_MzrrL=f@{>nml0m_>?(m$w33AFP_a$ z_G&k&YWYR1Ve%Ui`lS0ytCYUV`%(g1_Jm6gG~&Np%%Sz(VdIozN-X+<%8SY!gHFOc znI+%^ghDAP$8x=sl!j~^^V1TOFa4T?&cbf#V8-OSrQB#EMJ(E$$z6+%bSI=FCL|`( zhzyc3?$@7YywPCIO`BQ7`t|&tU`>{{kVUNCHFY9$Ee%neqdn`IcWK>sp8WY!+;@h! za~F%>yNAUQcmB!uDeY!Vne<}aHT63sI4kG4da6_9#%V23if7UyTa;4EwhdlaS&gaW zF^EAkxB$lNGpI#H#aiB;@+MoHHP?E(?fd*k#JPFYi zJ#pkAid0lY)by2u2QFVea8PD(TFaJc>8)C+c>~w29W*#IGpgBh^;)$V+7fr}g{b0B z^$*-R6#e&NHV>X#Neqq*1Dw`>%<54LZf+^Dg^L-~pw z{2exJ2Ya#TL**r<(<@D8~q?Kn;`}4ckV9%5m}@?=DtjSfdwOHCw-f z`K=k!!NV5IYlpIO{hQRO|H^ZtR=o4(z#(mx0>TFJ5_t_EOpq36v8D`-1wt_h1_(8& ztjOa_Nr#3@??{U!rMuP;!(fL((SepkXJQ}>5IagC)&fHG=`l=%nPeI1RYqKnW1NK{7Q3BVqm>S~hRk^to2+-<>>nUDL)ZcW2DpzM;)a zO>6YS?;~yvliF#)Pxs&$(SZoxjT4bh zF*1S%E1Cy4v_MC&PE=P^lrN=1705(r1lFDn7;~mU?hgO%yO*~^(%L)c-E~7m1A)DlWlE}b=uQSaE4^2>US9Fme$qZ)c?aNmjYTJ`|=up>TTrXD2``dIKmysefF zc$RWv$$%#;kplys?7{jQtWOxky6baO--4!@C~Hb0bX*YX(~UJn&vnDcc0Of$w1D!W z!jCb0r^zHk=|z{G3PcjK1C>ut%sVC?U9w$%2Xl*mpOe<5e#bpAj@i!}^d+;jhZ?DN&%)w46l}i7{=r3KL% z9y6@(lpOia2Pdy>8rIl1VI=Py{La|?K2?T|9@%a4g^%BVZ~w^F%UFFl$2Du92q_o; z4rF%*$Av;K_$F$NAV@H|h2xD(pN2L(Vs+P3Ea1xUc9g)UOiwst z>F7~q;1t#sbM=SEVE~}TIDVM59LEpxgE(u;+Dziv;=nzVSUbKSDhz$i?_#>>9x_g` z$ea$;)N0k~vMPDSbWHHcmSyy;1e@iYB30@ZFBC?W7kw(`+B~{KE7O(CBg(KjA^<>p zO?rZFb|yMK*%1|Pi-@L*2YPu^5*ZY;(Gb07Mz2Lnj!{SSwG{&vZk#I@)#xp!^xuxg zXeIJl?-$)BlypbGw)XoxHn2VQM^D*Se1zZZ^KhY(F&yo?!G~rPEp9{&yfT{q(EA7O z35LG_3D7IpK&GKf1os$v%kX2-%Pvv@=-P7X@6fz!o*PGpp{vy_|D7_rR&Ct&Vm&f2iHTgz9zXqz)O`^25&a2X?usb}sn& z{f$%3H%acXB;%EhT8#>8V{5$eT1wC5^V)U2+~JKO{0s14>*9O%$*5da!?a+1>6|9( z5eA%sTA12&dY<#~prx~|BJ^2B!`@qDy(HTvS0q{2f^4FjEeI_>L6?KzZJ>L^S-Ms& zJV-R0l+%A*PrP{Q;n(#p*F(G!SNcIcCK5cA<16w@YKdD7|wCX^s25FyqB<7VbFu?U!G@IdIT|!@nOH?Wx;v z-=I%^@K$x~Te)IFQlkw;{>?Ykz5CXJ!AjfFD_wHA*%1diz46|v_4_&wne=A6@Wlt) zw{O##7ymfgbNrQBdE`A#vR?}VseN)xpJ3DIBByK_G zqN)$?!X-60t)xs6T9(rEG{5N*@60VYlozwG6GLm1sCJ8zA=Vz9ATog9sOa=)1>5>i zNUYlmCFSv3H)hYdHDSc%Y41*`z3^s>yqO<7_hA2rEe6VQ^Z&DS%Z{m2R@)-^BR-(} z2Jez-U(a6t z9D27tR*1+1M;F#9TQ>3_t_v#hhU_Kp;1`J?j65+j&Pmh6CgRhcWTX| za>{?bn{-Fb=dN`*%<2h`twDn#F1GoA>qgn0iRd#pEc(|H(D9{;2!V7klq!yHA2lrf z21d_=xieFXbCXtvIi_4VG_NTau9Yn>W^J)KL@b#N(TN~bF9xE>|0Rtat}9`?PY0)^ zcAIo(@tbe7nB4!we;0cFsYEl@iKvV4$k!Yd8!uLQ6N0gYmFcFVpX6w)k_QKHnCQ;L%K1#|d zCr2hDiEebcse6y=EtJ$viEX|7a*h@aHM%L)D}_m-k1~Y1Dw%CnR#wq2qoq=YK9FoQ z?Hi8u4%3Z};5Wl8idctM7oiVuN5Cvb2=*c$Qg{NUj#UqeG)NlTM0v(xT044|1L((8 z;6QOp)Zu;Ge86Z@0ba}wQX0S}&z_y{b?4(Kf0|)kU2f^aO{nLFlw2DZ+fQd;_np`<8I7IBE5Eeo{1bK3l z4-u`Tsi}?E~ntcW5iym%09JW6ABl++7Q)d-@3JH*N%E|#ggnpS7pm5Tf< zQ*Z&{jRRE@*nGZa@@}OmO_$T8dEtVQ z{f7;G?<4s{WF`yU!&3J$*Qy8%oUiv5l@C!Dg?@LLpSk)oG)S-FdzfEsjTos0vf!&V zd#Wg<*eO1OFnMbGFk(>_mR1v^y;+zA;k%OJbOZ?3vyOQ2)JZZ&59FqrMlZDp{kP@x z-&Piuy_!jl)-18-QNp`KWocrgTiwzr`nSF~t%Gor3?xxN2=4?@G_Q{NrL*~kfoA}(f`t~2qe;%{@)X=wQ zj_BKGB&*H+Ke%!I(xK0P9CY zS#+XDx;8P-mghS}S55vv-M8yl{R@hIGe zqWRhq4+=9>qBGJ`#VkMx1ssvda?kTS*VL~YQt71^o9)>n@8A4s3G9zc`$F2*+tZ;xsz@DCR1@_!c(U<60tvs#FkK}^A~aZd zukZxWAP$emLLZ$|-oyV|iIQ00-e1@D?7o9P z?!}H>{!k27A3v|pRqtdCF8BR}y|{O+W5!JWe*L|Fsi0SsFr!h;`5&{cqkC=4{)j!i z+QKyN`dQ%I<)2&$^1gkB7exWr=CN1k5A;;pLe(XhEa{~=#LSm25C3fTG~~hXNQIUy z$pb|C3EW3gkpT_-;>6n14%i87;Y^#_EF&ApskYGNn>=c1v*pV#S5%iASgsZwF?U_g zkloFPk_;cfWJEt$&tPK@2BCNi_yli2M9qo^_b#>7kUQ3Ich>VMBxcPqQRik*$^t20-w{%eGKKVbLnAm*fNFI2yk|F#w5+Srj4MSM~3 zJ`l=c7_Kd;Vw(f7uOIEem7W}lO_5WRS$^gwKC*DVt>f+hexHQ}AcOC#!=gGe0=f49 zn%2yg6>N5mdrVW$%QtM-VcQZlf1ho`j%%R`e0=}X(wiO&K<05PQD^Yg)8rf5_`~h1 zUTM*^jqUn`m2E9bkfPv1oeQN zXm5-9QG`@YQzAuK6aGEz`K^d;t{q8QL$q9y)33KHiGWK~`zUW=6G<3R4wMrocl*zz zNrxx#gD=&o{qjq7>Nd7b?fll*y%Q&PN_x3*?JQYo4WhO;SHs8rXh-MQJ3KBdB;F)Gx*lX+10m!3!ERz|WzjHzXG_!gLD560MWN z=#3O9xk@r+HkAgG{`1TWy{cDurrzWU-QCajOpdAkobA@o*%1wb8`g0QSrAb#?B$xU z0&l1VN)7NB?G=apK&TlKq07G%G|ArD3c$)Gks$%<09QMVYA3eDb<5o^^FMYCJ9RVD zR?M%kBz}c#&D(qk`>gn&sOm#bl%z(1lHycimD)-p#nzodHvgnX{5tKM z37hbceaAg$q%Yb?;=%<)Z@6IVrYu9#Hsr!4=UOk&N?fym+ zH%=?pO_5m94)rE)4hdDLvq^+(WwAgABncuGY#CAJ%`u|WLLm!Krv|U^r)buDkw>l+Sp~C z%e(lcJFGbKuS@D(7Qp{v0a(YgdUEuw>aWTS487A#U?kO*AQyscIyFpW z@Ss)6Gy+JTVIVONvRl9+E?WX!N#`27bF|+ao~Oeqr|Ylw4F0H!wS^5j)K|}j4jm7A z+G!0!e`X_(Q5#Xa4H1>F*1|Lz{zge^1+J0Fl?6PacT%nGZJe*XBev=AketLIQ#Be_ zqbDHL)~_c_;nUYMXFW7{Ksu+O!=y?alV|UiUwX2a*_BuL0NV3zy^7se6=?wcy(fq< z6yVVDmqr~>g`tCL8dbo_P2d$V6NjMxhE?<`Ak>-4m=YQMc zh7w@D#<`L$Zmh0ux{~KDlx?iuV*V(*WRsiy%x|fz?;>>N2-V4!XHEZ%f3&+~kDHzR z)a5{9A0cCp8)$Z5RRLD*|L7>9jF*^Tpu`ECl=xbb*hL70qKOUcScS(3T$01~%HfyQ zxrNx`i@F>X;srHM(8~ec_L@#HfwO;5%tU@-S|N;Dk_~3owC4k&&LaqP3f=szHQ#MWH4+T@&SiZMz zp4!IXN+vbIDrxp0NNVseD>Tv~78bzrtV@BeBV=M3sn{(PFHHWOzodi~F?NT?D3`pI z*%A2?vT=*$mU6Qt8@%XqR%pLn+ZfzA5`LmvdQ%I~c@~}WWs%-1aDwLt30>kqdC}t7QW01(G(_ZSxNk_Zvs42j| zPD@i7Z)R-C;^M6z74oxF#?1fVBk#G7v;%p{u6*slarJLy-jj73p3GJE?^jvUuPg4i zzznoE{_t5;!qsyJ51vzt{#MVENANmUN}Nr1K*?jX{oyGR*7_!h6Qr97+f)9mm6dh*@KU-^v+Th{ky$yq-CiE&f>@hx}NSn1hHBa}YGF5Du@C;I~9Z_n0{A=tpA?dRalyeFN?_jMK!(*&St15|oTdO8n3dr^T0F| z(l9dy( zUS*q?>C(E%-n0&>9c#Yax=hX0)26dVne3%3K)#gs64jY7%$^0Ax=RJm8C0<(Rs_2n z)fthGC9BDtg8jghrlv7)zposFei~g;Aqme0jz4>BAIlj!^*__&QGm%&9zfa@u>&n-wy8gh{m7H%_iHKV$X+xr+CTWlUWt%TxJr{vLaUrCen7 zS!;fjU#yY-?Qg$*dpYsDC%=9Rx|}F}D7OMGg8ns=W;iQmkDheD(DIZ`aJksz^hUK4 zS<@Deq0+B6Y!tLAoFyo+#I03|AE?hG-YX})ra6rasII;Zk3i^h;W&_wix|nwoksVU zpa#^osmu)^P<><2$9hsDAyI)VObsrSHM8{|AIJ7Y)O07ytDBP2rsAL6I>C{$kSM;Z9`}x^g@}eNX+>eh_c7Y>mqF+s^l?3UKJkdJL z)nQSqg9*%zspeNpbn^LGI@GjE`lppFHAJn7zuuory?2ndI8p^9b!t?!=mtlR# zO1_+LBr94OHM7^kP3+ZKnTO6SVWE>_+YD?zKM&0_srRZOYfuBQrfppcv^u0i^51Fy=jYUlu*)IWWN!yga z$WNFndr#SYVxX|-XtDhmV1tcUe72ovBe%W$Fc8~4pBR-p^5V?)d*);=o%PldwKe}Q zZ~QC&VY2s;a(BbMsYPd(pEz;x>l@e#mN;jgatBbyW3L`b^!k>xu2=vzwtoRYNNW&S zCZ6|{w>ZUu%?;ZT>9iT@nHU9weB@@PrOEX_{C@xJ;WO8=MzedjmHV{pom8i3r+bga zT~}LwcHqq!U%Vg7i~1x~?Af;Ajs_jmUT9jqdUy(BSF2?e&h>c(lfV%!S1y_YTk&+TB}KL@-{;Mu$f zgy2)dk{F7MMz+mxVnW8;l3_3{f$A#BkS0=xkMcQRIH-D^YOf5Q@)qOUlniC7chIbI z(^Hl&lb2K7bur-h3vke$r6DGZW+Aq~mjRR!Y?z%6+}Y(Mr!qlFj&eCADk8gBi;t)6 zwv9b8k{93n=&X#{hzb1ilSALLxZn7X{4vk}`nrtgUdd8t9&dXEFq8$?y`hEb9p*^A zmV@0YqiZb@Ya0+)Xjxh;FQ6*8+1rOZ2Li{I*1b`gt&AWu4B8gG=FxiBDwGx`4BX*x z7N}kkDG$Z-i+-N=PQT3o2e;1~IsMLbew!EOvdP zVbGL?k5>M{uSfD^xqsB{t-Ef#Msn1HSGBz))`YHjUpgGH>6d?#!3i|4UA(2h%{XYJ1NpsD(pF7oA}XKl$rm^DdvT_^7bt-Y^}?Dr~San z-vj!+ydaW4$38B{(lA2#Umo(&-LeW2ZDK!rds#s4mbz)>MJ_`Nu`Nlj{1^Or>RDWpIvA5KF@;1}7~?JpoMWgXf`kvweKYKKs@K&&gh~ce(=`1-8OIo9(UMs28REXl4x#Fm|*g-ga?G+9Yo&jWd zDAYP6SH4qyNayA$m4g$TR_51_^BajTB?ebcY1U;(HO0;f`*bP4%CC)gocFZ+f;^{< zUuK04-AU$KqOM$C=$!;aIDUHnDl(*%d~~twPH50YFj$FMM+(%W6g5AWpc%viQ`Be& zh@v3K?1XAD0b+OX%B0iXQIX`4im>06k`AkmsoOYG3*bfCHAe)=_VO8xj_&!befwW` zf7ob@?F#2=%c3K#)Sg`ijg*hbBL{ctschbRia+2NA3R{SS;TQ|wfC>xXU^_A|Lu+~ z)Cad^$2X9vYQ=xrvPI^pFFK(0y-i3JSO`&~?V-lZ3sa*-iVej{=zUY>k|^aY~-S@OGEGUw&iJBHh0|Ma6+^r|}?_TgHP_7PCMP zJxC?5?2c7Amt@*y-tsh+`5&{?9eA3`-VOY>pVrIz<5a+#bx>-4UQjDe8mIZ|87hCu zhnh5@vHB8Ug78ur;OW(JDur2T27_d3)Pg2AZ};YbdswbOcRE~gQM7Zu15Ij*EZb4Q zPH!NmhtrgZaGOx;8FZW3Ilt|_%B6ClUH2|&ShaiKl)y^LIM!pqmi6=SyodA3ujfzy zq1wW{$6>^7&6U^7jv+t&A%Enp>CM|PbLu*oWD#oLk9LU&gQq%6W4fmb8)IbTEWIA0 z++r-g#H*&o8wLwIR*J@6RNz$c;9{z)0}ZBW7h+xWW^qVgnfm$!1EY_(1OZ@Pq=k%u zm{IbjJT~|nh8@wr@?Q1U&CgdBu^x*yWzAEbL$lrn<(m(W|ES9AynTTI=KXWg#4!sL zvTO~I|NRu}jFfsY3cWuw(1F;=U7;jtk=9j!CyOcG%nzw;2cOJf4Ee524Qj3x)X<>g2#9P$) zzp6)beCMI(ora6fXgpa3n!u9}9P&o_ye_INzu3Z`wB@VW0OEx$upgwUs1gWY3`@W| z;fpCg-nU48iN-?6YetV8C^Q!4B+RLCXfG2B2qcw~xP-iFoVPI>e3wbs#@hRd@(#{= zEZ(?!ArSS7a`)t^pHxuQ>HRWm>ZC=2d+YKwn1iIJD?}o%AErYLL83iniSeFRSEhO) zRpqe%j5#5$M}N8z!Kz%P`V{~Jb1qbEktxTv;mL6%ns(WC=6K=Hd2HMp!$V?~0mllD z$ftRDWbhEami6OnWMwex_nAEW$uH_#yh9-;ty&(_h^c}P=jaMW;L#whrPIw)jVOVf z)?^`iNtzSR2&|tIX+I~_>SY|vgh8aH`5CjBKoHt$eb0BJu5veW4@kdK3%%Z6uI^ly zw~hDxmHotD_?FGsmbZb;_y(=!KRuAMyaVYUp48#-X5i`U^sik}F-aLcGh#4oMpfx8 zO%eW)c4pKQJ+i#B!7XcTzFoJYT6Oi0+6K;TOz(t&SoM&P_3JxlFBd}A@#33 z?_XwWv1OO z;iI6)hU*Z`qV(-+9Bw>ro}M=2#FO8WvD=nDza}J2SaY{BK4u$puFB#Mx4LsH?BEYp ztzxbn6>_f~{o>~Fa=8_bU%!6BR*7ZtKeuh?zps){p3GuFtThYDy2RIhfAP|H%7CKP zKc74M6XAS6f&zNFNg#FwH}=@DaDl~o82+@yVAx9y2D&<2ar?<&tPXpx@Vd`n{D#e9 zu&D$djUlOLaj!7!V){Qm^F-Xjps&G#)R-cSOOjau18d+m5i`*imgI$}yVSG!gZ94p zSQyTCkDVfJle<-lzVQ{i%Ijv$PQw$n8I+7<2Xwm4Bn@dOPA_UCc-d*0*EeJBui6E~!L^UaRIcpHjIe(Ik2|8aXG{QBqZsbdSnPO=3K zK@FDy%kr>okMXn@VZsTV?|A^jqtalUO z*GxKqtmOa6l+#l*#Dkv5T?Nu~7u6|uW3NA8D(ByLukrpk>#=C#>IJah`@TDU>Sx7P z#=FxnmiDb$jHR$67P692p#>Ty5tT?%Bj5(h zf-rPyExnYuBG?Sg@HENo9980sT+P!x5v6lpp7O>&d=W2g@d3=g>+_)WCu#+YDI-rX zbpZW~u`gA2|L;)t`6q<`gpRm$IV|%-5zQ^rf=tnzNah$wG$S%(UHHof<;jOW?aznq)7qilXOEAs=M$+dV9_wKyU@04ek z4lHFMzi#-2MXcNR9aDDj^B*t$m|xgd_&w3(17sX-V)Zm(uvNnYNr)@r$Ys~*V!?vN z2@~ql;44F2YM}ulU4ohB9-%-(F%AdXg!TwU-E48_M!aZAp;R}cFYylE7*5SaXhOvQ z)xZKdXRsA%`r~JxdI+5TCJiiX=Z{zVUCGNUP?oTOe}59(CRXhX)j7R=FR}E0eH@&O z-6bRyQIpUbeKe=8HJnbUAst5+MK1KKftHeTqANg@Xt8MqEA`5-)1cUa0tp#Y^oxEd zXbU&1>=L`&P%;c3#M_m3@s#MR7ujq4zs&UqyIl0kw&koGf3R+wobLTt9y**=D)|0M zTjlZ0O-ydE0<^`VWs}1--LIPM)`ITiNCNGd69WJ8#owrHDWH%C-8pS#QSNR-d|C~EJn;GPNzrXkMM>E@ZZ#nnW=bU47F0o)Oj2+UVnB0^oIANkLMxmqVx~M%- zpwOZy&}B#z4sc3TLwY_VDl3YQH2XLIa~ob0?drW_W%y5rocLrwLSky1D>-2e+j8}G z*UstVuD>S=Sk2L+ei5HQF8u9P>*XwIH6bo)R*yH=vg;zhQ=5&;SPeUP)k;9qUch{< zm`}rN?pLKBkNH$y5JCBTx3ZzIC%yvo@uYZ1T`E^EoNPoL=?ndk8ac^FG!zl*&k zLvz~BXNZ^=_7K%%70*xjJ#_y)in&KX5~>(&gzXKJ$S}qxS(EX=;wJU43dz6!!#+Gt z_F)lS3`=o@WwQU9rKtRr?a3CGeq__d#xGb@mS-v}`-RxRrvJ!36;Aua>nVHQS-B?$E4PE6UClGrd2q;0voROH7$VY09MB+PUNRQ^KNV%zizDSPrFX)TkdL$P;jx=4!fo~KyL#;m; zkNno?e(BQ>-N`%lap#wges|*VpNAF<8k{|Bl;_-0rSywk`Zry$Z&OZ-iIo~1dGqaq ztJ{u9Z_};qYCFvueLPf#-3`ze3O7=q>W7!p8^r&y11>DeG!2K8k=9(XYj z$xaQ?m)Ypi9D>fw`_={Sp?=Lp)T$XzV7uvF3VkFaFe?yZ;&Iq!X)dWYj|f4vqTfC2 zLs1j4x@znbGwoY3)W*mkKiL0-p;nnk1S7}a;PU7d2$@0k^PNDW7jJ;^?S9h67n+=v zkO6MlybtVJM$FyfO^;Yjk@CXs%3I4Jd;5xB_CY|dMMHC}VS7z;K2?)g4`cv*2Dny( z6nR|FGs{j$_3}|5m>i`)f(;I5@?=r$+N5*1s}#6nsLByMxe}!c83PAb=}-gw0WQVU z5{Z53t>+RYyh&!Z_q}|uVg8uD~veY6;@Jxbds_E>3i0+bXc=ze3*sGQ9Bj&=cB$Bc+wl(9h&d+O>ZnXA7Ua--I@(OCEgVfrW`12j9#WL2+{GP?L)N3!T_}51W_& z;D|AGWs}iE;|+1#F$}*QVtdiAuvk|5KmYuH@-GBF&aKc&A3|>FEf2tI^bIgJ0Y48- zDh9myIPU&ezk;z2#?=3R`4x19k}L(oE{|akSlL6L-pCiV#c|vZ8#pqfFPO|ceq_VO zQwpj#h(SYobRETYz1g0H@s@z*OkM?t?p1Ke+-h8n7?&KXF>Z^BWtix4&kd2N*@6tO zf*A_{uY${BCZMVU=?~at^4280cUzVY^ky`=n6$ARb;U0Tx@JGx(?#kSKzquFoAGflU7|fOhFINss z?bKsOKXLKzSOCht*xG;Ip$)l9@<8!x;5Vp&S%zbt>$M>1Hz9wHfh?1bCWCS;9M6vk zC2mn19SxO9GRXftZo7zrw)@uE_Si_yB3qGsqOiqm4e|Veo;E7xtBf?06aoFsFk6@( zmKrB4p4=ujKmsL9J(+|WrPIXu&}tw&HG&16|Cj}rWGDu3N&M{+UXO?6Z)MS&x6MaM zfQ+laEqwKDJt_te`k8>y>AkY=vzuq~Zc-01L>ZK`phUtN_tC=jT8O~Y7?fz?N){c> zufLYo{l29wT}d>jBDpjaI8$KQ(AW}~tOZv`@w*7l=8GSS-eazT88`E94(-B{#NPuZ z(!pVy(LnEH(z?OR_A{}sZEwZ~^aC#Dd(_pT9*h-juWLa*Tx0BGEI$jDNs27UY}t21 zOF{DuErc#HWvMZ%J0=CmGiJ7~@v^cW1q8X7D`1n%utIoYbyy+fcU+i}&kt`wG3py8 z25NJ~^FHD$+0$`H?lZMR60(~Q%B0SYZ@uMVF{(!h^mi=0;Y<2g;>M4pHjk<&cMqy{ zLSo`{{v%K4I?L&_pyv$5*>W@$c{H_h`k^a_blh^W<@m^b$ID$TNAy~5PdS{>i{)GcIip+_-mD!j2j5?~OLpIV;Y0XTeuMdw0>_y!MxT~Kk~rE5naz+oov9r`T!2DU=`9CIg)`$XFDs)*;YQ;t*7T(b5HB`L97gTl`dUgx&E%2^zidZbLUJ}6CQp( zW%isYYDHST*U)QXH|7(ASvXAfk1Quz%3OosEtyl6Sr`Xjb418ln2&X|e-;E4)U5^S z+BN1-C)B?C{M%=`^!#w^3Fcwl+NWpa_v_xJA6z`%WcQh6%ieYK8{UNeW5y5Q*SyIC z#*gWbLe4f`bOZEU=!itTKALJcNvtMtMsCH&o8%V!%V!-LEZGs<>t(5foKRN4> z9qtDB89_Ufx1AI)(~*^=44&jd>uIBKqMsY_oE^&Kl)hVX*>P>V6f`_&n3)AsTw3_#&oK+PJRWJzm_Y~KSk`0%To zXn+QnYPTOEOjtYI`wB$>nQaAX5p96vtzA#EwVbTQ->-Gqe1hCnK>3)w@#CW=34AqX+;O9^R6Z_WtG!pj6+ z2ndni1GZ)k=|X;)Y!!<2nK-x>rT;c!KN53^MI^MZ-ZWkp%Y>7aQky61E7<;NJ`^NdE~9*r`FKElX~FUZkOPf10X5iRkfHjzGH1t;wYjHx&`z$N_O4?~ z&$0ueCH+Z|L08@a;|jsJ5;4M(@IIKwW$fPn%eYY60U9I5W%7>FxI!L3u4E_wd5mZB zxT7q89XonVlw~Q?%9LSM#1;CJdhSV9ze^X4?i{54Us$y;XgO2#Rg(iUR?ULmd@SFS zr_ZoYtYR~QOVW`b7{a}np>p6eFrb0ykCbmBhC-_fxQJX~L_x^*h*#KL_Bu5&?;$5DygeaG-n&w5ZZF`+rT0CP))YcCxYXm?^YF6XkAAxCE!?Ieo8A z@(Hj;d^^S}i>nX_ulx241-cv!v1b*4LK?5d=m=wY_kw-AU$OvW11+N8aOcQvGGZer zwN{=cgql-kd^o~Wmq6ew@WQK_?nhNlHpiAcSf%h23!r+#F_yt&CS2m%Doh zXw}IpXGWY1n!Pq#J)zwBv#J=cYTk7&7VSN(RQ>p>$Y$dgXY&Ma4j&siX@Qu`re6J+ z&+<-W-;)jwgpi$bGs{5-AETAmb#TOH!+mqLIIoM-%Aj2s5Dp7{YURTv&cD3WO7T6; z0t+9DBC0g|Q4yP@o}ic!GGlbdnpxd=98Kmc!MpSyUkCtwjv!Ou8WwU?iJ(xdmnis_;u_(kC0o=#_t{E9SR)5 zWIn??(ZBtP-W7aI6m7p!6&uf~rn0j>_B|e6^IR=P$6J8L6Mg$`agthsC{l+rmcp_~ z7LSTys%s@mO4k8exR`t)Zd6@D5OiEtkA!$EjR~t)00#-1jZ=&&c>J?9 zuZs^^H6$UtHY$6L_~(mS3$kNdPF%2gW35^1#IY5#Si{3P>&3_iYt*X4r{!MN2E6q| zmEGB=zEy?|Y7#OfZCjs-(-~Vffd$xemCe3Vdc-ka#2Srt)R1emPJ2>cBMd$kYlM72 z^BNfvz)u+eS|geAQyGBh$`tCVe6cclFe>kS4 zCGffSe8rA=Eyh)9vS-;Iec9@4>y2gOHJ)s~QOQ**7|T{%dnyzXGZtOLRGrg;Di^)ejFGI3G}WC*UK#{aEUYNWaPvR>M?X5ExMFcccP(j zM_-I4N{QYRP0DpNDc8}YTt_#g=PyRz!t)lvW6fcqB{A6~h;m6hy5BRKW{2$+S6lY) zNJ^p#t%ge$^;wnj-gQB5F}^|En6fd1zgl{eEYxavWm6wMzv@svpRj*v4&dkL8xH;S zbNjoP^9vd`#ml8+HFjD$w2TM-2{VT*H3Nxhs*VD7fEqYZ1EQSJ2%smY^5^0cSU~Em z0Z+0*9l}|_#%8~!G|U;#b~fnnZ~_D%MuOJiYDpkELTMx>47%iJ#%fzUPewMe z#_Y1fH_op~g^?o(Lzq*qz#_-Ou1A$!(|Xqn2@ydRVjH-`l?7t@QP!YuUmp8MnPmYr zo+#W0sl(y_9Hl;R)Pe??jA|YB%2kM2!kT>SIgq{<;<3Ovz_;%zusHLeLLnE;Bsg@- z(q+@jRw-#No9q&8L&pf73?0M4Wfdj(aBG)NQy&QNwdY&$J7dAOJzp{9_=*LdrJLSb z;#rh~`hTB`HxgdULU(7D(2G@KV`ImTPZW#AHRl&BFrjzfSn^SPkMW&I(ab$SF=na@03_6I!M?%Zcb}>J*@Fcef8e+;> zNerf(DNh4cP|iM0QC3<>OYQct$CH2U^8=oJ*Lbr&V@LP%q>miY$HS8^v#J#{GvdV6 z&s|r=)e1v~#&ZyQI$qn`T;cM3pXKJ--xidXi)vHJQj38Io$?Q>mGBf%P ztky33P^~f}rezJU-2C`p(Wr^Crdxgcp5H$8p85E` zYJn|U(yBw9Y=BCkE_ZX^s!R3LIJ*YpAk;2a9SIXy^}tdR7YsP7$%8U zrjlH5s3G`*ItA`JDefl<+)t$BRX45i6E1gZfjc!NufFNYIxhEf1@7lkFfMm<^V%EE zMeEXIVPyty8U(>I+|Pi%X+M|XJeJS?;KOFeqLw4-|4sV8cb z896O0qe{zz!$jl8%Gz%A)#tCjBW|7i?9Em!3l6iIC$Hzuo-A%onlpaDPrnQpGkXe) zpFEqL&5C=uWCpE!>2~GCtTqh?%5~?u{}s`$IQTneXigogidb&4Z@n#y+TwbRgNYDl z(7)mGASZ&egiN?Z*vaJJ13RF^z2pLSathirk)Bvlb|=znT~#Jc9Pl|%v6Y1VH0!^U zm==$22{`hPch(j*QK~bsf7^d|+I~M|$doC>y`<+B;vxq2((9T-x0m2ZNbt?y5`4Ef zZDnZzgAxs=E#?pZKT37WLk%CN*)a&l4Q?*yiHv`DQc7N&X$fGY!E#FQFTsEG@G{>5 z{0C2O;Zmi#BKB_oZysM(a>$Tr(?~{+5i`^y@RF8A<&QE(rE*>EmwRe#u-~f$K8S)e z*j)3>;M+CjAYl_>$5VL{!iXEbPAP*@mGI+N#l3~hw*DU$$4~P88`ghtdd*}pgAFau zIu+f`V{z-my)V}85``b%Jue=r7-L_NEhGE?X^h4u{GVgA#=tN}z1Rz3D-#H+B$3il zseGd+@8fY-=I#A$&!T=aRxi&U2B$)13`@F}u;TvQFSqrZ|JnZ7ZP#TM?`Y^4i|x-s z`i0rt!TQ1(YAn{l?o3n?!V>G)zfZ6hDt| z#lnz$0Eo*;LBg8Paxpd|Yud=FPh`v)+hFM6lP@?Th7PY3oLM@h9-msSeJJV$_qRui z4vtrVl`bXg5!-=iBccWmjBI;uJez--BuwtiP=dQ@io1P^yH^T{O;R}w zk7Hh-shnO@Ql#8XU3o8>o`ipwKxcja|8J&!}$OWLQsTzLab&qD>M>&k0b{0s&w zd#3s52MN5oCzjcK?;pM4@#{jR!P5$!DM9qRC(yV{!Ikj0cCQcaE6p* z6pChb>=B7LLuqzaCo#&-oc82IC0Risf~YX3B2r3D?A5GZDO`AkAl6!Jc{nCW>}6e* z)tohYUR*EylZz8gSyHvoWsT1$y+W5YIn^K-wcL8E8-tPGv0j9hnwT`Qh{ zuW(`Lil*=JZ#Zk#RD4qSH5Z3pVAZHcZk||W-|H+3se#BDX14)FUYanc&821)9VK2s zQ}8?6f^ML6G(NRjtWx*GHcGPnrhm$|q38~MN_p*(PZ3X(pYq4%M#$LQxW~liq#9(b zq13RA2Y#^x726V_D*k|1ms=vmF0_hv$${cUce5*~{dfJXyHW2+l$7ZUf(> z{K#NxdY~toO#Cp~_z3K4bRG7o={^LS^=G*}*>acQ+ zyJfH8-qRH(z&hZz`KY6o0E<2hG(Ao$uUChH-`D8AYQeKulm{tJ4altl3(&aCA=Uz2 z6zkW5U?IPVxR@|7`qxQ?J0}Q3D2~lU}e9`;*|b;SAUIck}ka0xX8S zA?wJ^ZGzHbkO}B$MZy16H9_$rcKH4`U}`n7kA*Z#@xzrZUJ$=9 zhwH*by7*$>*D6g!U_QI&(Gl0I0gXCO+)^ils;F8-37IeEPdT=jYknu@Bb781y?!(# z5z?qlmOmM!E=#lm^Fk3&6z%cVw4o?WJXLoG(uFnn>l^;YV)p)r`(>?nks>aN-_Z5* z_R@DRT=>}A8zZFZo!=_Q;2Vgfs(})@W&?sj@(qigX*k?rADR~e9WrFf2*wI!%p6L^ zSWUW_Trg;1uLeSW);1@9$(48_aLZ(tDpeQ>xAoCEr*yg-$KS%={B%JK)^B!%z`B5U(3jZQ z!|XrOnBLO#$Ur|SK@3CiZ|RgSs$(CoJ&G8R8s!{X|#T~j;=$a#_2jLV@fqn z>7K8`DUurKiHu+*ubA8Vu|VA=RRA^Zank@##x%N$x7oO##7{Ms^~=xix2!4yG{P&q z@39Zwc}H)^_{k^iJgxcji2BXLng<&lGA-x&@yb8V!fr=WFP*a`KkbAXmZ&PWg$AA;^kdVTiK8GBeEru~+lakh}q? zM#-lsiadzlRG#rpKjE#2z}vHYWbT9SsXr;kB008w5JnpW{I?v49F?)~a#Y5H$BznD zwLUNuH$m`&U8JT)4H@>~BD=-l*A8Kn=fn2U{UW@Fo`6fA?$KQKWw0y;49WjCrB>{B z{)Ct>Gk|zM_Q{IEo_ZD#odLJF3O>-i#MU{Wp^zhei)!LaD{FptVn!NP+VA z`g^RR5`Jk#jmeXatba>Sh~hILP?9!%S#C+(@+nKUiV8-C6t|5i`o_KyzK6=T+Q71x zsZ*EO39^T)n0+sX5Qv4lDb{%4*E*!Z2&AM$Ktr8{bJe`^&>hUKS5Qv%Vkxdg@#>^> zB~_Pv3|Mbd<8ODYD=)S9y)Z&#b-qfzE(Cg3HBd-({5}NTF&!z}MZhnu*JF*aZ@jX1 z;Vw;lvu@1g8EovbJI9;VoiJnI(Xj`<%jiFFf_KXJG3f&*^yxjZd<&=!O-}8~V-+`T z7T31i5m$nGvxpsEukcU+_L%Y1^4qlyo|zTwqdAevl?C1DnX0d zs;M=eq7{S|ZA7&#r&7W=44NojLGV)}#EpfN$PFwc{H2coY)!f~9l_+{#nB?elj(=C zf~Kg1Rx!B}Jqsw8Y0-^^l*?9Hx~FA!dYzBF@R(fl_4_NTp-An48{H^3h7W(Rm zpYDH{{`Hy&w*Ax5qw>dOuU#+^y!dJG+yqAQ#MfJ0&A#$l9?11l; z-g|IrxLdK*Ce<8)RScaf^9A0)Vcd}zpTno0)A%gl5R0bnKSm*XV}OtpOBrg6 z)u({Q`^E&U6GjO;MIWkiEx%d&7+ z^gm{s0}V7EYfX_&yD73M4P}E#8pDwkVSuzz`$ED~?3RwbR53v&aQYxvl(jkMgy+J& zKhPLv&ZZ-%spNet?dmP@B>NzDRvqt);5`kCezYHjFQWqDegm{99Z`dh=#_lj+Y&i2 z#-hdQ>5s7~W}!mch@LC(LV$&soU}xrrleEw4%l3POi}uK6!lHUL#nhH2|gUI1W#*RVF#)r~S^R?vZ_ip>l+Avg#5kBh|u z1d$bV0J0}jE0smsBK($fay;vM^5jg;zVhA!c;fzdeDPv__N=%Al3T<_cxOk7%MV~X zf0KLi-1*ClILAs9zNMPbk;uIW@{QQ1wOOM1mc!}ifZmt*R3$vVBnc4@FF5o1>Oh{K71iAb#&2DJYOAt!h=#8{h>dvOoxAv z{2Q%Qf%iw)w)_1X|Kgbz*O~MH8eS*Ac!CTsr(oHsZi{)5@44#F)Zoc+zdXL1B z+OK#;TSu3+bSa{b?4e5vT^e#WlGI1DssP=2$hn$`fb<}%W^bNrRFr?RFhV># za~sqO32hMGq&c#T^dba$k6fpn4eZX7sWO3XEv~X3mNX%)MbO0Sk|xM^Ojr`1wFsZ_ zH2M5?vC45@zW*tmR_v$c^K0}=Ht_hZsXP_GKP zAMyuh{Qbvm1EB|3#~PHg4c1CZU$V(WHRj?^E5ojtJc7hOCl&CO{w4=s|;ac$h9BDpI^+nKK8`wNpm)BS&PE4 zYo~~q;M-^3{eIA~?2#*%j9;@b2UI>tj8Q9Nx1v!IsHCq_y03JfVQ2sEgDzug9*aTC z>>=oxj~O(fDV0***-AeqMt=OgxO;QPm5KRlr!06&oLdif##j;R`ttO9xT5_*U395TYWltE494*ysndX;QR4ObZCI~(+}^bnszU1s-AxitH;Rt zwP-aZ@OQso!|UdV zbt5FM28MbW!zJa<97i`W-aw=*&vO$NEC(1;@v0AS3xPGqDLbyppPlmHk^2JodWnB4cPQwIlo zc+WO-a#XeP-ttvApKxu?A8m$SKk*Ge`|^g@m%TB2YkNCNjG#&0bl&=5bkzu6g7Vk7qP!&=<#Hw{m z#RUYfhWuLi^L2as#-nFp%K1?>6!q`3;%Lb0WB7!%eA4uXYuTl9-={Yfh3(pQ;~#ns zU+sK&npa#2V67XCUo7>ir;5H-zsGq?MlOAbX^ztMVn|v8B598HXwG1Az-UpGr5`3L z#R9#8C&dKj(-Om}tR3>K9lqIM7eTjx#*qW+C!P7KIV-lzn)dVuzbTp1Us$u8z0$H{kLAkN z+%+w0X{1NIEUqYj0Y4CL>!rm>P2S&y%Cd>kpx%1ma@Q7)hR zs&6xKZ~L;|?=@;ZYIv=ki>5BXJSK>5>+7Z^nTxSe#q)^wIr=Qb2)S)C z{S9J#WFFWJYzmPeb<=VpW5qI$gm>8WAN~?Qu;kB&b~<*HtxRt{s6)_zRQ?$|l*2b@ z%asA`XKZMZcK*d>z0W_}eDsv~nXm4ny?DOpCub&3Q-ZCZW;1nlu_XG&5x~q~Bu2oL zYz*_6dPGT&vj}djY;c^UHKa#zF4NqpYXRC4ks|8jAP(+yqN19bETYXtq?Mjs+Ggjd zykS1Lhw{U_PwqYV@0!vNcl8?m!I2Y}iEZ2wpOxnM`!KtPK#Z3`!&3Z}G+% zooS?0@H@=mb~DcoF$fdKfZ=FXt+mJ)a)Ur%VRrr;{^H4zK%lbJNy*An;;<==e^1x8 zLnjemjI5#Xp~uF*y_Y?j$RFQp!oi)|g?4$9SAI9)P#*2s_M+R)5!f?y^VY&+=%DKy z(4sF|8rT?)aydnRT`6QUn7mLL3UuPD&@71%g5^`RU&}-9?pdBJ6S~CW;l7OWS>?$x zDSr_++B$kiTe=j{JND2e1($sx&>oi0LycJ}HPrXt$PD}Me$HN(Hq})4Bx+V*QNG(6MhuGs|OEb6~;pQrcCRKwia51 zubK(byM?V9x(-Fw%_bBS9#dw5R?Zh@v!gzFa;O9lO0+#e*x~u`4>_1~&s*Z&n|v87 zvH8a9^=EC|btT!hh*hl2Zsyv|c@D;OGUfkQQ z+w610F!FvyKcRk18=ya%XD*Qu49DkT~`H_#z# z8|eZx0sd02t~^{T&(u@9Z;0QP4dfCQ%HZ>aWDYp%i6-`y+-l^He4PGQkD)LA^y;;=(hA( z&?qAx9i<_Z{L<1;45u55~A0{=6bkY87;Os#LX_pNCn3eg6G6rMHn?NUb1B%0eBM zRuHD-M$MH()jSdKgMmn4KU3NkrXi&cRpxah#6fvaq-3^ANY?VBPocKU{*|orMfa-r zPc9H^#6zGS!^h8JiOjL|ulXlWF4_9d?oFposmNIqt9MY7KqL=m{3@11m&(rMB<31u{TDay46M8+@`c^p{dJQ zlL+xHd%4@Bj`e#Ure96uu{;R1@g4A5Kko4+K2KesRJ1i?d#>4D{GbuN=M6s3eolXG zhOK}9Mr4@;i6P1cj8}ob3|6F_E7f!ofqNky!NsADgI0V5c&*KX2lr48^>&0c&ssWrbpQA8JvG!w_JV^fSL^pk zUQgd+3zX?v1Yiw=riW;b!?9ve59J{6g^|s(7cb84dluhQNqo!d+xFvoV*TTxBwBlM z=Vv${P2UpkSTLxY;^`y4ZIQKPY~Owoz0nq<86Zaklr4h3a%(UFxfjqe(U+>n;MP64 z!?tvBR`W*h^nRVzbD;VZKa90VVlx8ZZ)7vrb8;^lsF8dYzAcH(EJe@HWDO-nR1zQY zzP7(H)==A1S_v6xpiCG$tUy%E`q!AruZ^x0(iZoxLbxMJUk+m;pJO`ty~Rh(=dAF& zHT0uK@^;82tPLVYY9&x?NvbUPFLPOHNd_l*JnREdD6<&Es+g;3lDtPGCjh z-!zB0Jc?ITF5m=5X(fUw5yJ-Dk-LP+IME@>R0t4i@7#>;-9`?7wMT}czLGhtN8&5P zGddHcEGzm;NwHl5?|j|Z!g%5e+nP;AOq5)h$4rw2}0zMr9K15jW=WH+8j%fVl z_QYMe*M7jod7Y8fqXO+z7p3DRiEOa@$B_K%4`Wl;R59aVc7*($ovm zT`5INDl1c&flx-?ay7O1T*5(7)AX>K%l&kLyQa(C2w&jJd%^S)^shF>4{LFG-oCA1$t(&b<;X=&CL$b9cFQB5{P4Y|)Y&>cw{_c`>D#tuuW0*XPWBFO z(AMcQwr-x0y@L%J$j=Vk+qq@)POr3hp$ogvxdq*8{>sB9om;-}N~f01JF|Y%w@;X` zZQcvhw~rscb)GV5`i>p5o4>YW>%7A9P1KQ13hT7(*QaC4wtf3_XxX-3|Ce#EZ+re@ zn||pXTeRuluRZPcP}>R~r|idmxonUKz_Qxq{t$v6d75d6^u#c}KwM+V3wRRfc19SR ziO+Sh+TbEtQ(I3)vCh;gzAe3IQ}$>Q2V#)VM!i%DT(5?ja?;gj`k!TQRAsPShh_x-{CZFqTSkj6^931aq>6_j8!<#l9%|^(I6Z#8vjH-kKeQBBXZtB zD`Co1wOBOLw`DkZWV|oZ2T+&n2oF&2!oVMwD0aAFF4*t5P*@q*OR8k?Af_c6i0@Dq z46nY!zH`!CaYmG6-+6|4KUCr{nr`5I1JMzpifyG9Z_-UHv}_oPS{1$fXBBHEhZVC% zAvqanBvP*;9ox7@KpRXs5E2m^krJWw$SYl(@Ihyx0`&{Zi!(*>kd|1f04D**4f`4& z74D380;&K-H!T^N@OeZ4Vk=h%E2kKp@+nR8PooNg@5melOp}ZHT*k)F!iG2g}qt*-k;VxIbgqt-9ippvV){c73ZqX9-%)SH{ zB#pj=7M)ivp&`#KnQeYhA;~j;Fb$pvvz&$4H8t3U6PqY5q(F-gm-=#iiaAUMHwKYe zg%r||O)w%Xl&QaYQd%fFxjQ9T6g5H!pMcOYcq0W{?c#jx#tF4pi)NFjE(*VW_MC@J zIRA6_qWtp@(@)Hs_xg+r%1&?Z#*IrY4_`i)uRC~@d(rmm!~t}ud?1!A$jM#E!6&vA z-3f4Eg_3|jBN_LK+ELzu>g*H|Cz?x!|GNexP(7Q_p03}3_}kMmVF=fX1#}-Njks2m z*C*sP)wjYH`^-X@MjEshz$KE!P~a%+jHtQEF-P$=GY}o?3jGUuLV$}%*&(ZmK;Hrl zLlz>#5clCo!F|-&!FwRv@E(j5_d)Hr52=a!keaw(ReswO1zHV#9Qf**1zMW^0N+%* zKzmv~AR5{A90145?1&azM?XMT;R#$ViS8YYdoXIAP>**&%KAoOyzsLZQeP>Nj~+2 zwOSq$A;C6Ji!gafEhkq>HDYlIf%2>+SS13yEhcXpoy<~TX)YX2y2b)`16dFo8=Ddf zSrBKE1<*+W$pKgbhtwL;g=1bKP!b@AeY~tR%KZ9@B7pfv#49g}Y3jbsqx*-CAAe7L z?a=VA1gr4p;Mc>44Sx&toh7ERX}rR_mn*K1fo)rA@|-Em!D3@KCR{i&We#%3=nNjg z87vFmOaeIA5q%%!ZW*lJNDG2#YK|0Xl`6|DA!u@$mDq>_wo0x_ag{JVQxc8NfV9jC z^m+wXg}4edeUsFSFF>}MmKhI6TUFPwcNPB5w?o8y z_PpvH#@}q{-NCx-@;>A(JFFGkC`(DHk@ITK-5HrVHLK_R%?{RjHKz;vwi8iKRhY+w za*VbO($~$RMEF?|B)!RdMRq>Ww{pxh!AC?PCW|cjU{abbzN8?Tmw-toU}8@2>;x8( zz$lJWC%z6ETj8Rdztbr6+>^Pb|Gv(C{@VKsyFX=hg!kx^Jgmmw;&zI%#$NiRF>AGb z-czOcpebxf_qE3YWEaV}qF>Z#%p=COSf7V&=V@7-ed zIBzX}K3@EF^~`BjfeovOl7C#DSJF19wsEGuR~GBpABJ}*QsOyMEE)qy58?=$QUbbJ ziP#bV&6&rnOFHZj1QfOyQIgo=vx2s8qxBy$6n&lZ;(4LSJAM)Wc-bG(ZT$Wp z;Ja-_9_zYlL$MrXI-4}PFfXA(Ku?^)4chbZSYbQ-uJ-0=Z#;w~ne&$8y z+R7Z;wu-_Xa}7IFI0o^vgVdPei?_{rA$#W=8TDHCf4N1QelOPZ!pxMm=GJ)*zg_vK zwAVm8K<_An;gyO)#B6{TrlTyuYYfbUBqRfCVE9)wM=2?mA0Z?NEJ$f{_9W;E%F&}F zV~6jl>G9Gmq0PdoOGVCpMZ_(0^cItJ66}dAx=T&xT^AM z=;6sAl4J|T7!NGD(G~GFe?`7HBQ)wH)Qg+r{}jyyXj>jDwm>NvBHZ*4q0(~254HHj zI1rbX6i4(yXDBV+PXy!{(y4$z_~eR!RgN=;o)M|ew@_PefOkwjt9#h9dTsuuo}D`M zU_Co_=qZl8@7?3Mz&jjds~7TTRvkOMsmGf9!yD}BLk9Qi*L%p2J`Y!^!yhg|Ty2p$ zg1E*2B}c6bu2BlPbi?%nBrRNH1^gyE86PqzgI6@LUJRL1oNR$4={1GPCjjIMV0z46 zf{C&7L5APU&7@=wBKrrz8S{k_OEU@!L&qu@9>hT6m7DWx&F`AIcyVS|QF3XwWh~ns zFUGPtVjM3kMzBTR+w472m%aBA#-0o9Y$;+#RN1Sa#`Vfx(7TPAUKW3$GzCaYi!LFP zO`=osLZnYlFMooVO<3_mEkb`2m_uaovxJzyzHn64Ac{pSK0cHbF$U*Cd}xvydGPQX zcVAz8Z^q28XDD9VxRs}NiN!e+dHGSVj$Fgo(nTl@I`7ZL&x%9CCn{AZil11_2=bP6 zDEiC3*S^Y@%+3^j#%JMnne97>At$e-gu@HA_70hEZXzD0jI+S~Wpl6fppU(4t- zY_sn(2=E)9F~a%sGkx%x7WTLBnRr_OUnD;RjJ^Dw9mSt9z+3V&T`)GU{7ix^*7un> z-)CMe{!H=MurGrVjjV~D%H^O1y{bj%9hKq4NC1cSrAHW1DD+LCI2i1HO|i*)I5Osd zJ6MTXX+#vw0!JsU|4BkL0?;V2=;0h&L}5Rho*;z%fio`|DD4J4w$uwAw58W;t6Wcw z&S6d#JN_p6Fy3RfZ|1LCH+SJWwfuTTw0?g6wF&ieB5H^>VtCCX;?vD6;qTxZ%$0k1 zy=%wC``4cd={gu1!uFzS>bE#IPVg5B$P~qI>quuYeVZSr29adS>xMfW)}z@9g6@mM#Gt~aF-CDZrVK$P z)|n4i^4{KcYT3fGycuuoZJE1>zt1l(&h<9IFK*-Wl%EjSQE+zT;N|%!^K6$qQ$b># zCn-M_9#x*>^JFZiAw+U6MjBvyMpJyT93S%Apd0yher>}C`UC4T+0-;%SsFMkp4VVI zk9xma@Rx_xXXVvp$N?FR^j^i54ur}DobK|d1J=McLUTUzEKv&hEv#r8stcZQyC+aq?DwWlkz2B_#6?k%@*2yM#LaRpmv(`!qi)H-uR{6OLrE}xjAj>t=Vt<{8GDLmwd<~@3-4B zd!(mU$uc9Cw41fX{?C?~qmHBnMvhtBZuVv#vJ~;QLwS1-EMm5tGE13l$-%vO9&z%| zpu8JLHYHc>bE5YRPr%!^j&6&s+WT~`n}^WH#4TF!g{UnPVQZ*yU%ow2k39H>#Fm?Z z@Q5Yqfgp$pVGHtA3se@D{m+4g)OCcme=?H?kK{8U$qA)UAVXZ2kd4FEmbLiWwIsc5ur%V zZJ0EY=Rip6wNel%P;RL0@Y#yCQU1?KQbAcF&&Y?dbLAMOxKgr%I{0bVL{OR+%DN+TaiqllO-QLTir4CfPgDy%t*S64T2J7eUMZ_@+l4zMWTgT~%a z)H00pE&M%Puz=NFuz*isCq+Ycl6JOxQBU@Y?N{)@I8zLnKB%VbYoxYQ;oFwqRpTjt z?Dh1Z<~*0I zJqeA+;+)^P^WxFWov~9!j2ra%=e$LJzOr*s_xRH1>ArqBWSsMwc2xMUG5N*!Zr}Fo z+{sSl^<&jM_CDd4hhTIV?AYCho_SE2v|$Q;*2E~u=e$lIr(7vxoR)Q$CV;WJayrHX zDUyr_RbeSqH6B#KgDSM{G|>b+pavK6fiyzsL7Xcu-oywJ3rLrEWM8OX)W3HG$#7rB^1wmqBlWEt zJe0Oh*(tYA-#@uBl@W84gk2kRtc+<@rkMa&ZAOzP$(h7U&m7LlBU1u(!!J}> zR_BX`u%HOV<0t9cQ3~o6&(bJ?#_X|7H>|jZ(lIL)&K07%fW7lO@ z5@U82aJ}E_15YE|wTYJQU*uXa$7FDrg5lG&fXx9#aLc5SN8&CBP9-HLSB#KGk$&zd zNmX559CbN;`kDS^4uYWfuJ3WZ>v>DKWf6-l?_{4p?1htV)Fcq9dcAw>P)_a!;>L)z7c;oTKHRx(>mvEjc`UQOA*EIyb97 zD0A|QFAneg!gJ3*+iAipZ|v#5xmS;29bahGzCnl4?PeZ|8UFI*&1c_jZ39p2CPq4c zvA>OYNi^(eF7A>Yla!IL$ zD-dtELW9M%fxJE|ug&DOrM$M2*H`4Vo4odx*FN$(KwgK*Yo@%8lh;Y|nkBC@I_#O7{X;BMw}_bPCAFK~A*aJMSRYias;*8=ye z1@3f^Fv|V9+-)!kSKdpepF^_rSkuFCE;klyqRTzAz&*+3#*$5Sx#twPvs~`^1@3H@ zdjU9{?h-^w0_ZR@DlC*-VZiz0l0ZfHLB`}11G%ChwC+7j1+n8{D5?#?ebCFDhxHg(rS|<(BTm^XpUt>8jHR8j-(j^g3cF7o zkbZ$hdb_a*Z+DnaC5rDK=`prmgC2#ykC6YI6*J^N_Hp`z@vn~QIeGl6VlPi@(Yixi zP_rjivF1(Nv}u}27dVM$wdwIv`);+X7oKDF&yN^!UYvHsvI?WOZyznb-d=cMd;6DF zrR8Y(?|xnV`;}Aes>Fzo3a36OjJQ8lzkw#&-TR62O28;-^TwfKM`hc~dqYEAYPG#; zXn9;qghr{=D13|9ILwzA5I5>20%}@5MyW=AUtKwjclq3XK{n}0f?X3EBk#q++z3?c zNL!O-v9Wnh1Yz_YMSbijU=S#POMhw^<#=J^!speHm`W|XZ+&y|dVYb|tM+u=9^F#T zpFiJ9Rk^Ae-+%6v!Rk7u6DLsXA*Ds4hE! zV2MN>zW`HyuCxJR(o1=5sDF78rVt}9(843AsFkJ!%SzdVj5EECLq#SC(r9GuKB7i6 zRE3*5JcP&do!;%N`mja~TD`DlD+^dTq=TC+8p@*kH+|}v7oQ%vENl3{A#LBl_$ESO z{#A(pN~yLkaHP#)3{1KWAUrhHE`x?D3agri!0GoB5aUTqWxuYu%KDV%U7nschP1VI zMSt-%m^YAiw&t3mck+crX;cD~(%JYK!y!RZ*=72E@DtODJbE6Jsq$(BNf*8*cfq{X zY}LF4xqQvsnd-b5Dr>s&?Op2^ZhCvy8s~|9d*9iQF?B3@psZBaz~YOubuej4MomqB zXo&0GG*RdU7#35o8%BsFCjx$?HL8RM|d6E29znyQt|84%6 zF9hH!RT9{;D{ZI8%osE?z_*;R=Q`Q=wvC$1Si5NGuz5HYy^4NQ zc4O=jhyql%_0vZ$eZI!%{ZhYbfxBvocB?89AYySbCq`;YRf6$p!DXuw`-To+iWI@v zHA{54+>+Tj5cR;hkpej`Qt=6JVtoGlxVyMd$MjL(iy1$RNblI|Qghv=pbq-5 zX)&XFygEGNSf z{nRKY)CHi*dKz<5c7};KjR_mX=|&jR1V-0vb~02ke0b%-W|b4(@89K7-e$^FwbH{I z%H0)2pChrJht;K6&p7y}_1=o)xib9I@<HLdjOqObFK!- zACq?!!$CybL9PuFB9c(jT()=xdUBz5U(Al*zQRTUB&Ad7b>opCtIgRzIfLd44rtBlR zM8+)q1>aD@%Di4qCd+X-;D{nZM z&bONKZQi?yeMTL+a_iL2AA_{uY3Z30=8qcGqzZqIx7;Xh)wsM*yPTKPtyA`h!C6(y zVOMIv68#4Apbp^ewBQigb{dQ>5bWM>ej4*JoQoRMq2tcl|Sk*RI)pa;?Nw!5=oT*2m zBnM*@M#_@Lf+a#0ahKy%j%^P+j!JF&Zn0lc$ZcONOQ9QIjW&>m*^iD1BDRZNF?Y8K zPm~>Al>c~ExuTzxX(FXhn@d>Qg#Idgp%}meoe7E<=XZ^Z;^25oa zKYFGQ&CC*aM(|aAI<)WAt@ZqUdserdli29e%KW{{+xAKC_AdEQ+F&QnWA0@jw*pQ1 zDw0PUbN9lV4(KuZ)d@56 z|9bbpr+rq5LwxFVw&vA&jb9#=Ib`#P`ES1T_6G5T?!~k5HR@t;ipKw$QyAmaRGWlC zkQgf_XN@cLtQ2K-h%&w=U+iZ;MJs#ytV7s_+xmbGc494puo{qMay6jqT4kBBCKg#% z{3E0rn=-kuh2ii{bLf^RlU6z^*_BWcl_0Qjp~}vy7tVdgubsbeMalo^$B83806gsv$1sRbgj#ux$Q0{x%LE3?=eVhmexLwq8x-ay8{sOw11-RnK( z-Omn-ro0u0`o=I%oBel;s5EklgTqJ{(+4KE+8B%Uxflz&3A)JojD{veOnYVChqPQR}QkIc8!#Ag|q6n zQ~Es|rMzjk7Y@N7F7F!}+MstgT##0OK7LIG z_@EDX#R!iWrF^2?Ei5Iq0cLv+C;W4q@I^~APc@7T*^*~)<3xd_r$5*w= zjTkS*d}wlVH~zI`^ooIf(?V3qBM|s7EhDm#Wt7fZH_}*HX&V!`%_o}@cvta*hwb>} z_D?MCjQ5+r$IO{E^*8d;s|)gXeA7k5l;AK|`wqv5yA;({U%~o^LA!0M1?U);0Nu=^ zaap8}q%5LYB|z#2kJJN)Qf>-DVRUHP6Xhgy0BY<=bO5z}BC#VOEfAz?$ISj$CyV>F zn|&j`&H8@%k1XQguGPzSsc+}5-oYzv`Lo&jjI%)~VIT19cBae~ABg*oGnX%VI=*xD zqqDyCE{FA|iYslk5_-iII8aLY*4uLY46RiwsaI<+X<1?t6Q)=joe^j(y2hAj0Jhk3 z9`@1ufBg8V_?|^io;`b<_>%gvste9;+i@&+aNl0zsMZQB2DPh{TIEP;={4jbwG3p9 zg_D}4mf7IJa7-9T498`Y>*xZc)fVN{rMJ%sg6hh5zW-L-W>Me%z{2>!U8|Pwap$bw z&MP5L^AfAcoX?`#TrE*hWIuEH)6-4fy@J(4P8O)OGgdFq)>J1?$&(E7w6GCFj!P;w zhOBWrv3AWVZ%B&Mnh_R9?R?0)$>ZTY4k<$mmw}%wRfZ7{!7tj!;TMC!&zaOQ^&htI zFUd-bQ5gJ7{;)xR@`3-vFnKn7&DDJ;g^Fn-6c8E)h8jk4Zz8(u&iwsQm>4-*j0!u1 zA&pQLJsic};1Pvgm5ttCMFz$tN2nm*6Mm5@|K-S<&!#T8G41Dg^THxePLA86By1S+6}9UwX(DKN87mwG(eY{Azep0h8x zbD?-$UEuv>F#SLcE0EQf$5$s%0My0+PC3DtjqEA8*yyThd@j z!!KZwDwdOCd_^%QB~}z@BP`-%#K+2Ln@}*@Y>CJpBjH2!6hM?7?^__s?jH7s2*yfz zxq=Zu$5hjBS}WMnwGt)^&hp!SlCL0vl1LKKf-2AtOUH>-*)*%<=(!$UjBO*R6mi33 z*q<;R&?uZ#aCwO9q(Sjh)0+H{^NaX`vyy+j_eZ%yNq?=|;#q&-C7kR_%iFhSRSUZU zjh=jD|FsX#b~tvW-5w6qdd_1b60KUD4P@-C^{V5-{)6W|1AFxtsH#A^-K}^bBR4nd^JWz% zOgeYx{ezj~7R{Z6nZRyPmViQ{Y{M+LZHxKfXQ#GO61y0{j0_+>I3W{dsf-Xply2$% zmk)v|WJ#NAmk~@zIbfn;{YR1$pR#WN%!q(tgB=2a<3FmidC7Z9eEbPIcmnkNi%0xI zH`m3-XL)Ph$UA?6^ZD_ge?Gp|1U}lwA(WXIz1^0oF|^(`Yyb;G^^-a1*+kwLgQRC= zruUeKKP0^q-^BfTx*`!UTy#IBs;hJ zndq|O9)C_l0;?b z;KMLgks&&>db_a7_Wz=#C`x5r-V^s!rf5q_sqBMY-ifx>_n$uLb;IV5~%#i4; zIOPH&eoe*|Sy|W5V#(OKGvY*aS#<$yslnX=pH!%`g3<~*Mc9;*sBEUnjBPM0I#{?G zUMUNYHspg@0-))ibcmpe&2f~Zv7AV_yiK4h+De!x_zR=kR)v2mLC<-|@j1`Yy<9da zm$iWZQrDWE$Jm^}B`fphD216#99iy-`a323V4w7ex1AJ5AIQEj*qrBo>#9H!tqUVam>>xX^ zc!lUmGz&e=f!_}W&xkRwDUUY=LUfOP2;!aSajDw{D7D~_?B49UL>SOawg}6DAC@OJ z5vih+w&dZmbRbxS%Z>y!JF?b*f>&JMF_-xHYtN73Pv*Mq5do~>%FRmcExEZ{)X1O* z7d|Kq8a_P8d;a~xpt0Wl;%X=#8Mr)m#hZcNg(XnTo6&641DH(&<*k^|fN}v1hA!O$ z#sdqhH{APLm?Di(ASe;?g3I1qtTBKlQYjRg1`<}FaZvI~YAEKB%D-n39_5c~`PY84 z=d_@=oqEyIy%NXWx+~%SxAtj*Wj3&FsU! zG_>DdN_|6fV?P92gXZ;&QIR_8{>JK()%EVH*+EK>Uf>bolspzh0=-GQfI5mm{CSI; z!R;QlB7sja-Bdn2;p}hkxNET{Vz#|B@UZ=4>C*Q<`|{|#DfdrZwy+Xzh{g8b&U8WuufM`^@9WEC&HR9ke1DX-t-k ze6Jx(X0}J`!~EbRAZ;_r3^yx8gczZXRLl+SLgmVZPJQ*7eYQO?IpK=Z`#M?Y4!){Z zMj%Nvf8}VjlgJ!9ecIh()Y>=9zzO@dAAc(dThM2$6aB^!rDdv&{g_Zw=<{m@*Oj z#(Qq*KZfiX`00Y@va~~=SC6#wozF_!wh)IJ@36N}k|c)C)d@fx?h#FqKms2KXx;+T#=GiZa?h&sGD}wyEwW(7MnQI9L3FD~efO&`_Sk0! z671J#ZS5n0-|VBi*RFZfBxHF}?HdW>bM4L6*T6WL&#`ogF|wq|w}=CmDChIvD9-g_ zTposcKqoV$oJqYMF-92u9>ImqCD?}4jglNFpk+D;icXFXwd~n5oD>MpuRL8FYYgT;Kg8Bj;z6Di2CnqEZFAgmwWN4Z3@S)HVMK8yn}|{+Xsh=Lm;*{ z$)FaO?*S=d7H;!FPPeVYD=UYJhmP3o#rMaECt&LlH|&XS1%5bHtq#0Piz3#adEAsI zn%UWPYWi5Cni>x*Xg3B-=a?)^w>nhfR7_k`-rZ_Buy3NYpHA2h+8gaJgKGofvd>J| zQo?A8EZziQlxlR32v5w&cOKNN+lx3_m1-VA^v)2tbvcY{6L66Lc_M!~M`Zlf9@wJq z#@xOcp_|X^)x^I2klAK}`Pb8z_IfNq#61x_uTx+-aG4DmM)AA^^tFt4x^VqlsjmgS zcVCkfBMp`>B%(7EvcCHkTmaM;vc75&Nh3$t3*?O&fg?8#hK09KQUw zl=pcl{(Kq8!$-ZVyoL)p7{i>E!G?5O9qqvSdgqxww?x8Ps+pX+!%FCSo>K*n~ zq*9^?breous4jNzeyi;lNR7lPjM@~6Uy-v4nj5{0=W zICJqG&1x=@I8K-%s|LvX%t@aeht1E(W0~7Jm_vQA;z)6*Bn`suD|H9P8uF<3Zu=pn zItZf=teFOE&D`&^W_(F(4PZhxASaE{fI7(fPO13x26!ZW>?`0w-aa_KIG#)yx~!7_ zJ;v)rCfnE(MTjKx*D(ocvOsZ^Mocp@X^br7WbtMaR>r)U^HzoJi^NO8)r%(2ORG%( zZQmCnH8|n31^cn1Cr%r^vvSe4*Ty}#^Pqj?M9JzuX1=?VXdgQs%EbIAhB+@{$rz@` zlJRYApJJyL!Y7Ea>B;n(gwnja+Xp8WC!)Ra3Y6%{e-H4@v|0odtLTq_vL8-e!qcIK z&|N4Kez=eA?`wZhS@}Vd$oR0P0A9m?*w6;;_@6}`e+&=Vo{(*)c@N=^+DLjPch=wv zI(Z)Z_K*^5JYA*KMp9*)yVQIy2S!8!xmszr`E1>H(|gAp zepCFV66l@#m1tleJ8mZCF1ur8#6*nkh@BH?v)vMtAO{nGFKMxfxFJJ0eIQ8=`ed;> zvau2z42Ssj%6@nRkQ@gOli$v>8y)p|`xgS>(qTc1IXIKA1T9jG2P5#60&87t?b|Cp2bp3X<|IAo#Q1-?aXjAEh!MxBkN!#f zXUH0td65`hw*F3gjH7j#SKHmyWTfIn>q%N6aaD#fF_OT0K(43nK=p|`-vrq3VA+>S zvTqWK9kFT#t(pP%f%9t&xuTGVn&N5#kvM5v)TYHj%>iyY=D@7J#aVRk`($S^{ixjz z%A!5yq9^+z0Qu+_Ur2pX{QB;@q(5Q?&2X+{;$KO|l!)PpjQeKbGbz+2;U!QU2|pPn zT}{G%!VGZp@%F)S#c?zP`ZdPKVg9C!e|8VDS$b!cz0FJ*A|8^nAT|p8vPkQ^l<)9; zu)nn&b2!&n6v@bM0}RNyt8U!$u@Rp$%0Tc5B&A^Bwof{4pc;|A?Kw!`o${L14+nkZFMA?!@h$DOnxhX@e^x5bXc=bq}w` zy>zdiI3OlJ`raEI+I}wfKl}VUI|h!AKK|*BTZg|M%;?lvTq;`C7Xfir*=uj+eDh^o zL|mHpy|C=q&*$48M$#a6_Dy_1G(LaxYWp>44~WEZ24Ai?2(}HIxkh*U6X-!Oq3F&b z@Ifwi;~4NkiZL>R&4iign}2=bx5E5Gc5VV~x@sz> z^gWTj+kGqP{Pq$!ofeoqq>@Pa7P;V91>dUf3I`9CpWoVYk5;$VqtpwOV_ta7ELy`z9nD|1qH{i~_a^ z*p{W+GXHfppE_Rnd?G<*$;+3JPU~?yAurs$EYoqw&8~cEu{28-ErCg3cR5cv;tHbs zp*xweDrQ`o-1eT^c03nI*5Ml@>B3A7-_wPe0c42Lotc5hV)nc1o?krZ_TbEKANgIRoZkA% zbC=KTdqL;t7tVh+WG>#CBAWg);q~Z@xStlWqh z4utl5CuUd&-h5aY?9C@DBg>f~Pf9JmGRFVqy>i|J59H^28nNLN& z#XcNwFG<-m=joTuIrpZr8;9O8e9&;@b#qOPlOdM@kNkK~j$G1v=I*CZVAT&bxYa-qkCvN9P^=8dKqMS(8ENj})*eNF%c!%h zC)USD$Jg32@3BpB=InUnCRPuU+YMjXUcM`E&ug2Xc>$}XvPbfaNTiYH&MK~&R@|mV zb*K^h*h6Rw5<+K3{1^}^!oMioE%Lj?QxQW~Q6ww`FcEI3Fg>XzRP*ooh=yX-`m!cd zE(+C>gt%{k$tC3oe$+)DT)~kLWGOXwl^QTH!b^w-X6AqvG9?8{wd|_w%Su5`-9md* zK+LyC)@m*@Q@Um>UmI9eW_nUn%=Hroq)Z&%bJ3!-5@9X>>oTc^TvrWqls8#4;4#6v!5F-X#C;6iFZxh zcQ|`NL;Lu|yPz15Zy`*TW~xKmrvIcvo1#$**zYaW2cOl~)Je%=dEP`tiop_~2^vqC z)TC{@VWybm{&bVoU}OEuHf?!LNV~{wLJn#8ejp-hXw#;`%P>~RhbqvfZB+E2c~124 z7eu=kUn*Vv^6G&%Ts!cVYp%Y;}JhwD>zUan9X5W45*S>At`16EYhu(|*(FN6IRPNaU?|5Wklo{} zIKx_S#aTgW*z7xE#4`KgkeImAo_-`%oc-X24Y!*iXt6mBNecA`m7fL{4UC}@2iCO} z4$8Qq*sc}tmg0vKxljz{d-YtDBEc|MqrpQV%lFdVzmXiKCM8_H7gi|>5GDn66rIDx zZN(?{>N*$oo;rWUjEPT&mehLSqRX!A-K&$BCLW)@vC+d5Cp|X#wjq7Gce&zh(C>O+ zzA4ZczOQWDMZsH~6&i)RI%3Fh6)q;8E|nSXQ|d<9!2O8jM@hB^PweIng`}Lxyz_Rs z=2@xsiLA@Uj-R=F`kbdfd1rFL6{**Bz3|GOfyUF!kFI#^o^emDD=m2Mi=aKKb(5yo zURN~fa!|V?)_g6f9Wn|e#_T{)7^~e%%82D6gW^(E$;8E_=30C8Ix)cRWR5U*MlUs= zco1^42-H}P-I5Mn?=0hfIXYYSYIL@zNrP@4;+DD1^LHF+eyn*6eVBJ7H_vXKkAFmB zXwSTzKH>JVEDs81bMn+gYG*aaFC3>8jod$$(jD2}&pbME=)-&_bj=oS0JT!5LUVdhRH4WF87-a2)`Oy(ohM0;&q ze3WlOP9SM0#l@UdM=#IfMD?&Y=0(S!oK99|N-&HJo4mGep|$w+(%Z(*Tm8VpO9S@Z zg$t+OH?UPv=T4XR7TqqIJ$d4awNGAX56uu$ZY}HC`I^4hUUCu29fdI;efJUq)ORn< zz-i=lQ{aU-S^^2t>E&r)dS;p!M93 zzzH3t!?N6*D~bzc85PH0Ma7GU$38gpvhG7}>2cZ6>k67TYtyD#i?*S$&;PB*EdzV? zym{cI&Dx&VJhyE-%p7NaU@K0s1l%6XyVvLNQPNMIOOYc9R9TK66+U;UFRPH)(sNi5 z`Q$}CoYQ?n^apdq>BE7>(IsYvnPlT0TI6-(9#WJ*Bc&u2odsK>(@J;aNr_D^)P-Et z-Gz!#T9Aj$lZBAB6FVTe1fIly%$Qj@$eNNe3RWcg{>#;tm{PhWdyy<67}%xu-IuPGMHzjo!|CHG!4yH$SgNmmcwld$Zqj8nhYA5hr) z#<7?8zQ{D!UUT2Q{nBojFySWb%c1D{*$-4rf!*fhk@NYcXv*?gebAzhEN^Y=8zyC$ zL=rgLfp(`StVM|@9(5IZn3;n+hsnv+B)za8klu`M=SfF;JW34$5013|vFnS^c8mCt zP0NRmDUX&sKJ(yZ4IdvdW5LfT!ESIOnJyl-V?Rbei+&+aYQpJN^s(6&Ag zZeuHYNTjZ`qS2Mza;#`J&QyC`Uwm6jo-A2*gdyVNtV*TigV8S0G~o9* z=$wHY&uXiB7{*VvpiQtUAteZYitJqw(buAUrrF2s zvqkI+Ds>6fJzXNU*oo0f71_;(gsz3?!etRtM%ZvtWH_AfbIU3Z8L5iicrLUrk0YBp zxKR)q!VG;V(A-F-;m#I!t~xb0VDGj6C|gtY`isw5^B($j`4y+_Lr4r?{obx_yWRm! zRBmcRDb_aP#Dw<3 ze{nxjOapwj9RuZ(SZa)rCrXXviewqAO5=`%mnm45ot00)vLji?@XMR-8;MKiU>WF{ z;+_mdZJ!~gtuL8bDL`$yFuDb6*?G-oi-mvrWKh2$<38Mr^8V1>_kM5x{@ut)U;b## zTfO9}$vJfw+*Y<#m}&Myw_dk<-gVck4?+T_V`mm)Heql=PdOLfo7JsM_Y@~JGhUYS z##h^p{1jUuhwiC_ahwwf^oagG>P2y6o%rB|=(S`=h8GmoyHIcxo*qFz0V>~&8S-xe$%G*cKdS;Sto2f*2gi^sYp$eYKyW)@}QEeN>Q#k}ge^P=-JGmlk1 z;-Wk#fcCz@J=%|Hn$ax(+QB{YA86gI&Ad%JCIs*AedopT1-Y4hM)WDjwLhhG7-fBG z^$RV4#Z!y4bJhMgh}=sqCNQ9lvpNd(6caS@YSaKlEYu8T#08)#Q1vlk|!Bk+Z#}|pFdp2x*J<#;cz4L@3p#qt+f5PugA_Tfs=WBSAlk)L2DLW}YscTYE(x4=8dm`KgVG2J- zGLpz8qOn9zS`rh5(sL6I^w_v(&jyk5!b{?!PV<6OXY}qYtS@j_geFb1>gQZ?bH84l zE)I8g@b3|NANbcdW-|UBy1|{qgzJi{Pcac9otj9*46)pUXO;=Ky=$!^+%WWfoYN3;hb)wbaNAQD^>=|kt9R0d z3ak%bi4!swI90^lS4ky!7YBFSZMX>U{~zfj>G5aM34yv)ux6^ei&cvr+P)s?G_+Vqu=_wIG+<$Fc%i&umnee$aAB3M3f?A-&}ce$`b>LjafqfS>}d2!oL z;Vy_Z_ciC4%Yx4b!r~2Vu|+t)_+(<`jMTOiIHD+{t%JD;w^D9#Zl%<4Y^4w!-%1I% z{uu9RhRfmcQatB5;P_sI>jASVxC3J~^}ds}A{vW&Ceu4R;+<=GEaW`^H)kyN1F5l! zFxGOy-)zn>_2m25n?qwku;cWhGA0_am-(2vfT3G>->en-dISe{G9!qJe~EjfX>AstP4l;RZCcb68dq}D zZ2a?9U>n>8{3AOP~FrmY8tx zp|Q_Wer57Yw~WOM51jLnxN^srA1;_{AO1&=&GxTzJGK!$mSEA?HcMrDBa`}O=$Z@_ z4P@0(t&vyL_Ndn6=k$fdNforxplO>HGDWd6RN&Sug7B(1zrxyLzxwvKN4~>NOTT>+ zEZ=PJezVJa_A3=;FzwVF(MklGpB85UGvb;-;F?s>|FVe1^e;z-VYXufu=7b_rez_- zv^N5d>=l2uK2Y)!HVs`pOj0w*ze-$_);>6BmHpYaP4=hj%rzos(-zTWwRm~d8~0Cm zeXJO>cj7C@%vayF%PT8I?>7tI5k)Ul+S~U9SHYh)-(K2&g>64BI>m^xlVcX+Pd(Mq z<%K({(P`bx!C!AG;(p#Uq`L;<*hLD*rpLuVKAL8(>P>2&>2!HZ#T9cP?p;IMF;3m!eq(zp?MKa8*@1O)*1tv40fli^6Klm8 zz1NsN)d%-g@1ge^_?2{uKq`B`Ks9d*Hi#-p5<`u#K!HE|38`sl2ksz8<>O6G&lJ)7 zYX$DV{oVzY6Anz?`RN{ehB;xH*n9W2?XMmANJ;U_BcESXvE8OzfMk1h;1kTgF8TZu zWbqHXD}bZd1p)U=M92Ke3iKLH@UMF^86tJFWG^-@>_Vz8Y*X9|QOL?X@I|2ii|Dw( zSbJ*ZGkag`tpn}WR&4)U<*v>gQTH z@R_2>=yR_qx4hSB0Z(Q*tAW_lARb~nWXn^Ux$GuL_c&8G!H@Rrsfso-54{oXY}RVH zu+)A|^t#K+IUR4FZVy`BX0Nb<)?9dsK`ZB?9c6_2a-rOCj;HYs!a0=4Nh+7uf`(zF z133XSwoR)>EyPG>+>U;RlRJ!F@aE6<(VvfQ>-gZo#V@a&IlQE)7#FC!$sX6uH0@tL zJ!XG4cFt}4*UaD1k-EzDws!<5lPZLrN}4xqNIn*6Wj&E?_*R_dBI^+j@_$5ERGBwK z8wA!{%}zcM?229;rZLU>yLlk=o{@<7I_{2Fw~YTcIt+qXu>bh-Mc7EVo;W|FYerE9 zY$UKW&fqM*o4A2T{{-hZ_IzxRQl10O_gjbQHE5;gft536u3XsrvYx%?4ertLp4Ls< zTr>F6He^=?w+_=qBC($2Qv%;GX&;H0$ zKY#ZFT0f=emQN{g?k}<*P7DWz#dG5);)(}x!*y3{C8S3Sbelvy9dj9L60|wdpv3Ds z3}$d{3UY=5LHd{PFo1f|#CsS7q3R$$gv-DZTVzll$9TFPWcJD!XhusXpC0sHi%hSFL&-MLBl3<6&?#SVL*HXyQN> zW2~g$Zj6P{IWp*(c{p8%6d<&9z>aF_z+zf~MkLK}IV|1~+m1E64L6^JjHN~mpAD#i z0ym>z?0Fd5u?m>>aC*~xV<+t!#Z4F?mmxyNnm20!bLV>)m%ay^HTFZ`56&ub?pgMw z6RF4wW1fu&S2naAuh|-@mrVv4lFGJc*ULvbS$UkkuMcfuVX7lb$fsqtaF5A!sBi(e zxcO|?S#x>fwX6HxGIrL?Yk$0Q$U*CQv1jjy*KV_a+OXqq&)###vjz7Jx%Ik-MP3*C zmuSfR<-noqZnbwLub#1aAFLMBn1EG&7&|rr^zI}nuGiRSus}KUHb;P|;?Y1M5L1jM zMgSRaO^kG3C%!`KVf!1qsr{7@^~5LmuPU-^NyOZ-{x`=1tp(%@w$pAXhu zgn7>eueC!oBn7AU?U4*ww-5gYRcSjl&pPf)bkM3dec%8E>9L_i?xB{zzu0-=iY#dH zXY?Z=8G#GlIhQs~b}S6jm4+RxcBA)B{Y!E3ipt_Wdw;H6J@>i3*KMD@+Du!1dd`9e zLPqqpIrc$27T$hnquqMb2gHdtgAOL5 zf$N8$jaZXqf6VxB>zy;-eL3fay`Xb{&>71I`v$%aC%d$~zKrfU_t-IM6~a0rhUm=4 zCeztsr&x zq_;~%<@(v%uD)jTyr<3F<)`O8Xtq0POqe_O)S^(u2g}9Jix1HZ?CUK9bZ!92FeQ>L zEgalws_O69q*v&(Bvv2zpr@!d%|+c~gP0Yoeg42nq>g-whAJLQjvNBM^vQJUESyzH ztzalqu#n5%+PB9nO?i8%Gs)jy-#0pWnR(*q^>1EQ*mZf)g4v^AdG%!Fx;fjfxn|2F zYt7tWMcXH*o=!SuM+@7|Zq>GY{aFu)^G}4!|GrXuzwB?A^-Wl}?oM?1d?^=v>%_vF zgqt^HT=PnxLo)H>_F-wUz&; z-AfV1EZA0LQiGqI-P?B5n-A<6Q@K2O+*_~wRO|T-^VeT{&8E2<&D65fbMBA+7X0x3 z$Lw#v%PaqALG;v@`u>v&$)`Xw3>@r25=RZ zWYpqcK6Ma0-(*GWTV;Q7twCU*ps=Y zH9@rhHN5r66K3lCBbdVgNT7dW4jI_nw?*RQeXtNN%B#YdswGgmZKR$oe8vg*a=raH z0jp`cKFt<~j%TNHYJVOgB}D&B*{23Mv%<;gH^+Qwf1DsDeLVHA-$a|oy}EeCgbSwG zKi$13)ok)`hRFK-$|b+dw(aBNmc4n?l(B)gRxGSH?I~J|!S?mAjIP8=w7?EWJ^-yn zv;TUBXW_ihB54&2a3m2s+><08$&^pCd;vw;;Xa>-UL7`L93%OR4Lh@}HP;I{01eoR+hDJzdsus%U#M?%9JY z#7|4!BYo@!OnXM~ISA%VMDqg1FjO);2Cb$MWL#V0sYvUBc0_~Le1jH#`n*x3{t=6+ z73B~0G5Pv5`BsUP*&oS~zrw9@=u>(Q-%SBS#S=`8WHe@}UI#Hmz%(@YQ@sq@3Xvv_ zx%nYPeH-^kB?jPYD++5;yr{>L3%6H)zPcq&eP zwFO$*BwE~AgDOrvDRGGAKx%pd8;CelPz(V|XH=|&ebAb>(BA&F*%tGnh!JuZV3}p2+W@k6 z2t`;0s%sy!q~cGVxMFfC8seUjWce>l%IiUc!R%AH(@|~7;r(W)woK4!io`=*h%Qh#QuK6wMxCp&;$WAy779tm-M2DzJH zz2K*+TU4ePlU!MEk-DiA!Hd-6UvPqWg8`8o`NyU*xfH=LpJE{Dq-Ijl3AD>XG+Lv= z6Q~IyBTJCdgZpyx1ltjTL(@?e{?xcW3#QCRPv@2QUAo}(o(sj^qOl9NnRDCOoq7*n zx*>3Su^sC|Whgz1UwTT!ccm83R}PDgT7IXwvD2YFfzn%}H@8EhXRHXSQLJaMB6OGi zJkOm#Du+cIc_cAdHC4;w^L%I;q{~G5bgWf6+#FtaL8kmurmtfLdGHV_MqN5=C;UWU z3{UF7O61%qiV#xTJm1`f8d~_XtY`XDx89p>qJF*oPd{qj99&0wzP=LiwMh^}l4{Y34Lr5mfOHGg zF`$EfAggn`#Ae-QaaZ91&u_H*z=`nMRe=$z<@Krh8=iq2Z-GSHM01CU>>~&x6OmNI z`U3V+G9nkL-nHLa zXPQr;?lynANrw9j#%WpF++mQbjVADJmq z)aCn@L%bI;Q&>zxE_*~SiNv~c3*eUabnq1?X;Sw{xfu$~fv4`I0~#%<|_E~2z4Flr4MQ!QoO9%fe+Z&BtQ?)X*^*!cC!hYs22Shw4)zYY|h-Ww<^72V6qY}CEQPQ-pN zi-X<3+0LDzZ2l|tk|iqi4>~>#@GZB?^VB2Lb9VV7+aGepiwOj+mv*ec(qV~?%3yHZ zhK;Xs78`B=#Eo(Y5m3@EUv8b-o(W1*5HsI;>)UTTT-40j_~1sjDsph_DtWf3;Ii#DPI^k|nc;Nz&M9e08~an+$q za1D`$R{Ogm@lT-3jJ+;2GaZec9P*CcRAaK%*dsbBO!U}EmIpd?N<$Af{=nE-)$mTW zPH+svo#|ux8>>ZManTvyND7@`hIPO3r2K{vHuJ+*PIpW*dwP6X&#L+}DEcnsfAQ z6CKVOkFKrp;qw7qAZIGB7U!!~QbF#D9A~E0p-a$K`b_Aie*#@*d|wl^vr2O8<{Fc= z#@C|r8JINl;vI@==;5LP#&*_J&?TbE zh2}=7w5FMV0$m0U(~>}so#o7w$L8HCF&S>0aCfVQt_R$kIk+cdFYRR9(b)`gsYJ|j z$7gv<*d#dE+y(ppyO06eTqE8F+6rXpq&-c&$*#H3$Br<^T+Mi|y+w13#wF9C3VE;*DZm`B;nBMrz)spU&Md1#mk1F24Cux+LSuFAr~%fC2jfh5fXjz!oHZ8NK3G~wiSuwoGc}iM=w_fu zYlmlNVJ{!%S)wH@X(YxQF5c`urL|0R@U9MBBCdC#l^eDqy+&xva6v&X^Bi%u7TA7Z;@?YRnm$$0z8JG5k6`I$Lry%PfrE z#2KA+uh8e9q}0@kjWg5*L;g!7YF$i5N19N5&~wC0oi{Ij~ZzgZIN3 zc_pNCp@P;UC;CSIz!^DgtVCvlP|$kpvk4luQ$xFmYmw@};&ALhF0q63W))L4y5$c# zj7Hi#+qSS^E5e?XcR`*bBx^*nGB^VXg;t(~gGjwP;d$CA`>c-ki`ZQ zQk&q3^+YryX$fp%wS;HIGj$kN=eiC^O3Wy5KO$U~%OR%r?V1-9I7tXj?p0Q525QUv z^vI)Bg=S62LD@Vx{J=9S-Vx&4?c>G_nNfB3p7A^1w{mk_BF(yCX|u=d&Evwkm(F;0 z&W4@W-E#NPo>!Ij+quS0TF@`WrP=YDF8kz-8*Zh&a1UhF2jLsQCD4)i{C-{BprjA` z!|43ZcFu2#ApXER2j>w(LpM{ehpYR_l@mRWV!LB=%+Bz6v_D$te0O+w~{b-X{?(% zBZEP$*I64!zX(uoxltxAQP5gfYT%4K2>S2YA=Ah;E+c4B6?i|Gj(}>MC z&=34;%=Z1+N3zix@(BHD5dA2^)5w1OeS1;%plnnG4kp;6^b|i(41u*Le0qeAfi5i> zpJL;2(OXFfrH_{@c_Pnxl2p9_4M}ygmQ?DbFVA*l504zKuSZ^Kyo{MTS>Jl(WCvP~ zOp=O5CQ0?ME2;F@Ili$iJ@zUw#?uZ%WUN7Am(P@!7;ZM zkcr5d6>%<_kydGZ6?7{^;acr)g(#dSFjJ1(7n-_K zF4986)<=5}_bFswC|ux|Z-u!kYak6xw`5ON8J)|`4Sd=2ZsE@K)(`Dp?)&(GgWpA) zSYVo2pvvJ(47e{r_6CZh#C324Ctc=b_qDb_L6?^(#Y_44WMAEz^B<&<$ zx|aWPV}rW`HSeT4JCJ#Yr5*83$XI^2-rJgU)ZV@qa`Z6E(X*L{%ZxW`kVs9PkvV?I zBlqEv%b-&@QbTK#d5+YOBjkZH{Bck)jG^NHpWJG;wtf>U5Yi3 zbALjOVzv`!8+tw(L#6Xz#o#lb6$3M4K4r$w?rvx8j?O3LPhl-Rd=a28cc2@=cNA}M z+Nhj!>^W?Td&B1fM*1z*#yX20KI+qmVL9j6GC&Uweg|l2DXfh(#h$9ZR$;kTv3-CZ z$r4qLT^#G;N>t?&DGxfBL;!ttaFZUpIMy8TRcB?D6{4Yk0Q4m+Q{~tzVcpH zV;=x?%i!Pi*ehdKVHK*6?LZrK?S5iHupH1B8#t`_DjxZ5r|cqe2e052^66e@4~gkv zlD`sSa0m$p2H}o|k*=WHUSx$Xev*Ys7fHkr$4w^%LT-P{rY~)Cs z7`tOo+~~L|ae=Yh_s1O}fFb}y_R}|t;s&8}Yh*ADaU7spfAGw~SS4PF?g6fZ!3xAK zm=ajqRpR{U4mk(xW%6M|ZV)}iFRrhMB`U40@?rB{W=dS`K=%hls_(oE$6^FMgKBD6?(g_RI==vm3K5suRI0^36CxMb!NN!{52hdc6+>SF5?VCqQEYrGx~V;?JcZ>-?_oayTZ&|% zL;t#6>tD(~FK^_?b?LE8(Ic-Eb;Jc{jI7QKIWnCYG%}qTPq=4>9$V>la_mAq_Bz}k zcBX$bH|~Vayp`jV&qt2G&iDpv;door_!Uj#PjK-XNr&~|_Pxw0=*h*LGS%f2{swVM zedXf>za3ysflq;;jU6th@Civ)Ns7ZM>#_6pGN)i{=7=H85!cx9W+`(DM&X_J8$Yj( z%W`A6%PAU{M$XAT1h~ATaRH|s(ww3(Npnv2zL0fe*~4Av%yORVh1NXS(wQaffxu24 z8M!!26O!o8e&D=Enq^C8met2DGfHb3+ripDHf%3!wKGOmSld@vWBF77*6fp9&XRkO zeQ97P(+~LZ^8vBnv6g+KF)uviQHi|*&j&?+^}^$M0qY0VF<)+M2Jbr9XsVq3CzvlU z*7VjiQC{EkfC-|K;KTFf3X9trllW-Nygt|s&-=;e#vXot9%kx+hxheSo@u;&g}K;; zHzpd#_&FRYRw;8W#>WaRH&(iwqA^c&Fpo!c@`unwNJ+ev>^rd_I2+kFk>vU$B`)Gkt350C`T?HsAZ7yw3Vkhuo7qTR>_f98pl8lM*~qOR(T!Y$RvYkWRgL1 zTp6Ut&hU**F+w@^YSG7y5vs8(>L5m_ymHe2NIrQv{%YfE*C(&WMiO6@X z6W!fdh@uR|SrG7*zC66cs?9!V`HpqQQSg%^=QQQv<3z0zdEEr^Q}44jLOW_=TqjR* z&iRFGJJT}bj`3ufA?35ldl4(0nj%)S(U8tk?fz9Xh2m_2QY z^0F1xIc9%hzO;4a>Mii7jo9&isgfKq~96g;|wry)!;3MEZ3!b*GQ2mgN zj%AFl&(FraWtEAk>KYhKo0w#Y@>-GeFz8yEj2_9rw3_;1zs0&vziXEaPi<6x_mJGSzjgghX&!S^++V7L# z$OXT58nPH}WJmgGf3w=XJTk}YfIsjUj@_%H>sq5q0aq&?#o1=v%k%8Q-zY)fT#bvi zr<4u`$rN~o=ZTmE#xD^$?mYFbsqLIOJa*`J>=-%rDlw{tu@UQMnZ{$YS5A(-O2pML zcB(_e>Ub|V*157(>oI+ZeR=Z0Xau=uWQd}sjcFvSH^w_ zO$rIjafSm;ol%|jQA}Ua&7sKn*lJ{LOa(Nmd6|CSim3{k{e#3+t(_CTf9zf-ZuGDN zO((1~w$_uJ6F&A5Rua9S%v=)|1oiKa<;Nz#N{(dLg<5~4?yJs4D1o-BVdN(W>6v8)*S9g)U5CX$_nuOFcwqwzzwLgJ@mB7>mJIxUiTRnG#) zLs~LJjb!Yy4AQ?6wfKUbt}^m zbO(QRhvuwj%e?EQOm*lI>>v%z`+}f>53i&j8g#r!!|IVCSzK7A4dHB#0ylPN zs7H>ISjV0LbZ5evv7jjMmF|ja=mg-cqQIaDdMANj#3xMewZ;N;VesmvOmBx{P~&Ue z#Mpey(A|xnTx|H{BWPtgREI7R_tya3*r7^w=rZ*B(R9&cH*wZSLKEH+leLgX8gC7q z?ZunvBB6&H&$)Eb(9J*>6$>7RnbzW&{>_=`eis|o!)d1J4(`>VDdMehSBcKb$7A@R z!H0KfSUs}Rd>NK8mv~SVTXUj@t_RFjRcInsv>{`DkTEY21uiapHWTLRZ07wLeB0U4 z6s^rw8m*E5IDRiG{F*gkI2kt!6cqNt!jek8jAPU9o^;mww(T6=F*37~_N ziTl6Ds<^@Lg|05XB>I-8*{^1y*>}pmzh|6hScmJ+q?d}$3<0OL=bif{uUQ$+97hss z8KvYO!_qnkf+g#6sml#6w6hWt8Y|&IuM%`FR3E#A!)ZgJr>*vMHWM`alZxzRaxOS4 z=#2Y<-&uyd6g`SQZJiS|wg6|5JXz>O3>#}>^<4?A&$+OCXKW*&dlL^jCs%Q-A^Zql z?>=#YiZi?g=p3HC9D8M~mpglX&V`-Xhv4d-I4UWYe62;F85k8%{Fgp87~ z9z*M9PA)2TLzwESfE{3Y4N~m&4nNJY9+A)4$1)jrzk0pqpp({MJR`toeS5Ufw~F$_ zFA?+Ixv+&r@=R%}v+V%Yf8c$9W;{mPYibx}xv|w<0ll}C^}?q$udY0Qi;aie)m7Z8 zeZ3Nzcw0iRMDHrSYZSE3;9%%bBMrTd*T(TtXlQ5|`hhJ6k@+ z^1gZuy~4^yISbNTTS<&Z7j(jmIl2z%X{5hU#-7&Fv|g0y>@unAkPMKp#kXq-pm5fH zI|)lXBK0Ej@N4W8w9Xlp8W-#q2YNO9o7!%1p}`}n;~jk?gSkqMPab})cWKNKwUasf zIKRf(G{HaC)NB!sDJl(zG_Vv-FBq% zeBJcUAJ%j8ADT8FHx>W$-l{fd^}kv=zV5jG-RA>Yr%z0LV9I@g+9U~Kfdzl&bw=(o zOSeugcWOzU<#3!|zXW$)sJT;150&xGu&lk3E($B^E=@xXn*!Zc`EroW6gbM59B}rD zM|aiVP_l`kS%OPw%7W6k=<_Jm;V%E!*b!H2e$lW>UNrjYq3I#=!Ymq-`aUTm3TjPA?9#Ts2iT2<2;{%8lG6}D@2enIwW90 z29FcV=W*~Mn9a{GL|QnFKY*Wqi}xPk=T#1TTYkPIb^>wHaTxzKrq;GttJn{y4Pu2! z`|K!B6wXIDSyi?u&kj+85X}zjzX9XV;?KGIGjfLTcgz!iXHOM>?xOzAk;xFv7k___ z=E;A@Jmq*Cl@8$s2^pmR~+ab?%2E^30h?fzXmSo z6Kdgb0eH35S88O<1IlkZA9(!A9P>6t4jJ2WG{3MGL*q6Azo2Pzqm=SICklWD0qAJe z5$glb1?bJuPsGJS6#c~Kr_jgYC;t0*{+xqdT*aSn0^bYcCi`-0oYf1O{V2(sd_Lk` z%&6+c{DCQ_#H5m$k$^=P$41i9xzkK2Ko@sNQak)TI5GiX?=aef;TtK4}hc%69KG5SUoAvTSl@8D;X+uq?#*G2|4Z&L-BW#n}`?5tR&?55-h59x6m&Xw=Z%a3AZm@|J$b`{qVx?A9!W` z;zyR3iV^P&v48k_p?!MZE3d6yv}&RGaL@gVHh(&9-^A&q_m95uzT59xGrHohcfUDh z`r=84O0X}g$;JOcE+Ka)u#Y7ha)&Ttgw7@UY)Z+Cl?evJZLp4zv{;!Eai3_T){!+? zqO+>xy8<%nZO*rnn*U1ULpR@wbs8derW29;cc%na)@sAJ;a|Xwxr9H1OYoWg9^Q{(>rB>Glui<^Jvyd_VeZ`FYHLq#4g zZZG?F7B#MVdsnlaZ_yfZptm6M+%v$rU_5NRBn$fzDDr zA{6IDn>dSu>2?FZZyvrw`FkPCv4af8+56vBJ;h!|kGvasge&#Bb`5@y9rUxr2R(hi zKLej+2On8ELot2O12TVQUtNEqBAMDkfo|f9zjilUyRkz)#c6M<8p!JL*Y08tIK`oh z&a+8!@Tfj^iMi7qTgz7!;j2D&iCNzrTc1^GjyyI;U}fBRIOnuE_O!;kKBRp!=VasY znG0m>csN(%Ip5H@d(T4Fw>V~tw?S@o=Nf8fwn{cs)N=ec{`fIL&vo%vbgxKtXfw>p zG0A6WdsIL{A!3JjA22{Nfow(vT!J7rQIM{v=xMRA)w5+t37#LSN-TZ*ga&H9S>y z($F5kQeRplVdH5rh2MFXJ%TEOz>Sbun90*rucjn1pT5|yOx=G~4PL==n(~pQobE2} za?fhL7OLLWpFvJ*vB$++$w6JGDWxXm?MSKF-T1`a?HW44SvhEn!5o&+l{}9@_F2J8 zSlYb9pX&hAk3-+Uim%7>+H755oK!j&^MONS=&v#=cM9J~2)=@Efa5`l&4Kr02h}(D z#3PMY=dXZv{p~(+-5{Acvkx>KU_Hw39&h)FZ^JqYY!rGPoXXGp+E-b_`CRyjo|{*R zrLpHRPCxtVz-H?T%>0=1{E}EDp5Fs_PX#~VzhwBE?W??Xv3PzpKj)poG{H{6_s@l{ zeS~T9B7d%?V?f6?!tMFg{g7w8QvB|ox;g{1o^!@$f)6dm{wD`|kb9~ro-bF>)!)^S zw2?67L$#3KuCaPA$$6Ko3nbYIZpB*ET)o%Yt9|_2E(@MM0`dfBDI~}Lw^hwDPl9Rt z0Mn8)A^5aGCIo1WJq^A0|4hvf_E9h`^mlM3GCM$x;l@Fcp*WK@aN=ceP5rIV1;4e5 zAlH^LcKkW`I_QbdJi<)q(vH;CIy@}U@^c(HEqR0b8%W+5E~41!&NmcqR5XHQ&vm{b z`Gaz_C4USTL$QCo-%u8#`UdIp!@~{n4eSTT>N(V3c+UcwR~x)T)t2|e&Fhg+)`zz~PX(TP4%z702?cIl(#8!~?MH&~DTuX-uST;n}> zhqMF#eI4^KtiegraYPn#iq=L&UH;5}51ZgU{GHYay5EIrjWi#4c~9c-2EKt2cvWX1 z!t1Q6wne?X_5^S^5r$VDS&8x;iQshS8*JrQ=WlRU;3)G!j8|&-B+Un!lBgikJto+i zVLp)7OtG=W{_iTPysPpHb8|scQ^oGB1CMLZhY^*cZ;#_g)sdb<_ z$iz%@cyl47*PCp$f9kA2y!#H;%4h|=dfYJR<}b2`;t#n3L-k%~A35zrZ`C<$uuz+D z7-!{IIHvN9b1aj}i#96eQKhgP(7bi#%__G@Zvv(R`=oU3NzTI01{zaz%Cc7+hlleO|@ z;T?Y{cs)9D>B`$Wbdzs*S-;_YRO0KMtFceQ8`9-BVsC+7--RC5Z#aKsxW+e}=X=9< z`VAML>aT(NhMq%hyrGHn4VuG^p=b3QF1YI}1+VAO3Ug4sVdNY>)NkmBIfT?Vd~Z;l z)8#6x)oq&RoJ~XJdZscl8ZDK6O%2=cA;T`;PX1 z`VJ=-O=?0R_Z{v3^c@`)#fWp`C{n?@!o1Mjf{D-Csny_qVjfgP+Y%zcZsi!&PsU^H|D#`9jzb6BfdnJ2XLuGEZGL8kqaWtx1$`8R9M^Swdoq%?;qVZ@4^ zH(X#mA@XW`gCjMmtFzUMWtx1$1p~B9({DfriSiZ>Z_^ukgul{n=!iL_)%XVL%1PWH z*Oz3Pd_%{ZwM^4*KqoML?lHG1n;tyMUD%jM!F6YFk*XeMUSyeuw~$O5C}o<*f1=-} zIv|mi_j{Q5^c=}~fuSy+YEOpccrum%<`b4R^Bm~jxRJ;?AsL#q2Fu~5)%GSanfDCF zracoFVW2M8c*x%DsmrD3SMXjMkNq6a_NlbGw*;jI+uvSgwc|YootXC?GzyF(EBRgk z?trIV6HF18DQvW6d#-~!Yqfm3OU-%{qWAiAXDLCrw{mc&u`djhj>ltTCwwW|s5RDO zHv^xxbk>;0ui~9Ru)gOc+!hiiBz$TwUtOz#BW8Mkevz6s(O}GG|X}yxaN4rjthEy~%pAhF#Fs*##1Bvg_p7 zYmLv{YIZ$#OD9)^cL1L>G&W9}5&ERj)K*;~Zo^(*7NRO#j7)nv7|(mUzrEP1vgn+} z7`)e)aL2g0kO8PP!}ANA=kQTL2F$|wdl()vu8U)M_*n5=R=<(v4|tLd*0(~Rk=ja0 zcc>z(>xe2hI?xZfR8gLT3RTO4u|thmIzQ#C{rj+h9Ex+~l1@qNM9$2^*ui9`3Q13j z+95wcLF?RshR$kMAG^D_)E!$_?KS+9vAY}ZyJPeEkW6df8=GXk#CsDilh-!x5m4%!zq#`PT9$va-qX1JNbDX8#Q{&DFd8w8peiz zQ`qMkCQbo7bINlao|dKIL$Vw)|^5 zRFQrKlGijgsFI`i55v-0raL1Kvfr^bBgDbom5e<2YF3&(fzKtz^4L(^YaYpO?9|?# zhaKgq0yeKQyD~k_vKKq__?74ZAN1?|d?Dc9WctCAK+jEB?VuBGhYP%Dbpm#OIPkyl z^9sOIWH^^%zy$A96F&v`o=k7V{`fia%`i?^`?}zZLDqt6 zPfF%K%}Mzc32qhvU2K=cZQjrg-_zWU+BSRA3Xnn244r)}Bn*VnTb(Y z@mG~})$vbp@J}`QU$}HYc<@JF1g;H4`}hBw_*2h+&IB+L(UXG_R(pg$uo6hrPmE)l zC+0`)O{|uv&%K$-(I6X_oz!wC1WG&2>t(MfbBg`XSGH~qOo?7B63xxgYt7B2=3Qm> zdDt9~LO)`@do@j!v!m`s=EXKAJ2#ePquN zTR#%N*a_AvrR&$1M&A*e?eyp~z+UAB!Unk^QY)EWS;Jx|@277Pk&vWsDBpWi0UD`C zYB_(Pt>qGzhziOE)_uC`qbKdF4vTxmqz~<@o-*U3znSr+fs|+q^P|(pfs+%n15QKF z#7Qxzij%uIsWdo_-#d{JbJx-Z(R z-%PMahy|sAt*1W>5y!^;lKgpSEEf2kKR06d znfN>1gdQ?7`0uix+6_3%k>vqxqC~uec%GxLC_brBUC8}i;0U@8X6QicyoNkS``iGD z>Fu7>UEJ>GOlZ%K^2MuD-|0ex%!7S3jIH|ZO0Q`X;LRRQW3zP^db>G(E4DpIheKC@ z@)XAoy?{LBoCyRkAZEJRN{dC+HsBdWe(RCl4 zW2M)pC3&v+9le}ynC?5YCk9=vRc=kJ7T~_)cSvu{X7h9R9n9M_4|K=J{XhH;n5+XXXgA3(i+i`T7ARunvhOf;9xahv{T>EHXV7Rl)tM0T+)u`K{h+l zsjXe!-a20#7=0X>4!d`(M&ht(3#sryA6Ww?2&T^PE!fm{ptm6BaQAOVCq>`x6H9%T+hj4fZK-mpaVp8{#>FhN{nw33R2+8^c34o9XmLu-j+^rMJ=EX;OwuWnj+&SK*J_(r0XuUOdi5r44QQl=3nR)yRra%r>nkp1{41O;C%@=}W zeq{Jg4#ym0__MKx4l;ay=lM1K{CbRkFT;0n#+SRkAI5)%;Ria;ZzXuUEj&L(!9$P{ z1rP^eF(!)I0Z;0H3X~!}iCpfQMkN-(kO^g1R$5w>R5I@q8-apA4cx zR(#<+-^=jRL6LdE#(4g@^ZXtA2;l#UYPO*a|1k~}8ox2aH^4ak`1wHxekebOCk@gJ zx4nvgI`D<|VTC7ZAwz^GKFv%(b7Wehg8isD!r~F#KWu%oU9M`2+r( zh

S&jh1n&JeeMOkU4{=4@cYr6k z(ax=L|s)4kSST31>;ptckO_^V^T9xBEDa;HjTYh`K}oyXu_h<1|<>J?*}M z_pN2n9G6MC$WAI?$U?FnI``L8W)^o)BV!P5DDM^Dp<4A|-bV83(N%hr%JcH;Mvmvf zabV=(>{)?-Z6BOhoQIA_ZSX+;dyKbxbtFaJ@`~Go>%13f#KV-=OQ~q$bHK<788q}q z!3^#&4)djubc-6QXmCR$?(J^ht_2LE4uQDY_m6s@>c_n*5h&F(Lr&e$^R`oV+yy%Jm$1XX53TRMp- zTrza)av|@CQ{}l#7(OW#Vfbhu`!gXB&m>3y305S)ZQ{N3ES_8=bjPfxUdXZH$+=XJ znuGretdmtM`KG~}D_7Zz#W1=F5j{YnX5e&fuoLxoru79!-6YqGjZC#(KD2lj(0T>< z7E^c>)5x#ry~UT%Y3P!fAgSc!zFxHNxAe-i zd6O4EEtZ$goR=Xcu74xL9xPTh7L7l@Wz8S64(@z#+}ke>e{uilY2e1{a?iA{3XY(v zaT8V~dvssmY-72Hr~+H=p<-k>>KbgdhjhNA+}(gSc0AY~E6v>(a1wqdzTxNbfcu$rJo9co2}{f}e$KawlH4&%uLYUbl(0V=f z@6dW%%1)O&4%M#1$BYxNyI95W*qv^zGoF`VztTAToaMNLA1+4lbEX{2JDeCajdfwl`a34YDYx57q$G&3m&&*5FJa`QdcW z!F&sx@f-8=+4c=A_4xeY@xM0W08bJ0!K`u2V|b3BgKv#sk})5;*R`m2z$g4K31=L7 zO2NXX&$sCRKWS8oj$*6X&3xbdK9Cvc8W>}?c*23e^;wbt+BN>B-BZ`C1Fy+w%S(hvucm84bN}l zwTV?pSxM89wk91ZMWmB}6r~Cxy(ghd5s}`J zB1pHOVn750q$*87K=dsj5}JUtP(lfWl0blvKmy6V7jlz((@-hd?{oGh5ES3<_viii z{qf7@wX-`rJ3Djc%$YN1_UzSO9RK36Qa+^`mzq=ReCZOUyOds7`iIiVWonffQs!{k zSId4=_V;on%N;7`EdO-{y@Fqb+7%oX)ru`EeqQmie>eXL{=fO#0;~aT0;U8U2{;>& z9AH!`UFqXW36(2UUR~Ms(%>roRTfvdQ}wN?d#cr}How}B)$UhsUH!A_7hkUV@{U(J zzw+rTNi{mx*juwy&9OC4*37Qex7OTRKiB%LmaVq6cFWpRYyVa|wN8yX?dy!F^In|| zb)xE|)h$r>jk?n_ll){nqvOzE=CSA+KF-(5At64eq`E z?(3^w&uloX;hsi*jm9=Q(b&6j$Hu#x6m9Zh6L-_VrU#n6*lcO@0?ns4k8a`DVswjR zEnjW1SoVK3^`Ubum7#Gwt=;NTM;Nak+!4KN? zXt%oE?e;C(zt?_shpHX=cUaruMo0gSA9lRascNTHo&7t1(mA`!2VK(M81P1TSEcLl zuHoHkciY+hjqa{0_*Mwg8-mdfZ)VFW@&ashY zM}~}CFmmOnVx#7bT0Uyys9#3?Hu|N}UyM!|?HZ$v@f}lk%;+(Z@4of!sj;t*T|M^E zxOd0Jyf^s0JL6l8UpC%0q0xlx6Anx`HR1lh*>WaYC;Ck+GqK*pUK6KHTsv{!#1j)Q zPV%1AZqnvSk&~`Zx;weh58-`pf$*-|zH6jSm)mSop)AKl1x% z{zu8vx__*F%-hPJ4E*HSCvl(L|J3i(S3X_y>G4k=O)oXQ%k+fLDt)&7Gj~XlkeVSQ zLMDZ*2{{sScE)Qn=FBWH^YfYaKR^EY@h{&0;`Z!%vj@)pX^wTyr*nRp`@-Ch=7xRQ z;>)F9o|so`UZ;5<&pR={-2Be-=gr@HF&%*nQ zdM#SGSY14F@yW&RB~_O!ToSgl$kJ9zLzdc?wOqF0Yw`7@uN~hE{AR~D>B~DV-?*aW ziX|%|SC(J-&dQ%x+E=~2YV@jIs|&9#y?W&8tKYu$?Y3{-Yu;aTZEfSVE7sLq*L>Z| zb;;k=|8DAcw)Gv?hi(YiuxDe_jZxnReZS}XKQ_I!Y5S(QANu^T?}rONHu!PczpDJ} z%YUW*)cmK$<*y7`UG-)||frQ??2TXt=&zIDRZeLt7_dG^n{wyE2OZhL>*ylrc? zg>E~x?fSOVUpoA9V7vGBQQI$Ve-zp+bYrM(NAn%ScU;+#vUB{-sGT{xe0TZps=q66 z*IT46}wODsj}z8J=cHj`Rls9`rdc;M(k_5 zuhTx;{`&jB+wb~q)Ne5dsydYke0t>TBU_Gy9l3ra z<48_efv|wEMqzJ+jSTxVY+2Zru;XFZ!`w%`j+Q@K|7hol_b0KH&J2f~o9m!JIU(hfZBSm2&F$)7t4$r@Nf)b9%(-Pfjm8{oU!Ur;nY!b~^Qpex}TsSI@LRGvv&) zGfU3=bY}ONurpWBq(9cjt z2A}PJcJkSIXTLvt@a&bdY3Dkh+jTDDT;jPuB8x;;i)<13R^-^o&m&hyhDM%>ycwB& zzR>wP=ifd*=KPHFE6@LO{>1tC^Y<^P7m8e{a-rFU9v8-32)VHQ!nO-1FT`KSx>)dH zrHhR(_P99uV#vki7q?$Lc`@!{=B2kUy?1HWrEf3oymb1~%}ZHP1*6`NS`f7{YJb$l zsJN)i=mOCJ(T$?pM)!%H5dBH?(&%;3yP}Uo$3)+b{`0cmkE`OU_f@~ErLOv4t$wxc)ecvOUj69m;;a9I3=!BT%EXvaV_II#SM)c6E`DnW!$E?(701^H{-J73&od+FBjh+{*Cy) z@$bY>ik}`oFMfIay7(XCe~Ax|kBd)=e|WRl%^EiYZ+5=f^X9uZXWsnw=Jz)b-n?=% z?UvWAinm_B)%n(lThni?xD|S9->tA)*KcJc6ild`&?KQ-!svt<39A!!C7er0N>man zBvwtVm)Iilt;Dg3UnH(g{5A1>qCL^LUFvq-+nsKYxIO*$irb;L&)mL!`{A7ucWT{f ze`na8@pnGHv-Hm9J7ITX?zob?lKhhzC3R04oisCPb<&=s3rV)5$H_j)FC+&f*Gdjd z?w33zc|r0I$%m3-lGAKnwu-hUww|_kZ8L4FY@xPOwwtzWdqMk)_R97;_NMk8_R;nb z`wIIn_HcWgJ=0OZ;qR#FXygcVbaRY!Om}?a*y;##oO4`rBstuUM|ZvNmbmMGxBA_= zcOCa?+nt0C~aWcn6&rPW~MDnTb;HwZExD~vtG8>KYl-V8*KSvs>zwPFE6L?{d%G*T8@OA$ySV$fN4O`tKXrfU{>Ht* zz0JMf9qzv5j&~=!Q{CC_$LW^z!s#!hmrt*fUMsypdh_(4^e*Y|q)$&@p1v)8Px_(s znDm?J*%<{hif5F~sGLzFqfJJ~j2;<%Glpi2$(WQeEn{Y8;mp37`!kPbMrI~uKFCtD zie#0^s+v_Vt3_7FtX^3|v);>^mNh$TY1X=|Em^;!9ZC&Rtz2yL&7ax~@@;y53S-_d&5V|Fi{SxaWv09(sV6?Rs^Qn5(CX5A^BcJxiPz zt^X-TC?mv3^`Q7dJuRwglSDcFOMW?JsVJ{D5LMK&VzJg)6eIm)umubN13?3@8q5M+ zzyvT{af{i?4)L;HRJ7NJiWT%H7ezU*(xN%{ zG}g}xUp+$fQY(t3T6mNlw>N8reFMO30u?!o0U&=Sa+ClWwmWgHB zQSpxcp7_k#k@ziQq54?N)Puzk)g{`17V1p#u|8L<*6xb7YDwOpeki68KTZ1{go$@( z&$p}%L^o?=F`lvndQCGj^gz)^(hqy_kNmdFeCtv1h8`-W>q|sC^6ICr5#Q*i#30LR zQC@irP^<`1XGGA;Y%@VH-Vw6{)7@|B9>n(Gj*?uF*GF^<&mr|d_&?8gK zRW)O`-cStHPKeFgSK=#et5~Rw5i=xB)otP<^%YT1J1IU^ABll#Uw$)8Jo=XqkH-_T z%Jg_3VKu@=YGHVwpqR+4co}8cVc8@qLc@`k7NQ?91b91vYBUy8n#9%7>Y zg&58n&3auGwJGmly_*=OSD~#byS`9NQ&))N(6x%@q|7JaO&Q;XW0|(5=&lbCi!9%Z zx1qrx>k2VV-ylBH+ZeId_M)T35U*HM& z{~^$Et=12^Ulz5k{^AAQPxRK;i8Xo;uwK-(xWroQ9&2Qrc1={bM4I(oqxTUbtsjat z)^*h9cTvImI_Y|YE@HTK4&iW7M;~Q4I3H;}BdS@G$m2`nvCPNIoBL{r8G0`x&Z`Dx z`9rMIqeON1&DX1-IAbj+4q7jXWtK#-+(#F`dR>AyDvNH~4bfS)hd!USFCa?lny77_ z!%JQYdA=m3=jCmwC#LG{DC=V}h4vi@Cep_5fK5U?!1Dc|rqxI6pii%n<-pqg6S8N4 zK1^)X7E`AiT(iJ`8%0^`5b*--vs2$E*6>#9_sE*n)&(Na@;U9n+KzsmI$aPy(GE+z zUxnUTVu>XU-iQ|CpmSrq+g|pnDPFVmho26L<@$1XYCPc>@j2m_);gl4e#p3K=G|AU zw&XxN7qs6=8Q>S%Z?o>?8C{V}EyP;c5A>hJC@`9}o+j{nQ%h~0(+d8&FW$C(E&4$3 z;nrT_bxRHLD&Jn}q4QhIUPncsb*gCTg&eeQ5Q|CszFuC8uxt~RE&d`H-dN7>EY;C7 zL`}_KjMqyD;r%jp)!b0s&u~xxdETG6&p58j@kStp{UwjYS6@SJ14}D-v?cQU2T@pA zB?g1K$_nukVORCC7^U72&D5jfCFEGJzDT^S?iBOrzh7JW^851BMGvnnB2axtj0Js= zF-?&*uWN(pAILH5TcVnWI2a6aV7DD6gB8dL*lO8Twl76UK$OzSE(pTkJ zQdG3i>xeIOZ&5?-EcU32#CxiSEYC!SzsGaui)Pl#9_VR=-wNIjfPVEwKko*jKjoKl zbQvCXg*JtTh`@{XyaXpytXp#%CQPw0sTNP&;)b^ z9kk_Q5#!=I%|_ZLqJn;047YSfcD_X4E-sc@niKkprFsb0sV}m89`e07AA;{k!m5ZPAfueXcA)Zf>Fs`^8MrjZ%Li%pCI)tS)Y7R9vCOlGnNxx00-bFkA5X}to%T?lKNF1 z&}T|Ym^viS`&%gM#q*?Y`P>kF(^8r?l698l`46GgNo70yV+f6;&IrBsQjfKS=LmNa zItinoXDKD!cu2Szono&Ri-$G3t<;myRX0l=lYY8ENk`8?*Cl*ghUy%~LK#Lf&dFy$ zpTCCs7~`O97fG+bhK!{@(Z3ethHvJE!%UqSz4bqaCJ&+8<`2V#k8F3@7XNMNg-$N@ zV#y=2-ArCVH<$Xc)Y0>YQcsn7saY4PlZF_FO-LC_-~UG-AEX?~55-{|M&2$YJOy@{ za#!}7ze39J{E#v}|Ffj*$q(dF{^#T!OPT%*WZ!fc-+(P3g0fh_T#a$S91nJjne=IM ze2}48J~=i>^BRHmo2-EhVW{0%h!g@ zQqL%1d56Bg!uZy_j&5T1586rc9z48P^6X#dW0FT?yK=2Dd0jEvSnA?Z|3+sr^=@=) zb50@qww${#U!v__=2^*7?>759p{aMvK8emRc~m~r%u~(>85`vDrEdMCd@>#BDc_D~ z%EkP^oGVH>K>1|ZO!*}Hyws^>IdbQKrc9wMQXXhh|E7P?55JOf$(#qGf6Fv^y!_WN zkGIYFvz%8~V17;imU@;flbk>1$*H`unzDy-m~$8A8y+3nc<9|n^z-RJpO$l5^#43L zX3~^-jiiSca}7B^Fxx}UrSr+bygrmC*G&DC`IwxO<=yurZ)hfIA!%oZd9ox=ug?pa zr^va4q`N75{~9vCG3Ox6Ys|R^bLzbMK0h=fo*(}ldH$FB<=vN8zkd#S3!u62+do~G zv}64FmhgMp`isBH&wS_Y^u@-BeCN-~+r0Vje+vI9Hy0yEnJ?$foBt`yg%cEEp?AEKX=Ti zq5qf@Bke!3|C#n5$uH6lB*WbCRQ7W@Hs*!7W8>^6{VDI+QZFgw$&2U6F>mIRu`PG( zlFvndE+=J9UYOU1^Tr^{$((wo+^=K|vs5%f)R&A|dN{Idq)}FXWTctb(A}iGH)W_P z-_hsM?WLaoL~cv{U&`d%xq>q#GB+>_6y zUwk1al3rtTc;UjQ!-KLp9ewJ0rj~LZ<@sYZThnJNi-m=i?bR06#+8bmP`f1Fk>*DU z&tC$rRo#StKhFGROuMdK+lf}JqVsjY93kHETCH+H`A%Nbn^;0Q@t3|#@|hMtp8`6W zsj6nRcv)3V(=D9nyvLgRr>Y$2-d6rB7OPdYSosEc-kCRXs;=s~W~OFOa@AX&Xl$&f zbBV0U!)j5j^4#J@Vol>B&(w7~v2;xhKM#$9sFpf8gy^ zz{jGSk1)?T@;uv1#>q$dm`7eb%*X1(O(sobk>nvy2Hqqgt&B4deZ0(@C=Xwv^Rf77 zl2WRoKzl2{$t_y|qF6NDs`}6hw1byd0WU9EI_}YZWOd9Z@pRcFB-Nl4CAWGBA8zq9 zfq8|Utd#l3y^z`}OAYa5>21Cq} za+7W2xvH6jmicp!>O~tp`IA+XT=V3Q`zRA_tI8+IJIz0NT_(*tmlyN>nYlbU*K?ak zmdD)kC4LfTW+(4cd7h+|>l&y+>VQe}e@s<;(HEvq(E`=|%iQ|cKt zUd_}LdXJCht5w$KX$!Qa+6FCD+pnGBU8E>2Mz5mR(S!7^`cQqm{*gXg|4!em@8BB& z=k=?4qMprnq5Ukyc@eHi5KK=#$i~7IdU%|hUe>MNQ{`LKv_&4(p z^q=DYng1sLE&f0I@8JEQLjjh6;sGTCDg{&xs2wn{l3K}H$+uFmN@Xfls8pj;`$~f= zji?flrrtmPP|q<8`Fn-(PuAL>T5q7%zlbn#N?a6nz6fY2eo8T=s#0BfRq3e=Rz~I4 zdcCrpT0c-r<<|NnwZ2BJQI>LRJ&Rf|)Rt-AYdf^xvl^otB;}fMOgU#9Wym}N@Cr~^ zHB_(~DLcUy@Uv0U@G>kpf8?YnX_8nuBXRSeJe`adhI4JDU^x+}V)WEioXGcO4TlKx%?5Avs}P!jgnV3A1i}o$ygY zms?wI{(NiQt+lt7-&%HS&aHQE^}N;nMr!fB1%9_4P>(C8On#Szd-|8eAvIF)yHo1r zgX{Z?9Xe+M8 znR<-$;D7m3KT|{08R|^+b9ENh&5zYZ>SA??x>Q}Jel4(Ds^6%~)fMVWb(NSUzED@I z->PfWwdy+cJ26|#QMagD)t}XE>M!bcF;{%4hN?T%o$4+X%Y!(^Qx07K_AUHC@e6Gu13LTfMLTE|!R;>L2Q#>I3zm`bd4u zH^IKv3TcHkKdq=%Oe?N_s!mtGP}gfUw3=Eit+rM`E2tIGeCe6nwb>$6o1@Le&b@S zv>&t|ML1U4ll1k|yk{OE&Wdx|zeJ=suO(^QM3nZ6h}O0vwRUJb#bt3t#Av&cRLUU{B`Um=lT85U1^qr%BrgT=i=%4D-wJa@LyU&+wx+>iizF>^B z@2R|njGm_SQr=d2>k3i~l~t+$x>f0;^wmGsKhewT<&=I(e`SC&5UKX1GDt74&r`-L z6OeH<^Zt)BI>{E;{LT8odHT!^bZczss#7wUAd3{JpXwehR^d4_8&E5c&J{*47o63UY|Ptq2kTn zquKZD9T3{8PuVAjBl`4dQY%!K*>DG`=k+0jF}WEKno)Hg*9x`N?BpM+RqfWhd+*SY zz_OvO0{fH=2=EUI4eQoBG%T=eK%YLfLak5ANLfD}UCL8hubQFOS89cNdvbWQcWA4! zp`y>cc{0B@dshw!4VgEu>^y3acOLfa`95Boe;bYpTJt7J~cbN**l0L22jKTHAAZh)e0?GGxTMGLN#|(Q|9{5d$afcRzerU_IZoB zJ$vsL)iuY2K4n8IQ&|7G`~09=Zk()T;hLeX=I-+seS7bCSp=5d&#MWJK;jG42*rnZ zr{bs9P<9qo*zYacq;8e6e!`n2V5NQ2+5sgv+^hC(->ABIr1fa~T9uMAP9NB%S&h>2 z$ntv4N~L|}k#*vL;Cf}{k=K$B`ZuX8kG!Xk?AEx9Jo4!o^mTRnT#u3!mp5zRUQ>-Rn`22+lh2TSLP{ZH`(KGnTN≤FHXCuU9Qce=@vmZ{%9^5??OaVQJ8!4u8-w;yt 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 + + + 1dp + 2dp + 3dp + 4dp + 6dp + 8dp + 12dp + 16dp + 24dp + + + + 12dp + + + + 24dp + 48dp + 200dp + + + + 8dp + 12dp + 16dp + 24dp + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 05eaab6228..5387734770 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,8 +2,15 @@ include(":app") include(":domain") include(":network") include(":common") + +// region Core modules include(":core:datasource") include(":core:utils") +include(":core:ui") +// endregion Core modules + +// region Feature modules include(":features:referral:data") include(":features:referral:domain") -include(":features:referral:presentation") \ No newline at end of file +include(":features:referral:presentation") +// endregion Feature modules \ No newline at end of file From 098f73eb380ecb3b1f4f417f83a3e86e98c0ad4f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 16 Nov 2022 09:51:52 +0300 Subject: [PATCH 054/372] Updated on 2026-08-14 --- .../redux/middlewares/WalletMiddleware.kt | 36 ++++++++++++------- app/src/main/res/values-ru/strings_final.xml | 1 + app/src/main/res/values/strings_final.xml | 1 + 3 files changed, 26 insertions(+), 12 deletions(-) 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 30a72f0fd4..ea47afc58a 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 @@ -1,5 +1,6 @@ package com.tangem.tap.features.wallet.redux.middlewares +import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType @@ -17,6 +18,7 @@ import com.tangem.tap.common.extensions.dispatchDebugErrorNotification 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.dispatchToastNotification import com.tangem.tap.common.extensions.onCardScanned import com.tangem.tap.common.extensions.shareText import com.tangem.tap.common.extensions.stripZeroPlainString @@ -45,6 +47,7 @@ import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager +import com.tangem.wallet.R import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch @@ -118,10 +121,9 @@ class WalletMiddleware { true, ), ) - store.dispatch(WalletAction.LoadWallet.Success( - action.wallet, - action.blockchain - )) + store.dispatch( + action = WalletAction.LoadWallet.Success(action.wallet, action.blockchain) + ) } } store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline) @@ -257,9 +259,17 @@ class WalletMiddleware { return } val newAction = prepareSendAction(action.amount, store.state.walletState) - store.dispatch(newAction) - if (newAction is PrepareSendScreen) { - store.dispatch(NavigationAction.NavigateTo(AppScreen.Send)) + if (newAction is PrepareSendScreen && newAction.walletManager == null) { + store.dispatch(NavigationAction.PopBackTo(screen = AppScreen.Home)) + FirebaseCrashlytics.getInstance().recordException( + IllegalStateException("PrepareSendScreen: walletManager is null") + ) + store.dispatchToastNotification(R.string.internal_error_wallet_manager_not_found) + } else { + store.dispatch(newAction) + if (newAction is PrepareSendScreen) { + store.dispatch(NavigationAction.NavigateTo(AppScreen.Send)) + } } } } @@ -366,11 +376,13 @@ class WalletMiddleware { val currency = walletManager.wallet.blockchain.currency if (show) { - dispatchOnMain(WalletAction.SetWalletRent( - wallet = walletManager.wallet, - minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"), - rentExempt = ("${rentExempt.stripZeroPlainString()} $currency") - )) + dispatchOnMain( + WalletAction.SetWalletRent( + wallet = walletManager.wallet, + minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"), + rentExempt = ("${rentExempt.stripZeroPlainString()} $currency") + ) + ) } else { dispatchOnMain(WalletAction.RemoveWalletRent(walletManager.wallet)) } diff --git a/app/src/main/res/values-ru/strings_final.xml b/app/src/main/res/values-ru/strings_final.xml index fe86cb0019..ad4f23ca35 100644 --- a/app/src/main/res/values-ru/strings_final.xml +++ b/app/src/main/res/values-ru/strings_final.xml @@ -152,4 +152,5 @@ Вы хотите выйти из процесса активации? В этом случае вам будет необходимо начать процесс заново. Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком. + Внутренняя ошибка: не удается найти менеджер кошельков diff --git a/app/src/main/res/values/strings_final.xml b/app/src/main/res/values/strings_final.xml index f5e11c6885..70e85346c6 100644 --- a/app/src/main/res/values/strings_final.xml +++ b/app/src/main/res/values/strings_final.xml @@ -154,4 +154,5 @@ Do you want to exit the activation process? In this case, you will need to start from the beginning. You have used a card from another wallet. Tap the card associated with this wallet + Internal error: wallet manager not found From b925625342f8cba4d74c953e78222fbbf4e8082c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 16 Nov 2022 16:28:08 +0300 Subject: [PATCH 055/372] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../tangem/tap/proxy/DerivationManagerImpl.kt | 14 +++++ .../tangem/tap/proxy/UserWalletManagerImpl.kt | 27 ++++++++++ .../api/referral/models/ReferralResponse.kt | 2 +- features/referral/domain/build.gradle.kts | 4 ++ .../referral/domain/ReferralInteractor.kt | 10 ++++ .../referral/domain/ReferralInteractorImpl.kt | 54 +++++++++++++++++++ .../domain/converter/TokensConverter.kt | 19 +++++++ .../referral/domain/models/ReferralData.kt | 2 +- libs/crypto/.gitignore | 1 + libs/crypto/build.gradle.kts | 29 ++++++++++ libs/crypto/src/main/AndroidManifest.xml | 2 + .../tangem/lib/crypto/DerivationManager.kt | 8 +++ .../tangem/lib/crypto/UserWalletManager.kt | 40 ++++++++++++++ .../com/tangem/lib/crypto/models/Token.kt | 10 ++++ settings.gradle.kts | 3 +- 16 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt create mode 100644 app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt create mode 100644 features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt create mode 100644 features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt create mode 100644 features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt create mode 100644 libs/crypto/.gitignore create mode 100644 libs/crypto/build.gradle.kts create mode 100644 libs/crypto/src/main/AndroidManifest.xml create mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt create mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt create mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/models/Token.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fe20c4709a..37328a93f3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -109,6 +109,7 @@ dependencies { implementation(project(":domain")) implementation(project(":network")) implementation(project(":common")) + implementation(project(":libs:crypto")) /** AndroidX libraries */ implementation(AndroidX.coreKtx) diff --git a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt new file mode 100644 index 0000000000..a323e29196 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.proxy + +import com.tangem.crypto.DerivationManager + +class DerivationManagerImpl : DerivationManager { + + override fun deriveMissingBlockchains(networkId: String?) { + TODO("Not yet implemented") + } + + override fun hasDerivation(): Boolean { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt new file mode 100644 index 0000000000..45843af752 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.proxy + +import com.tangem.crypto.UserWalletManager +import com.tangem.crypto.models.Token + +class UserWalletManagerImpl : UserWalletManager { + + override fun getUserTokens(): List { + TODO("Not yet implemented") + } + + override fun getWalletId(): String { + TODO("Not yet implemented") + } + + override fun isTokenAdded(token: Token): Boolean { + TODO("Not yet implemented") + } + + override fun addToken(token: Token) { + TODO("Not yet implemented") + } + + override fun getWalletAddress(token: Token): String { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt index ef8823e877..061aa55155 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt @@ -26,7 +26,7 @@ data class Token( @Json(name = "symbol") val symbol: String, @Json(name = "networkId") val networkId: String?, @Json(name = "contractAddress") val contractAddress: String?, - @Json(name = "decimalCount") val decimalCount: Int, + @Json(name = "decimalCount") val decimalCount: Int?, ) data class Referral( diff --git a/features/referral/domain/build.gradle.kts b/features/referral/domain/build.gradle.kts index 561cbe2b15..bcee4c51cc 100644 --- a/features/referral/domain/build.gradle.kts +++ b/features/referral/domain/build.gradle.kts @@ -26,6 +26,10 @@ android { dependencies { + /** Libs */ + implementation(project(":core:utils")) + implementation(project(":libs:crypto")) + /** Time */ implementation(Library.jodatime) diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt new file mode 100644 index 0000000000..a59b0e3b56 --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.referral.domain + +import com.tangem.feature.referral.domain.models.ReferralData + +interface ReferralInteractor { + + suspend fun getReferralStatus(): ReferralData + + suspend fun startReferral(): ReferralData +} \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt new file mode 100644 index 0000000000..3e2dd73ba7 --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -0,0 +1,54 @@ +package com.tangem.feature.referral.domain + +import com.tangem.crypto.DerivationManager +import com.tangem.crypto.UserWalletManager +import com.tangem.crypto.models.Token +import com.tangem.feature.referral.domain.converter.TokensConverter +import com.tangem.feature.referral.domain.models.ReferralData +import com.tangem.feature.referral.domain.models.TokenData + +internal class ReferralInteractorImpl( + private val repository: ReferralRepository, + private val derivationManager: DerivationManager, + private val userWalletManager: UserWalletManager, + private val tokensConverter: TokensConverter, +) : ReferralInteractor { + + private val tokensForReferral = mutableListOf() + + override suspend fun getReferralStatus(): ReferralData { + val refStatus = repository.getReferralStatus(userWalletManager.getWalletId()) + saveRefTokens(refStatus.tokens) + return refStatus + } + + override suspend fun startReferral(): ReferralData { + if (tokensForReferral.isNotEmpty()) { + val token = tokensConverter.convert(tokensForReferral.first()) + deriveOrAddTokens(token) + val publicAddress = userWalletManager.getWalletAddress(token) + return repository.startReferral( + walletId = userWalletManager.getWalletId(), + networkId = token.networkId ?: "", + tokenId = token.id, + address = publicAddress, + ) + } else { + throw IllegalStateException("tokens for ref is empty") + } + } + + private fun deriveOrAddTokens(token: Token) { + if (!derivationManager.hasDerivation()) { + derivationManager.deriveMissingBlockchains(token.networkId) + } + if (!userWalletManager.isTokenAdded(token)) { + userWalletManager.addToken(token) + } + } + + private fun saveRefTokens(tokens: List) { + tokensForReferral.clear() + tokensForReferral.addAll(tokens) + } +} \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt new file mode 100644 index 0000000000..0a8c565e68 --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt @@ -0,0 +1,19 @@ +package com.tangem.feature.referral.domain.converter + +import com.tangem.crypto.models.Token +import com.tangem.feature.referral.domain.models.TokenData +import com.tangem.utils.converter.Converter + +class TokensConverter : Converter { + + override fun convert(value: TokenData): Token { + return Token( + id = value.id, + name = value.name, + symbol = value.symbol, + networkId = value.networkId, + contractAddress = value.contractAddress, + decimalCount = value.decimalCount, + ) + } +} \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt index ba5f803e59..79216aec7d 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt @@ -36,7 +36,7 @@ data class TokenData( val symbol: String, val networkId: String?, val contractAddress: String?, - val decimalCount: Int, + val decimalCount: Int?, ) data class ReferralInfo( diff --git a/libs/crypto/.gitignore b/libs/crypto/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/libs/crypto/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libs/crypto/build.gradle.kts b/libs/crypto/build.gradle.kts new file mode 100644 index 0000000000..b6d1313ae0 --- /dev/null +++ b/libs/crypto/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + id("com.android.library") + kotlin("android") +} + +android { + + defaultConfig { + compileSdk = AppConfig.compileSdkVersion + minSdk = AppConfig.minSdkVersion + targetSdk = AppConfig.targetSdkVersion + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + isCoreLibraryDesugaringEnabled = false + } +} + +dependencies { + + /** Coroutines */ + implementation(Library.coroutine) +} \ No newline at end of file diff --git a/libs/crypto/src/main/AndroidManifest.xml b/libs/crypto/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..1fd7238d41 --- /dev/null +++ b/libs/crypto/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt new file mode 100644 index 0000000000..8e77e85480 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt @@ -0,0 +1,8 @@ +package com.tangem.crypto + +interface DerivationManager { + + fun deriveMissingBlockchains(networkId: String?) + + fun hasDerivation(): Boolean +} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt new file mode 100644 index 0000000000..8bb117cd15 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt @@ -0,0 +1,40 @@ +package com.tangem.crypto + +import com.tangem.crypto.models.Token + +/** + * Provider for user tokens data + */ +interface UserWalletManager { + + /** + * Returns all user tokens (merged from local and backend) + */ + fun getUserTokens(): List + + /** + * Returns user walletId + */ + fun getWalletId(): String + + /** + * Checks that token added to user wallet + * + * @param token to receive referral payments + */ + fun isTokenAdded(token: Token): Boolean + + /** + * Adds token to wallet if its not + * + * @param token to add to wallet + */ + fun addToken(token: Token) + + /** + * Returns wallet public address for token + * + * @param token for which find address + */ + fun getWalletAddress(token: Token): String +} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/Token.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/Token.kt new file mode 100644 index 0000000000..86c314a573 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/Token.kt @@ -0,0 +1,10 @@ +package com.tangem.crypto.models + +data class Token( + val id: String, + val name: String, + val symbol: String, + val networkId: String?, + val contractAddress: String?, + val decimalCount: Int?, +) \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 05eaab6228..d5d7b47b7d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,4 +6,5 @@ include(":core:datasource") include(":core:utils") include(":features:referral:data") include(":features:referral:domain") -include(":features:referral:presentation") \ No newline at end of file +include(":features:referral:presentation") +include(":libs:crypto") \ No newline at end of file From b47cc52e7054ba1e3e4c78eff734c3f87ac6bed0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 16 Nov 2022 13:03:38 +0300 Subject: [PATCH 056/372] Updated on 2026-08-14 --- app/build.gradle.kts | 2 +- buildSrc/src/main/java/Dependency.kt | 3 +- buildSrc/src/main/java/Versions.kt | 3 +- .../com/tangem/core/ui/components/AppBar.kt | 4 +- .../com/tangem/core/ui/components/Spacers.kt | 19 + .../java/com/tangem/core/ui/res/IconColor.kt | 19 + core/ui/src/main/res/drawable/ic_discount.xml | 9 + core/ui/src/main/res/drawable/ic_tether.xml | 17 + core/ui/src/main/res/values/dimens.xml | 18 + .../referral/presentation/build.gradle.kts | 22 +- .../referral/models/ReferralStateHolder.kt | 37 ++ .../feature/referral/ui/ReferralScreen.kt | 362 ++++++++++++++++++ .../src/main/res/values-ru/string.xml | 20 + .../src/main/res/values/string.xml | 20 + 14 files changed, 546 insertions(+), 9 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/res/IconColor.kt create mode 100644 core/ui/src/main/res/drawable/ic_discount.xml create mode 100644 core/ui/src/main/res/drawable/ic_tether.xml create mode 100644 features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt create mode 100644 features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt create mode 100644 features/referral/presentation/src/main/res/values-ru/string.xml create mode 100644 features/referral/presentation/src/main/res/values/string.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 847507ba5f..d4505537cb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -176,7 +176,7 @@ dependencies { } implementation(Library.accompanistAppCompatTheme) implementation(Library.accompanistSystemUiController) - implementation(Library.skydovesVeil) + implementation(Library.xmlShimmer) implementation(Library.viewBindingDelegate) implementation(Library.armadillo) implementation(Library.googlePlayServicesWallet) diff --git a/buildSrc/src/main/java/Dependency.kt b/buildSrc/src/main/java/Dependency.kt index 84f6380c8c..f93b728008 100644 --- a/buildSrc/src/main/java/Dependency.kt +++ b/buildSrc/src/main/java/Dependency.kt @@ -34,6 +34,7 @@ object Library { const val appsflyer = "com.appsflyer:af-android-sdk:" + Versions.appsflyer const val armadillo = "at.favre.lib:armadillo:" + Versions.armadillo const val coil = "io.coil-kt:coil:" + Versions.coil + const val composeShimmer = "com.valentinilk.shimmer:compose-shimmer:" + Versions.composeShimmer const val coroutine = "org.jetbrains.kotlinx:kotlinx-coroutines-core:" + Versions.coroutine const val desugarJdkLibs = "com.android.tools:desugar_jdk_libs:" + Versions.desugarJdkLibs const val googlePlayCore = "com.google.android.play:core:" + Versions.googlePlayCore @@ -52,7 +53,6 @@ object Library { const val okHttpLogging = "com.squareup.okhttp3:logging-interceptor:" + Versions.okhttp const val otaliastudiosCameraView = "com.otaliastudios:cameraview:" + Versions.otaliastudiosCameraView const val shopifyBuySdk = "com.shopify.mobilebuysdk:buy3:" + Versions.shopifyBuySdk - const val skydovesVeil = "com.github.skydoves:androidveil:" + Versions.skydovesAndroidVeil const val spongecastleCryptoCore = "com.madgag.spongycastle:core:" + Versions.spongycastleCryptoCore const val reKotlin = "org.rekotlin:rekotlin:" + Versions.rekotlin const val retrofit = "com.squareup.retrofit2:retrofit:" + Versions.retrofit @@ -60,6 +60,7 @@ object Library { const val timber = "com.jakewharton.timber:timber:" + Versions.timber const val viewBindingDelegate = "com.github.kirich1409:viewbindingpropertydelegate-noreflection:" + Versions.viewBindingDelegate + const val xmlShimmer = "com.github.skydoves:androidveil:" + Versions.xmlShimmer const val zendeskChat = "com.zendesk:chat:" + Versions.zendeskChat const val zendeskMessaging = "com.zendesk:messaging:" + Versions.zendeskMessaging const val zxingQrBarcodeScanner = "me.dm7.barcodescanner:zxing:" + Versions.zxingQrBarcodeScanner diff --git a/buildSrc/src/main/java/Versions.kt b/buildSrc/src/main/java/Versions.kt index b52ccb4d7b..54b4716dc7 100644 --- a/buildSrc/src/main/java/Versions.kt +++ b/buildSrc/src/main/java/Versions.kt @@ -28,6 +28,7 @@ object Versions { const val appsflyer = "6.5.1" const val armadillo = "0.9.0" const val coil = "2.1.0" + const val composeShimmer = "1.0.3" const val coroutine = "1.5.2" const val desugarJdkLibs = "1.1.5" const val firebase = "26.0.0" @@ -46,10 +47,10 @@ object Versions { const val retrofit = "2.8.1" const val retrofitMoshiConverter = "2.6.0" const val shopifyBuySdk = "12.0.0" - const val skydovesAndroidVeil = "1.1.2" const val spongycastleCryptoCore = "1.58.0.0" const val timber = "4.7.1" const val viewBindingDelegate = "1.5.6" + const val xmlShimmer = "1.1.2" const val zendeskChat = "3.3.5" const val zendeskMessaging = "5.2.4" const val zxingQrBarcodeScanner = "1.9.8" diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/AppBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/AppBar.kt index 6810ba11e8..6b5e3dc55d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/AppBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/AppBar.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.Icon @@ -48,7 +49,8 @@ fun AppBarWithBackButton(text: String? = null, onBackClick: () -> Unit) { Row( modifier = Modifier .background(MaterialTheme.colors.primaryVariant) - .padding(all = dimensionResource(R.dimen.spacing16)), + .padding(all = dimensionResource(R.dimen.spacing16)) + .fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing16)), verticalAlignment = Alignment.CenterVertically, ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt new file mode 100644 index 0000000000..60ce6bf8bc --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt @@ -0,0 +1,19 @@ +package com.tangem.core.ui.components + +import androidx.annotation.DimenRes +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource + +@Composable +fun VerticalSpacer(@DimenRes spaceResId: Int) { + Spacer(modifier = Modifier.height(dimensionResource(id = spaceResId))) +} + +@Composable +fun HorizontalSpacer(@DimenRes spaceResId: Int) { + Spacer(modifier = Modifier.width(dimensionResource(id = spaceResId))) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/IconColor.kt b/core/ui/src/main/java/com/tangem/core/ui/res/IconColor.kt new file mode 100644 index 0000000000..72cc8936bb --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/IconColor.kt @@ -0,0 +1,19 @@ +package com.tangem.core.ui.res + +import androidx.compose.material.Colors +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +enum class IconColorType(val lightColor: Color, val darkColor: Color) { + PRIMARY1(lightColor = Black, darkColor = White), + PRIMARY2(lightColor = White, darkColor = Dark6), + SECONDARY(lightColor = Dark2, darkColor = Dark1), + INFORMATIVE(lightColor = Light5, darkColor = Dark2), + INACTIVE(lightColor = Light4, darkColor = Dark4), + ACCENT(lightColor = Meadow, darkColor = Meadow), + WARNING(lightColor = Amaranth, darkColor = Amaranth), + ATTENTION(lightColor = Tangerine, darkColor = Tangerine) +} + +@Composable +fun Colors.iconColor(type: IconColorType): Color = if (isSystemInDarkTheme) type.darkColor else type.lightColor \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_discount.xml b/core/ui/src/main/res/drawable/ic_discount.xml new file mode 100644 index 0000000000..5045eccd5b --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_discount.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_tether.xml b/core/ui/src/main/res/drawable/ic_tether.xml new file mode 100644 index 0000000000..e3f94b5dbb --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_tether.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/core/ui/src/main/res/values/dimens.xml b/core/ui/src/main/res/values/dimens.xml index cd873bfb37..3863794b73 100644 --- a/core/ui/src/main/res/values/dimens.xml +++ b/core/ui/src/main/res/values/dimens.xml @@ -13,19 +13,37 @@ + 6dp 12dp + 16dp + 11dp + 16dp 24dp + 28dp + 40dp 48dp + 56dp + 84dp + 86dp + 102dp + 164dp 200dp + 2dp + 4dp 8dp + 10dp 12dp 16dp 24dp + 32dp + 44dp + 50dp + 54dp \ No newline at end of file diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts index 4a5dd97a5c..05a5ad278c 100644 --- a/features/referral/presentation/build.gradle.kts +++ b/features/referral/presentation/build.gradle.kts @@ -1,8 +1,6 @@ plugins { id("com.android.library") kotlin("android") - kotlin("kapt") - id("com.google.dagger.hilt.android") } android { @@ -22,11 +20,25 @@ android { targetCompatibility = JavaVersion.VERSION_1_8 isCoreLibraryDesugaringEnabled = false } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = Versions.compose + } } dependencies { + /** Core modules */ + implementation(project(":core:ui")) - /** DI */ - implementation(Library.hilt) - kapt(Library.hiltKapt) + /** Compose */ + implementation(Compose.foundation) + implementation(Compose.material) + implementation(Compose.uiTooling) + + /** Other libraries */ + implementation(Library.composeShimmer) } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt new file mode 100644 index 0000000000..4c592299c1 --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt @@ -0,0 +1,37 @@ +package com.tangem.feature.referral.models + +data class ReferralStateHolder( + val headerState: HeaderState, + val referralInfoState: ReferralInfoState, + val effects: Effects, +) { + + data class HeaderState(val onBackClicked: () -> Unit) + + sealed interface ReferralInfoContentState + + sealed interface ReferralInfoState { + + data class ParticipantContent( + val award: String, + val address: String, + val discount: String, + val onAgreementClicked: () -> Unit, + val onParticipateClicked: () -> Unit, + ) : ReferralInfoState, ReferralInfoContentState + + data class NonParticipantContent( + val award: String, + val discount: String, + val purchasedWalletCount: Int, + val code: String, + val onCopyClicked: () -> Unit, + val onShareClicked: () -> Unit, + val onAgreementClicked: () -> Unit, + ) : ReferralInfoState, ReferralInfoContentState + + object Loading : ReferralInfoState + } + + data class Effects(val showErrorToast: Boolean) +} diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt new file mode 100644 index 0000000000..c7894c0bea --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -0,0 +1,362 @@ +package com.tangem.feature.referral.ui + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.AppBarWithBackButton +import com.tangem.core.ui.components.VerticalSpacer +import com.tangem.core.ui.res.ButtonColorType +import com.tangem.core.ui.res.IconColorType +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TextColorType +import com.tangem.core.ui.res.White +import com.tangem.core.ui.res.buttonColor +import com.tangem.core.ui.res.iconColor +import com.tangem.core.ui.res.textColor +import com.tangem.feature.referral.models.ReferralStateHolder +import com.tangem.feature.referral.models.ReferralStateHolder.ReferralInfoState +import com.tangem.feature.referral.presentation.R +import com.valentinilk.shimmer.shimmer + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_Participant_InLightTheme() { + TangemTheme(isDarkTheme = false) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.ParticipantContent( + award = "10 USDT", + address = "ma80...zk8q2", + discount = "10%", + onAgreementClicked = {}, + onParticipateClicked = {}, + ), + effects = ReferralStateHolder.Effects(showErrorToast = false), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_Participant_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.ParticipantContent( + award = "10 USDT", + address = "ma80...zk8q2", + discount = "10%", + onAgreementClicked = {}, + onParticipateClicked = {}, + ), + effects = ReferralStateHolder.Effects(showErrorToast = false), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_NonParticipant_InLightTheme() { + TangemTheme(isDarkTheme = false) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.NonParticipantContent( + award = "10 USDT", + discount = "10%", + purchasedWalletCount = 3, + code = "x4JdK", + onCopyClicked = {}, + onShareClicked = {}, + onAgreementClicked = {}, + ), + effects = ReferralStateHolder.Effects(showErrorToast = false), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_NonParticipant_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.NonParticipantContent( + award = "10 USDT", + discount = "10%", + purchasedWalletCount = 3, + code = "x4JdK", + onCopyClicked = {}, + onShareClicked = {}, + onAgreementClicked = {}, + ), + effects = ReferralStateHolder.Effects(showErrorToast = false), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_Loading_InLightTheme() { + TangemTheme(isDarkTheme = false) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.Loading, + effects = ReferralStateHolder.Effects(showErrorToast = false), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_Loading_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.Loading, + effects = ReferralStateHolder.Effects(showErrorToast = false), + ), + ) + } +} + +/** + * Referral program screen for participant and non-participant + * + * @param stateHolder state holder + */ +@Composable +fun ReferralScreen(stateHolder: ReferralStateHolder) { + Column( + modifier = Modifier + .background(color = MaterialTheme.colors.primary) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Header(stateHolder = stateHolder) + VerticalSpacer(spaceResId = R.dimen.spacing16) + ReferralInfo(stateHolder = stateHolder) + } +} + +@Composable +private fun ColumnScope.Header(stateHolder: ReferralStateHolder) { + AppBar(stateHolder = stateHolder) + Image( + painter = painterResource(R.drawable.ill_businessman_3d), + contentDescription = null, + modifier = Modifier + .padding(horizontal = dimensionResource(id = R.dimen.spacing32)) + .fillMaxWidth() + .height(dimensionResource(R.dimen.size200)), + ) + VerticalSpacer(spaceResId = R.dimen.spacing24) + Text( + text = stringResource(id = R.string.referral_title), + modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.spacing50)), + color = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY1), + textAlign = TextAlign.Center, + maxLines = 2, + style = MaterialTheme.typography.h2, + ) +} + +@Composable +fun ReferralInfo(stateHolder: ReferralStateHolder) { + when (val state = stateHolder.referralInfoState) { + is ReferralInfoState.ParticipantContent, + is ReferralInfoState.NonParticipantContent -> { + Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.spacing32))) { + ConditionForYou(state = state as ReferralStateHolder.ReferralInfoContentState) + ConditionForYourFriend(state = state) + } + } + is ReferralInfoState.Loading -> { + Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.spacing32))) { + LoadingCondition(iconResId = R.drawable.ic_tether) + LoadingCondition(iconResId = R.drawable.ic_discount) + } + } + } +} + +@Composable +private fun AppBar(stateHolder: ReferralStateHolder) { + AppBarWithBackButton( + text = stringResource(R.string.details_referral_title), + onBackClick = stateHolder.headerState.onBackClicked, + ) +} + +@Composable +private fun ConditionForYou(state: ReferralStateHolder.ReferralInfoContentState) { + Condition(iconResId = R.drawable.ic_tether) { + when (state) { + is ReferralInfoState.ParticipantContent -> InfoForYou(award = state.award, address = state.address) + is ReferralInfoState.NonParticipantContent -> InfoForYou(award = state.award) + } + } +} + +@Composable +private fun ConditionForYourFriend(state: ReferralStateHolder.ReferralInfoContentState) { + Condition(iconResId = R.drawable.ic_discount) { + when (state) { + is ReferralInfoState.ParticipantContent -> InfoForYourFriend(discount = state.discount) + is ReferralInfoState.NonParticipantContent -> InfoForYourFriend(discount = state.discount) + } + } +} + +@Composable +private fun LoadingCondition(@DrawableRes iconResId: Int) { + Condition(iconResId = iconResId) { + ShimmerCondition() + } +} + +@Composable +private fun Condition(@DrawableRes iconResId: Int, infoBlock: @Composable () -> Unit) { + Row( + modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.spacing16)), + horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.spacing12)), + verticalAlignment = Alignment.Top, + ) { + Box( + modifier = Modifier + .background( + color = MaterialTheme.colors.buttonColor(type = ButtonColorType.SECONDARY), + shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius16)), + ) + .size(dimensionResource(id = R.dimen.size56)), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = iconResId), + contentDescription = null, + modifier = Modifier.size(dimensionResource(id = R.dimen.size28)), + tint = MaterialTheme.colors.iconColor(type = IconColorType.PRIMARY1), + ) + } + infoBlock() + } +} + +@Composable +private fun InfoForYou(award: String, address: String? = null) { + ConditionInfo(title = stringResource(id = R.string.referral_point_currencies_title)) { + Text( + text = buildAnnotatedString { + append(stringResource(id = R.string.referral_point_currencies_description_prefix)) + withStyle(SpanStyle(color = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY1))) { + append(" $award ") + } + append( + String.format( + stringResource(id = R.string.referral_point_currencies_description_suffix), + if (!address.isNullOrBlank()) address else "", + ), + ) + }, + color = MaterialTheme.colors.textColor(type = TextColorType.TERTIARY), + style = MaterialTheme.typography.body2, + ) + } +} + +@Composable +private fun InfoForYourFriend(discount: String) { + ConditionInfo(title = stringResource(id = R.string.referral_point_discount_title)) { + Text( + text = buildString { + append(stringResource(id = R.string.referral_point_discount_description_prefix)) + append( + String.format( + stringResource(id = R.string.referral_point_discount_description_value), + " $discount", + ), + ) + append(" ") + append(stringResource(id = R.string.referral_point_discount_description_suffix)) + }, + color = MaterialTheme.colors.textColor(type = TextColorType.TERTIARY), + style = MaterialTheme.typography.body2, + ) + } +} + +@Composable +private fun ConditionInfo(title: String, subtitleContent: @Composable () -> Unit) { + Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.spacing2))) { + Text( + text = title, + color = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY1), + style = MaterialTheme.typography.subtitle1, + ) + subtitleContent() + } +} + +@Composable +private fun ShimmerCondition() { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = dimensionResource(id = R.dimen.spacing4)) + .shimmer(), + verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.spacing10)), + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(dimensionResource(id = R.dimen.radius6))) + .width(dimensionResource(id = R.dimen.size102)) + .height(dimensionResource(id = R.dimen.size16)) + .background(White), + ) + Box( + modifier = Modifier + .clip(RoundedCornerShape(dimensionResource(id = R.dimen.radius6))) + .width(dimensionResource(id = R.dimen.size40)) + .height(dimensionResource(id = R.dimen.size11)) + .background(White), + ) + } +} + diff --git a/features/referral/presentation/src/main/res/values-ru/string.xml b/features/referral/presentation/src/main/res/values-ru/string.xml new file mode 100644 index 0000000000..27e42e7c03 --- /dev/null +++ b/features/referral/presentation/src/main/res/values-ru/string.xml @@ -0,0 +1,20 @@ + + + Реферальная программа + Приведи друга в Tangem + Вы + Получите + за каждый кошелек, который купит ваш друг на ваш адрес в сети Tron %@ + Ваш друг + Получит + %@ скидку + при покупке карточки на сайте tangem.com + Нажимая на эту кнопку вы принимаете + условия участия + в реферальной программе + Участвовать + Вы приняли + Ваш персональный код + %d кошельков + Ваши друзья купили + \ No newline at end of file diff --git a/features/referral/presentation/src/main/res/values/string.xml b/features/referral/presentation/src/main/res/values/string.xml new file mode 100644 index 0000000000..7042ed2411 --- /dev/null +++ b/features/referral/presentation/src/main/res/values/string.xml @@ -0,0 +1,20 @@ + + + Referral program + Refer your friends to Tangem + You + Will get + for each wallet bought by your friend on your Tron network address %s + Your friend + Will get a + %s discount + when buying a card on tangem.com + By tapping this button you accept + terms and conditions + of the referral program + Participate + You\'ve accepted + Your personal code + %d wallets + Your friends bought + \ No newline at end of file From 47089f06068ee3dec0ccad5f7f8c60924f3fe2f3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 18 Nov 2022 13:00:15 +0300 Subject: [PATCH 057/372] Updated on 2026-08-14 --- .../com/tangem/core/ui/components/Buttons.kt | 31 +++- core/ui/src/main/res/drawable/ic_copy.xml | 9 ++ core/ui/src/main/res/drawable/ic_share.xml | 9 ++ .../src/main/res/values-ru/string_common.xml | 5 + core/ui/src/main/res/values/string_common.xml | 5 + .../feature/referral/ui/AgreementText.kt | 77 ++++++++++ .../referral/ui/NonParticipateBottomBlock.kt | 139 ++++++++++++++++++ .../referral/ui/ParticipateBottomBlock.kt | 50 +++++++ .../feature/referral/ui/ReferralScreen.kt | 55 +++++-- 9 files changed, 362 insertions(+), 18 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_copy.xml create mode 100644 core/ui/src/main/res/drawable/ic_share.xml create mode 100644 core/ui/src/main/res/values-ru/string_common.xml create mode 100644 core/ui/src/main/res/values/string_common.xml create mode 100644 features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt create mode 100644 features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt create mode 100644 features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index e9e49c5c71..65b331e15c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -101,6 +101,7 @@ fun Preview_PrimaryEndIconButton_Disabled_InDarkTheme() { /** * Primary button with an icon at the beginning of the layout * + * @param modifier button modifier * @param text button text * @param iconResId button icon res id * @param enabled controls the enabled state of the button @@ -110,8 +111,14 @@ fun Preview_PrimaryEndIconButton_Disabled_InDarkTheme() { * >Figma component */ @Composable -fun PrimaryStartIconButton(text: String, @DrawableRes iconResId: Int, enabled: Boolean = true, onClicked: () -> Unit) { - PrimaryButtonRow(enabled = enabled, onClicked = onClicked) { +fun PrimaryStartIconButton( + modifier: Modifier = Modifier, + text: String, + @DrawableRes iconResId: Int, + enabled: Boolean = true, + onClicked: () -> Unit, +) { + PrimaryButtonRow(modifier = modifier, enabled = enabled, onClicked = onClicked) { Icon( painter = painterResource(id = iconResId), contentDescription = null, @@ -124,6 +131,7 @@ fun PrimaryStartIconButton(text: String, @DrawableRes iconResId: Int, enabled: B /** * Primary button with an icon at the end of the layout * + * @param modifier button modifier * @param text button text * @param iconResId button icon res id * @param enabled controls the enabled state of the button @@ -133,8 +141,14 @@ fun PrimaryStartIconButton(text: String, @DrawableRes iconResId: Int, enabled: B * >Figma component */ @Composable -fun PrimaryEndIconButton(text: String, @DrawableRes iconResId: Int, enabled: Boolean = true, onClicked: () -> Unit) { - PrimaryButtonRow(enabled = enabled, onClicked = onClicked) { +fun PrimaryEndIconButton( + modifier: Modifier = Modifier, + text: String, + @DrawableRes iconResId: Int, + enabled: Boolean = true, + onClicked: () -> Unit, +) { + PrimaryButtonRow(modifier = modifier, enabled = enabled, onClicked = onClicked) { Text(text = text) Spacer(modifier = Modifier.width(dimensionResource(R.dimen.spacing8))) Icon( @@ -145,10 +159,15 @@ fun PrimaryEndIconButton(text: String, @DrawableRes iconResId: Int, enabled: Boo } @Composable -private fun PrimaryButtonRow(enabled: Boolean, onClicked: () -> Unit, content: @Composable (RowScope.() -> Unit)) { +private fun PrimaryButtonRow( + modifier: Modifier, + enabled: Boolean, + onClicked: () -> Unit, + content: @Composable (RowScope.() -> Unit), +) { Button( onClick = onClicked, - modifier = Modifier + modifier = modifier .fillMaxWidth() .height(dimensionResource(R.dimen.size48)), enabled = enabled, diff --git a/core/ui/src/main/res/drawable/ic_copy.xml b/core/ui/src/main/res/drawable/ic_copy.xml new file mode 100644 index 0000000000..d01cdf9823 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_copy.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_share.xml b/core/ui/src/main/res/drawable/ic_share.xml new file mode 100644 index 0000000000..e35c52d223 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_share.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/values-ru/string_common.xml b/core/ui/src/main/res/values-ru/string_common.xml new file mode 100644 index 0000000000..ebb5d6b913 --- /dev/null +++ b/core/ui/src/main/res/values-ru/string_common.xml @@ -0,0 +1,5 @@ + + + Поделиться + Копировать + \ No newline at end of file diff --git a/core/ui/src/main/res/values/string_common.xml b/core/ui/src/main/res/values/string_common.xml new file mode 100644 index 0000000000..ad2cf0d79f --- /dev/null +++ b/core/ui/src/main/res/values/string_common.xml @@ -0,0 +1,5 @@ + + + Share + Copy + \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt new file mode 100644 index 0000000000..133b6b374e --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt @@ -0,0 +1,77 @@ +package com.tangem.feature.referral.ui + +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TextColorType +import com.tangem.core.ui.res.textColor +import com.tangem.feature.referral.presentation.R + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_AgreementText_InLightTheme() { + TangemTheme(isDarkTheme = false) { + Box(modifier = Modifier.background(MaterialTheme.colors.primary)) { + AgreementText(firstPartResId = R.string.referral_tou_not_enroled_prefix, onClicked = {}) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_AgreementText_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + Box(modifier = Modifier.background(MaterialTheme.colors.primary)) { + AgreementText(firstPartResId = R.string.referral_tou_not_enroled_prefix, onClicked = {}) + } + } +} + +@Composable +fun AgreementText(@StringRes firstPartResId: Int, onClicked: () -> Unit) { + val agreementText = annotatedAgreementString(firstPart = stringResource(firstPartResId)) + ClickableText( + text = agreementText, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = dimensionResource(id = R.dimen.spacing54)), + style = MaterialTheme.typography.caption.copy(textAlign = TextAlign.Center), + maxLines = 2, + onClick = { + val clickableSpanStyle = requireNotNull(agreementText.spanStyles.getOrNull(1)) + if (it in clickableSpanStyle.start..clickableSpanStyle.end) { + onClicked() + } + }, + ) +} + +@Composable +private fun annotatedAgreementString(firstPart: String): AnnotatedString { + return buildAnnotatedString { + withStyle(SpanStyle(color = MaterialTheme.colors.textColor(type = TextColorType.TERTIARY))) { + append("$firstPart ") + } + withStyle(SpanStyle(color = MaterialTheme.colors.textColor(type = TextColorType.ACCENT))) { + append(stringResource(id = R.string.common_terms_and_conditions)) + } + withStyle(SpanStyle(color = MaterialTheme.colors.textColor(type = TextColorType.TERTIARY))) { + append(" ${stringResource(id = R.string.referral_tou_suffix)}") + } + } +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt new file mode 100644 index 0000000000..59b7ab8148 --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt @@ -0,0 +1,139 @@ +package com.tangem.feature.referral.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryStartIconButton +import com.tangem.core.ui.components.SmallInfoCard +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TextColorType +import com.tangem.core.ui.res.textColor +import com.tangem.feature.referral.presentation.R + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_NonParticipateBottomBlock_InLightTheme() { + TangemTheme(isDarkTheme = false) { + Column(Modifier.background(MaterialTheme.colors.primary)) { + NonParticipateBottomBlock( + purchasedWalletCount = 3, + code = "x4JdK", + onCopyClicked = {}, + onShareClicked = {}, + onAgreementClicked = {}, + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_NonParticipateBottomBlock_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + Column(Modifier.background(MaterialTheme.colors.primary)) { + NonParticipateBottomBlock( + purchasedWalletCount = 3, + code = "x4JdK", + onCopyClicked = {}, + onShareClicked = {}, + onAgreementClicked = {}, + ) + } + } +} + +@Composable +fun NonParticipateBottomBlock( + purchasedWalletCount: Int, + code: String, + onCopyClicked: () -> Unit, + onShareClicked: () -> Unit, + onAgreementClicked: () -> Unit, +) { + Column( + modifier = Modifier + .padding( + top = dimensionResource(id = R.dimen.spacing24), + bottom = dimensionResource(id = R.dimen.spacing16), + ) + .padding(horizontal = dimensionResource(id = R.dimen.spacing16)), + verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.spacing16)), + ) { + SmallInfoCard( + startText = stringResource(id = R.string.referral_friends_bought_title), + endText = String.format( + stringResource(id = R.string.referral_wallets_purchased_count), + purchasedWalletCount, + ), + ) + PersonalCodeCard(code = code) + AdditionalButtons(onCopyClicked = onCopyClicked, onShareClicked = onShareClicked) + AgreementText(firstPartResId = R.string.referral_tou_enroled_prefix, onClicked = onAgreementClicked) + } +} + +@Composable +private fun PersonalCodeCard(code: String) { + Column( + modifier = Modifier + .shadow( + elevation = dimensionResource(id = R.dimen.elevation2), + shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius12)), + ) + .background( + color = MaterialTheme.colors.secondary, + shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius12)), + ) + .fillMaxWidth() + .padding(vertical = dimensionResource(id = R.dimen.spacing12)), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.spacing4)), + ) { + Text( + text = stringResource(id = R.string.referral_promo_code_title), + color = MaterialTheme.colors.textColor(type = TextColorType.TERTIARY), + maxLines = 1, + style = MaterialTheme.typography.subtitle2, + ) + Text( + text = code, + color = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY1), + maxLines = 1, + style = MaterialTheme.typography.h2, + ) + } +} + +@Composable +private fun AdditionalButtons(onCopyClicked: () -> Unit, onShareClicked: () -> Unit) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.spacing16)), + ) { + PrimaryStartIconButton( + modifier = Modifier.weight(1f), + text = stringResource(id = R.string.common_copy), + iconResId = R.drawable.ic_copy, + onClicked = onCopyClicked, + ) + PrimaryStartIconButton( + modifier = Modifier.weight(1f), + text = stringResource(id = R.string.common_share), + iconResId = R.drawable.ic_share, + onClicked = onShareClicked, + ) + } +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt new file mode 100644 index 0000000000..7294cbd3f7 --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt @@ -0,0 +1,50 @@ +package com.tangem.feature.referral.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryEndIconButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.referral.presentation.R + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ParticipateBottomBlock_InLightTheme() { + TangemTheme(isDarkTheme = false) { + Column(Modifier.background(MaterialTheme.colors.primary)) { + ParticipateBottomBlock(onAgreementClicked = {}, onParticipateClicked = {}) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ParticipateBottomBlock_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + Column(Modifier.background(MaterialTheme.colors.primary)) { + ParticipateBottomBlock(onAgreementClicked = {}, onParticipateClicked = {}) + } + } +} + +@Composable +fun ParticipateBottomBlock(onAgreementClicked: () -> Unit, onParticipateClicked: () -> Unit) { + Column { + AgreementText( + firstPartResId = R.string.referral_tou_not_enroled_prefix, + onClicked = onAgreementClicked, + ) + PrimaryEndIconButton( + modifier = Modifier.padding(all = dimensionResource(id = R.dimen.spacing16)), + text = stringResource(id = R.string.referral_button_participate), + iconResId = R.drawable.ic_tangem, + onClicked = onParticipateClicked, + ) + } +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index c7894c0bea..46de8b45c4 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -14,7 +14,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -167,7 +169,8 @@ fun ReferralScreen(stateHolder: ReferralStateHolder) { Column( modifier = Modifier .background(color = MaterialTheme.colors.primary) - .fillMaxSize(), + .fillMaxSize() + .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, ) { Header(stateHolder = stateHolder) @@ -201,18 +204,20 @@ private fun ColumnScope.Header(stateHolder: ReferralStateHolder) { @Composable fun ReferralInfo(stateHolder: ReferralStateHolder) { when (val state = stateHolder.referralInfoState) { - is ReferralInfoState.ParticipantContent, + is ReferralInfoState.ParticipantContent -> { + Conditions(state = state) + VerticalSpacer(spaceResId = R.dimen.spacing44) + ParticipantBottomBlock(state = state) + } is ReferralInfoState.NonParticipantContent -> { - Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.spacing32))) { - ConditionForYou(state = state as ReferralStateHolder.ReferralInfoContentState) - ConditionForYourFriend(state = state) - } + Conditions(state = state) + VerticalSpacer(spaceResId = R.dimen.spacing24) + NonParticipantBottomBlock(state = state) } is ReferralInfoState.Loading -> { - Column(verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.spacing32))) { - LoadingCondition(iconResId = R.drawable.ic_tether) - LoadingCondition(iconResId = R.drawable.ic_discount) - } + LoadingCondition(iconResId = R.drawable.ic_tether) + VerticalSpacer(spaceResId = R.dimen.spacing32) + LoadingCondition(iconResId = R.drawable.ic_discount) } } } @@ -225,6 +230,13 @@ private fun AppBar(stateHolder: ReferralStateHolder) { ) } +@Composable +private fun Conditions(state: ReferralStateHolder.ReferralInfoContentState) { + ConditionForYou(state = state) + VerticalSpacer(spaceResId = R.dimen.spacing32) + ConditionForYourFriend(state = state) +} + @Composable private fun ConditionForYou(state: ReferralStateHolder.ReferralInfoContentState) { Condition(iconResId = R.drawable.ic_tether) { @@ -248,7 +260,7 @@ private fun ConditionForYourFriend(state: ReferralStateHolder.ReferralInfoConten @Composable private fun LoadingCondition(@DrawableRes iconResId: Int) { Condition(iconResId = iconResId) { - ShimmerCondition() + ShimmerInfo() } } @@ -335,7 +347,7 @@ private fun ConditionInfo(title: String, subtitleContent: @Composable () -> Unit } @Composable -private fun ShimmerCondition() { +private fun ShimmerInfo() { Column( modifier = Modifier .fillMaxWidth() @@ -360,3 +372,22 @@ private fun ShimmerCondition() { } } +@Composable +private fun ParticipantBottomBlock(state: ReferralInfoState.ParticipantContent) { + ParticipateBottomBlock( + onAgreementClicked = state.onAgreementClicked, + onParticipateClicked = state.onParticipateClicked, + ) +} + +@Composable +private fun NonParticipantBottomBlock(state: ReferralInfoState.NonParticipantContent) { + NonParticipateBottomBlock( + purchasedWalletCount = state.purchasedWalletCount, + code = state.code, + onCopyClicked = state.onCopyClicked, + onShareClicked = state.onShareClicked, + onAgreementClicked = state.onAgreementClicked, + ) +} + From b0d9a8957f5be94c903f9a8ea4749b84d97bf7b8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 15 Nov 2022 00:17:49 +0500 Subject: [PATCH 058/372] Updated on 2026-08-14 --- .../com/tangem/tap/common/DialogManager.kt | 3 ++- .../note/redux/OnboardingNoteMiddleware.kt | 12 +++++++++++- .../tap/features/wallet/redux/WalletAction.kt | 5 ++++- .../middlewares/WalletDialogsMiddleware.kt | 2 +- .../wallet/redux/models/WalletDialog.kt | 5 ++++- ...ssianCardholdersWarningBottomSheetDialog.kt | 18 ++++++++++++++++-- 6 files changed, 38 insertions(+), 7 deletions(-) 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 ed8d4542b9..4d1c3734ea 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -152,7 +152,8 @@ class DialogManager : StoreSubscriber { primaryButtonRes = state.dialog.primaryButtonRes, primaryButtonAction = state.dialog.onOk, ) - is WalletDialog.RussianCardholdersWarningDialog -> RussianCardholdersWarningBottomSheetDialog(context) + is WalletDialog.RussianCardholdersWarningDialog -> + RussianCardholdersWarningBottomSheetDialog(context, state.dialog.data) else -> null } dialog?.show() 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 ef0c5a743a..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 @@ -10,6 +10,7 @@ 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.getTopUpUrl @@ -24,8 +25,11 @@ import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.hasWallets import com.tangem.tap.domain.extensions.makePrimaryWalletManager import com.tangem.tap.features.demo.DemoHelper +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 @@ -172,11 +176,17 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch 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)) - val topUpUrl = walletManager.getTopUpUrl() ?: return store.dispatchOpenUrl(topUpUrl) } OnboardingNoteAction.Done -> { 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 8843a8eb40..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() - object RussianCardholdersWarningDialog : 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/WalletDialogsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt index 64d2122fa2..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 @@ -38,7 +38,7 @@ class WalletDialogsMiddleware { ) } is WalletAction.DialogAction.RussianCardholdersWarningDialog -> { - store.dispatchDialogShow(WalletDialog.RussianCardholdersWarningDialog) + 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/models/WalletDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/models/WalletDialog.kt index 68718acd45..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 } - object RussianCardholdersWarningDialog : 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/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt index 526b991006..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 @@ -5,14 +5,22 @@ 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) : BottomSheetDialog(context) { +class RussianCardholdersWarningBottomSheetDialog( + context: Context, + private val dialogData: WalletDialog.RussianCardholdersWarningDialog.Data?, +) : BottomSheetDialog + (context) { private var binding: DialogRussiansCardholdersWarningBinding? = null @@ -32,7 +40,13 @@ class RussianCardholdersWarningBottomSheetDialog(context: Context) : BottomSheet } binding?.btnYes?.setOnClickListener { - store.dispatch(WalletAction.TradeCryptoAction.Buy(checkUserLocation = false)) + 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)) + } dismiss() } binding?.btnNo?.setOnClickListener { From 4424a8b9b6af6c03d1b5fb3b585db9b1381d9fbf Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 15 Nov 2022 02:27:54 +0500 Subject: [PATCH 059/372] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 18 ++++++++-- .../com/tangem/tap/common/moshi/Adapters.kt | 16 +++++++++ .../tap/persistence/PreferencesStorage.kt | 8 ++--- .../tap/persistence/ToppedUpWalletStorage.kt | 21 ++++++++++- .../tangem/network/common/MoshiConverter.kt | 35 +++++++------------ 5 files changed, 69 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/moshi/Adapters.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 568ca4af60..5074f09e8d 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -27,6 +27,7 @@ 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 @@ -38,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 @@ -70,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() @@ -88,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, @@ -124,7 +138,7 @@ class TapApplication : Application(), ImageLoaderFactory { config = config, isDebug = BuildConfig.DEBUG, logConfig = LogConfig.analyticsHandlers, - jsonConverter = MoshiJsonConverter.INSTANCE, + jsonConverter = MoshiConverter.INSTANCE, ) factory.build(Analytics, buildData) } 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/persistence/PreferencesStorage.kt b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt index 38a03e482d..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) { @@ -21,12 +21,12 @@ class PreferencesStorage(applicationContext: Application) { 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, MoshiJsonConverter.INSTANCE) + toppedUpWalletStorage = ToppedUpWalletStorage(preferences, MoshiConverter.INSTANCE) } var chatFirstLaunchTime: Long? diff --git a/app/src/main/java/com/tangem/tap/persistence/ToppedUpWalletStorage.kt b/app/src/main/java/com/tangem/tap/persistence/ToppedUpWalletStorage.kt index 7ec028e8ea..e270b2b820 100644 --- a/app/src/main/java/com/tangem/tap/persistence/ToppedUpWalletStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/ToppedUpWalletStorage.kt @@ -2,7 +2,10 @@ 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 @@ -44,7 +47,9 @@ class ToppedUpWalletStorage( private fun restore(): MutableSet { val json = preferences.getString(KEY, null) ?: return mutableSetOf() return try { - jsonConverter.fromJson(json, jsonConverter.typedList(BasicTopUpFilter.Data::class.java))!! + 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() @@ -54,4 +59,18 @@ class ToppedUpWalletStorage( 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/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 From 327d84b8cf0064c7f43f66e08975cd91705d5dd4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 19 Nov 2022 16:42:19 +0500 Subject: [PATCH 060/372] Updated on 2026-08-14 --- .../onboarding/products/note/redux/OnboardingNoteMiddleware.kt | 1 - 1 file changed, 1 deletion(-) 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 97e13e9aaf..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 @@ -30,7 +30,6 @@ 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.features.wallet.redux.WalletAction import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager From ed03d8530e93f0b89a8700914610a86f56b89d0e Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 19 Nov 2022 17:02:42 +0300 Subject: [PATCH 061/372] Updated on 2026-08-14 --- app/build.gradle.kts | 3 +- .../main/java/com/tangem/tap/MainActivity.kt | 2 + .../main/res/drawable/ic_more_vertical_24.xml | 10 - app/src/main/res/menu/menu_wallet.xml | 10 +- core/ui/build.gradle.kts | 3 + .../appbar/AppBarWithAdditionalButtons.kt | 169 ++++++++++ .../AppBarWithBackButton.kt} | 4 +- .../appbar/models/AdditionalButton.kt | 8 + .../components/atoms/BottomSheetIndicator.kt | 60 ++++ .../com/tangem/core/ui/res/TangemTheme.kt | 7 + .../main/res/drawable/ic_more_vertical.xml | 10 + .../ui}/src/main/res/drawable/ic_scan.xml | 2 +- core/ui/src/main/res/values/dimens.xml | 5 + .../referral/presentation/build.gradle.kts | 3 + .../referral/models/ReferralStateHolder.kt | 5 +- .../ui/AgreementBottomSheetContent.kt | 80 +++++ .../feature/referral/ui/ReferralScreen.kt | 302 +++++++++++------- .../src/main/res/values-ru/string.xml | 4 +- 18 files changed, 542 insertions(+), 145 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_more_vertical_24.xml create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt rename core/ui/src/main/java/com/tangem/core/ui/components/{AppBar.kt => appbar/AppBarWithBackButton.kt} (94%) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/AdditionalButton.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/atoms/BottomSheetIndicator.kt create mode 100644 core/ui/src/main/res/drawable/ic_more_vertical.xml rename {app => core/ui}/src/main/res/drawable/ic_scan.xml (94%) create mode 100644 features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7b8485d96e..dc249a446b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -111,6 +111,7 @@ dependencies { implementation(project(":common")) implementation(project(":core:ui")) implementation(project(":libs:crypto")) + implementation(project(":features:referral:presentation")) /** AndroidX libraries */ implementation(AndroidX.coreKtx) @@ -144,7 +145,7 @@ dependencies { /** DI */ implementation(Library.hilt) - implementation(Library.hiltKapt) + kapt(Library.hiltKapt) /** Other libraries */ implementation(Library.materialComponent) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index e557e59247..aa9e63c2dc 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -30,6 +30,7 @@ import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.features.shop.redux.ShopAction import com.tangem.wallet.R import com.tangem.wallet.databinding.ActivityMainBinding +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -49,6 +50,7 @@ private val mainCoroutineContext: CoroutineContext get() = Job() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope") val mainScope = CoroutineScope(mainCoroutineContext) +@AndroidEntryPoint class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder { private var snackbar: Snackbar? = null diff --git a/app/src/main/res/drawable/ic_more_vertical_24.xml b/app/src/main/res/drawable/ic_more_vertical_24.xml deleted file mode 100644 index fd51d1fbf2..0000000000 --- a/app/src/main/res/drawable/ic_more_vertical_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/menu/menu_wallet.xml b/app/src/main/res/menu/menu_wallet.xml index edd88afb93..c5848ecb6a 100644 --- a/app/src/main/res/menu/menu_wallet.xml +++ b/app/src/main/res/menu/menu_wallet.xml @@ -2,9 +2,9 @@

+ android:id="@+id/details_menu" + android:title="@string/details_title" + android:icon="@drawable/ic_more_vertical" + app:iconTint="@color/darkGray6" + app:showAsAction="ifRoom" /> diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index ee2eab0e2a..3c509f8c3f 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -35,4 +35,7 @@ dependencies { implementation(Compose.foundation) implementation(Compose.material) implementation(Compose.uiTooling) + + /** Other libraries */ + implementation(Library.accompanistSystemUiController) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt new file mode 100644 index 0000000000..73cca24307 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt @@ -0,0 +1,169 @@ +package com.tangem.core.ui.components.appbar + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.appbar.models.AdditionalButton +import com.tangem.core.ui.res.IconColorType +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TextColorType +import com.tangem.core.ui.res.iconColor +import com.tangem.core.ui.res.textColor + +/** + * App bar with title and two additional buttons + * + * @param startButton button information attached to the left edge + * @param endButton button information attached to the right edge + * + * @see + * Figma Component + */ +@Composable +fun AppBarWithAdditionalButtons( + text: String, + startButton: AdditionalButton? = null, + endButton: AdditionalButton? = null, +) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(dimensionResource(id = R.dimen.size56)) + .padding(all = dimensionResource(id = R.dimen.spacing16)), + ) { + if (startButton != null) { + IconButton(modifier = Modifier.align(Alignment.CenterStart), onClick = startButton.onIconClicked) { + Icon( + painter = painterResource(id = startButton.iconRes), + contentDescription = null, + modifier = Modifier.size(dimensionResource(id = R.dimen.size24)), + tint = MaterialTheme.colors.iconColor(type = IconColorType.PRIMARY1), + ) + } + } + + Text( + text = text, + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY1), + maxLines = 1, + style = MaterialTheme.typography.subtitle1, + ) + + if (endButton != null) { + IconButton(modifier = Modifier.align(Alignment.CenterEnd), onClick = endButton.onIconClicked) { + Icon( + painter = painterResource(id = endButton.iconRes), + contentDescription = null, + modifier = Modifier.size(dimensionResource(id = R.dimen.size24)), + tint = MaterialTheme.colors.iconColor(type = IconColorType.PRIMARY1), + ) + } + } + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +fun Preview_AppBarWithAdditionalButtons_InLightTheme() { + TangemTheme(isDarkTheme = false) { + AppBarWithAdditionalButtons( + text = "Tangem", + startButton = AdditionalButton( + iconRes = R.drawable.ic_scan, + onIconClicked = {}, + ), + endButton = AdditionalButton( + iconRes = R.drawable.ic_more_vertical, + onIconClicked = {}, + ), + ) + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +fun Preview_AppBarWithAdditionalButtons_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + AppBarWithAdditionalButtons( + text = "Tangem", + startButton = AdditionalButton( + iconRes = R.drawable.ic_scan, + onIconClicked = {}, + ), + endButton = AdditionalButton( + iconRes = R.drawable.ic_more_vertical, + onIconClicked = {}, + ), + ) + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +fun Preview_AppBarWithOnlyStartButtons_InLightTheme() { + TangemTheme(isDarkTheme = false) { + AppBarWithAdditionalButtons( + text = "Tangem", + startButton = AdditionalButton( + iconRes = R.drawable.ic_scan, + onIconClicked = {}, + ), + ) + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +fun Preview_AppBarWithOnlyStartButtons_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + AppBarWithAdditionalButtons( + text = "Tangem", + startButton = AdditionalButton( + iconRes = R.drawable.ic_scan, + onIconClicked = {}, + ), + ) + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +fun Preview_AppBarWithOnlyEndButtons_InLightTheme() { + TangemTheme(isDarkTheme = false) { + AppBarWithAdditionalButtons( + text = "Tangem", + endButton = AdditionalButton( + iconRes = R.drawable.ic_more_vertical, + onIconClicked = {}, + ), + ) + } +} + +@Preview(widthDp = 360, heightDp = 56, showBackground = true) +@Composable +fun Preview_AppBarWithOnlyEndButtons_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + AppBarWithAdditionalButtons( + text = "Tangem", + endButton = AdditionalButton( + iconRes = R.drawable.ic_more_vertical, + onIconClicked = {}, + ), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/AppBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt similarity index 94% rename from core/ui/src/main/java/com/tangem/core/ui/components/AppBar.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt index 6b5e3dc55d..99d6aa527a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/AppBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt @@ -1,6 +1,5 @@ -package com.tangem.core.ui.components +package com.tangem.core.ui.components.appbar -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -48,7 +47,6 @@ fun PreviewAppBarWithBackButtonInDarkTheme() { fun AppBarWithBackButton(text: String? = null, onBackClick: () -> Unit) { Row( modifier = Modifier - .background(MaterialTheme.colors.primaryVariant) .padding(all = dimensionResource(R.dimen.spacing16)) .fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing16)), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/AdditionalButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/AdditionalButton.kt new file mode 100644 index 0000000000..2517f5ef41 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/AdditionalButton.kt @@ -0,0 +1,8 @@ +package com.tangem.core.ui.components.appbar.models + +import androidx.annotation.DrawableRes + +data class AdditionalButton( + @DrawableRes val iconRes: Int, + val onIconClicked: () -> Unit, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/BottomSheetIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/BottomSheetIndicator.kt new file mode 100644 index 0000000000..edf127e645 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/BottomSheetIndicator.kt @@ -0,0 +1,60 @@ +package com.tangem.core.ui.components.atoms + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.IconColorType +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.iconColor + +/** + * Bottom sheet indicator + * + * @see + * Figma Component + */ +@Composable +fun BottomSheetIndicator() { + Box( + modifier = Modifier + .fillMaxWidth() + .height(dimensionResource(id = R.dimen.size20)), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .width(dimensionResource(id = R.dimen.size32)) + .height(dimensionResource(id = R.dimen.size4)) + .background( + color = MaterialTheme.colors.iconColor(type = IconColorType.INACTIVE), + shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius2)), + ), + ) + } +} + +@Preview(heightDp = 20, showBackground = true) +@Composable +fun Preview_BottomSheetIndicator_InLightTheme() { + TangemTheme(isDarkTheme = false) { + BottomSheetIndicator() + } +} + +@Preview(heightDp = 20, showBackground = true) +@Composable +fun Preview_BottomSheetIndicator_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + BottomSheetIndicator() + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 2e065ba6ee..ebd0da2f57 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -4,6 +4,8 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.darkColors import androidx.compose.material.lightColors import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import com.google.accompanist.systemuicontroller.rememberSystemUiController internal val isSystemInDarkTheme: Boolean = false // TODO: use isSystemInDarkTheme() for automatic color detection @@ -12,6 +14,11 @@ fun TangemTheme( isDarkTheme: Boolean = isSystemInDarkTheme, content: @Composable () -> Unit, ) { + val systemUiController = rememberSystemUiController() + SideEffect { + systemUiController.setSystemBarsColor(color = if (isDarkTheme) Black else Light1, darkIcons = !isDarkTheme) + } + MaterialTheme( colors = if (isDarkTheme) DarkColors else LightColors, typography = TangemTypography, diff --git a/core/ui/src/main/res/drawable/ic_more_vertical.xml b/core/ui/src/main/res/drawable/ic_more_vertical.xml new file mode 100644 index 0000000000..9f5887ceb4 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_more_vertical.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_scan.xml b/core/ui/src/main/res/drawable/ic_scan.xml similarity index 94% rename from app/src/main/res/drawable/ic_scan.xml rename to core/ui/src/main/res/drawable/ic_scan.xml index 1e4cd949d6..31f4345be7 100644 --- a/app/src/main/res/drawable/ic_scan.xml +++ b/core/ui/src/main/res/drawable/ic_scan.xml @@ -2,7 +2,7 @@ android:viewportHeight="24" android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> - + diff --git a/core/ui/src/main/res/values/dimens.xml b/core/ui/src/main/res/values/dimens.xml index 3863794b73..98fbe4b379 100644 --- a/core/ui/src/main/res/values/dimens.xml +++ b/core/ui/src/main/res/values/dimens.xml @@ -19,10 +19,14 @@ + 0dp + 4dp 11dp 16dp + 20dp 24dp 28dp + 32dp 40dp 48dp 56dp @@ -45,5 +49,6 @@ 44dp 50dp 54dp + 2dp \ No newline at end of file diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts index 05a5ad278c..73a9372e4b 100644 --- a/features/referral/presentation/build.gradle.kts +++ b/features/referral/presentation/build.gradle.kts @@ -34,6 +34,9 @@ dependencies { /** Core modules */ implementation(project(":core:ui")) + /** AndroidX */ + implementation(AndroidX.appCompat) + /** Compose */ implementation(Compose.foundation) implementation(Compose.material) diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt index 4c592299c1..0f9a5666b3 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt @@ -4,6 +4,7 @@ data class ReferralStateHolder( val headerState: HeaderState, val referralInfoState: ReferralInfoState, val effects: Effects, + val agreementBottomSheetState: AgreementBottomSheetState ) { data class HeaderState(val onBackClicked: () -> Unit) @@ -16,7 +17,6 @@ data class ReferralStateHolder( val award: String, val address: String, val discount: String, - val onAgreementClicked: () -> Unit, val onParticipateClicked: () -> Unit, ) : ReferralInfoState, ReferralInfoContentState @@ -27,11 +27,12 @@ data class ReferralStateHolder( val code: String, val onCopyClicked: () -> Unit, val onShareClicked: () -> Unit, - val onAgreementClicked: () -> Unit, ) : ReferralInfoState, ReferralInfoContentState object Loading : ReferralInfoState } data class Effects(val showErrorToast: Boolean) + + data class AgreementBottomSheetState(val url: String) } diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt new file mode 100644 index 0000000000..a1f8adc86c --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt @@ -0,0 +1,80 @@ +package com.tangem.feature.referral.ui + +import android.view.ViewGroup.LayoutParams +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons +import com.tangem.core.ui.components.atoms.BottomSheetIndicator +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.referral.presentation.R + +/** + * Bottom sheet content with referral program terms of use + * + * @param url link to the html page + */ +@Composable +fun AgreementBottomSheetContent(url: String) { + Column( + modifier = Modifier + .background(color = MaterialTheme.colors.secondary) + .fillMaxWidth() + .height(LocalConfiguration.current.screenHeightDp.dp - dimensionResource(id = R.dimen.spacing16)), + ) { + BottomSheetIndicator() + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + AppBarWithAdditionalButtons(text = stringResource(id = R.string.details_referral_title)) + AgreementHtmlView(url = url) + } + } +} + +@Composable +private fun AgreementHtmlView(url: String) { + AndroidView( + modifier = Modifier.background(MaterialTheme.colors.secondary), + factory = { + WebView(it).apply { + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + webViewClient = WebViewClient() + settings.apply { + javaScriptEnabled = false + allowFileAccess = false + } + loadUrl(url) + } + }, + update = { it.loadUrl(url) }, + ) +} + +@Preview +@Composable +fun Preview_AgreementBottomSheet_InLightTheme() { + TangemTheme(isDarkTheme = false) { + AgreementBottomSheetContent(url = "https://tangem.com/en/") + } +} + +@Preview +@Composable +fun Preview_AgreementBottomSheet_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + AgreementBottomSheetContent(url = "https://tangem.com/en/") + } +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index 46de8b45c4..a9935a04cc 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -17,10 +17,16 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material.BottomSheetScaffold +import androidx.compose.material.BottomSheetState +import androidx.compose.material.BottomSheetValue +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.Text +import androidx.compose.material.rememberBottomSheetScaffoldState import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -32,8 +38,8 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.AppBarWithBackButton import com.tangem.core.ui.components.VerticalSpacer +import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.ButtonColorType import com.tangem.core.ui.res.IconColorType import com.tangem.core.ui.res.TangemTheme @@ -46,126 +52,51 @@ import com.tangem.feature.referral.models.ReferralStateHolder import com.tangem.feature.referral.models.ReferralStateHolder.ReferralInfoState import com.tangem.feature.referral.presentation.R import com.valentinilk.shimmer.shimmer - -@Preview(widthDp = 360, showBackground = true) -@Composable -fun Preview_ReferralScreen_Participant_InLightTheme() { - TangemTheme(isDarkTheme = false) { - ReferralScreen( - stateHolder = ReferralStateHolder( - headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), - referralInfoState = ReferralInfoState.ParticipantContent( - award = "10 USDT", - address = "ma80...zk8q2", - discount = "10%", - onAgreementClicked = {}, - onParticipateClicked = {}, - ), - effects = ReferralStateHolder.Effects(showErrorToast = false), - ), - ) - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -fun Preview_ReferralScreen_Participant_InDarkTheme() { - TangemTheme(isDarkTheme = true) { - ReferralScreen( - stateHolder = ReferralStateHolder( - headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), - referralInfoState = ReferralInfoState.ParticipantContent( - award = "10 USDT", - address = "ma80...zk8q2", - discount = "10%", - onAgreementClicked = {}, - onParticipateClicked = {}, - ), - effects = ReferralStateHolder.Effects(showErrorToast = false), - ), - ) - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -fun Preview_ReferralScreen_NonParticipant_InLightTheme() { - TangemTheme(isDarkTheme = false) { - ReferralScreen( - stateHolder = ReferralStateHolder( - headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), - referralInfoState = ReferralInfoState.NonParticipantContent( - award = "10 USDT", - discount = "10%", - purchasedWalletCount = 3, - code = "x4JdK", - onCopyClicked = {}, - onShareClicked = {}, - onAgreementClicked = {}, - ), - effects = ReferralStateHolder.Effects(showErrorToast = false), - ), - ) - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -fun Preview_ReferralScreen_NonParticipant_InDarkTheme() { - TangemTheme(isDarkTheme = true) { - ReferralScreen( - stateHolder = ReferralStateHolder( - headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), - referralInfoState = ReferralInfoState.NonParticipantContent( - award = "10 USDT", - discount = "10%", - purchasedWalletCount = 3, - code = "x4JdK", - onCopyClicked = {}, - onShareClicked = {}, - onAgreementClicked = {}, - ), - effects = ReferralStateHolder.Effects(showErrorToast = false), - ), - ) - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -fun Preview_ReferralScreen_Loading_InLightTheme() { - TangemTheme(isDarkTheme = false) { - ReferralScreen( - stateHolder = ReferralStateHolder( - headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), - referralInfoState = ReferralInfoState.Loading, - effects = ReferralStateHolder.Effects(showErrorToast = false), - ), - ) - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -fun Preview_ReferralScreen_Loading_InDarkTheme() { - TangemTheme(isDarkTheme = true) { - ReferralScreen( - stateHolder = ReferralStateHolder( - headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), - referralInfoState = ReferralInfoState.Loading, - effects = ReferralStateHolder.Effects(showErrorToast = false), - ), - ) - } -} +import kotlinx.coroutines.launch /** * Referral program screen for participant and non-participant * * @param stateHolder state holder */ +@OptIn(ExperimentalMaterialApi::class) @Composable fun ReferralScreen(stateHolder: ReferralStateHolder) { + val coroutineScope = rememberCoroutineScope() + val bottomSheetScaffoldState = rememberBottomSheetScaffoldState( + bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed), + ) + + TangemTheme { + BottomSheetScaffold( + sheetContent = { AgreementBottomSheetContent(stateHolder.agreementBottomSheetState.url) }, + scaffoldState = bottomSheetScaffoldState, + sheetShape = RoundedCornerShape( + topStart = dimensionResource(id = R.dimen.radius16), + topEnd = dimensionResource(id = R.dimen.radius16), + ), + sheetElevation = dimensionResource(id = R.dimen.elevation24), + sheetPeekHeight = dimensionResource(id = R.dimen.size0), + content = { + ReferralContent( + stateHolder = stateHolder, + onAgreementClicked = { + coroutineScope.launch { + if (bottomSheetScaffoldState.bottomSheetState.isCollapsed) { + bottomSheetScaffoldState.bottomSheetState.expand() + } else { + bottomSheetScaffoldState.bottomSheetState.collapse() + } + } + }, + ) + }, + ) + } +} + +@Composable +fun ReferralContent(stateHolder: ReferralStateHolder, onAgreementClicked: () -> Unit) { Column( modifier = Modifier .background(color = MaterialTheme.colors.primary) @@ -175,7 +106,7 @@ fun ReferralScreen(stateHolder: ReferralStateHolder) { ) { Header(stateHolder = stateHolder) VerticalSpacer(spaceResId = R.dimen.spacing16) - ReferralInfo(stateHolder = stateHolder) + ReferralInfo(stateHolder = stateHolder, onAgreementClicked = onAgreementClicked) } } @@ -202,17 +133,17 @@ private fun ColumnScope.Header(stateHolder: ReferralStateHolder) { } @Composable -fun ReferralInfo(stateHolder: ReferralStateHolder) { +fun ReferralInfo(stateHolder: ReferralStateHolder, onAgreementClicked: () -> Unit) { when (val state = stateHolder.referralInfoState) { is ReferralInfoState.ParticipantContent -> { Conditions(state = state) VerticalSpacer(spaceResId = R.dimen.spacing44) - ParticipantBottomBlock(state = state) + ParticipantBottomBlock(state = state, onAgreementClicked = onAgreementClicked) } is ReferralInfoState.NonParticipantContent -> { Conditions(state = state) VerticalSpacer(spaceResId = R.dimen.spacing24) - NonParticipantBottomBlock(state = state) + NonParticipantBottomBlock(state = state, onAgreementClicked = onAgreementClicked) } is ReferralInfoState.Loading -> { LoadingCondition(iconResId = R.drawable.ic_tether) @@ -373,21 +304,150 @@ private fun ShimmerInfo() { } @Composable -private fun ParticipantBottomBlock(state: ReferralInfoState.ParticipantContent) { +private fun ParticipantBottomBlock(state: ReferralInfoState.ParticipantContent, onAgreementClicked: () -> Unit) { ParticipateBottomBlock( - onAgreementClicked = state.onAgreementClicked, + onAgreementClicked = onAgreementClicked, onParticipateClicked = state.onParticipateClicked, ) } @Composable -private fun NonParticipantBottomBlock(state: ReferralInfoState.NonParticipantContent) { +private fun NonParticipantBottomBlock(state: ReferralInfoState.NonParticipantContent, onAgreementClicked: () -> Unit) { NonParticipateBottomBlock( purchasedWalletCount = state.purchasedWalletCount, code = state.code, onCopyClicked = state.onCopyClicked, onShareClicked = state.onShareClicked, - onAgreementClicked = state.onAgreementClicked, + onAgreementClicked = onAgreementClicked, ) } +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_Participant_InLightTheme() { + TangemTheme(isDarkTheme = false) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.ParticipantContent( + award = "10 USDT", + address = "ma80...zk8q2", + discount = "10%", + onParticipateClicked = {}, + ), + effects = ReferralStateHolder.Effects(showErrorToast = false), + agreementBottomSheetState = ReferralStateHolder.AgreementBottomSheetState( + url = "", + ), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_Participant_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.ParticipantContent( + award = "10 USDT", + address = "ma80...zk8q2", + discount = "10%", + + onParticipateClicked = {}, + ), + effects = ReferralStateHolder.Effects(showErrorToast = false), + agreementBottomSheetState = ReferralStateHolder.AgreementBottomSheetState( + url = "", + ), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_NonParticipant_InLightTheme() { + TangemTheme(isDarkTheme = false) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.NonParticipantContent( + award = "10 USDT", + discount = "10%", + purchasedWalletCount = 3, + code = "x4JdK", + onCopyClicked = {}, + onShareClicked = {}, + + ), + effects = ReferralStateHolder.Effects(showErrorToast = false), + agreementBottomSheetState = ReferralStateHolder.AgreementBottomSheetState( + url = "", + ), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_NonParticipant_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.NonParticipantContent( + award = "10 USDT", + discount = "10%", + purchasedWalletCount = 3, + code = "x4JdK", + onCopyClicked = {}, + onShareClicked = {}, + + ), + effects = ReferralStateHolder.Effects(showErrorToast = false), + agreementBottomSheetState = ReferralStateHolder.AgreementBottomSheetState( + url = "", + ), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_Loading_InLightTheme() { + TangemTheme(isDarkTheme = false) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.Loading, + effects = ReferralStateHolder.Effects(showErrorToast = false), + agreementBottomSheetState = ReferralStateHolder.AgreementBottomSheetState( + url = "", + ), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +fun Preview_ReferralScreen_Loading_InDarkTheme() { + TangemTheme(isDarkTheme = true) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.Loading, + effects = ReferralStateHolder.Effects(showErrorToast = false), + agreementBottomSheetState = ReferralStateHolder.AgreementBottomSheetState( + url = "", + ), + ), + ) + } +} + diff --git a/features/referral/presentation/src/main/res/values-ru/string.xml b/features/referral/presentation/src/main/res/values-ru/string.xml index 27e42e7c03..4a6e997f8e 100644 --- a/features/referral/presentation/src/main/res/values-ru/string.xml +++ b/features/referral/presentation/src/main/res/values-ru/string.xml @@ -4,10 +4,10 @@ Приведи друга в Tangem Вы Получите - за каждый кошелек, который купит ваш друг на ваш адрес в сети Tron %@ + за каждый кошелек, который купит ваш друг на ваш адрес в сети Tron %s Ваш друг Получит - %@ скидку + %s скидку при покупке карточки на сайте tangem.com Нажимая на эту кнопку вы принимаете условия участия From 11334b807c042cf3b2c9d11e0856252ca0064b13 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 20 Nov 2022 13:09:31 +0300 Subject: [PATCH 062/372] Updated on 2026-08-14 --- .../datasource/api/referral/ReferralApi.kt | 3 ++- .../api/referral/models/ReferralResponse.kt | 17 +++++++++++++---- .../referral/converters/ReferralConverter.kt | 12 ++++++------ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/referral/ReferralApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/referral/ReferralApi.kt index 42445f931d..e7eaf31343 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/referral/ReferralApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/referral/ReferralApi.kt @@ -10,7 +10,8 @@ import retrofit2.http.Query /** * Api for referral feature */ -interface ReferralApi { +interface +ReferralApi { /** Returns referral status by [walletId] */ @GET("referral/{walletId}") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt index 061aa55155..a21aee2286 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/referral/models/ReferralResponse.kt @@ -14,17 +14,26 @@ data class ReferralResponse( data class Conditions( @Json(name = "award") val award: Int, - @Json(name = "discount") val discount: Int, - @Json(name = "discountType") val discountType: DiscountType, + @Json(name = "discount") val discount: Discount, + @Json(name = "awards") val awards: List, @Json(name = "tosLink") val tosLink: String, - @Json(name = "tokens") val tokens: List, +) + +data class Discount( + @Json(name = "amount") val amount: Int, + @Json(name = "discountType") val discountType: DiscountType, +) + +data class Award( + @Json(name = "amount") val amount: Int, + @Json(name = "token") val token: Token, ) data class Token( @Json(name = "id") val id: String, @Json(name = "name") val name: String, @Json(name = "symbol") val symbol: String, - @Json(name = "networkId") val networkId: String?, + @Json(name = "networkId") val networkId: String, @Json(name = "contractAddress") val contractAddress: String?, @Json(name = "decimalCount") val decimalCount: Int?, ) diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt index bc55726c4b..bbd136e5f2 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt @@ -18,10 +18,10 @@ class ReferralConverter @Inject constructor() : Converter Date: Thu, 17 Nov 2022 21:46:18 +0300 Subject: [PATCH 063/372] Updated on 2026-08-14 --- .../features/tokens/redux/TokensMiddleware.kt | 2 +- .../com/tangem/tap/proxy/CardStateHolder.kt | 13 ++++ .../tangem/tap/proxy/DerivationManagerImpl.kt | 35 +++++++-- .../tangem/tap/proxy/UserWalletManagerImpl.kt | 73 ++++++++++++++++--- .../referral/domain/ReferralInteractorImpl.kt | 6 +- .../domain/converter/TokensConverter.kt | 2 +- .../tangem/lib/crypto/DerivationManager.kt | 6 +- .../tangem/lib/crypto/UserWalletManager.kt | 8 +- .../com/tangem/lib/crypto/models/Token.kt | 4 +- 9 files changed, 119 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/proxy/CardStateHolder.kt 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 6ccf1b9b58..7b2322f79d 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 @@ -184,7 +184,7 @@ class TokensMiddleware { } } - private fun deriveMissingBlockchains( + fun deriveMissingBlockchains( scanResponse: ScanResponse, currencyList: List, onSuccess: (ScanResponse) -> Unit, diff --git a/app/src/main/java/com/tangem/tap/proxy/CardStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/CardStateHolder.kt new file mode 100644 index 0000000000..2729f1fc99 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/proxy/CardStateHolder.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.proxy + +import com.tangem.common.card.Card +import com.tangem.domain.common.ScanResponse + +class CardStateHolder { + + val scanResponse: ScanResponse? = null + + fun getActualCard(): Card? { + return scanResponse?.card + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt index a323e29196..e3aca88845 100644 --- a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt @@ -1,14 +1,37 @@ package com.tangem.tap.proxy -import com.tangem.crypto.DerivationManager +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.lib.crypto.DerivationManager +import com.tangem.tap.features.tokens.redux.TokensMiddleware -class DerivationManagerImpl : DerivationManager { +class DerivationManagerImpl( + val tokesMiddleware: TokensMiddleware, + val cardStateHolder: CardStateHolder, +) : DerivationManager { - override fun deriveMissingBlockchains(networkId: String?) { - TODO("Not yet implemented") + override suspend fun deriveMissingBlockchains(networkId: String) { + if (cardStateHolder.scanResponse != null) { //todo how to get Currency list from token + tokesMiddleware.deriveMissingBlockchains( + cardStateHolder.scanResponse, emptyList(), + ) { + + } + } } - override fun hasDerivation(): Boolean { - TODO("Not yet implemented") + override fun hasDerivation(networkId: String): Boolean { + val scanResponse = cardStateHolder.scanResponse + val blockchain = Blockchain.fromNetworkId(networkId) + if (scanResponse != null && blockchain != null) { + val derivationPath = blockchain.derivationPath(cardStateHolder.getActualCard()?.derivationStyle)?.rawPath + if (derivationPath.isNullOrEmpty()) return false + return scanResponse.hasDerivation( + blockchain, + derivationPath, + ) + } + return false } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index 45843af752..d8e04f93c9 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -1,27 +1,80 @@ package com.tangem.tap.proxy -import com.tangem.crypto.UserWalletManager -import com.tangem.crypto.models.Token +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.lib.crypto.UserWalletManager +import com.tangem.lib.crypto.models.Token +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.domain.extensions.getUserWalletId +import com.tangem.tap.domain.tokens.UserTokensRepository +import com.tangem.tap.domain.tokens.models.BlockchainNetwork +import com.tangem.tap.features.wallet.redux.WalletAction +import org.rekotlin.Store -class UserWalletManagerImpl : UserWalletManager { +class UserWalletManagerImpl( + private val userTokensRepository: UserTokensRepository, + private val cardStateHolder: CardStateHolder, + private val mainStore: Store, +) : UserWalletManager { - override fun getUserTokens(): List { - TODO("Not yet implemented") + override suspend fun getUserTokens(): List { + val card = cardStateHolder.getActualCard() + if (card != null) { + userTokensRepository.getUserTokens(card) + .filter { it.isToken() } + .map { + Token( + id = it.coinId ?: "", + name = it.currencyName, + symbol = it.currencySymbol, + networkId = it.blockchain.toNetworkId(), + contractAddress = null, + decimalCount = it.decimals, + ) + } + } + return emptyList() } override fun getWalletId(): String { - TODO("Not yet implemented") + return cardStateHolder.getActualCard()?.getUserWalletId() ?: "" } - override fun isTokenAdded(token: Token): Boolean { - TODO("Not yet implemented") + override suspend fun isTokenAdded(token: Token): Boolean { + val card = cardStateHolder.getActualCard() + if (card != null) { + return userTokensRepository.getUserTokens(card) + .any { it.coinId.equals(token.id) } //todo ensure that its the same ids + } + return false } + //todo check is it sync operation override fun addToken(token: Token) { - TODO("Not yet implemented") + val card = cardStateHolder.getActualCard() + val blockchain = Blockchain.fromNetworkId(token.networkId) + if (card != null && blockchain != null) { + val addAction = WalletAction.MultiWallet.AddToken( + token = com.tangem.blockchain.common.Token( + id = token.id, + name = token.name, + symbol = token.symbol, + contractAddress = token.contractAddress + ?: "", //todo clarify where to find that if data from back null + decimals = token.decimalCount ?: 0, //todo clarify where to find that if data from back null + ), + blockchain = BlockchainNetwork( + blockchain, + card, + ), + save = true, + ) + mainStore.dispatch(addAction) + } } override fun getWalletAddress(token: Token): String { - TODO("Not yet implemented") + return "" } } \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 3e2dd73ba7..69695dd1b8 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -1,8 +1,8 @@ package com.tangem.feature.referral.domain -import com.tangem.crypto.DerivationManager -import com.tangem.crypto.UserWalletManager -import com.tangem.crypto.models.Token +import com.tangem.lib.crypto.DerivationManager +import com.tangem.lib.crypto.UserWalletManager +import com.tangem.lib.crypto.models.Token import com.tangem.feature.referral.domain.converter.TokensConverter import com.tangem.feature.referral.domain.models.ReferralData import com.tangem.feature.referral.domain.models.TokenData diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt index 0a8c565e68..5a98e0f2a0 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt @@ -1,6 +1,6 @@ package com.tangem.feature.referral.domain.converter -import com.tangem.crypto.models.Token +import com.tangem.lib.crypto.models.Token import com.tangem.feature.referral.domain.models.TokenData import com.tangem.utils.converter.Converter diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt index 8e77e85480..ed5f1ffe3f 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt @@ -1,8 +1,8 @@ -package com.tangem.crypto +package com.tangem.lib.crypto interface DerivationManager { - fun deriveMissingBlockchains(networkId: String?) + suspend fun deriveMissingBlockchains(networkId: String) - fun hasDerivation(): Boolean + fun hasDerivation(networkId: String): Boolean } \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt index 8bb117cd15..0ea6a506d3 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt @@ -1,6 +1,6 @@ -package com.tangem.crypto +package com.tangem.lib.crypto -import com.tangem.crypto.models.Token +import com.tangem.lib.crypto.models.Token /** * Provider for user tokens data @@ -10,7 +10,7 @@ interface UserWalletManager { /** * Returns all user tokens (merged from local and backend) */ - fun getUserTokens(): List + suspend fun getUserTokens(): List /** * Returns user walletId @@ -22,7 +22,7 @@ interface UserWalletManager { * * @param token to receive referral payments */ - fun isTokenAdded(token: Token): Boolean + suspend fun isTokenAdded(token: Token): Boolean /** * Adds token to wallet if its not diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/Token.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/Token.kt index 86c314a573..eb9593843e 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/Token.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/Token.kt @@ -1,10 +1,10 @@ -package com.tangem.crypto.models +package com.tangem.lib.crypto.models data class Token( val id: String, val name: String, val symbol: String, - val networkId: String?, + val networkId: String, val contractAddress: String?, val decimalCount: Int?, ) \ No newline at end of file From ebcf2f40e5cc332ee95d4c1afa5e33e6ac50d879 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 18 Nov 2022 18:06:25 +0300 Subject: [PATCH 064/372] Updated on 2026-08-14 --- .../{CardStateHolder.kt => AppStateHolder.kt} | 4 +- .../tangem/tap/proxy/DerivationManagerImpl.kt | 44 +++++-- .../tangem/tap/proxy/UserWalletManagerImpl.kt | 123 ++++++++++++++---- core/datasource/build.gradle.kts | 1 - .../referral/domain/ReferralInteractorImpl.kt | 26 ++-- .../domain/converter/TokensConverter.kt | 35 +++-- .../referral/domain/models/ReferralData.kt | 2 +- .../tangem/lib/crypto/DerivationManager.kt | 12 +- .../tangem/lib/crypto/UserWalletManager.kt | 16 +-- .../com/tangem/lib/crypto/models/Currency.kt | 12 ++ .../tangem/lib/crypto/models/NativeToken.kt | 8 ++ .../lib/crypto/models/NonNativeToken.kt | 10 ++ .../com/tangem/lib/crypto/models/Token.kt | 10 -- 13 files changed, 222 insertions(+), 81 deletions(-) rename app/src/main/java/com/tangem/tap/proxy/{CardStateHolder.kt => AppStateHolder.kt} (65%) create mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/models/Currency.kt create mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/models/NativeToken.kt create mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/models/NonNativeToken.kt delete mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/models/Token.kt diff --git a/app/src/main/java/com/tangem/tap/proxy/CardStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt similarity index 65% rename from app/src/main/java/com/tangem/tap/proxy/CardStateHolder.kt rename to app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index 2729f1fc99..130dd64665 100644 --- a/app/src/main/java/com/tangem/tap/proxy/CardStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -2,10 +2,12 @@ package com.tangem.tap.proxy import com.tangem.common.card.Card import com.tangem.domain.common.ScanResponse +import com.tangem.tap.features.wallet.redux.WalletState -class CardStateHolder { +class AppStateHolder { val scanResponse: ScanResponse? = null + val walletState: WalletState? = null fun getActualCard(): Card? { return scanResponse?.card diff --git a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt index e3aca88845..dd6d2a6e1c 100644 --- a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt @@ -1,31 +1,55 @@ package com.tangem.tap.proxy import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.lib.crypto.DerivationManager +import com.tangem.lib.crypto.models.Currency +import com.tangem.lib.crypto.models.NonNativeToken +import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.tokens.redux.TokensMiddleware +import kotlin.coroutines.suspendCoroutine class DerivationManagerImpl( - val tokesMiddleware: TokensMiddleware, - val cardStateHolder: CardStateHolder, + private val tokesMiddleware: TokensMiddleware, + private val appStateHolder: AppStateHolder, ) : DerivationManager { - override suspend fun deriveMissingBlockchains(networkId: String) { - if (cardStateHolder.scanResponse != null) { //todo how to get Currency list from token - tokesMiddleware.deriveMissingBlockchains( - cardStateHolder.scanResponse, emptyList(), - ) { - + override suspend fun deriveMissingBlockchains(currency: Currency) = suspendCoroutine { continuation -> + val blockchain = Blockchain.fromNetworkId(currency.networkId) + val card = appStateHolder.getActualCard() + if (blockchain != null && card != null) { + val appToken = if (currency is NonNativeToken) { + Token( + symbol = currency.symbol, + contractAddress = currency.contractAddress, + decimals = currency.decimalCount, + ) + } else null + val blockchainNetwork = BlockchainNetwork(blockchain, card) + val appCurrency = com.tangem.tap.features.wallet.models.Currency.fromBlockchainNetwork( + blockchainNetwork, + appToken, + ) + if (appStateHolder.scanResponse != null) { + tokesMiddleware.deriveMissingBlockchains( + scanResponse = appStateHolder.scanResponse, + listOf(appCurrency), + ) { + continuation.resumeWith(Result.success(true)) + } } + } else { + continuation.resumeWith(Result.failure(IllegalStateException("no blockchain or card found"))) } } override fun hasDerivation(networkId: String): Boolean { - val scanResponse = cardStateHolder.scanResponse + val scanResponse = appStateHolder.scanResponse val blockchain = Blockchain.fromNetworkId(networkId) if (scanResponse != null && blockchain != null) { - val derivationPath = blockchain.derivationPath(cardStateHolder.getActualCard()?.derivationStyle)?.rawPath + val derivationPath = blockchain.derivationPath(appStateHolder.getActualCard()?.derivationStyle)?.rawPath if (derivationPath.isNullOrEmpty()) return false return scanResponse.hasDerivation( blockchain, diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index d8e04f93c9..cac76b8ab3 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -1,68 +1,140 @@ package com.tangem.tap.proxy import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.DerivationParams +import com.tangem.blockchain.common.DerivationStyle +import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.common.card.Card +import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.toNetworkId import com.tangem.lib.crypto.UserWalletManager -import com.tangem.lib.crypto.models.Token +import com.tangem.lib.crypto.models.Currency +import com.tangem.lib.crypto.models.NativeToken +import com.tangem.lib.crypto.models.NonNativeToken import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.extensions.getUserWalletId +import com.tangem.tap.domain.extensions.makeWalletManagerForApp import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.redux.WalletAction +import org.rekotlin.Action import org.rekotlin.Store class UserWalletManagerImpl( private val userTokensRepository: UserTokensRepository, - private val cardStateHolder: CardStateHolder, + private val appStateHolder: AppStateHolder, private val mainStore: Store, + private val walletManagerFactory: WalletManagerFactory, ) : UserWalletManager { - override suspend fun getUserTokens(): List { - val card = cardStateHolder.getActualCard() + override suspend fun getUserTokens(): List { + val card = appStateHolder.getActualCard() if (card != null) { userTokensRepository.getUserTokens(card) .filter { it.isToken() } .map { - Token( - id = it.coinId ?: "", - name = it.currencyName, - symbol = it.currencySymbol, - networkId = it.blockchain.toNetworkId(), - contractAddress = null, - decimalCount = it.decimals, - ) + if (it is com.tangem.tap.features.wallet.models.Currency.Token) { + NonNativeToken( + id = it.coinId ?: "", + name = it.currencyName, + symbol = it.currencySymbol, + networkId = it.blockchain.toNetworkId(), + contractAddress = it.token.contractAddress, + decimalCount = it.token.decimals, + ) + } else { + NativeToken( + id = it.coinId ?: "", + name = it.currencyName, + symbol = it.currencySymbol, + networkId = it.blockchain.toNetworkId(), + ) + } } } return emptyList() } override fun getWalletId(): String { - return cardStateHolder.getActualCard()?.getUserWalletId() ?: "" + return appStateHolder.getActualCard()?.getUserWalletId() ?: "" } - override suspend fun isTokenAdded(token: Token): Boolean { - val card = cardStateHolder.getActualCard() + override suspend fun isTokenAdded(currency: Currency): Boolean { + val card = appStateHolder.getActualCard() if (card != null) { return userTokensRepository.getUserTokens(card) - .any { it.coinId.equals(token.id) } //todo ensure that its the same ids + .any { it.coinId.equals(currency.id) } //todo ensure that its the same ids } return false } //todo check is it sync operation - override fun addToken(token: Token) { - val card = cardStateHolder.getActualCard() + override fun addToken(currency: Currency) { + val action = if (currency is NativeToken) { + val card = appStateHolder.getActualCard() + if (card != null) { + addNativeTokenToWalletAction(currency, card) + } else { + throw IllegalStateException("card not found") + } + } else { + addNonNativeTokenToWalletAction(currency as NonNativeToken) + } + + mainStore.dispatch(action) + } + + override fun getWalletAddress(currency: Currency): String { + val blockchain = Blockchain.fromNetworkId(currency.networkId) + val card = appStateHolder.getActualCard() + if (blockchain != null && card != null) { + val blockchainNetwork = BlockchainNetwork(blockchain, card) + val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork) + if (walletManager != null) { + return walletManager.wallet.address + } else { + throw IllegalStateException("no wallet manager found") + } + } else { + throw IllegalStateException("no blockchain or card found") + } + } + + private fun addNativeTokenToWalletAction(token: NativeToken, card: Card): Action { + val blockchain = Blockchain.fromNetworkId(token.networkId) + val scanResponse = appStateHolder.scanResponse + if (blockchain != null && scanResponse != null) { + val blockchainNetwork = BlockchainNetwork(blockchain, card) + var walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork) + if (walletManager == null) { + walletManager = walletManagerFactory.makeWalletManagerForApp( + scanResponse = scanResponse, + blockchain = blockchain, + derivationParams = createDerivationParams(card.derivationStyle), + ) + } + return WalletAction.MultiWallet.AddBlockchain( + blockchain = blockchainNetwork, + walletManager = walletManager, + save = true, + ) + } else { + throw IllegalStateException("blockchain is not supported") + } + } + + private fun addNonNativeTokenToWalletAction(token: NonNativeToken): Action { + val card = appStateHolder.getActualCard() val blockchain = Blockchain.fromNetworkId(token.networkId) if (card != null && blockchain != null) { - val addAction = WalletAction.MultiWallet.AddToken( + return WalletAction.MultiWallet.AddToken( token = com.tangem.blockchain.common.Token( id = token.id, name = token.name, symbol = token.symbol, - contractAddress = token.contractAddress - ?: "", //todo clarify where to find that if data from back null - decimals = token.decimalCount ?: 0, //todo clarify where to find that if data from back null + contractAddress = token.contractAddress, + decimals = token.decimalCount, ), blockchain = BlockchainNetwork( blockchain, @@ -70,11 +142,12 @@ class UserWalletManagerImpl( ), save = true, ) - mainStore.dispatch(addAction) + } else { + throw IllegalStateException("no card or blockchain found") } } - override fun getWalletAddress(token: Token): String { - return "" + private fun createDerivationParams(derivationStyle: DerivationStyle?): DerivationParams? { + return derivationStyle?.let { DerivationParams.Default(derivationStyle) } //todo clarify if its need to add Custom } } \ No newline at end of file diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index c85c0841b9..7d8f651682 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -6,7 +6,6 @@ plugins { } android { - defaultConfig { compileSdk = AppConfig.compileSdkVersion minSdk = AppConfig.minSdkVersion diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 69695dd1b8..8b2716f539 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -1,11 +1,11 @@ package com.tangem.feature.referral.domain -import com.tangem.lib.crypto.DerivationManager -import com.tangem.lib.crypto.UserWalletManager -import com.tangem.lib.crypto.models.Token import com.tangem.feature.referral.domain.converter.TokensConverter import com.tangem.feature.referral.domain.models.ReferralData import com.tangem.feature.referral.domain.models.TokenData +import com.tangem.lib.crypto.DerivationManager +import com.tangem.lib.crypto.UserWalletManager +import com.tangem.lib.crypto.models.Currency internal class ReferralInteractorImpl( private val repository: ReferralRepository, @@ -24,13 +24,13 @@ internal class ReferralInteractorImpl( override suspend fun startReferral(): ReferralData { if (tokensForReferral.isNotEmpty()) { - val token = tokensConverter.convert(tokensForReferral.first()) - deriveOrAddTokens(token) - val publicAddress = userWalletManager.getWalletAddress(token) + val currency = tokensConverter.convert(tokensForReferral.first()) + deriveOrAddTokens(currency) + val publicAddress = userWalletManager.getWalletAddress(currency) return repository.startReferral( walletId = userWalletManager.getWalletId(), - networkId = token.networkId ?: "", - tokenId = token.id, + networkId = currency.networkId, + tokenId = currency.id, address = publicAddress, ) } else { @@ -38,12 +38,12 @@ internal class ReferralInteractorImpl( } } - private fun deriveOrAddTokens(token: Token) { - if (!derivationManager.hasDerivation()) { - derivationManager.deriveMissingBlockchains(token.networkId) + private suspend fun deriveOrAddTokens(currency: Currency) { + if (!derivationManager.hasDerivation(currency.networkId)) { + derivationManager.deriveMissingBlockchains(currency) } - if (!userWalletManager.isTokenAdded(token)) { - userWalletManager.addToken(token) + if (!userWalletManager.isTokenAdded(currency)) { + userWalletManager.addToken(currency) } } diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt index 5a98e0f2a0..4ee8e69395 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt @@ -1,19 +1,32 @@ package com.tangem.feature.referral.domain.converter -import com.tangem.lib.crypto.models.Token import com.tangem.feature.referral.domain.models.TokenData +import com.tangem.lib.crypto.models.Currency +import com.tangem.lib.crypto.models.NativeToken +import com.tangem.lib.crypto.models.NonNativeToken import com.tangem.utils.converter.Converter -class TokensConverter : Converter { +class TokensConverter : Converter { - override fun convert(value: TokenData): Token { - return Token( - id = value.id, - name = value.name, - symbol = value.symbol, - networkId = value.networkId, - contractAddress = value.contractAddress, - decimalCount = value.decimalCount, - ) + override fun convert(value: TokenData): Currency { + return if (value.decimalCount != null && + value.contractAddress != null + ) { + NonNativeToken( + id = value.id, + name = value.name, + symbol = value.symbol, + networkId = value.networkId, + contractAddress = value.contractAddress, + decimalCount = value.decimalCount, + ) + } else { + NativeToken( + id = value.id, + name = value.name, + symbol = value.symbol, + networkId = value.networkId, + ) + } } } \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt index 79216aec7d..f5cc061cb0 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt @@ -34,7 +34,7 @@ data class TokenData( val id: String, val name: String, val symbol: String, - val networkId: String?, + val networkId: String, val contractAddress: String?, val decimalCount: Int?, ) diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt index ed5f1ffe3f..b9b2cfecea 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/DerivationManager.kt @@ -1,8 +1,18 @@ package com.tangem.lib.crypto +import com.tangem.lib.crypto.models.Currency + interface DerivationManager { - suspend fun deriveMissingBlockchains(networkId: String) + /** + * Derives missing blockchain and returns flag true if did it successfully + * + * @param currency to derive (Native token or not) + */ + suspend fun deriveMissingBlockchains(currency: Currency): Boolean + /** + * Checks that given [networkId] has derivations + */ fun hasDerivation(networkId: String): Boolean } \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt index 0ea6a506d3..6df9887170 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/UserWalletManager.kt @@ -1,6 +1,6 @@ package com.tangem.lib.crypto -import com.tangem.lib.crypto.models.Token +import com.tangem.lib.crypto.models.Currency /** * Provider for user tokens data @@ -10,7 +10,7 @@ interface UserWalletManager { /** * Returns all user tokens (merged from local and backend) */ - suspend fun getUserTokens(): List + suspend fun getUserTokens(): List /** * Returns user walletId @@ -20,21 +20,21 @@ interface UserWalletManager { /** * Checks that token added to user wallet * - * @param token to receive referral payments + * @param currency to receive referral payments */ - suspend fun isTokenAdded(token: Token): Boolean + suspend fun isTokenAdded(currency: Currency): Boolean /** * Adds token to wallet if its not * - * @param token to add to wallet + * @param currency to add to wallet */ - fun addToken(token: Token) + fun addToken(currency: Currency) /** * Returns wallet public address for token * - * @param token for which find address + * @param currency for which find address */ - fun getWalletAddress(token: Token): String + fun getWalletAddress(currency: Currency): String } \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/Currency.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/Currency.kt new file mode 100644 index 0000000000..82afac8e25 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/Currency.kt @@ -0,0 +1,12 @@ +package com.tangem.lib.crypto.models + +/** + * Currency data class that can be native blockchain Token + * or custom Token (used in this lib to replace and divide logic Currency from app module) + */ +abstract class Currency( + open val id: String, + open val name: String, + open val symbol: String, + open val networkId: String, +) \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/NativeToken.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/NativeToken.kt new file mode 100644 index 0000000000..f8ab7464d6 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/NativeToken.kt @@ -0,0 +1,8 @@ +package com.tangem.lib.crypto.models + +data class NativeToken( + override val id: String, + override val name: String, + override val symbol: String, + override val networkId: String, +) : Currency(id, name, symbol, networkId) \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/NonNativeToken.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/NonNativeToken.kt new file mode 100644 index 0000000000..a2d812cd1b --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/NonNativeToken.kt @@ -0,0 +1,10 @@ +package com.tangem.lib.crypto.models + +class NonNativeToken( + override val id: String, + override val name: String, + override val symbol: String, + override val networkId: String, + val contractAddress: String, + val decimalCount: Int, +) : Currency(id, name, symbol, networkId) \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/Token.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/Token.kt deleted file mode 100644 index eb9593843e..0000000000 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/Token.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.lib.crypto.models - -data class Token( - val id: String, - val name: String, - val symbol: String, - val networkId: String, - val contractAddress: String?, - val decimalCount: Int?, -) \ No newline at end of file From dca991651ce0b544952e993f67bb8a9fa47deda1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 20 Nov 2022 15:07:49 +0300 Subject: [PATCH 065/372] Updated on 2026-08-14 --- app/build.gradle.kts | 12 +++++- .../com/tangem/tap/proxy/AppStateHolder.kt | 11 +++++ .../tangem/tap/proxy/DerivationManagerImpl.kt | 3 +- .../tangem/tap/proxy/UserWalletManagerImpl.kt | 11 +++-- .../com/tangem/tap/proxy/di/ProxyModule.kt | 41 +++++++++++++++++++ .../di/CoroutineDispatcherProviderModule.kt | 16 ++++++++ domain/build.gradle.kts | 8 +++- .../referral/data/ReferralRepositoryImpl.kt | 3 +- .../referral/di/ReferralRepositoryModule.kt | 29 +++++++++++++ .../domain/converter/TokensConverter.kt | 3 +- .../domain/di/ReferralDomainModule.kt | 31 ++++++++++++++ .../referral/presentation/build.gradle.kts | 9 ++++ .../presentation/ReferralViewModel.kt | 11 +++++ 13 files changed, 173 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt create mode 100644 core/utils/src/main/java/com/tangem/utils/di/CoroutineDispatcherProviderModule.kt create mode 100644 features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt create mode 100644 features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt create mode 100644 features/referral/presentation/src/main/java/com/tangem/feature/referral/presentation/ReferralViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dc249a446b..e68e3543e7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -111,7 +111,11 @@ dependencies { implementation(project(":common")) implementation(project(":core:ui")) implementation(project(":libs:crypto")) + + /** Features */ implementation(project(":features:referral:presentation")) + implementation(project(":features:referral:domain")) + implementation(project(":features:referral:data")) /** AndroidX libraries */ implementation(AndroidX.coreKtx) @@ -139,9 +143,13 @@ dependencies { implementation(Firebase.firebaseCrashlytics) /** Tangem libraries */ - implementation(Tangem.blockchain) + implementation(Tangem.blockchain) { + exclude(module = "joda-time") + } implementation(Tangem.cardCore) - implementation(Tangem.cardAndroid) + implementation(Tangem.cardAndroid) { + exclude(module = "joda-time") + } /** DI */ implementation(Library.hilt) diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index 130dd64665..eb4bfba228 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -2,12 +2,23 @@ package com.tangem.tap.proxy import com.tangem.common.card.Card import com.tangem.domain.common.ScanResponse +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.domain.tokens.UserTokensRepository +import com.tangem.tap.features.tokens.redux.TokensMiddleware import com.tangem.tap.features.wallet.redux.WalletState +import org.rekotlin.Store +/** + * Holds objects from old modules, that missing in DI graph + * Object sets manually to use in new modules and [AppStateHolder] proxies its to DI + */ class AppStateHolder { val scanResponse: ScanResponse? = null val walletState: WalletState? = null + val userTokensRepository: UserTokensRepository? = null + val mainStore: Store? = null + val tokesMiddleware: TokensMiddleware? = null fun getActualCard(): Card? { return scanResponse?.card diff --git a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt index dd6d2a6e1c..e27bfd5238 100644 --- a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt @@ -8,15 +8,14 @@ import com.tangem.lib.crypto.DerivationManager import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.NonNativeToken import com.tangem.tap.domain.tokens.models.BlockchainNetwork -import com.tangem.tap.features.tokens.redux.TokensMiddleware import kotlin.coroutines.suspendCoroutine class DerivationManagerImpl( - private val tokesMiddleware: TokensMiddleware, private val appStateHolder: AppStateHolder, ) : DerivationManager { override suspend fun deriveMissingBlockchains(currency: Currency) = suspendCoroutine { continuation -> + val tokesMiddleware = requireNotNull(appStateHolder.tokesMiddleware) { "tokesMiddleware is null" } val blockchain = Blockchain.fromNetworkId(currency.networkId) val card = appStateHolder.getActualCard() if (blockchain != null && card != null) { diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index cac76b8ab3..1a7f3fbcd5 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -12,24 +12,21 @@ import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.NativeToken import com.tangem.lib.crypto.models.NonNativeToken -import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.extensions.getUserWalletId import com.tangem.tap.domain.extensions.makeWalletManagerForApp -import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.redux.WalletAction import org.rekotlin.Action -import org.rekotlin.Store class UserWalletManagerImpl( - private val userTokensRepository: UserTokensRepository, private val appStateHolder: AppStateHolder, - private val mainStore: Store, private val walletManagerFactory: WalletManagerFactory, ) : UserWalletManager { override suspend fun getUserTokens(): List { val card = appStateHolder.getActualCard() + val userTokensRepository = + requireNotNull(appStateHolder.userTokensRepository) { "userTokensRepository is null" } if (card != null) { userTokensRepository.getUserTokens(card) .filter { it.isToken() } @@ -62,6 +59,8 @@ class UserWalletManagerImpl( override suspend fun isTokenAdded(currency: Currency): Boolean { val card = appStateHolder.getActualCard() + val userTokensRepository = + requireNotNull(appStateHolder.userTokensRepository) { "userTokensRepository is null" } if (card != null) { return userTokensRepository.getUserTokens(card) .any { it.coinId.equals(currency.id) } //todo ensure that its the same ids @@ -81,7 +80,7 @@ class UserWalletManagerImpl( } else { addNonNativeTokenToWalletAction(currency as NonNativeToken) } - + val mainStore = requireNotNull(appStateHolder.mainStore) { "mainStore is null" } mainStore.dispatch(action) } diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt new file mode 100644 index 0000000000..07d3fb1e8d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt @@ -0,0 +1,41 @@ +package com.tangem.tap.proxy.di + +import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.lib.crypto.DerivationManager +import com.tangem.lib.crypto.UserWalletManager +import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.proxy.DerivationManagerImpl +import com.tangem.tap.proxy.UserWalletManagerImpl +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class ProxyModule { + + @Provides + @Singleton + fun provideAppStateHolder(): AppStateHolder { + return AppStateHolder() + } + + @Provides + @Singleton + fun provideUserWalletManager(appStateHolder: AppStateHolder): UserWalletManager { + return UserWalletManagerImpl( + appStateHolder = appStateHolder, + walletManagerFactory = WalletManagerFactory(), + ) + } + + @Provides + @Singleton + fun provideDerivationManager(appStateHolder: AppStateHolder): DerivationManager { + return DerivationManagerImpl( + appStateHolder = appStateHolder, + ) + } +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/di/CoroutineDispatcherProviderModule.kt b/core/utils/src/main/java/com/tangem/utils/di/CoroutineDispatcherProviderModule.kt new file mode 100644 index 0000000000..df85354292 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/di/CoroutineDispatcherProviderModule.kt @@ -0,0 +1,16 @@ +package com.tangem.utils.di + +import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface CoroutineDispatcherProviderModule { + + @Binds + fun bindCoroutineDispatcherProvider(coroutineDispatcherProvider: AppCoroutineDispatcherProvider): CoroutineDispatcherProvider +} \ No newline at end of file diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 6e1c48864e..6909db2791 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -75,9 +75,13 @@ dependencies { implementation(project(":common")) /** Tangem libraries */ - implementation(Tangem.blockchain) + implementation(Tangem.blockchain) { + exclude(module = "joda-time") + } implementation(Tangem.cardCore) - implementation(Tangem.cardAndroid) + implementation(Tangem.cardAndroid) { + exclude(module = "joda-time") + } /** Other libraries */ implementation(Library.reKotlin) diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt index bf2387f070..789d5d3bc8 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -7,9 +7,8 @@ import com.tangem.feature.referral.domain.ReferralRepository import com.tangem.feature.referral.domain.models.ReferralData import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext -import javax.inject.Inject -internal class ReferralRepositoryImpl @Inject constructor( +internal class ReferralRepositoryImpl( private val referralApi: ReferralApi, private val referralConverter: ReferralConverter, private val coroutineDispatcher: CoroutineDispatcherProvider, diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt new file mode 100644 index 0000000000..7a88c423cc --- /dev/null +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.referral.di + +import com.tangem.datasource.api.referral.ReferralApi +import com.tangem.feature.referral.converters.ReferralConverter +import com.tangem.feature.referral.data.ReferralRepositoryImpl +import com.tangem.feature.referral.domain.ReferralRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class ReferralRepositoryModule { + + @Provides + @Singleton + fun provideReferralRepository( + referralApi: ReferralApi, + referralConverter: ReferralConverter, + coroutineDispatcherProvider: CoroutineDispatcherProvider, + ): ReferralRepository { + return ReferralRepositoryImpl( + referralApi, referralConverter, coroutineDispatcherProvider, + ) + } +} \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt index 4ee8e69395..2f756567f1 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/converter/TokensConverter.kt @@ -5,8 +5,9 @@ import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.NativeToken import com.tangem.lib.crypto.models.NonNativeToken import com.tangem.utils.converter.Converter +import javax.inject.Inject -class TokensConverter : Converter { +class TokensConverter @Inject constructor() : Converter { override fun convert(value: TokenData): Currency { return if (value.decimalCount != null && diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt new file mode 100644 index 0000000000..0f0c22b0f0 --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.referral.domain.di + +import com.tangem.feature.referral.domain.ReferralInteractor +import com.tangem.feature.referral.domain.ReferralInteractorImpl +import com.tangem.feature.referral.domain.ReferralRepository +import com.tangem.feature.referral.domain.converter.TokensConverter +import com.tangem.lib.crypto.DerivationManager +import com.tangem.lib.crypto.UserWalletManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class ReferralDomainModule { + + @Provides + @Singleton + fun provideReferralInteractor( + referralRepository: ReferralRepository, + derivationManager: DerivationManager, + userWalletManager: UserWalletManager, + tokensConverter: TokensConverter, + ): ReferralInteractor { + return ReferralInteractorImpl( + referralRepository, derivationManager, userWalletManager, tokensConverter, + ) + } +} \ No newline at end of file diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts index 73a9372e4b..b2763c706c 100644 --- a/features/referral/presentation/build.gradle.kts +++ b/features/referral/presentation/build.gradle.kts @@ -1,6 +1,8 @@ plugins { id("com.android.library") kotlin("android") + kotlin("kapt") + id("com.google.dagger.hilt.android") } android { @@ -42,6 +44,13 @@ dependencies { implementation(Compose.material) implementation(Compose.uiTooling) + /** Domain */ + implementation(project(":features:referral:domain")) + /** Other libraries */ implementation(Library.composeShimmer) + + /** DI */ + implementation(Library.hilt) + kapt(Library.hiltKapt) } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/presentation/ReferralViewModel.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/presentation/ReferralViewModel.kt new file mode 100644 index 0000000000..e106965a14 --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/presentation/ReferralViewModel.kt @@ -0,0 +1,11 @@ +package com.tangem.feature.referral.presentation + +import androidx.lifecycle.ViewModel +import com.tangem.feature.referral.domain.ReferralInteractor +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class ReferralViewModel @Inject constructor(referralInteractor: ReferralInteractor) : ViewModel() { + +} \ No newline at end of file From 544855747a44d011f8bded8ec2eee9bbca15de46 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 21 Nov 2022 23:17:36 +0300 Subject: [PATCH 066/372] Updated on 2026-08-14 --- .../com/tangem/core/ui/components/Buttons.kt | 16 +- .../com/tangem/core/ui/components/Cards.kt | 4 +- .../appbar/AppBarWithAdditionalButtons.kt | 12 +- .../components/appbar/AppBarWithBackButton.kt | 4 +- .../components/atoms/BottomSheetIndicator.kt | 4 +- .../com/tangem/core/ui/res/ButtonColor.kt | 8 + .../main/java/com/tangem/core/ui/res/Color.kt | 45 ---- .../java/com/tangem/core/ui/res/IconColor.kt | 12 + .../tangem/core/ui/res/TangemColorPalette.kt | 48 ++++ .../com/tangem/core/ui/res/TangemColors.kt | 237 ++++++++++++++++++ .../com/tangem/core/ui/res/TangemDimens.kt | 61 +++++ .../com/tangem/core/ui/res/TangemShapes.kt | 22 ++ .../com/tangem/core/ui/res/TangemTheme.kt | 219 +++++++++++++--- .../java/com/tangem/core/ui/res/TextColor.kt | 11 + .../ui/AgreementBottomSheetContent.kt | 4 +- .../feature/referral/ui/AgreementText.kt | 4 +- .../referral/ui/NonParticipateBottomBlock.kt | 4 +- .../referral/ui/ParticipateBottomBlock.kt | 4 +- .../feature/referral/ui/ReferralScreen.kt | 21 +- 19 files changed, 620 insertions(+), 120 deletions(-) delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/res/Color.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index 65b331e15c..cc09da8dbc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -27,7 +27,7 @@ import com.tangem.core.ui.res.textColor @Preview(widthDp = 328, heightDp = 48, showBackground = true) @Composable fun Preview_PrimaryStartIconButton_Enabled_InLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { PrimaryStartIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) } } @@ -35,7 +35,7 @@ fun Preview_PrimaryStartIconButton_Enabled_InLightTheme() { @Preview(widthDp = 328, heightDp = 48, showBackground = true) @Composable fun Preview_PrimaryStartIconButton_Enabled_InDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { PrimaryStartIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) } } @@ -43,7 +43,7 @@ fun Preview_PrimaryStartIconButton_Enabled_InDarkTheme() { @Preview(widthDp = 328, heightDp = 48, showBackground = true) @Composable fun Preview_PrimaryStartIconButton_Disabled_InLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { PrimaryStartIconButton( text = "Manage tokens", iconResId = R.drawable.ic_tangem, @@ -56,7 +56,7 @@ fun Preview_PrimaryStartIconButton_Disabled_InLightTheme() { @Preview(widthDp = 328, heightDp = 48, showBackground = true) @Composable fun Preview_PrimaryStartIconButton_Disabled_InDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { PrimaryStartIconButton( text = "Manage tokens", iconResId = R.drawable.ic_tangem, @@ -69,7 +69,7 @@ fun Preview_PrimaryStartIconButton_Disabled_InDarkTheme() { @Preview(widthDp = 328, heightDp = 48, showBackground = true) @Composable fun Preview_PrimaryEndIconButton_Enabled_InLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) } } @@ -77,7 +77,7 @@ fun Preview_PrimaryEndIconButton_Enabled_InLightTheme() { @Preview(widthDp = 328, heightDp = 48, showBackground = true) @Composable fun Preview_PrimaryEndIconButton_Enabled_InDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) } } @@ -85,7 +85,7 @@ fun Preview_PrimaryEndIconButton_Enabled_InDarkTheme() { @Preview(widthDp = 328, heightDp = 48, showBackground = true) @Composable fun Preview_PrimaryEndIconButton_Disabled_InLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = false, onClicked = {}) } } @@ -93,7 +93,7 @@ fun Preview_PrimaryEndIconButton_Disabled_InLightTheme() { @Preview(widthDp = 328, heightDp = 48, showBackground = true) @Composable fun Preview_PrimaryEndIconButton_Disabled_InDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = false, onClicked = {}) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt index 57a47dff46..ef25595ace 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Cards.kt @@ -22,7 +22,7 @@ import com.tangem.core.ui.res.textColor @Preview(widthDp = 328, heightDp = 48, showBackground = true) @Composable fun Preview_SimpleInfoCard_InLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { SmallInfoCard(startText = "Balance", endText = "0.4405434 BTC") } } @@ -30,7 +30,7 @@ fun Preview_SimpleInfoCard_InLightTheme() { @Preview(widthDp = 328, heightDp = 48, showBackground = true) @Composable fun Preview_SimpleInfoCard_InDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { SmallInfoCard(startText = "Balance", endText = "0.4405434 BTC") } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt index 73cca24307..34f25fe87c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt @@ -79,7 +79,7 @@ fun AppBarWithAdditionalButtons( @Preview(widthDp = 360, heightDp = 56, showBackground = true) @Composable fun Preview_AppBarWithAdditionalButtons_InLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { AppBarWithAdditionalButtons( text = "Tangem", startButton = AdditionalButton( @@ -97,7 +97,7 @@ fun Preview_AppBarWithAdditionalButtons_InLightTheme() { @Preview(widthDp = 360, heightDp = 56, showBackground = true) @Composable fun Preview_AppBarWithAdditionalButtons_InDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { AppBarWithAdditionalButtons( text = "Tangem", startButton = AdditionalButton( @@ -115,7 +115,7 @@ fun Preview_AppBarWithAdditionalButtons_InDarkTheme() { @Preview(widthDp = 360, heightDp = 56, showBackground = true) @Composable fun Preview_AppBarWithOnlyStartButtons_InLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { AppBarWithAdditionalButtons( text = "Tangem", startButton = AdditionalButton( @@ -129,7 +129,7 @@ fun Preview_AppBarWithOnlyStartButtons_InLightTheme() { @Preview(widthDp = 360, heightDp = 56, showBackground = true) @Composable fun Preview_AppBarWithOnlyStartButtons_InDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { AppBarWithAdditionalButtons( text = "Tangem", startButton = AdditionalButton( @@ -143,7 +143,7 @@ fun Preview_AppBarWithOnlyStartButtons_InDarkTheme() { @Preview(widthDp = 360, heightDp = 56, showBackground = true) @Composable fun Preview_AppBarWithOnlyEndButtons_InLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { AppBarWithAdditionalButtons( text = "Tangem", endButton = AdditionalButton( @@ -157,7 +157,7 @@ fun Preview_AppBarWithOnlyEndButtons_InLightTheme() { @Preview(widthDp = 360, heightDp = 56, showBackground = true) @Composable fun Preview_AppBarWithOnlyEndButtons_InDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { AppBarWithAdditionalButtons( text = "Tangem", endButton = AdditionalButton( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt index 99d6aa527a..629f6f7592 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt @@ -21,7 +21,7 @@ import com.tangem.core.ui.res.TangemTheme @Preview(widthDp = 360, heightDp = 56, showBackground = true) @Composable fun PreviewAppBarWithBackButtonInLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { AppBarWithBackButton(text = "Title", onBackClick = {}) } } @@ -29,7 +29,7 @@ fun PreviewAppBarWithBackButtonInLightTheme() { @Preview(widthDp = 360, heightDp = 56, showBackground = true) @Composable fun PreviewAppBarWithBackButtonInDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { AppBarWithBackButton(text = "Title", onBackClick = {}) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/BottomSheetIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/BottomSheetIndicator.kt index edf127e645..800b674c51 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/BottomSheetIndicator.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/BottomSheetIndicator.kt @@ -46,7 +46,7 @@ fun BottomSheetIndicator() { @Preview(heightDp = 20, showBackground = true) @Composable fun Preview_BottomSheetIndicator_InLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { BottomSheetIndicator() } } @@ -54,7 +54,7 @@ fun Preview_BottomSheetIndicator_InLightTheme() { @Preview(heightDp = 20, showBackground = true) @Composable fun Preview_BottomSheetIndicator_InDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { BottomSheetIndicator() } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/ButtonColor.kt b/core/ui/src/main/java/com/tangem/core/ui/res/ButtonColor.kt index cb6607c8a3..427be16fe5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/ButtonColor.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/ButtonColor.kt @@ -3,7 +3,15 @@ package com.tangem.core.ui.res import androidx.compose.material.Colors import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.res.TangemColorPalette.Dark5 +import com.tangem.core.ui.res.TangemColorPalette.Dark6 +import com.tangem.core.ui.res.TangemColorPalette.DarkGreen +import com.tangem.core.ui.res.TangemColorPalette.Light1 +import com.tangem.core.ui.res.TangemColorPalette.Light2 +import com.tangem.core.ui.res.TangemColorPalette.MagicMint +import com.tangem.core.ui.res.TangemColorPalette.Meadow +@Deprecated("Use TangemTheme.colors") enum class ButtonColorType(val lightColor: Color, val darkColor: Color) { PRIMARY(lightColor = Dark6, darkColor = Light1), SECONDARY(lightColor = Light2, darkColor = Dark5), diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/Color.kt b/core/ui/src/main/java/com/tangem/core/ui/res/Color.kt deleted file mode 100644 index 4d37ecee45..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/res/Color.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.core.ui.res - -import androidx.compose.ui.graphics.Color - -// region Base -val Black = Color(0xFF000000) -val White = Color(0xFFFFFFFF) -// endregion Base - -// region Dark -val Dark1 = Color(0xFF919191) -val Dark2 = Color(0xFF656565) -val Dark3 = Color(0xFF494949) -val Dark4 = Color(0xFF3B3B3B) -val Dark5 = Color(0xFF303030) -val Dark6 = Color(0xFF1E1E1E) -// endregion Dark - -// region Light -val Light1 = Color(0xFFF5F5F5) -val Light2 = Color(0xFFEBEBEB) -val Light3 = Color(0xFFD3D3D3) -val Light4 = Color(0xFFC9C9C9) -val Light5 = Color(0xFFB0B0B0) -// endregion Light - -// region Green -val Meadow = Color(0xFF1ACE80) -val MagicMint = Color(0xFFA3EBCC) -val DarkGreen = Color(0xFF06311F) -// endregion Green - -// region Blue -val Cobalt = Color(0xFF0029FF) -val Azure = Color(0xFF007AFF) -// endregion Blue - -// region Red -val Amaranth = Color(0xFFDE1010) -val Flamingo = Color(0xFFED7979) -// endregion Red - -// region Yellow -val Tangerine = Color(0xFFFFB71B) -// endregion Yellow \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/IconColor.kt b/core/ui/src/main/java/com/tangem/core/ui/res/IconColor.kt index 72cc8936bb..11dab52a7b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/IconColor.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/IconColor.kt @@ -3,7 +3,19 @@ package com.tangem.core.ui.res import androidx.compose.material.Colors import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.res.TangemColorPalette.Amaranth +import com.tangem.core.ui.res.TangemColorPalette.Black +import com.tangem.core.ui.res.TangemColorPalette.Dark1 +import com.tangem.core.ui.res.TangemColorPalette.Dark2 +import com.tangem.core.ui.res.TangemColorPalette.Dark4 +import com.tangem.core.ui.res.TangemColorPalette.Dark6 +import com.tangem.core.ui.res.TangemColorPalette.Light4 +import com.tangem.core.ui.res.TangemColorPalette.Light5 +import com.tangem.core.ui.res.TangemColorPalette.Meadow +import com.tangem.core.ui.res.TangemColorPalette.Tangerine +import com.tangem.core.ui.res.TangemColorPalette.White +@Deprecated("Use TangemTheme.colors") enum class IconColorType(val lightColor: Color, val darkColor: Color) { PRIMARY1(lightColor = Black, darkColor = White), PRIMARY2(lightColor = White, darkColor = Dark6), diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt new file mode 100644 index 0000000000..5f9617639f --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt @@ -0,0 +1,48 @@ +package com.tangem.core.ui.res + +import androidx.compose.ui.graphics.Color + +@Suppress("unused") +object TangemColorPalette { + // region Base + val Black = Color(0xFF000000) + val White = Color(0xFFFFFFFF) + // endregion Base + + // region Dark + val Dark1 = Color(0xFF919191) + val Dark2 = Color(0xFF656565) + val Dark3 = Color(0xFF494949) + val Dark4 = Color(0xFF3B3B3B) + val Dark5 = Color(0xFF303030) + val Dark6 = Color(0xFF1E1E1E) + // endregion Dark + + // region Light + val Light1 = Color(0xFFF5F5F5) + val Light2 = Color(0xFFEBEBEB) + val Light3 = Color(0xFFD3D3D3) + val Light4 = Color(0xFFC9C9C9) + val Light5 = Color(0xFFB0B0B0) + // endregion Light + + // region Green + val Meadow = Color(0xFF1ACE80) + val MagicMint = Color(0xFFA3EBCC) + val DarkGreen = Color(0xFF06311F) + // endregion Green + + // region Blue + val Cobalt = Color(0xFF0029FF) + val Azure = Color(0xFF007AFF) + // endregion Blue + + // region Red + val Amaranth = Color(0xFFDE1010) + val Flamingo = Color(0xFFED7979) + // endregion Red + + // region Yellow + val Tangerine = Color(0xFFFFB71B) + // endregion Yellow +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt new file mode 100644 index 0000000000..371e70d22e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt @@ -0,0 +1,237 @@ +package com.tangem.core.ui.res + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color + +@Suppress("unused", "MemberVisibilityCanBePrivate") +@Stable +class TangemColors internal constructor( + text: Text, + icon: Icon, + button: Button, + background: Background, + control: Control, + stroke: Stroke, + field: Field, +) { + var text by mutableStateOf(text) + private set + var icon by mutableStateOf(icon) + private set + var button by mutableStateOf(button) + private set + var background by mutableStateOf(background) + private set + var control by mutableStateOf(control) + private set + var stroke by mutableStateOf(stroke) + private set + var field by mutableStateOf(field) + private set + + @Stable + class Text internal constructor( + primary1: Color, + primary2: Color, + secondary: Color, + tertiary: Color, + disabled: Color, + accent: Color = TangemColorPalette.Meadow, + warning: Color = TangemColorPalette.Amaranth, + attention: Color = TangemColorPalette.Tangerine, + constantWhite: Color = TangemColorPalette.White, + ) { + var primary1 by mutableStateOf(primary1) + private set + var primary2 by mutableStateOf(primary2) + private set + var secondary by mutableStateOf(secondary) + private set + var tertiary by mutableStateOf(tertiary) + private set + var disabled by mutableStateOf(disabled) + private set + var accent by mutableStateOf(accent) + private set + var warning by mutableStateOf(warning) + private set + var attention by mutableStateOf(attention) + private set + var constantWhite by mutableStateOf(constantWhite) + private set + + fun update(other: Text) { + primary1 = other.primary1 + primary2 = other.primary2 + secondary = other.secondary + tertiary = other.tertiary + disabled = other.disabled + accent = other.accent + } + } + + @Stable + class Icon internal constructor( + primary1: Color, + primary2: Color, + secondary: Color, + informative: Color, + inactive: Color, + accent: Color = TangemColorPalette.Meadow, + warning: Color = TangemColorPalette.Amaranth, + attention: Color = TangemColorPalette.Tangerine, + ) { + var primary1 by mutableStateOf(primary1) + private set + var primary2 by mutableStateOf(primary2) + private set + var secondary by mutableStateOf(secondary) + private set + var informative by mutableStateOf(informative) + private set + var inactive by mutableStateOf(inactive) + private set + var accent by mutableStateOf(accent) + private set + var warning by mutableStateOf(warning) + private set + var attention by mutableStateOf(attention) + private set + + fun update(other: Icon) { + primary1 = other.primary1 + primary2 = other.primary2 + secondary = other.secondary + informative = other.informative + inactive = other.inactive + accent = other.accent + warning = other.warning + attention = other.attention + } + } + + @Stable + class Button internal constructor( + primary: Color, + secondary: Color, + disabled: Color, + positive: Color = TangemColorPalette.Meadow, + positiveDisabled: Color, + ) { + var primary by mutableStateOf(primary) + private set + var secondary by mutableStateOf(secondary) + private set + var disabled by mutableStateOf(disabled) + private set + var positive by mutableStateOf(positive) + private set + var positiveDisabled by mutableStateOf(positiveDisabled) + private set + + fun update(other: Button) { + primary = other.primary + secondary = other.secondary + disabled = other.disabled + positive = other.positive + positiveDisabled = other.positiveDisabled + } + } + + @Stable + class Background internal constructor( + primary: Color, + secondary: Color, + plain: Color, + action: Color, + fade: Color, + ) { + var primary by mutableStateOf(primary) + private set + var secondary by mutableStateOf(secondary) + private set + var plain by mutableStateOf(plain) + private set + var action by mutableStateOf(action) + private set + var fade by mutableStateOf(fade) + private set + + fun update(other: Background) { + primary = other.primary + secondary = other.secondary + plain = other.plain + action = other.action + fade = other.fade + } + } + + @Stable + class Control internal constructor( + checked: Color, + unchecked: Color, + key: Color, + ) { + var checked by mutableStateOf(checked) + private set + var unchecked by mutableStateOf(unchecked) + private set + var key by mutableStateOf(key) + private set + + fun update(other: Control) { + checked = other.checked + unchecked = other.unchecked + key = other.key + } + } + + @Stable + class Stroke internal constructor( + primary: Color, + secondary: Color, + transparency: Color, + ) { + var primary by mutableStateOf(primary) + private set + var secondary by mutableStateOf(secondary) + private set + var transparency by mutableStateOf(transparency) + private set + + fun update(other: Stroke) { + primary = other.primary + secondary = other.secondary + transparency = other.transparency + } + } + + @Stable + class Field internal constructor( + primary: Color, + focused: Color, + ) { + var primary by mutableStateOf(primary) + private set + var focused by mutableStateOf(focused) + private set + + fun update(other: Field) { + primary = other.primary + focused = other.focused + } + } + + fun update(other: TangemColors) { + text.update(other.text) + icon.update(other.icon) + button.update(other.button) + background.update(other.background) + control.update(other.control) + stroke.update(other.stroke) + field.update(other.field) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt new file mode 100644 index 0000000000..441b0bfd52 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -0,0 +1,61 @@ +package com.tangem.core.ui.res + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Immutable +data class TangemDimens internal constructor( + // region Elevation + val elevation0: Dp = 0.dp, + val elevation1: Dp = 1.dp, + val elevation2: Dp = 2.dp, + val elevation3: Dp = 3.dp, + val elevation4: Dp = 4.dp, + val elevation6: Dp = 6.dp, + val elevation8: Dp = 8.dp, + val elevation12: Dp = 12.dp, + val elevation16: Dp = 16.dp, + val elevation24: Dp = 24.dp, + // endregion Elevation + // region Radius + val radius2: Dp = 2.dp, + val radius4: Dp = 4.dp, + val radius6: Dp = 6.dp, + val radius12: Dp = 12.dp, + val radius16: Dp = 16.dp, + val radius28: Dp = 28.dp, + // endregion Radius + // region Size + val size0: Dp = 0.dp, + val size4: Dp = 4.dp, + val size8: Dp = 8.dp, + val size11: Dp = 11.dp, + val size16: Dp = 16.dp, + val size20: Dp = 20.dp, + val size24: Dp = 24.dp, + val size28: Dp = 28.dp, + val size32: Dp = 32.dp, + val size40: Dp = 40.dp, + val size48: Dp = 48.dp, + val size56: Dp = 56.dp, + val size84: Dp = 84.dp, + val size86: Dp = 86.dp, + val size102: Dp = 102.dp, + val size164: Dp = 164.dp, + val size200: Dp = 200.dp, + // endregion Size + // region Spacing + val spacing2: Dp = 2.dp, + val spacing4: Dp = 4.dp, + val spacing8: Dp = 8.dp, + val spacing10: Dp = 10.dp, + val spacing12: Dp = 12.dp, + val spacing16: Dp = 16.dp, + val spacing24: Dp = 24.dp, + val spacing32: Dp = 32.dp, + val spacing44: Dp = 44.dp, + val spacing50: Dp = 50.dp, + val spacing54: Dp = 54.dp, + // endregion Spacing +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt new file mode 100644 index 0000000000..99a87a701e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt @@ -0,0 +1,22 @@ +package com.tangem.core.ui.res + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Shape + +@Immutable +data class TangemShapes internal constructor( + val roundedCornersSmall: Shape, + val roundedCornersSmall2: Shape, + val roundedCornersMedium: Shape, + val roundedCornersLarge: Shape, + val bottomSheet: Shape, +) { + constructor(dimens: TangemDimens) : this( + roundedCornersSmall = RoundedCornerShape(size = dimens.radius2), + roundedCornersSmall2 = RoundedCornerShape(size = dimens.radius4), + roundedCornersMedium = RoundedCornerShape(size = dimens.radius12), + roundedCornersLarge = RoundedCornerShape(size = dimens.radius28), + bottomSheet = RoundedCornerShape(size = dimens.radius16), + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index ebd0da2f57..8ff81a2640 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -1,53 +1,200 @@ package com.tangem.core.ui.res +import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme -import androidx.compose.material.darkColors -import androidx.compose.material.lightColors +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.Typography import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import com.google.accompanist.systemuicontroller.rememberSystemUiController +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.staticCompositionLocalOf internal val isSystemInDarkTheme: Boolean = false // TODO: use isSystemInDarkTheme() for automatic color detection @Composable fun TangemTheme( - isDarkTheme: Boolean = isSystemInDarkTheme, + isDark: Boolean = false, + typography: Typography = TangemTheme.typography, + dimens: TangemDimens = TangemTheme.dimens, content: @Composable () -> Unit, ) { - val systemUiController = rememberSystemUiController() - SideEffect { - systemUiController.setSystemBarsColor(color = if (isDarkTheme) Black else Light1, darkIcons = !isDarkTheme) - } + val themeColors = if (isDark) darkThemeColors() else lightThemeColors() + val rememberedColors = remember { themeColors } + .also { it.update(themeColors) } + + val shapes = remember { TangemShapes(dimens) } MaterialTheme( - colors = if (isDarkTheme) DarkColors else LightColors, - typography = TangemTypography, - content = content, + colors = materialThemeColors(colors = themeColors, isDark = isDark), + typography = typography, + ) { + CompositionLocalProvider( + LocalTangemColors provides rememberedColors, + LocalTangemTypography provides typography, + LocalTangemDimens provides dimens, + LocalTangemShapes provides shapes, + ) { + ProvideTextStyle( + value = TangemTheme.typography.body1, + content = content, + ) + } + } +} + +object TangemTheme { + val colors: TangemColors + @Composable + @ReadOnlyComposable + get() = LocalTangemColors.current + + val typography: Typography + @Composable + @ReadOnlyComposable + get() = LocalTangemTypography.current + + val dimens: TangemDimens + @Composable + @ReadOnlyComposable + get() = LocalTangemDimens.current + + val shapes: TangemShapes + @Composable + @ReadOnlyComposable + get() = LocalTangemShapes.current +} + +@Stable +@Composable +private fun materialThemeColors( + colors: TangemColors, + isDark: Boolean, +): Colors { + return Colors( + primary = colors.background.primary, + primaryVariant = colors.background.secondary, + secondary = colors.button.primary, + secondaryVariant = colors.text.accent, + background = colors.background.primary, + surface = colors.background.plain, + error = colors.text.warning, + onPrimary = colors.text.primary1, + onSecondary = colors.text.primary1, + onBackground = colors.text.primary1, + onSurface = colors.text.primary1, + onError = colors.text.primary2, + isLight = !isDark, ) } -private val LightColors = lightColors( - primary = Light1, - primaryVariant = Light1, - secondary = White, - background = Light1, - surface = White, - error = Amaranth, - onPrimary = Dark6, - onSecondary = Dark6, - onBackground = Dark6, - onSurface = Dark6, -) +@Composable +private fun lightThemeColors(): TangemColors { + return TangemColors( + text = TangemColors.Text( + primary1 = TangemColorPalette.Dark6, + primary2 = TangemColorPalette.White, + secondary = TangemColorPalette.Dark2, + tertiary = TangemColorPalette.Dark1, + disabled = TangemColorPalette.Light4, + ), + icon = TangemColors.Icon( + primary1 = TangemColorPalette.Black, + primary2 = TangemColorPalette.White, + secondary = TangemColorPalette.Dark2, + informative = TangemColorPalette.Light5, + inactive = TangemColorPalette.Light4, + ), + button = TangemColors.Button( + primary = TangemColorPalette.Dark6, + secondary = TangemColorPalette.Light2, + disabled = TangemColorPalette.Light2, + positiveDisabled = TangemColorPalette.MagicMint, + ), + background = TangemColors.Background( + primary = TangemColorPalette.White, + secondary = TangemColorPalette.Light1, + plain = TangemColorPalette.White, + action = TangemColorPalette.Black, + fade = TangemColorPalette.White, + ), + control = TangemColors.Control( + checked = TangemColorPalette.Meadow, + unchecked = TangemColorPalette.Light2, + key = TangemColorPalette.White, + ), + stroke = TangemColors.Stroke( + primary = TangemColorPalette.Light2, + secondary = TangemColorPalette.Dark4, + transparency = TangemColorPalette.White, + ), + field = TangemColors.Field( + primary = TangemColorPalette.Light1, + focused = TangemColorPalette.Light2, + ), + ) +} -private val DarkColors = darkColors( - primary = Black, - primaryVariant = Black, - secondary = Dark6, - background = Black, - surface = Dark6, - error = Amaranth, - onPrimary = White, - onSecondary = White, - onBackground = White, - onSurface = White, -) \ No newline at end of file +@Composable +private fun darkThemeColors(): TangemColors { + return TangemColors( + text = TangemColors.Text( + primary1 = TangemColorPalette.White, + primary2 = TangemColorPalette.Dark6, + secondary = TangemColorPalette.Light5, + tertiary = TangemColorPalette.Dark1, + disabled = TangemColorPalette.Dark3, + ), + icon = TangemColors.Icon( + primary1 = TangemColorPalette.White, + primary2 = TangemColorPalette.Dark6, + secondary = TangemColorPalette.Dark1, + informative = TangemColorPalette.Dark2, + inactive = TangemColorPalette.Dark4, + ), + button = TangemColors.Button( + primary = TangemColorPalette.Light1, + secondary = TangemColorPalette.Dark5, + disabled = TangemColorPalette.Dark6, + positiveDisabled = TangemColorPalette.DarkGreen, + ), + background = TangemColors.Background( + primary = TangemColorPalette.Dark6, + secondary = TangemColorPalette.Black, + plain = TangemColorPalette.Black, + action = TangemColorPalette.Light4, + fade = TangemColorPalette.Black, + ), + control = TangemColors.Control( + checked = TangemColorPalette.Meadow, + unchecked = TangemColorPalette.Dark4, + key = TangemColorPalette.Light1, + ), + stroke = TangemColors.Stroke( + primary = TangemColorPalette.Dark5, + secondary = TangemColorPalette.Dark1, + transparency = TangemColorPalette.Dark6, + ), + field = TangemColors.Field( + primary = TangemColorPalette.Dark5, + focused = TangemColorPalette.Dark4, + ), + ) +} + +private val LocalTangemColors = staticCompositionLocalOf { + error("No TangemColors provided") +} + +private val LocalTangemTypography = staticCompositionLocalOf { + TangemTypography +} + +private val LocalTangemDimens = staticCompositionLocalOf { + TangemDimens() +} + +private val LocalTangemShapes = staticCompositionLocalOf { + error("No TangemShapes provided") +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TextColor.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TextColor.kt index 44f94e1157..0fce11d9e7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TextColor.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TextColor.kt @@ -3,7 +3,18 @@ package com.tangem.core.ui.res import androidx.compose.material.Colors import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.res.TangemColorPalette.Amaranth +import com.tangem.core.ui.res.TangemColorPalette.Dark1 +import com.tangem.core.ui.res.TangemColorPalette.Dark2 +import com.tangem.core.ui.res.TangemColorPalette.Dark3 +import com.tangem.core.ui.res.TangemColorPalette.Dark6 +import com.tangem.core.ui.res.TangemColorPalette.Light4 +import com.tangem.core.ui.res.TangemColorPalette.Light5 +import com.tangem.core.ui.res.TangemColorPalette.Meadow +import com.tangem.core.ui.res.TangemColorPalette.Tangerine +import com.tangem.core.ui.res.TangemColorPalette.White +@Deprecated("Use TangemTheme.colors") enum class TextColorType(val lightColor: Color, val darkColor: Color) { PRIMARY1(lightColor = Dark6, darkColor = White), PRIMARY2(lightColor = White, darkColor = Dark6), diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt index a1f8adc86c..e7d30282c8 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt @@ -66,7 +66,7 @@ private fun AgreementHtmlView(url: String) { @Preview @Composable fun Preview_AgreementBottomSheet_InLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { AgreementBottomSheetContent(url = "https://tangem.com/en/") } } @@ -74,7 +74,7 @@ fun Preview_AgreementBottomSheet_InLightTheme() { @Preview @Composable fun Preview_AgreementBottomSheet_InDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { AgreementBottomSheetContent(url = "https://tangem.com/en/") } } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt index 133b6b374e..cbdcdaeb50 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt @@ -25,7 +25,7 @@ import com.tangem.feature.referral.presentation.R @Preview(widthDp = 360, showBackground = true) @Composable fun Preview_AgreementText_InLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { Box(modifier = Modifier.background(MaterialTheme.colors.primary)) { AgreementText(firstPartResId = R.string.referral_tou_not_enroled_prefix, onClicked = {}) } @@ -35,7 +35,7 @@ fun Preview_AgreementText_InLightTheme() { @Preview(widthDp = 360, showBackground = true) @Composable fun Preview_AgreementText_InDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { Box(modifier = Modifier.background(MaterialTheme.colors.primary)) { AgreementText(firstPartResId = R.string.referral_tou_not_enroled_prefix, onClicked = {}) } diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt index 59b7ab8148..2706490417 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt @@ -26,7 +26,7 @@ import com.tangem.feature.referral.presentation.R @Preview(widthDp = 360, showBackground = true) @Composable fun Preview_NonParticipateBottomBlock_InLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { Column(Modifier.background(MaterialTheme.colors.primary)) { NonParticipateBottomBlock( purchasedWalletCount = 3, @@ -42,7 +42,7 @@ fun Preview_NonParticipateBottomBlock_InLightTheme() { @Preview(widthDp = 360, showBackground = true) @Composable fun Preview_NonParticipateBottomBlock_InDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { Column(Modifier.background(MaterialTheme.colors.primary)) { NonParticipateBottomBlock( purchasedWalletCount = 3, diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt index 7294cbd3f7..f6c6eced81 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt @@ -16,7 +16,7 @@ import com.tangem.feature.referral.presentation.R @Preview(widthDp = 360, showBackground = true) @Composable fun Preview_ParticipateBottomBlock_InLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { Column(Modifier.background(MaterialTheme.colors.primary)) { ParticipateBottomBlock(onAgreementClicked = {}, onParticipateClicked = {}) } @@ -26,7 +26,7 @@ fun Preview_ParticipateBottomBlock_InLightTheme() { @Preview(widthDp = 360, showBackground = true) @Composable fun Preview_ParticipateBottomBlock_InDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { Column(Modifier.background(MaterialTheme.colors.primary)) { ParticipateBottomBlock(onAgreementClicked = {}, onParticipateClicked = {}) } diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index a9935a04cc..7091674dcd 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -42,9 +42,9 @@ import com.tangem.core.ui.components.VerticalSpacer import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.ButtonColorType import com.tangem.core.ui.res.IconColorType +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TextColorType -import com.tangem.core.ui.res.White import com.tangem.core.ui.res.buttonColor import com.tangem.core.ui.res.iconColor import com.tangem.core.ui.res.textColor @@ -291,14 +291,14 @@ private fun ShimmerInfo() { .clip(RoundedCornerShape(dimensionResource(id = R.dimen.radius6))) .width(dimensionResource(id = R.dimen.size102)) .height(dimensionResource(id = R.dimen.size16)) - .background(White), + .background(TangemColorPalette.White), ) Box( modifier = Modifier .clip(RoundedCornerShape(dimensionResource(id = R.dimen.radius6))) .width(dimensionResource(id = R.dimen.size40)) .height(dimensionResource(id = R.dimen.size11)) - .background(White), + .background(TangemColorPalette.White), ) } } @@ -325,7 +325,7 @@ private fun NonParticipantBottomBlock(state: ReferralInfoState.NonParticipantCon @Preview(widthDp = 360, showBackground = true) @Composable fun Preview_ReferralScreen_Participant_InLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { ReferralScreen( stateHolder = ReferralStateHolder( headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), @@ -347,7 +347,7 @@ fun Preview_ReferralScreen_Participant_InLightTheme() { @Preview(widthDp = 360, showBackground = true) @Composable fun Preview_ReferralScreen_Participant_InDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { ReferralScreen( stateHolder = ReferralStateHolder( headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), @@ -370,7 +370,7 @@ fun Preview_ReferralScreen_Participant_InDarkTheme() { @Preview(widthDp = 360, showBackground = true) @Composable fun Preview_ReferralScreen_NonParticipant_InLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { ReferralScreen( stateHolder = ReferralStateHolder( headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), @@ -395,7 +395,7 @@ fun Preview_ReferralScreen_NonParticipant_InLightTheme() { @Preview(widthDp = 360, showBackground = true) @Composable fun Preview_ReferralScreen_NonParticipant_InDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { ReferralScreen( stateHolder = ReferralStateHolder( headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), @@ -420,7 +420,7 @@ fun Preview_ReferralScreen_NonParticipant_InDarkTheme() { @Preview(widthDp = 360, showBackground = true) @Composable fun Preview_ReferralScreen_Loading_InLightTheme() { - TangemTheme(isDarkTheme = false) { + TangemTheme(isDark = false) { ReferralScreen( stateHolder = ReferralStateHolder( headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), @@ -437,7 +437,7 @@ fun Preview_ReferralScreen_Loading_InLightTheme() { @Preview(widthDp = 360, showBackground = true) @Composable fun Preview_ReferralScreen_Loading_InDarkTheme() { - TangemTheme(isDarkTheme = true) { + TangemTheme(isDark = true) { ReferralScreen( stateHolder = ReferralStateHolder( headerState = ReferralStateHolder.HeaderState(onBackClicked = {}), @@ -449,5 +449,4 @@ fun Preview_ReferralScreen_Loading_InDarkTheme() { ), ) } -} - +} \ No newline at end of file From 48ce9b4fd271c71cde8cee4aa442516074474ae1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 11 Nov 2022 21:25:42 +0400 Subject: [PATCH 067/372] Updated on 2026-08-14 --- .../tap/common/extensions/Navigation.kt | 16 +- .../redux/navigation/NavigationState.kt | 6 +- core/ui/build.gradle.kts | 3 + .../com/tangem/core/ui/components/Buttons.kt | 681 ++++++++++++++---- .../core/ui/components/ButtonsDeprecated.kt | 185 +++++ .../core/ui/components/SystemBarsEffect.kt | 14 + .../tangem/core/ui/components/TextFields.kt | 414 +++++++++++ .../components/atoms/BottomSheetIndicator.kt | 1 + .../tangem/core/ui/components/atoms/Hand.kt | 72 ++ .../fragments/ComposeBottomSheetFragment.kt | 87 +++ .../core/ui/fragments/ComposeFragment.kt | 57 ++ core/ui/src/main/res/drawable/ic_alert.xml | 10 + core/ui/src/main/res/drawable/ic_close.xml | 10 + core/ui/src/main/res/drawable/ic_plus.xml | 10 + core/ui/src/main/res/values/styles.xml | 12 + 15 files changed, 1429 insertions(+), 149 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/ButtonsDeprecated.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsEffect.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/atoms/Hand.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeBottomSheetFragment.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeFragment.kt create mode 100644 core/ui/src/main/res/drawable/ic_alert.xml create mode 100644 core/ui/src/main/res/drawable/ic_close.xml create mode 100644 core/ui/src/main/res/drawable/ic_plus.xml create mode 100644 core/ui/src/main/res/values/styles.xml diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index a9504e668a..5e07c484b8 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -1,6 +1,7 @@ package com.tangem.tap.common.extensions import androidx.activity.OnBackPressedCallback +import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager @@ -29,12 +30,10 @@ import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.wallet.R import timber.log.Timber -private class Navigation - fun FragmentActivity.openFragment( screen: AppScreen, addToBackstack: Boolean, - fgShareTransition: FragmentShareTransition? = null + fgShareTransition: FragmentShareTransition? = null, ) { val transaction = this.supportFragmentManager.beginTransaction() val fragment = fragmentFactory(screen) @@ -48,9 +47,14 @@ fun FragmentActivity.openFragment( } } } - transaction.replace(R.id.fragment_container, fragment, screen.name) - if (addToBackstack && screen != AppScreen.Home) transaction.addToBackStack(null) - transaction.commitAllowingStateLoss() + if (screen.isDialogFragment) { + (fragment as DialogFragment).show(transaction, screen.name) + if (addToBackstack && screen != AppScreen.Home) transaction.addToBackStack(null) + } else { + transaction.replace(R.id.fragment_container, fragment, screen.name) + if (addToBackstack && screen != AppScreen.Home) transaction.addToBackStack(null) + transaction.commitAllowingStateLoss() + } } fun FragmentActivity.popBackTo(screen: AppScreen?, inclusive: Boolean = false) { diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt index 224e12755b..05ec744393 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt @@ -9,7 +9,9 @@ data class NavigationState( val activity: WeakReference? = null, ) : StateType -enum class AppScreen { +enum class AppScreen( + val isDialogFragment: Boolean = false, +) { Home, Shop, Disclaimer, @@ -19,5 +21,5 @@ enum class AppScreen { Details, DetailsSecurity, CardSettings, AppSettings, ResetToFactory, AddTokens, AddCustomToken, WalletConnectSessions, - QrScan + QrScan, } \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 3c509f8c3f..6881f67082 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -30,6 +30,8 @@ android { } dependencies { + /** AndroidX libraries */ + implementation(AndroidX.fragmentKtx) /** Compose */ implementation(Compose.foundation) @@ -38,4 +40,5 @@ dependencies { /** Other libraries */ implementation(Library.accompanistSystemUiController) + implementation(Library.materialComponent) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index cc09da8dbc..11dae7b514 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -1,183 +1,582 @@ package com.tangem.core.ui.components -import androidx.annotation.DrawableRes -import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.Button +import androidx.compose.material.ButtonColors import androidx.compose.material.ButtonDefaults +import androidx.compose.material.ButtonElevation +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Divider import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.State +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import com.tangem.core.ui.R -import com.tangem.core.ui.res.ButtonColorType import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TextColorType -import com.tangem.core.ui.res.buttonColor -import com.tangem.core.ui.res.textColor +import com.tangem.core.ui.res.TangemTypography -@Preview(widthDp = 328, heightDp = 48, showBackground = true) +/** + * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=97%3A103&t=TmfD6UBHPg9uYfev-4) + * */ @Composable -fun Preview_PrimaryStartIconButton_Enabled_InLightTheme() { - TangemTheme(isDark = false) { - PrimaryStartIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -fun Preview_PrimaryStartIconButton_Enabled_InDarkTheme() { - TangemTheme(isDark = true) { - PrimaryStartIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -fun Preview_PrimaryStartIconButton_Disabled_InLightTheme() { - TangemTheme(isDark = false) { - PrimaryStartIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem, - enabled = false, - onClicked = {}, - ) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -fun Preview_PrimaryStartIconButton_Disabled_InDarkTheme() { - TangemTheme(isDark = true) { - PrimaryStartIconButton( - text = "Manage tokens", - iconResId = R.drawable.ic_tangem, - enabled = false, - onClicked = {}, - ) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -fun Preview_PrimaryEndIconButton_Enabled_InLightTheme() { - TangemTheme(isDark = false) { - PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -fun Preview_PrimaryEndIconButton_Enabled_InDarkTheme() { - TangemTheme(isDark = true) { - PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -fun Preview_PrimaryEndIconButton_Disabled_InLightTheme() { - TangemTheme(isDark = false) { - PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = false, onClicked = {}) - } -} - -@Preview(widthDp = 328, heightDp = 48, showBackground = true) -@Composable -fun Preview_PrimaryEndIconButton_Disabled_InDarkTheme() { - TangemTheme(isDark = true) { - PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = false, onClicked = {}) - } +fun TextButton( + modifier: Modifier = Modifier, + text: String, + enabled: Boolean = true, + onClick: () -> Unit, +) { + TangemButton( + modifier = modifier, + text = text, + icon = TangemButtonIcon.None, + onClick = onClick, + enabled = enabled, + showProgress = false, + colors = TangemButtonsDefaults.textButtonColors, + size = TangemButtonSize.Text, + ) } /** - * Primary button with an icon at the beginning of the layout - * - * @param modifier button modifier - * @param text button text - * @param iconResId button icon res id - * @param enabled controls the enabled state of the button - * @param onClicked the lambda to be invoked when this button is pressed - * - * @see Figma component - */ + * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=97%3A62&t=TmfD6UBHPg9uYfev-4) + * */ @Composable -fun PrimaryStartIconButton( +fun TextButtonIconLeft( modifier: Modifier = Modifier, text: String, - @DrawableRes iconResId: Int, + icon: Painter, enabled: Boolean = true, - onClicked: () -> Unit, + onClick: () -> Unit, ) { - PrimaryButtonRow(modifier = modifier, enabled = enabled, onClicked = onClicked) { - Icon( - painter = painterResource(id = iconResId), - contentDescription = null, - ) - Spacer(modifier = Modifier.width(dimensionResource(R.dimen.spacing8))) - Text(text = text) - } + TangemButton( + modifier = modifier, + text = text, + icon = TangemButtonIcon.Left(icon), + onClick = onClick, + enabled = enabled, + showProgress = false, + colors = TangemButtonsDefaults.textButtonColors, + size = TangemButtonSize.Text, + ) +} + +@Composable +fun PrimaryButton( + modifier: Modifier = Modifier, + text: String, + showProgress: Boolean = false, + enabled: Boolean = true, + onClick: () -> Unit, +) { + TangemButton( + modifier = modifier, + text = text, + icon = TangemButtonIcon.None, + onClick = onClick, + colors = TangemButtonsDefaults.primaryButtonColors, + enabled = enabled, + showProgress = showProgress, + ) } /** - * Primary button with an icon at the end of the layout - * - * @param modifier button modifier - * @param text button text - * @param iconResId button icon res id - * @param enabled controls the enabled state of the button - * @param onClicked the lambda to be invoked when this button is pressed - * - * @see Figma component - */ + * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=68%3A47&t=TmfD6UBHPg9uYfev-4) + * */ @Composable -fun PrimaryEndIconButton( +fun PrimaryButtonIconRight( modifier: Modifier = Modifier, text: String, - @DrawableRes iconResId: Int, + icon: Painter, + showProgress: Boolean = false, enabled: Boolean = true, - onClicked: () -> Unit, + onClick: () -> Unit, ) { - PrimaryButtonRow(modifier = modifier, enabled = enabled, onClicked = onClicked) { - Text(text = text) - Spacer(modifier = Modifier.width(dimensionResource(R.dimen.spacing8))) - Icon( - painter = painterResource(id = iconResId), - contentDescription = null, - ) - } + TangemButton( + modifier = modifier, + text = text, + icon = TangemButtonIcon.Right(icon), + onClick = onClick, + colors = TangemButtonsDefaults.primaryButtonColors, + enabled = enabled, + showProgress = showProgress, + ) +} + +/** + * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=233%3A258&t=TmfD6UBHPg9uYfev-4) + * */ +@Composable +fun PrimaryButtonIconLeft( + modifier: Modifier = Modifier, + text: String, + icon: Painter, + showProgress: Boolean = false, + enabled: Boolean = true, + onClick: () -> Unit, +) { + TangemButton( + modifier = modifier, + text = text, + icon = TangemButtonIcon.Left(icon), + onClick = onClick, + colors = TangemButtonsDefaults.primaryButtonColors, + enabled = enabled, + showProgress = showProgress, + ) } @Composable -private fun PrimaryButtonRow( - modifier: Modifier, +fun SecondaryButton( + modifier: Modifier = Modifier, + text: String, + showProgress: Boolean = false, + enabled: Boolean = true, + onClick: () -> Unit, +) { + TangemButton( + modifier = modifier, + text = text, + icon = TangemButtonIcon.None, + onClick = onClick, + colors = TangemButtonsDefaults.secondaryButtonColors, + enabled = enabled, + showProgress = showProgress, + ) +} + +/** + * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=99%3A50&t=TmfD6UBHPg9uYfev-4) + * */ +@Composable +fun SecondaryButtonIconRight( + modifier: Modifier = Modifier, + text: String, + icon: Painter, + showProgress: Boolean = false, + enabled: Boolean = true, + onClick: () -> Unit, +) { + TangemButton( + modifier = modifier, + text = text, + icon = TangemButtonIcon.Right(icon), + onClick = onClick, + colors = TangemButtonsDefaults.secondaryButtonColors, + enabled = enabled, + showProgress = showProgress, + ) +} + +/** + * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=233%3A262&t=TmfD6UBHPg9uYfev-4) + * */ +@Composable +fun SecondaryButtonIconLeft( + modifier: Modifier = Modifier, + text: String, + icon: Painter, + showProgress: Boolean = false, + enabled: Boolean = true, + onClick: () -> Unit, +) { + TangemButton( + modifier = modifier, + text = text, + icon = TangemButtonIcon.Left(icon), + onClick = onClick, + colors = TangemButtonsDefaults.secondaryButtonColors, + enabled = enabled, + showProgress = showProgress, + ) +} + +// region Defaults +@Composable +private fun TangemButton( + modifier: Modifier = Modifier, + text: String, + icon: TangemButtonIcon, + onClick: () -> Unit, + colors: ButtonColors, + showProgress: Boolean, enabled: Boolean, - onClicked: () -> Unit, - content: @Composable (RowScope.() -> Unit), + size: TangemButtonSize = TangemButtonSize.Default, + elevation: ButtonElevation = TangemButtonsDefaults.elevation, ) { Button( - onClick = onClicked, modifier = modifier - .fillMaxWidth() - .height(dimensionResource(R.dimen.size48)), + .width(IntrinsicSize.Min) + .height(IntrinsicSize.Min) + .heightIn(size.toHeightDp()), + onClick = { + if (!showProgress) { + onClick() + } + }, enabled = enabled, - shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius12)), - colors = ButtonDefaults.buttonColors( - backgroundColor = MaterialTheme.colors.buttonColor(type = ButtonColorType.PRIMARY), - contentColor = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY2), - disabledBackgroundColor = MaterialTheme.colors.buttonColor(type = ButtonColorType.DISABLED), - disabledContentColor = MaterialTheme.colors.textColor(type = TextColorType.DISABLED), - ), - content = content, - ) -} \ No newline at end of file + elevation = elevation, + shape = size.toShape(), + colors = colors, + ) { + ButtonContent( + text = text, + buttonIcon = icon, + colors = colors, + showProgress = showProgress, + enabled = enabled, + ) + } +} + +@Composable +private fun ButtonContent( + text: String, + buttonIcon: TangemButtonIcon, + colors: ButtonColors, + enabled: Boolean, + showProgress: Boolean, +) { + val icon = @Composable { painter: Painter -> + Icon( + modifier = Modifier.size(TangemTheme.dimens.size20), + painter = painter, + tint = colors.contentColor(enabled = enabled).value, + contentDescription = null, + ) + } + + if (showProgress) Box( + modifier = Modifier.wrapContentSize(), + ) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.Center) + .size(TangemTheme.dimens.size24), + color = colors.contentColor(enabled = enabled).value, + strokeWidth = TangemTheme.dimens.size4, + ) + } else Row { + if (buttonIcon is TangemButtonIcon.Left) { + icon(buttonIcon.painter) + Spacer(modifier = Modifier.width(TangemTheme.dimens.size8)) + } + Text( + text = text, + style = TangemTypography.button, + color = colors.contentColor(enabled = enabled).value, + ) + if (buttonIcon is TangemButtonIcon.Right) { + Spacer(modifier = Modifier.width(TangemTheme.dimens.size8)) + icon(buttonIcon.painter) + } + } +} + +sealed interface TangemButtonIcon { + val painter: Painter? + + @Immutable + data class Left(override val painter: Painter) : TangemButtonIcon + + @Immutable + data class Right(override val painter: Painter) : TangemButtonIcon + + object None : TangemButtonIcon { + override val painter: Painter? = null + } +} + +enum class TangemButtonSize { + Default, + Text, +} + +@Composable +private fun TangemButtonSize.toHeightDp(): Dp = when (this) { + TangemButtonSize.Default -> TangemTheme.dimens.size48 + TangemButtonSize.Text -> TangemTheme.dimens.size40 +} + +@Composable +private fun TangemButtonSize.toShape(): Shape = when (this) { + TangemButtonSize.Default -> TangemTheme.shapes.roundedCornersMedium + TangemButtonSize.Text -> TangemTheme.shapes.roundedCornersSmall +} + +private object TangemButtonsDefaults { + val elevation: ButtonElevation + @Composable get() = ButtonDefaults + .elevation( + defaultElevation = TangemTheme.dimens.elevation0, + pressedElevation = TangemTheme.dimens.elevation0, + ) + + val primaryButtonColors: ButtonColors + @Composable get() = TangemButtonColors( + backgroundColor = TangemTheme.colors.button.primary, + contentColor = TangemTheme.colors.text.primary2, + disabledBackgroundColor = TangemTheme.colors.button.disabled, + disabledContentColor = TangemTheme.colors.text.disabled, + ) + + val secondaryButtonColors: ButtonColors + @Composable get() = TangemButtonColors( + backgroundColor = TangemTheme.colors.button.secondary, + contentColor = TangemTheme.colors.text.primary1, + disabledBackgroundColor = TangemTheme.colors.button.disabled, + disabledContentColor = TangemTheme.colors.text.disabled, + ) + + val textButtonColors: ButtonColors + @Composable get() = TangemButtonColors( + backgroundColor = Color.Transparent, + contentColor = TangemTheme.colors.text.secondary, + disabledBackgroundColor = Color.Transparent, + disabledContentColor = TangemTheme.colors.text.disabled, + ) +} + +@Immutable +private class TangemButtonColors( + private val backgroundColor: Color, + private val contentColor: Color, + private val disabledBackgroundColor: Color, + private val disabledContentColor: Color, +) : ButtonColors { + @Composable + override fun backgroundColor(enabled: Boolean): State { + return rememberUpdatedState(newValue = if (enabled) backgroundColor else disabledBackgroundColor) + } + + @Composable + override fun contentColor(enabled: Boolean): State { + return rememberUpdatedState(newValue = if (enabled) contentColor else disabledContentColor) + } +} +// endregion Defaults + +// region Preview +@Composable +private fun PrimaryButtonSample( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary), + ) { + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = "Manage tokens", + onClick = { /* no-op */ }, + ) + Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8)) + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + showProgress = true, + text = "Manage tokens", + onClick = { /* no-op */ }, + ) + Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8)) + PrimaryButtonIconRight( + modifier = Modifier.fillMaxWidth(), + text = "Manage tokens", + icon = painterResource(id = R.drawable.ic_tangem), + onClick = { /* no-op */ }, + ) + Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8)) + PrimaryButtonIconLeft( + modifier = Modifier.fillMaxWidth(), + text = "Manage tokens", + icon = painterResource(id = R.drawable.ic_tangem), + onClick = { /* no-op */ }, + ) + Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8)) + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = "Manage tokens", + enabled = false, + onClick = { /* no-op */ }, + ) + Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8)) + PrimaryButtonIconRight( + modifier = Modifier.fillMaxWidth(), + text = "Manage tokens", + icon = painterResource(id = R.drawable.ic_tangem), + enabled = false, + onClick = { /* no-op */ }, + ) + Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8)) + PrimaryButtonIconLeft( + modifier = Modifier.fillMaxWidth(), + text = "Manage tokens", + icon = painterResource(id = R.drawable.ic_tangem), + enabled = false, + onClick = { /* no-op */ }, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun PrimaryButtonPreview_Light() { + TangemTheme { + PrimaryButtonSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun PrimaryButtonPreview_Dark() { + TangemTheme(isDark = true) { + PrimaryButtonSample() + } +} + +@Composable +private fun SecondaryButtonSample( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary), + ) { + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + text = "Manage tokens", + onClick = { /* no-op */ }, + ) + Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8)) + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + showProgress = true, + text = "Manage tokens", + onClick = { /* no-op */ }, + ) + Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8)) + SecondaryButtonIconRight( + modifier = Modifier.fillMaxWidth(), + text = "Manage tokens", + icon = painterResource(id = R.drawable.ic_tangem), + onClick = { /* no-op */ }, + ) + Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8)) + SecondaryButtonIconLeft( + modifier = Modifier.fillMaxWidth(), + text = "Manage tokens", + icon = painterResource(id = R.drawable.ic_tangem), + onClick = { /* no-op */ }, + ) + Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8)) + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + text = "Manage tokens", + enabled = false, + onClick = { /* no-op */ }, + ) + Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8)) + SecondaryButtonIconRight( + modifier = Modifier.fillMaxWidth(), + text = "Manage tokens", + icon = painterResource(id = R.drawable.ic_tangem), + enabled = false, + onClick = { /* no-op */ }, + ) + Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8)) + SecondaryButtonIconLeft( + modifier = Modifier.fillMaxWidth(), + text = "Manage tokens", + icon = painterResource(id = R.drawable.ic_tangem), + enabled = false, + onClick = { /* no-op */ }, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SecondaryButtonPreview_Light() { + TangemTheme { + SecondaryButtonSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SecondaryButtonPreview_Dark() { + TangemTheme(isDark = true) { + SecondaryButtonSample() + } +} + +@Composable +private fun TextButtonSample( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary), + ) { + TextButton( + text = "Enabled", + onClick = { /* no-op */ }, + ) + Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8)) + TextButtonIconLeft( + text = "Enabled", + icon = painterResource(id = R.drawable.ic_plus), + onClick = { /* no-op */ }, + ) + Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8)) + TextButton( + text = "Enabled", + enabled = false, + onClick = { /* no-op */ }, + ) + Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8)) + TextButtonIconLeft( + text = "Enabled", + icon = painterResource(id = R.drawable.ic_plus), + enabled = false, + onClick = { /* no-op */ }, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun TextButtonPreview_Light() { + TangemTheme { + TextButtonSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun TextButtonPreview_Dark() { + TangemTheme(isDark = true) { + TextButtonSample() + } +} +// endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/ButtonsDeprecated.kt b/core/ui/src/main/java/com/tangem/core/ui/components/ButtonsDeprecated.kt new file mode 100644 index 0000000000..0ff2ce98a8 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/ButtonsDeprecated.kt @@ -0,0 +1,185 @@ +package com.tangem.core.ui.components + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.ButtonColorType +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TextColorType +import com.tangem.core.ui.res.buttonColor +import com.tangem.core.ui.res.textColor + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryStartIconButton_Enabled_InLightTheme() { + TangemTheme(isDark = false) { + PrimaryStartIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryStartIconButton_Enabled_InDarkTheme() { + TangemTheme(isDark = true) { + PrimaryStartIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryStartIconButton_Disabled_InLightTheme() { + TangemTheme(isDark = false) { + PrimaryStartIconButton( + text = "Manage tokens", + iconResId = R.drawable.ic_tangem, + enabled = false, + onClicked = {}, + ) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryStartIconButton_Disabled_InDarkTheme() { + TangemTheme(isDark = true) { + PrimaryStartIconButton( + text = "Manage tokens", + iconResId = R.drawable.ic_tangem, + enabled = false, + onClicked = {}, + ) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryEndIconButton_Enabled_InLightTheme() { + TangemTheme(isDark = false) { + PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryEndIconButton_Enabled_InDarkTheme() { + TangemTheme(isDark = true) { + PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = true, onClicked = {}) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryEndIconButton_Disabled_InLightTheme() { + TangemTheme(isDark = false) { + PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = false, onClicked = {}) + } +} + +@Preview(widthDp = 328, heightDp = 48, showBackground = true) +@Composable +fun Preview_PrimaryEndIconButton_Disabled_InDarkTheme() { + TangemTheme(isDark = true) { + PrimaryEndIconButton(text = "Manage tokens", iconResId = R.drawable.ic_tangem, enabled = false, onClicked = {}) + } +} + +/** + * Primary button with an icon at the beginning of the layout + * + * @param modifier button modifier + * @param text button text + * @param iconResId button icon res id + * @param enabled controls the enabled state of the button + * @param onClicked the lambda to be invoked when this button is pressed + * + * @see Figma component + */ +@Deprecated("Use PrimaryButtonIconRight instead") +@Composable +fun PrimaryStartIconButton( + modifier: Modifier = Modifier, + text: String, + @DrawableRes iconResId: Int, + enabled: Boolean = true, + onClicked: () -> Unit, +) { + PrimaryButtonRow(modifier = modifier, enabled = enabled, onClicked = onClicked) { + Icon( + painter = painterResource(id = iconResId), + contentDescription = null, + ) + Spacer(modifier = Modifier.width(dimensionResource(R.dimen.spacing8))) + Text(text = text) + } +} + +/** + * Primary button with an icon at the end of the layout + * + * @param modifier button modifier + * @param text button text + * @param iconResId button icon res id + * @param enabled controls the enabled state of the button + * @param onClicked the lambda to be invoked when this button is pressed + * + * @see Figma component + */ +@Deprecated("Use PrimaryButtonIconLeft instead") +@Composable +fun PrimaryEndIconButton( + modifier: Modifier = Modifier, + text: String, + @DrawableRes iconResId: Int, + enabled: Boolean = true, + onClicked: () -> Unit, +) { + PrimaryButtonRow(modifier = modifier, enabled = enabled, onClicked = onClicked) { + Text(text = text) + Spacer(modifier = Modifier.width(dimensionResource(R.dimen.spacing8))) + Icon( + painter = painterResource(id = iconResId), + contentDescription = null, + ) + } +} + +@Composable +private fun PrimaryButtonRow( + modifier: Modifier, + enabled: Boolean, + onClicked: () -> Unit, + content: @Composable (RowScope.() -> Unit), +) { + Button( + onClick = onClicked, + modifier = modifier + .fillMaxWidth() + .height(dimensionResource(R.dimen.size48)), + enabled = enabled, + shape = RoundedCornerShape(dimensionResource(id = R.dimen.radius12)), + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.colors.buttonColor(type = ButtonColorType.PRIMARY), + contentColor = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY2), + disabledBackgroundColor = MaterialTheme.colors.buttonColor(type = ButtonColorType.DISABLED), + disabledContentColor = MaterialTheme.colors.textColor(type = TextColorType.DISABLED), + ), + content = content, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsEffect.kt new file mode 100644 index 0000000000..34ac768e47 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsEffect.kt @@ -0,0 +1,14 @@ +package com.tangem.core.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import com.google.accompanist.systemuicontroller.SystemUiController +import com.google.accompanist.systemuicontroller.rememberSystemUiController + +@Composable +fun SystemBarsEffect( + block: SystemUiController.() -> Unit, +) { + val systemUiController = rememberSystemUiController() + SideEffect { block(systemUiController) } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt new file mode 100644 index 0000000000..f14d87c904 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt @@ -0,0 +1,414 @@ +package com.tangem.core.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.material.TextFieldColors +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemTypography + +/** + * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=213%3A218&t=TmfD6UBHPg9uYfev-4) + * */ +@Composable +fun OutlineTextField( + modifier: Modifier = Modifier, + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + label: String? = null, + placeholder: String? = null, + caption: String? = null, + enabled: Boolean = true, + isError: Boolean = false, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) { + TangemTextField( + modifier = modifier, + value = value, + onValueChange = onValueChange, + singleLine = true, + label = label, + placeholder = placeholder, + caption = caption, + enabled = enabled, + isError = isError, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + interactionSource = interactionSource, + ) +} + +// region Defaults +@Composable +private fun TangemTextField( + modifier: Modifier = Modifier, + value: TextFieldValue, + singleLine: Boolean, + onValueChange: (TextFieldValue) -> Unit, + label: String? = null, + placeholder: String? = null, + caption: String? = null, + enabled: Boolean = true, + isError: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + colors: TangemTextFieldColors = TangemTextFieldsDefault.defaultTextFieldColors, + size: TangemTextFieldSize = TangemTextFieldSize.Default, + onClear: () -> Unit = { onValueChange(TextFieldValue()) }, +) { + Column(modifier = modifier) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + .heightIn(size.toHeightDp()), + value = value, + textStyle = TangemTypography.body1, + onValueChange = onValueChange, + singleLine = singleLine, + enabled = enabled, + isError = isError, + visualTransformation = visualTransformation, + keyboardActions = keyboardActions, + keyboardOptions = keyboardOptions, + interactionSource = interactionSource, + shape = size.toShape(), + colors = colors, + label = { + if (!label.isNullOrEmpty()) { + Text( + text = label, + style = TangemTypography.caption, + color = colors.labelColor( + enabled = enabled, + error = isError, + interactionSource = interactionSource, + ).value, + ) + } + }, + placeholder = { + if (!placeholder.isNullOrEmpty()) { + Text( + text = placeholder, + style = TangemTypography.body1, + color = colors.placeholderColor(enabled = enabled).value, + ) + } + }, + trailingIcon = { + val iconRes by rememberUpdatedState( + newValue = if (isError) R.drawable.ic_alert else R.drawable.ic_close, + ) + IconButton( + modifier = Modifier.size(32.dp), + onClick = onClear, + enabled = !isError && enabled, + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(id = iconRes), + tint = colors.trailingIconColor(enabled = enabled, isError = isError).value, + contentDescription = "Clear input", + ) + } + }, + ) + + AnimatedVisibility( + visible = !caption.isNullOrEmpty(), + enter = fadeIn(), + exit = fadeOut(), + ) { + if (caption.isNullOrEmpty()) return@AnimatedVisibility + Column { + Spacer(modifier = Modifier.height(4.dp)) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = caption, + style = TangemTypography.body1, + color = colors.captionColor(enabled = enabled, isError = isError).value, + ) + } + } + } +} + +private enum class TangemTextFieldSize { + Default +} + +@Composable +private fun TangemTextFieldSize.toHeightDp(): Dp = when (this) { + TangemTextFieldSize.Default -> TangemTheme.dimens.size56 +} + +@Composable +private fun TangemTextFieldSize.toShape(): Shape = when (this) { + TangemTextFieldSize.Default -> TangemTheme.shapes.roundedCornersSmall2 +} + +private object TangemTextFieldsDefault { + val defaultTextFieldColors: TangemTextFieldColors + @Composable @Stable get() = TangemTextFieldColors( + textColor = TangemTheme.colors.text.primary1, + disabledTextColor = TangemTheme.colors.text.disabled, + backgroundColor = Color.Transparent, + cursorColor = TangemTheme.colors.icon.primary1, + errorCursorColor = TangemTheme.colors.icon.warning, + focusedIndicatorColor = TangemTheme.colors.icon.primary1, + unfocusedIndicatorColor = TangemTheme.colors.stroke.primary, + disabledIndicatorColor = TangemTheme.colors.stroke.primary, + errorIndicatorColor = TangemTheme.colors.icon.warning, + leadingIconColor = TangemTheme.colors.icon.informative, + disabledLeadingIconColor = Color.Transparent, + errorLeadingIconColor = TangemTheme.colors.icon.warning, + trailingIconColor = TangemTheme.colors.icon.informative, + disabledTrailingIconColor = Color.Transparent, + errorTrailingIconColor = TangemTheme.colors.icon.warning, + focusedLabelColor = TangemTheme.colors.text.primary1, + unfocusedLabelColor = TangemTheme.colors.text.secondary, + disabledLabelColor = TangemTheme.colors.text.disabled, + errorLabelColor = TangemTheme.colors.icon.warning, + placeholderColor = TangemTheme.colors.text.secondary, + disabledPlaceholderColor = TangemTheme.colors.text.disabled, + captionColor = TangemTheme.colors.text.tertiary, + disabledCaptionColor = TangemTheme.colors.text.disabled, + errorCaptionColor = TangemTheme.colors.icon.warning, + ) +} + +@Immutable +private data class TangemTextFieldColors( + private val textColor: Color, + private val disabledTextColor: Color, + private val cursorColor: Color, + private val errorCursorColor: Color, + private val focusedIndicatorColor: Color, + private val unfocusedIndicatorColor: Color, + private val errorIndicatorColor: Color, + private val disabledIndicatorColor: Color, + private val leadingIconColor: Color, + private val disabledLeadingIconColor: Color, + private val errorLeadingIconColor: Color, + private val trailingIconColor: Color, + private val disabledTrailingIconColor: Color, + private val errorTrailingIconColor: Color, + private val backgroundColor: Color, + private val focusedLabelColor: Color, + private val unfocusedLabelColor: Color, + private val disabledLabelColor: Color, + private val errorLabelColor: Color, + private val placeholderColor: Color, + private val disabledPlaceholderColor: Color, + private val captionColor: Color, + private val disabledCaptionColor: Color, + private val errorCaptionColor: Color, +) : TextFieldColors { + + @Composable + override fun leadingIconColor(enabled: Boolean, isError: Boolean): State { + return rememberUpdatedState( + when { + !enabled -> disabledLeadingIconColor + isError -> errorLeadingIconColor + else -> leadingIconColor + }, + ) + } + + @Composable + override fun trailingIconColor(enabled: Boolean, isError: Boolean): State { + return rememberUpdatedState( + when { + !enabled -> disabledTrailingIconColor + isError -> errorTrailingIconColor + else -> trailingIconColor + }, + ) + } + + @Composable + override fun indicatorColor( + enabled: Boolean, + isError: Boolean, + interactionSource: InteractionSource, + ): State { + val focused by interactionSource.collectIsFocusedAsState() + + val targetValue = when { + !enabled -> disabledIndicatorColor + isError -> errorIndicatorColor + focused -> focusedIndicatorColor + else -> unfocusedIndicatorColor + } + return if (enabled) { + animateColorAsState(targetValue, tween(durationMillis = 120)) + } else { + rememberUpdatedState(targetValue) + } + } + + @Composable + override fun backgroundColor(enabled: Boolean): State { + return rememberUpdatedState(backgroundColor) + } + + @Composable + override fun placeholderColor(enabled: Boolean): State { + return rememberUpdatedState(if (enabled) placeholderColor else disabledPlaceholderColor) + } + + @Composable + override fun labelColor( + enabled: Boolean, + error: Boolean, + interactionSource: InteractionSource, + ): State { + val focused by interactionSource.collectIsFocusedAsState() + + val targetValue = when { + !enabled -> disabledLabelColor + error -> errorLabelColor + focused -> focusedLabelColor + else -> unfocusedLabelColor + } + return rememberUpdatedState(targetValue) + } + + @Composable + override fun textColor(enabled: Boolean): State { + return rememberUpdatedState(if (enabled) textColor else disabledTextColor) + } + + @Composable + override fun cursorColor(isError: Boolean): State { + return rememberUpdatedState(if (isError) errorCursorColor else cursorColor) + } + + @Composable + fun captionColor(enabled: Boolean, isError: Boolean): State { + return rememberUpdatedState( + newValue = when { + !enabled -> disabledCaptionColor + isError -> errorCaptionColor + else -> captionColor + }, + ) + } +} +// endregion Defaults + +// region Preview +@Composable +private fun OutlineTextFieldSample( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary) + .padding(horizontal = 16.dp), + ) { + val text = TextFieldValue(text = "Input") + OutlineTextField( + value = text, + onValueChange = { /* no-op */ }, + ) + Divider(modifier = Modifier.padding(vertical = 8.dp)) + OutlineTextField( + value = text, + onValueChange = { /* no-op */ }, + label = "Default", + placeholder = "Placeholder", + caption = "Supporting text", + ) + Divider(modifier = Modifier.padding(vertical = 8.dp)) + OutlineTextField( + value = text, + onValueChange = { /* no-op */ }, + enabled = false, + label = "Disabled", + placeholder = "Placeholder", + caption = "Supporting text", + ) + Divider(modifier = Modifier.padding(vertical = 8.dp)) + OutlineTextField( + value = text, + onValueChange = { /* no-op */ }, + isError = true, + label = "Error", + placeholder = "Placeholder", + caption = "Supporting text", + ) + Divider(modifier = Modifier.padding(vertical = 8.dp)) + OutlineTextField( + value = TextFieldValue(), + onValueChange = { /* no-op */ }, + label = "Default without value", + placeholder = "Placeholder", + caption = "Supporting text", + ) + Divider(modifier = Modifier.padding(vertical = 8.dp)) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun OutlineTextFieldPreview_Light() { + TangemTheme { + OutlineTextFieldSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun OutlineTextFieldPreview_Dark() { + TangemTheme(isDark = true) { + OutlineTextFieldSample() + } +} +// endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/BottomSheetIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/BottomSheetIndicator.kt index 800b674c51..de03ea2174 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/BottomSheetIndicator.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/BottomSheetIndicator.kt @@ -23,6 +23,7 @@ import com.tangem.core.ui.res.iconColor * @see * Figma Component */ +@Deprecated(message = "Use Hand component instead") @Composable fun BottomSheetIndicator() { Box( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/Hand.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/Hand.kt new file mode 100644 index 0000000000..e3ade6def9 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/Hand.kt @@ -0,0 +1,72 @@ +package com.tangem.core.ui.components.atoms + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemTheme + +/** + * [Show in Figma](https://www.figma.com/file/17JyRbuUEZ42DluaFEuGQk/Atoms?node-id=135%3A25&t=Jo68S6ilyewVU8MV-4) + * */ +@Composable +fun Hand( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .padding(vertical = TangemTheme.dimens.spacing8) + .fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .width(TangemTheme.dimens.size32) + .height(TangemTheme.dimens.size4) + .background( + color = TangemTheme.colors.icon.inactive, + shape = TangemTheme.shapes.roundedCornersSmall, + ), + ) + } +} + +// region Preview +@Composable +private fun HandSample( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary) + .padding(TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.SpaceBetween, + ) { + Hand() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun HandPreview_Light() { + TangemTheme { + HandSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun HandPreview_Dark() { + TangemTheme(isDark = true) { + HandSample() + } +} +// endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeBottomSheetFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeBottomSheetFragment.kt new file mode 100644 index 0000000000..2a7d4c0864 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeBottomSheetFragment.kt @@ -0,0 +1,87 @@ +package com.tangem.core.ui.fragments + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.FloatRange +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme + +abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment() { + open val initialBottomSheetState = BottomSheetBehavior.STATE_EXPANDED + + @FloatRange(from = 0.0, to = 1.0) + open val expandedHeightFraction: Float? = null + + override fun getTheme(): Int = R.style.AppTheme_TransparentBottomSheetDialog + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return createComposeView(inflater.context) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + + (dialog as BottomSheetDialog).behavior.apply { + state = initialBottomSheetState + skipCollapsed = true + } + + return dialog + } + + private fun createComposeView(context: Context): View { + return ComposeView(context).apply { + setContent { + TangemTheme { + val backgroundColor = TangemTheme.colors.background.plain + SystemBarsEffect { + setSystemBarsColor( + color = backgroundColor, + ) + } + + ScreenContent( + modifier = Modifier + .fillMaxWidth() + .let { + if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it + } + .background( + color = backgroundColor, + shape = TangemTheme.shapes.bottomSheet, + ), + state = provideState().value, + ) + } + } + } + } + + @Composable + protected abstract fun provideState(): State + + @Composable + protected abstract fun ScreenContent( + modifier: Modifier, + state: ScreenState, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeFragment.kt new file mode 100644 index 0000000000..863f22f185 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/fragments/ComposeFragment.kt @@ -0,0 +1,57 @@ +package com.tangem.core.ui.fragments + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme + +abstract class ComposeFragment : Fragment() { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + return createComposeView(inflater.context) + } + + private fun createComposeView(context: Context): View { + return ComposeView(context).apply { + setContent { + TangemTheme { + val backgroundColor = TangemTheme.colors.background.primary + SystemBarsEffect { + setSystemBarsColor( + color = backgroundColor, + ) + } + + ScreenContent( + modifier = Modifier + .fillMaxSize() + .background(color = backgroundColor), + state = provideState().value, + ) + } + } + } + } + + @Composable + protected abstract fun provideState(): State + + @Composable + protected abstract fun ScreenContent( + modifier: Modifier, + state: ScreenState, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_alert.xml b/core/ui/src/main/res/drawable/ic_alert.xml new file mode 100644 index 0000000000..1d855289ee --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_alert.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_close.xml b/core/ui/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000000..ffc64fb7a8 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_plus.xml b/core/ui/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000000..b85852c706 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/values/styles.xml b/core/ui/src/main/res/values/styles.xml new file mode 100644 index 0000000000..001815837e --- /dev/null +++ b/core/ui/src/main/res/values/styles.xml @@ -0,0 +1,12 @@ + + + + + + + From a0407a2bb80e061c9087e27ad2b2d60736428903 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Nov 2022 11:54:10 +0300 Subject: [PATCH 068/372] Updated on 2026-08-14 --- {app => core/ui}/src/main/res/values-de/strings_final.xml | 0 .../ui}/src/main/res/values-de/strings_plurals_final.xml | 0 {app => core/ui}/src/main/res/values-fr/strings_final.xml | 0 .../ui}/src/main/res/values-fr/strings_plurals_final.xml | 0 {app => core/ui}/src/main/res/values-it/strings_final.xml | 0 .../ui}/src/main/res/values-it/strings_plurals_final.xml | 0 {app => core/ui}/src/main/res/values-ru/strings_final.xml | 2 +- .../ui}/src/main/res/values-ru/strings_plurals_final.xml | 0 {app => core/ui}/src/main/res/values/strings_final.xml | 2 +- {app => core/ui}/src/main/res/values/strings_plurals_final.xml | 0 10 files changed, 2 insertions(+), 2 deletions(-) rename {app => core/ui}/src/main/res/values-de/strings_final.xml (100%) rename {app => core/ui}/src/main/res/values-de/strings_plurals_final.xml (100%) rename {app => core/ui}/src/main/res/values-fr/strings_final.xml (100%) rename {app => core/ui}/src/main/res/values-fr/strings_plurals_final.xml (100%) rename {app => core/ui}/src/main/res/values-it/strings_final.xml (100%) rename {app => core/ui}/src/main/res/values-it/strings_plurals_final.xml (100%) rename {app => core/ui}/src/main/res/values-ru/strings_final.xml (99%) rename {app => core/ui}/src/main/res/values-ru/strings_plurals_final.xml (100%) rename {app => core/ui}/src/main/res/values/strings_final.xml (99%) rename {app => core/ui}/src/main/res/values/strings_plurals_final.xml (100%) diff --git a/app/src/main/res/values-de/strings_final.xml b/core/ui/src/main/res/values-de/strings_final.xml similarity index 100% rename from app/src/main/res/values-de/strings_final.xml rename to core/ui/src/main/res/values-de/strings_final.xml diff --git a/app/src/main/res/values-de/strings_plurals_final.xml b/core/ui/src/main/res/values-de/strings_plurals_final.xml similarity index 100% rename from app/src/main/res/values-de/strings_plurals_final.xml rename to core/ui/src/main/res/values-de/strings_plurals_final.xml diff --git a/app/src/main/res/values-fr/strings_final.xml b/core/ui/src/main/res/values-fr/strings_final.xml similarity index 100% rename from app/src/main/res/values-fr/strings_final.xml rename to core/ui/src/main/res/values-fr/strings_final.xml diff --git a/app/src/main/res/values-fr/strings_plurals_final.xml b/core/ui/src/main/res/values-fr/strings_plurals_final.xml similarity index 100% rename from app/src/main/res/values-fr/strings_plurals_final.xml rename to core/ui/src/main/res/values-fr/strings_plurals_final.xml diff --git a/app/src/main/res/values-it/strings_final.xml b/core/ui/src/main/res/values-it/strings_final.xml similarity index 100% rename from app/src/main/res/values-it/strings_final.xml rename to core/ui/src/main/res/values-it/strings_final.xml diff --git a/app/src/main/res/values-it/strings_plurals_final.xml b/core/ui/src/main/res/values-it/strings_plurals_final.xml similarity index 100% rename from app/src/main/res/values-it/strings_plurals_final.xml rename to core/ui/src/main/res/values-it/strings_plurals_final.xml diff --git a/app/src/main/res/values-ru/strings_final.xml b/core/ui/src/main/res/values-ru/strings_final.xml similarity index 99% rename from app/src/main/res/values-ru/strings_final.xml rename to core/ui/src/main/res/values-ru/strings_final.xml index ad4f23ca35..dc387d3f71 100644 --- a/app/src/main/res/values-ru/strings_final.xml +++ b/core/ui/src/main/res/values-ru/strings_final.xml @@ -5,7 +5,7 @@ 2 карты Доставка Бесплатно - У меня есть промо-код... + У меня есть промо-код… Итого Другие способы оплаты Купить сейчас diff --git a/app/src/main/res/values-ru/strings_plurals_final.xml b/core/ui/src/main/res/values-ru/strings_plurals_final.xml similarity index 100% rename from app/src/main/res/values-ru/strings_plurals_final.xml rename to core/ui/src/main/res/values-ru/strings_plurals_final.xml diff --git a/app/src/main/res/values/strings_final.xml b/core/ui/src/main/res/values/strings_final.xml similarity index 99% rename from app/src/main/res/values/strings_final.xml rename to core/ui/src/main/res/values/strings_final.xml index 70e85346c6..e434fc002d 100644 --- a/app/src/main/res/values/strings_final.xml +++ b/core/ui/src/main/res/values/strings_final.xml @@ -5,7 +5,7 @@ 2 cards Shipping Free - I have a promo code... + I have a promo code… Total Other payment methods Buy now diff --git a/app/src/main/res/values/strings_plurals_final.xml b/core/ui/src/main/res/values/strings_plurals_final.xml similarity index 100% rename from app/src/main/res/values/strings_plurals_final.xml rename to core/ui/src/main/res/values/strings_plurals_final.xml From 8593df1d85bbfa780ec5af3eea13d70e47f398e0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Nov 2022 12:00:34 +0300 Subject: [PATCH 069/372] Updated on 2026-08-14 --- .../com/tangem/tap/common/compose/Button.kt | 18 ++- .../common/compose/ComposeDialogManager.kt | 5 +- .../com/tangem/tap/common/compose/Spacer.kt | 106 ----------------- .../com/tangem/tap/common/compose/Warning.kt | 1 + .../features/home/compose/content/Content.kt | 10 +- .../compose/content/CurrenciesWeb3Content.kt | 14 +-- .../home/compose/views/HomeButtons.kt | 2 +- .../home/compose/views/StoriesProgressBar.kt | 9 +- .../addCustomToken/compose/FormFieldViews.kt | 15 ++- .../com/tangem/core/ui/components/Spacers.kt | 108 +++++++++++++++++- 10 files changed, 160 insertions(+), 128 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/compose/Spacer.kt diff --git a/app/src/main/java/com/tangem/tap/common/compose/Button.kt b/app/src/main/java/com/tangem/tap/common/compose/Button.kt index 690f35f388..b20f7470b8 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/Button.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/Button.kt @@ -1,7 +1,17 @@ package com.tangem.tap.common.compose -import androidx.compose.foundation.layout.* -import androidx.compose.material.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Text import androidx.compose.material.ripple.LocalRippleTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -16,6 +26,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.tangem.core.ui.components.SpacerH8 import com.tangem.tap.common.compose.extensions.stringResourceDefault import com.tangem.wallet.R @@ -137,8 +148,7 @@ fun ToggledRippleTheme( @Preview @Composable fun ButtonTest() { - Scaffold( - ) { + Scaffold { Column(modifier = Modifier.padding(16.dp)) { PreviewItem("Button") { RectangleButton(text = "Some button") {} diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt index fc496f1d9b..99042c3391 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import com.tangem.core.ui.components.SpacerH16 import com.tangem.domain.DomainDialog import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.global.DomainGlobalAction @@ -105,7 +106,7 @@ fun SimpleDialog( modifier = Modifier.padding(22.dp) ) { DialogTitle(title = title) - LazyColumn() { + LazyColumn { items(items) { item -> Row( modifier = Modifier @@ -154,4 +155,4 @@ fun ErrorDialog( } } ) -} +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt b/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt deleted file mode 100644 index ce0b190598..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.tangem.tap.common.compose - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - -/** -[REDACTED_AUTHOR] - */ -// ***************************** Horizontal -@Composable -fun SpacerH(height: Dp, modifier: Modifier = Modifier) { - Spacer(modifier = modifier.height(height)) -} - -@Composable -fun SpacerH4(modifier: Modifier = Modifier) { - SpacerH(4.dp, modifier) -} - -@Composable -fun SpacerH8(modifier: Modifier = Modifier) { - SpacerH(8.dp, modifier) -} - -@Composable -fun SpacerH16(modifier: Modifier = Modifier) { - SpacerH(16.dp, modifier) -} - -@Composable -fun SpacerH24(modifier: Modifier = Modifier) { - SpacerH(24.dp, modifier) -} - -@Composable -fun SpacerH32(modifier: Modifier = Modifier) { - SpacerH(32.dp, modifier) -} - -// ***************************** Vertical -@Composable -fun SpacerW(width: Dp, modifier: Modifier = Modifier) { - Spacer(modifier = modifier.width(width)) -} - -@Composable -fun SpacerW4(modifier: Modifier = Modifier) { - SpacerW(4.dp, modifier) -} - -@Composable -fun SpacerW8(modifier: Modifier = Modifier) { - SpacerW(8.dp, modifier) -} - -@Composable -fun SpacerW16(modifier: Modifier = Modifier) { - SpacerW(16.dp, modifier) -} - -@Composable -fun SpacerW24(modifier: Modifier = Modifier) { - SpacerW(24.dp, modifier) -} - -@Composable -fun SpacerW32(modifier: Modifier = Modifier) { - SpacerW(32.dp, modifier) -} - -// ***************************** Size -@Composable -fun SpacerS(size: Dp, modifier: Modifier = Modifier) { - Spacer(modifier = modifier.size(size)) -} - -@Composable -fun SpacerS4(modifier: Modifier = Modifier) { - SpacerS(4.dp, modifier) -} - -@Composable -fun SpacerS8(modifier: Modifier = Modifier) { - SpacerS(8.dp, modifier) -} - -@Composable -fun SpacerS16(modifier: Modifier = Modifier) { - SpacerS(16.dp, modifier) -} - -@Composable -fun SpacerS24(modifier: Modifier = Modifier) { - SpacerS(24.dp, modifier) -} - -@Composable -fun SpacerS32(modifier: Modifier = Modifier) { - SpacerS(32.dp, modifier) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/Warning.kt b/app/src/main/java/com/tangem/tap/common/compose/Warning.kt index 07c82d1384..0d19a8e2ea 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/Warning.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/Warning.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.tangem.common.module.ModuleMessage +import com.tangem.core.ui.components.SpacerH8 import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter import com.tangem.wallet.R diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt b/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt index 9ee80263c3..e8a6b447f7 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt @@ -2,7 +2,11 @@ package com.tangem.tap.features.home.compose.content import androidx.annotation.DrawableRes import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -20,8 +24,8 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.tangem.tap.common.compose.SpacerH16 -import com.tangem.tap.common.compose.SpacerH32 +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.SpacerH32 import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation import com.tangem.tap.features.home.compose.StoriesTextAnimation import com.tangem.wallet.R diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt b/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt index 198e71d324..85ec581b07 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp -import com.tangem.tap.common.compose.SpacerH +import com.tangem.core.ui.components.SpacerH12 import com.tangem.tap.common.compose.extensions.dpSize import com.tangem.tap.common.compose.extensions.halfHeight import com.tangem.tap.common.compose.extensions.toPx @@ -64,7 +64,7 @@ fun StoriesCurrenciesContent( targetOffset = animateTo.toPx(), contentDescription = "Currency row", ) - SpacerH(12.dp) + SpacerH12() } } } @@ -118,7 +118,7 @@ fun StoriesWeb3Content( @Composable private fun LightenBox(content: @Composable () -> Unit) { - Box() { + Box { content() Box( modifier = Modifier @@ -130,10 +130,10 @@ private fun LightenBox(content: @Composable () -> Unit) { Color.White.copy(alpha = 0f), Color.White.copy(alpha = 0.75f), Color.White.copy(alpha = 0.95f), - Color.White - ) - ) - ) + Color.White, + ), + ), + ), ) {} } } diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt index cb40186a87..1e3cc818b4 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.tangem.tap.common.compose.SpacerS8 +import com.tangem.core.ui.components.SpacerS8 import com.tangem.wallet.R @Composable diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt index 096b87cb0b..3779f57358 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt @@ -4,7 +4,12 @@ import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember @@ -13,7 +18,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.tap.common.compose.SpacerW4 +import com.tangem.core.ui.components.SpacerW4 @Composable fun StoriesProgressBar( diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt index fd792714be..bc4c0ae9c6 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt @@ -6,14 +6,25 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.SpacerH8 import com.tangem.domain.common.form.Field import com.tangem.domain.features.addCustomToken.TokenBlockchainField import com.tangem.domain.features.addCustomToken.TokenDerivationPathField import com.tangem.domain.features.addCustomToken.TokenField -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.* +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenContractAddressChanged +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDecimalsChanged +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDerivationPathChanged +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNameChanged +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNetworkChanged +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenSymbolChanged import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState import com.tangem.domain.redux.domainStore -import com.tangem.tap.common.compose.* +import com.tangem.tap.common.compose.BlockchainSpinner +import com.tangem.tap.common.compose.ClearButton +import com.tangem.tap.common.compose.ClosePopupTrigger +import com.tangem.tap.common.compose.OutlinedTextFieldWidget +import com.tangem.tap.common.compose.PasteButton +import com.tangem.tap.common.compose.TitleSubtitle import com.tangem.tap.common.extensions.getFromClipboard import com.tangem.wallet.R diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt index 60ce6bf8bc..8293073012 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt @@ -1,12 +1,17 @@ +@file:Suppress("unused") + package com.tangem.core.ui.components import androidx.annotation.DimenRes import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp @Composable fun VerticalSpacer(@DimenRes spaceResId: Int) { @@ -16,4 +21,105 @@ fun VerticalSpacer(@DimenRes spaceResId: Int) { @Composable fun HorizontalSpacer(@DimenRes spaceResId: Int) { Spacer(modifier = Modifier.width(dimensionResource(id = spaceResId))) -} \ No newline at end of file +} + +// region Horizontal +@Composable +fun SpacerH(height: Dp, modifier: Modifier = Modifier) { + Spacer(modifier = modifier.height(height)) +} + +@Composable +fun SpacerH4(modifier: Modifier = Modifier) { + SpacerH(4.dp, modifier) +} + +@Composable +fun SpacerH8(modifier: Modifier = Modifier) { + SpacerH(8.dp, modifier) +} + +@Composable +fun SpacerH12(modifier: Modifier = Modifier) { + SpacerH(12.dp, modifier) +} + +@Composable +fun SpacerH16(modifier: Modifier = Modifier) { + SpacerH(16.dp, modifier) +} + +@Composable +fun SpacerH24(modifier: Modifier = Modifier) { + SpacerH(24.dp, modifier) +} + +@Composable +fun SpacerH32(modifier: Modifier = Modifier) { + SpacerH(32.dp, modifier) +} +// endregion Horizontal + +// region Vertical +@Composable +fun SpacerW(width: Dp, modifier: Modifier = Modifier) { + Spacer(modifier = modifier.width(width)) +} + +@Composable +fun SpacerW4(modifier: Modifier = Modifier) { + SpacerW(4.dp, modifier) +} + +@Composable +fun SpacerW8(modifier: Modifier = Modifier) { + SpacerW(8.dp, modifier) +} + +@Composable +fun SpacerW16(modifier: Modifier = Modifier) { + SpacerW(16.dp, modifier) +} + +@Composable +fun SpacerW24(modifier: Modifier = Modifier) { + SpacerW(24.dp, modifier) +} + +@Composable +fun SpacerW32(modifier: Modifier = Modifier) { + SpacerW(32.dp, modifier) +} +// endregion Vertical + +// region Size +@Composable +fun SpacerS(size: Dp, modifier: Modifier = Modifier) { + Spacer(modifier = modifier.size(size)) +} + +@Composable +fun SpacerS4(modifier: Modifier = Modifier) { + SpacerS(4.dp, modifier) +} + +@Composable +fun SpacerS8(modifier: Modifier = Modifier) { + SpacerS(8.dp, modifier) +} + +@Composable +fun SpacerS16(modifier: Modifier = Modifier) { + SpacerS(16.dp, modifier) +} + +@Composable +fun SpacerS24(modifier: Modifier = Modifier) { + SpacerS(24.dp, modifier) +} + +@Composable +fun SpacerS32(modifier: Modifier = Modifier) { + SpacerS(32.dp, modifier) +} +// endregion Size \ No newline at end of file From 90bfea9e4cee859a9c595ed47f1478dd04290dae Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Nov 2022 16:21:05 +0300 Subject: [PATCH 070/372] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 12 ----------- .../tap/common/redux/global/GlobalAction.kt | 3 --- .../tap/common/redux/global/GlobalReducer.kt | 20 ++++++++----------- .../tap/common/redux/global/GlobalState.kt | 9 --------- .../details/redux/DetailsMiddleware.kt | 19 ++---------------- .../features/wallet/models/WalletWarning.kt | 2 +- .../tap/features/wallet/redux/WalletState.kt | 2 +- .../wallet/ui/WalletWarningConverter.kt | 7 +++++-- app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 11 +++++----- app/src/main/res/values-ru/strings.xml | 19 +++++++++--------- app/src/main/res/values/strings.xml | 13 ++++++------ .../main/res/values/strings_untranslated.xml | 18 ++++------------- 14 files changed, 41 insertions(+), 96 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index aa9e63c2dc..ad1892a9a5 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -19,8 +19,6 @@ import com.tangem.tap.common.IntentHandler import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.redux.NotificationsHandler -import com.tangem.tap.common.redux.global.AndroidResources -import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.common.shop.GooglePayService @@ -70,7 +68,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac tangemSdkManager = TangemSdkManager(tangemSdk, this) backupService = BackupService.init(tangemSdk, this) - store.dispatch(GlobalAction.SetResources(getAndroidResources())) store.dispatch( ShopAction.CheckIfGooglePayAvailable( GooglePayService(createPaymentsClient(this), this), @@ -78,15 +75,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac ) } - private fun getAndroidResources(): AndroidResources { - return AndroidResources( - AndroidResources.RString( - R.string.copy_toast_msg, - R.string.details_notification_erase_wallet_not_possible, - ), - ) - } - private fun systemActions() { WindowCompat.setDecorFitsSystemWindows(window, false) 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 c1e6d85f11..d4c5620fb3 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 @@ -29,9 +29,6 @@ sealed class GlobalAction : Action { data class ShowErrorNotification(override val error: TapError) : GlobalAction(), ErrorAction data class DebugShowErrorNotification(override val error: TapError) : GlobalAction(), DebugErrorAction - // android resources - data class SetResources(val resources: AndroidResources) : GlobalAction() - // dialogs data class ShowDialog(val stateDialog: StateDialog) : GlobalAction() object HideDialog : GlobalAction() 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 820506ab18..8c04197c58 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 @@ -14,9 +14,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState { val globalState = state.globalState return when (action) { - is GlobalAction.SetResources -> { - globalState.copy(resources = action.resources) - } is GlobalAction.Onboarding.Start -> { val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage val onboardingManager = OnboardingManager(action.scanResponse, usedCardsPrefStorage) @@ -34,7 +31,7 @@ fun globalReducer(action: Action, state: AppState): GlobalState { is GlobalAction.ScanFailsCounter.Reset -> { globalState.copy(scanCardFailsCounter = 0) } - is GlobalAction.SaveScanNoteResponse ->{ + is GlobalAction.SaveScanNoteResponse -> { domainStore.dispatch(DomainGlobalAction.SaveScanNoteResponse(action.scanResponse)) globalState.copy(scanResponse = action.scanResponse) } @@ -51,11 +48,12 @@ fun globalReducer(action: Action, state: AppState): GlobalState { is GlobalAction.UpdateWalletSignedHashes -> { val card = globalState.scanResponse?.card ?: return globalState val wallet = card.wallet(action.walletPublicKey) ?: return globalState - - val newCardInstance = card.updateWallet(wallet.copy( - totalSignedHashes = action.walletSignedHashes, - remainingSignatures = action.remainingSignatures - )) + val newCardInstance = card.updateWallet( + wallet.copy( + totalSignedHashes = action.walletSignedHashes, + remainingSignatures = action.remainingSignatures, + ), + ) globalState.copy(scanResponse = globalState.scanResponse.copy(card = newCardInstance)) } is GlobalAction.SetFeedbackManager -> { @@ -72,9 +70,7 @@ fun globalReducer(action: Action, state: AppState): GlobalState { } is GlobalAction.SetIfCardVerifiedOnline -> globalState.copy(cardVerifiedOnline = action.verified) - is GlobalAction.FetchUserCountry.Success -> globalState.copy( - userCountryCode = action.countryCode - ) + is GlobalAction.FetchUserCountry.Success -> globalState.copy(userCountryCode = action.countryCode) else -> globalState } } \ No newline at end of file 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 11c84ed856..b897f1b5dc 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 @@ -25,18 +25,9 @@ data class GlobalState( val scanCardFailsCounter: Int = 0, val dialog: StateDialog? = null, val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(), - val resources: AndroidResources = AndroidResources(), val userCountryCode: String? = null, ) : StateType -data class AndroidResources( - val strings: RString = RString(), -) { - data class RString( - val addressWasCopied: Int = -1, - val walletIsNotEmpty: Int = -1, - ) -} typealias CryptoCurrencyName = String data class OnboardingState( 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 b316098ff4..88191c0ee4 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 @@ -7,7 +7,6 @@ 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 import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState @@ -18,7 +17,6 @@ import com.tangem.tap.domain.extensions.getUserWalletId import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction -import com.tangem.tap.features.wallet.models.hasSendableAmountsOrPendingTransactions import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager @@ -56,21 +54,8 @@ class DetailsMiddleware { } } is DetailsAction.ReCreateTwinsWallet -> { - val wallet = - store.state.walletState.walletManagers.map { it.wallet }.firstOrNull() - if (wallet == null) { - store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet)) - store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins)) - } else { - if (wallet.hasSendableAmountsOrPendingTransactions()) { - val walletIsNotEmpty = - store.state.globalState.resources.strings.walletIsNotEmpty - store.dispatchNotification(walletIsNotEmpty) - } else { - store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet)) - store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins)) - } - } + store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet)) + store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins)) } is DetailsAction.CreateBackup -> { store.state.detailsState.scanResponse?.let { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/models/WalletWarning.kt b/app/src/main/java/com/tangem/tap/features/wallet/models/WalletWarning.kt index 3b80a95845..e2709d2bb9 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/models/WalletWarning.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/models/WalletWarning.kt @@ -8,7 +8,7 @@ sealed class WalletWarning( val edStringValueWithSymbol: String, ) : WalletWarning(1) - object TransactionInProgress : WalletWarning(10) + data class TransactionInProgress(val currencyName: String) : WalletWarning(10) data class BalanceNotEnoughForFee(val blockchainFullName: String) : WalletWarning(30) data class Rent(val walletRent: WalletRent) : WalletWarning(40) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index 6219fea552..dedbd21a08 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -350,7 +350,7 @@ data class WalletData( private fun assembleNonTypedWarnings(walletWarnings: MutableList) { if (currencyData.status == BalanceStatus.SameCurrencyTransactionInProgress) { - walletWarnings.add(WalletWarning.TransactionInProgress) + walletWarnings.add(WalletWarning.TransactionInProgress(currency.currencyName)) } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletWarningConverter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletWarningConverter.kt index 624d34da3e..799d029235 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletWarningConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletWarningConverter.kt @@ -27,8 +27,11 @@ class WalletWarningConverter( message.blockchainFullName, message.blockchainFullName, ) } - WalletWarning.TransactionInProgress -> { - context.getString(R.string.wallet_pending_transaction_warning) + is WalletWarning.TransactionInProgress -> { + context.getString( + R.string.token_details_send_blocked_tx_format, + message.currencyName + ) } is WalletWarning.Rent -> { context.getString( diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index cf8a46c64c..4bdd650d4b 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -174,7 +174,6 @@ RECHTLICHER HAFTUNGSAUSSCHLUSS Legen Sie die Karte zum Scannen an Legen Sie die Karte an - Ihr Kontostand in dieser Brieftasche ist nicht Null, oder Sie haben unbestätigte Transaktionen. Es ist unmöglich die Funktion der Brieftasche zu löschen. Dies ist die derzeit aktive Wahl diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d39650f3b7..2e751ab41c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -166,7 +166,6 @@ Avertissement Saisissez d\'abord le PayString requis Posez pour scanner Posez la carte - Votre solde sur ce portefeuille n\'est pas nul, ou vous avez des transactions non confirmées. Impossible de supprimer la fonction du portefeuille Cette option est déjà active diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 7eba948ed7..1eecc8a9d4 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -28,7 +28,7 @@ Il saldo sta per essere caricato… Blockchain non raggiungibile Conto non creato - Carica %1$s+ %2$s per creare un conto + Carica %s+ %s per creare un conto Carta vuota Crea un portafoglio per iniziare ad utilizzare la tua carta Tangem Questa carta non è supportata @@ -63,10 +63,10 @@ Normale Prioritario Includi commissione - Sarà inviato %1$s %2$s - ≈ %1$s (inc. commissione: %2$s) - Sarà inviato %1$s %2$s e %1$s %2$s - Saldo: %1$s %2$s + Sarà inviato %s %s + ≈ %s (inc. commissione: %s) + Sarà inviato %s %s e %s %s + Saldo: %s %s La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo Requisiti ID carta @@ -174,7 +174,6 @@ Questa nota legale è stata modificata l\'ultima volta 01.10.2020. Avvicina per scansionare Avvicina la carta - Il tuo saldo su questo portafoglio non è pari a zero o hai transazioni non confermate. Non è possibile eliminare la funzione portafoglio. E\' l\'opzione attualmente attiva diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 41ae2c46a7..bded5827f5 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -30,7 +30,7 @@ Загрузка баланса… Блокчейн недоступен Аккаунт не создан - Загрузите %1$s+ %2$s для создания учетной записи + Загрузите %s+ %s для создания учетной записи Карта пуста Создайте кошелек, чтобы начать пользоваться картой Tangem Эта карта не поддерживается @@ -64,10 +64,10 @@ Нормальный Приоритетный Включить коммисию - %1$s %2$s будет отправлено - ≈ %1$s (включая комиссию: %2$s) - %1$s %2$s и %3$s %4$s будут отправлены - Баланс: %1$s %2$s + %s %s будет отправлено + ≈ %s (включая комиссию: %s) + %s %s и %s %s будут отправлены + Баланс: %s %s Транзакция успешно подписана и отправлена в блокчейн. Баланс кошелька будет обновлен через некоторое время. Подробности ID карты @@ -114,7 +114,6 @@ Сначала введите желаемую PayString Приложите, чтобы отсканировать Приложите карту - Ваш баланс на этом кошельке не равен нулю, или у Вас есть неподтвержденные транзакции Это текущая активная опция Ошибка сканирования карты. Попробуй снова. Купить @@ -206,7 +205,7 @@ Копировать Поделиться + Добавить токены - Пожалуйста, дождитесь завершения транзакции, чтобы иметь возможность отправить средства + Пожалуйста, дождитесь завершения транзакции %s, чтобы иметь возможность отправить средства Проверка подлинности не удалась Эта карта может быть производственным образцом или подделкой Важная информация о безопасности \u26A0 @@ -319,11 +318,11 @@ Сброс до заводских настроек Код доступа доступен только для карт с резервной копией Сеть Solana взымает арендную плату в размере %s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %s, чтобы использовать его бесплатно. - %1s (%2s) + %s (%s) Эта карта не является векселем на предъявителя. В настоящее время мы не можем сопоставить количество подписей на карте с информацией в блокчейне. Это нормально, но в редких случаях может означать, что предыдущий владелец скрывает автономную подпись, что является проблемой безопасности.\n\nНе принимайте эту карту в качестве физического платежа от кого-то, кому Вы не доверяете.\n\nВо всех остальных отношениях - это совершенно безопасно.\n\nTangem — единственный аппаратный кошелек, предлагающий защиту от подсчета подписей. WalletConnect - Запрос на создание транзакции для %1s\n%2s\n\nСумма: %3s\nКомиссия: %4s\nВсего: %5s\nБаланс: %6s - Запрос на запуск сеанса для %1s\n\nURL: %2s + Запрос на создание транзакции для %s\n%s\n\nСумма: %s\nКомиссия: %s\nВсего: %s\nБаланс: %s + Запрос на запуск сеанса для %s\n\nURL: %s Сессии WalletConnect Сеанс WalletConnect открыт с %s Упс. Нет сессий. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2c1ebffd92..b4476d5d6e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -36,7 +36,7 @@ Blockchain is unreachable Account is not created - Load %1$s+ %2$s to create account + Load %s+ %s to create account Empty card Create wallet to start using Tangem card This card is not supported @@ -74,10 +74,10 @@ Normal Priority Include fee - %1$s %2$s will be sent - ≈ %1$s (inc. fee: %2$s) - %1$s %2$s and %3$s %4$s will be sent - Balance: %1$s %2$s + %s %s will be sent + ≈ %s (inc. fee: %s) + %s %s and %s %s will be sent + Balance: %s %s Transaction has been successfully signed and sent to blockchain node. Wallet balance will be updated in a while Details @@ -168,7 +168,7 @@ \nThis disclaimer was amended for the last time on October 1st, 2020.
Terms of service - No internet connection + No internet connection PayString verification failed PayString unsupported by blockchain @@ -185,6 +185,5 @@ Tap to scan Tap the card - Your balance on this wallet is not zero, or you have unconfirmed transactions This is the currently active option diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index 153f80c666..ee8cbaff9d 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -105,7 +105,7 @@ Copy Share + Add tokens - Please wait for the transaction to complete to be able to send funds + Please wait for %s transaction to complete to be able to send funds Authenticity check failed This card might be a production sample or counterfeit @@ -132,21 +132,11 @@ Card: %s Tap card to bind to wallet connect - - Request to create transaction for %1s - \n%2s - - \n\nAmount: %3s - \nFee: %4s - \nTotal: %5s - \nBalance: %6s + Request to create transaction for %s\n%s\n\nAmount: %s\nFee: %s\nTotal: %s\nBalance: %s Can\'t send transaction. Not enough funds. - Request to start a session for %1s\n\n - - URL: %2s - + Request to start a session for %s\n\nURL: %s Requesting to sign a message\n Message for %s:\n%s @@ -309,7 +299,7 @@ Amount to pay: %s
Solana network charges a rent of %s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %s to use it for free. - %1s (%2s) + %s (%s) Type here to search My suggestions From 1836e85084e0354466a02d9c5a63d3be163600dd Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Nov 2022 16:21:35 +0300 Subject: [PATCH 071/372] Updated on 2026-08-14 --- .../features/wallet/ui/WalletDetailsFragment.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 9d3dfca291..f40bf97ea5 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 @@ -191,10 +191,16 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), private fun setupCurrency(currencyData: BalanceWidgetData, currency: Currency) = with(binding) { tvCurrencyTitle.text = currencyData.currency - tvCurrencySubtitle.text = tvCurrencySubtitle.getString( - R.string.wallet_currency_subtitle, - currency.blockchain.fullName, - ) + + if (currency is Currency.Token) { + tvCurrencySubtitle.text = tvCurrencySubtitle.getString( + R.string.wallet_currency_subtitle, + currency.blockchain.fullName, + ) + tvCurrencySubtitle.show() + } else { + tvCurrencySubtitle.hide() + } } private fun setupButtons(selectedWallet: WalletData, isExchangeServiceFeatureOn: Boolean) = with(binding) { From 2b2a8d2f08a3618af54b01548b58f43265fbe886 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Nov 2022 21:45:39 +0300 Subject: [PATCH 072/372] Updated on 2026-08-14 --- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 22 +++++++++---------- .../main/res/values-ru/strings_blockchain.xml | 6 ++--- app/src/main/res/values/strings.xml | 15 ++++++------- .../main/res/values/strings_blockchain.xml | 6 ++--- .../main/res/values/strings_untranslated.xml | 5 ++--- 6 files changed, 26 insertions(+), 30 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 1eecc8a9d4..b9b5539110 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -67,7 +67,7 @@ ≈ %s (inc. commissione: %s) Sarà inviato %s %s e %s %s Saldo: %s %s - La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo + La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo Requisiti ID carta Emittente diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index bded5827f5..506d97136b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -8,7 +8,6 @@ Отмена Принять Удалить - %s Доступ к камере запрещен Вы не предоставили доступ к своей камере, измените настройки конфиденциальности Настройки @@ -68,7 +67,7 @@ ≈ %s (включая комиссию: %s) %s %s и %s %s будут отправлены Баланс: %s %s - Транзакция успешно подписана и отправлена в блокчейн. Баланс кошелька будет обновлен через некоторое время. + Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время Подробности ID карты Эмитент @@ -84,23 +83,23 @@ Если вы забудете код, Вы потеряете возможность использовать карту. Невозможно восстановить или изменить код, если вы его потеряете. Управление безопасностью Длительное удержание - Этот механизм защищает карту от бесконтактной атаки. Это обеспечит задержку между приемом и выполнением команды. После первой подписанной транзакции этот телефон будет привязан к карте, и транзакции будут подписаны без задержки. + Этот механизм защищает карту от бесконтактных атак. Между сканированием карты и выполнением команды будет добавлена задержка. Пароль Перед выполнением любой команды, влекущей за собой изменение состояния карты, вам необходимо будет ввести пароль Код доступа Перед сканированием карты вам нужно будет ввести правильный код доступа У Вас могут возникнуть проблемы с NFC на некоторых iPhone 7/7+ во время извлечения - Эта карта уже пополнялась и подписывала транзакции в прошлом. Рассмотрите возможность немедленного снятия всех средств, если Вы получили эту карту из ненадежного источника. + Эта карта ранее пополнялась и подписывала транзакции. Выведите средства как можно быстрее, если вы получили эту карту из ненадежного источника. Если это ваша карта, то не о чем беспокоиться. Предупреждение Эта карта не предназначена для работы с этим приложением Карта, которую Вы отсканировали, является картой для разработчиков. Не принимайте ее в качестве оплаты. Карты Tangem, выпущенные до сентября 2019 года, в настоящее время не могут быть извлечены с помощью iPhone. Мы усердно работаем с Apple, чтобы сделать это возможным в будущих версиях iOS. Нажмите, чтобы подписать - Приложите карту к телефону, как показано выше - Приложите, чтобы удалить кошелек + Чтобы подписать транзакцию, соедините телефон и карту в точности, как показано выше. + Чтобы сбросить настройки до заводских, соедините телефон и карту в точности, как показано выше. Приложите, чтобы создать кошелек - Приложите, чтобы изменить код доступа - Приложите, чтобы изменить пароль + Чтобы изменить код доступа, соедините телефон и карту в точности, как показано выше. + Чтобы изменить пароль, соедините телефон и карту в точности, как показано выше. Неверный PayString Условия использования Нет соединения с интернетом @@ -127,7 +126,7 @@ Сгенерируйте ключи кошелька на обеих картах, чтобы начать использовать свои Twin карты Tangem Twin Один кошелек. Две карты. - Одна, которую Вы держите в руках, и вторая карта с номером #%s + Одна, которую Вы держите в руках, и вторая карта с номером #%s. Пересоздать Twin кошелек Условия использования карты Tangem Twin @@ -135,7 +134,6 @@ Приложите Twin карту #%s Подготовка карты Создание кошелька - Процедура создания кошелька состоит из трех шагов. Вы должны пройти его до конца, иначе вам придется начинать сначала. Сканировать карту #%s Вы уже начали процесс пересоздания кошелька. Если Вы прервете его, вы не сможете использовать свои карты, пока не запустите его снова и не завершите пересоздание кошелька. Вы приложили ту же карту. Чтобы создать кошелек, вам нужно приложить другу Twin карту. @@ -230,7 +228,7 @@ Давайте сгенерируем все ключи на вашей карте и создадим безопасный кошелек Создать кошелек Пополните свой кошелек - Чтобы начать, просто пополните карту на любую сумму + Пополните кошелек на любую сумму, чтобы начать пользоваться картой Чтобы начать, просто пополните кошелек более чем на %s %s Купить криптовалюту Показать адрес кошелька @@ -263,7 +261,7 @@ Добавлены две резервные карты Резервная карта #%d Вы можете привязать до трех карт к одному кошельку - Чтобы начать процесс резервного копирования, добавьте еще максимум 2 карты + Чтобы начать процесс резервного копирования, добавьте одну или две резервные карты. Подготовьте основную карту с номером %s Подготовьте основную карту Вы можете добавить еще одну карту или завершить процесс резервного копирования diff --git a/app/src/main/res/values-ru/strings_blockchain.xml b/app/src/main/res/values-ru/strings_blockchain.xml index 9ccdfd276e..5baeca3d33 100644 --- a/app/src/main/res/values-ru/strings_blockchain.xml +++ b/app/src/main/res/values-ru/strings_blockchain.xml @@ -11,9 +11,9 @@ Неизвестная ошибка Неверный адрес Недопустимая сумма - Сумма превышает баланс - Общая сумма превышает баланс - Комиссия превышает баланс + Сумма превышает остаток + Отправляемая сумма превышает остаток + Комиссия превышает остаток Чтобы не платить повышенную комиссию при следующем пополнении кошелька, уменьшите сумму на %s XTZ Уменьшить на %s XTZ Нет, отправить все diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b4476d5d6e..25a90b2276 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,7 +9,6 @@ Cancel Accept Delete - %s Camera access denied You have not given access to your camera, please adjust your privacy settings @@ -78,7 +77,7 @@ ≈ %s (inc. fee: %s) %s %s and %s %s will be sent Balance: %s %s - Transaction has been successfully signed and sent to blockchain node. Wallet balance will be updated in a while + Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while Details Card ID @@ -95,25 +94,25 @@ If you forget the code, you will lose the ability to use the card. There is no way to recover or change your code if you lose it. Manage Security Long Tap - This mechanism protects against proximity attack on a card. It will enforce a delay between reception and execution of a command. After the first signed transaction this phone will be linked with the card and transactions will be signed immediately. + This mechanism protects against proximity attacks on a card. It will enforce a delay between reception and execution of a command. Passcode Before execution any command entailing a change of the card state you will have to enter the passcode. Access code You will have to submit the correct access code before scanning the card You may experience NFC problems with some iPhone 7/7+ during the extraction - This card has been already topped up and signed transactions in the past. Consider immediate withdrawal of all funds if you have received this card from an untrusted source. + This card has been already topped up and signed transactions in the past. Consider immediate withdrawal of all funds if you have received this card from an untrusted source. If it\'s your card, there is nothing to worry about. Warning This card is not designed to work with this app The card you scanned is a development card. Don\'t accept it as a payment Tangem cards manufactured before September 2019 cannot currently be extracted with an iPhone. We\'re working hard with Apple to make it possible in future versions of iOS. Tap to sign - Tap the card to your phone as shown above - Tap to erase wallet + To sign, connect your phone and the card exactly as it shown above + To reset to factory settings, connect your phone and the card exactly as it shown above Tap to create wallet - Tap to change the access code - Tap to change the passcode + To change the access code, connect your phone and the card exactly as it shown above + To change the passcode, connect your phone and the card exactly as it shown above Invalid PayString diff --git a/app/src/main/res/values/strings_blockchain.xml b/app/src/main/res/values/strings_blockchain.xml index 8014c2a199..f19a487b14 100644 --- a/app/src/main/res/values/strings_blockchain.xml +++ b/app/src/main/res/values/strings_blockchain.xml @@ -11,9 +11,9 @@ Unknown error Invalid address Invalid amount - Amount Exceeds Balance - Total Exceeds Balance - Fee Exceeds Balance + Amount exceeds balance + Total amount exceeds balance + Fee exceeds balance To avoid paying an increased commission the next time you top up your wallet, reduce the amount by %s XTZ Reduce by %s XTZ No, send all diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index ee8cbaff9d..676c8cc848 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -24,7 +24,6 @@ Tap the #%s twin card Preparing card Creating wallet - The wallet creation procedure consists of three steps. You must complete it to the end, otherwise you will have to start from the beginning. Scan the card #%s You have already started the process of recreating twin wallet. If you interrupt it, you won\'t be able to use your twin cards until you start it again and complete recreating the wallet. You\'ve tapped the same card. To create twin wallet you need to tap a second twin card. @@ -176,7 +175,7 @@ Create wallet Top up your wallet - To get started, simply top up the card with any amount + To get started, simply top up the wallet with any amount To get started, simply top up the wallet with more than %s %s. Buy crypto Show the wallet\'s address @@ -218,7 +217,7 @@ Backup card #%d You can backup your keys up to two other blank wallet cards - To start the backup process add up to two cards + To start the backup process add up to two backup cards. Prepare the primary card with number %s Prepare the primary card You can add one more card or finalize the backup process From 7fcb3a8525d1a554c255546ea7f6e64193158568 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Nov 2022 14:59:19 +0300 Subject: [PATCH 073/372] Updated on 2026-08-14 --- app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 506d97136b..2e55d1156d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -8,6 +8,7 @@ Отмена Принять Удалить + %s Доступ к камере запрещен Вы не предоставили доступ к своей камере, измените настройки конфиденциальности Настройки diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 25a90b2276..6ea8e27b1c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,6 +9,7 @@ Cancel Accept Delete + %s Camera access denied You have not given access to your camera, please adjust your privacy settings From 06afe233fbbea4ec88417774b8962fa2a0515f9a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Nov 2022 14:18:46 +0300 Subject: [PATCH 074/372] Updated on 2026-08-14 --- app/src/main/res/values-de/strings.xml | 30 ------ .../main/res/values-de/strings_blockchain.xml | 1 - app/src/main/res/values-fr/strings.xml | 30 ------ .../main/res/values-fr/strings_blockchain.xml | 1 - app/src/main/res/values-it/strings.xml | 30 ------ .../main/res/values-it/strings_blockchain.xml | 3 +- app/src/main/res/values-ru/strings.xml | 93 +------------------ .../main/res/values-ru/strings_blockchain.xml | 3 +- app/src/main/res/values/strings.xml | 30 ------ .../main/res/values/strings_blockchain.xml | 3 +- .../main/res/values/strings_untranslated.xml | 63 +------------ .../src/main/res/values-de/strings_final.xml | 14 --- .../src/main/res/values-fr/strings_final.xml | 14 --- .../src/main/res/values-it/strings_final.xml | 14 --- .../src/main/res/values-ru/strings_final.xml | 14 --- core/ui/src/main/res/values/strings_final.xml | 16 +--- .../feature/referral/ui/AgreementText.kt | 6 +- .../referral/ui/NonParticipateBottomBlock.kt | 2 +- .../referral/ui/ParticipateBottomBlock.kt | 2 +- .../src/main/res/values-ru/string.xml | 6 +- .../src/main/res/values/string.xml | 6 +- 21 files changed, 17 insertions(+), 364 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4bdd650d4b..967b2ab32f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -8,22 +8,11 @@ Akzeptieren Entfernen - Der Kamerazugriff wurde verweigert Sie haben keinen Zugang zur Kamera erteilt, bitte passen Sie Ihre Datenschutzeinstellungen an - Einstellungen - Willkommen bei Tangem. Haben Sie eine dieser Karten? - Willkommen bei Tangem. Scannen Sie Ihre Karten um zu starten. - Ja! Bitte scannen - Einkaufen - Halten Sie die \ncard hinter dem \nphone bereit und scannen Sie sie. - Eintippen Karte scannen - Scannen Absenden ein Wallet erstellen Tangem - eine Walletoption wählen - Token Tokens Verifizierte Bilanz Die Bilanz wird aufgeladen… @@ -41,14 +30,10 @@ von %s Adresse erkunden PayString erstellen - %s Wallet PayString erstellen - Karte: %s PayString Name $payid.tangem.com - Erstellen PayString sind Ihre einzigartigen Identifikationsdaten wie Telefonnummer, E-Mail-Adresse oder ABN. - Ihre PayString wurde erfolgreich erstellt und zum Clipboard kopiert Fehlerantwort beim Erstellen der PayString Diese PayString existiert bereits. Versuchen Sie mit einer anderen erneut. Adresse oder PayString @@ -64,7 +49,6 @@ Priorität inkl. Gebühr %s %s wird gesendet - ≈ %s (inkl. Gebühr: %s) %s %s und %s %s werden gesendet Bilanz: %s %s Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert @@ -74,34 +58,22 @@ Signiert %s Hasch App Währung - Einstellungen - Karte - Karte validieren - Sicherheit verwalteny - Wallet löschen Dieser Vorgang ist irriversibel. Wenn jemand nach dem Löschen des Wallets Geld überweist, können Sie es nicht mehr abheben. - Sollten Sie den Code verloren haben, haben Sie keinen Zugriff auf die Karte. Sie können Ihren Code nicht wiederherstellen und nicht ändern, wenn Sie ihn verlieren. - Sicherheit verwalten Langes Tippen Dieser Mechanismus schützt vor Annäherungsangriffen auf eine Karte. Es wird eine Verzögerung zwischen dem Empfang und der Ausführung eines Befehls erzwungen. Nach der ersten signierten Transaktion wird dieses Telefon mit der Karte verknüpft und die Transaktionen werden sofort signiert Passcode Bevor Sie einen Befehl ausführen, der eine Änderung des Kartenstatus zur Folge hat, müssen Sie den Passcode eingeben. Zugangscode Sie müssen den richtigen Zugangscode eingeben, bevor Sie die Karte scannen. - Sie können NFC-Probleme mit einigen iPhone 7/7+ während der Extraktion haben Diese Karte wurde früher bereits aufgeladen und Transaktionen wurden damit signiert. Ziehen Sie eine sofortige Auszahlung aller Beträge in Betracht, wenn Sie diese Karte von einer nicht vertrauenswürdigen Quelle erhalten haben. Warnung Diese Karte ist für die Zusammenarbeit mit Tangem nicht geeignet Die von Ihnen gescannte Karte ist eine Entwicklungskarte. Akzeptieren Sie sie nicht als Zahlungsmittel - Die vor September 2019 ausgestellten Karten können mit iPhone momentan nicht extrahiert werden. Wir arbeiten eng mit Apple zusammen, damit es in den nächsten Versionen von iOS möglich wäre. Tippen um zu signieren - Scannen Sie die Karte mit Ihrem Handy wie oben gezeigt - Tippen Sie um das Wallet zu löschen Tippen Sie um ein Wallet zu erstellen Tippen Sie um den Zugangscode zu ändern Tippen Sie um den Passcode zu ändern - Ungültige PayString @@ -159,7 +131,6 @@ RECHTLICHER HAFTUNGSAUSSCHLUSS Nutzungsbedingungen Keine Internetverbindung - Verifikation der PayString ist fehlgeschlagen PayString wird von der Blockchain nicht unterstützt PayString ist nicht registriert PayString-Anfrage ist fehlgeschlagen @@ -174,6 +145,5 @@ RECHTLICHER HAFTUNGSAUSSCHLUSS Legen Sie die Karte zum Scannen an Legen Sie die Karte an - Dies ist die derzeit aktive Wahl diff --git a/app/src/main/res/values-de/strings_blockchain.xml b/app/src/main/res/values-de/strings_blockchain.xml index 66998f875e..8b9715a9e6 100644 --- a/app/src/main/res/values-de/strings_blockchain.xml +++ b/app/src/main/res/values-de/strings_blockchain.xml @@ -6,7 +6,6 @@ Minimaler Betrag ist %s Restbestand zu klein Das Zielkonto ist nicht erstellt. Der abzusendende Betrag soll %s + Gebühr oder mehr sein - Um ein Konto zu erstellen, senden Sie 1+ XLM an diese Adresse Erhalt der Gebühr fehlgeschlagen Unbekannter Fehler Damit Sie beim nächsten Aufladen Ihrer Brieftasche keine erhöhte Provision zahlen, soll der Betrag um %s XTZ reduziert werden diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2e751ab41c..0694d102a5 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -9,22 +9,11 @@ J\'accepte Supprimer - L\'accès à la caméra est refusé Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité - Paramètres - Bienvenue à Tangem. Avez-vous une de nos cartes - Encore une fois, bienvenue dans Tangem. \nScannez votre carte pour commencer. - Oui! Scannez-la. - Magasin - Préparez-vous à appuyer votre carte au dos de votre téléphone. - Appuyez Scannez la carte - Scanner Envoyer Créer un portefeuille Tangem - Choisir une variante de portefeuille - Jeton d\'authentification Jetons d\'authentification Solde confirmé Solde est en cours de téléchargement … @@ -42,14 +31,10 @@ de %s Explorer l\'adresse Créer PayString - Portefeuille %s Créer PayString - Carte: %s Dénomination PayString $payid.tangem.com - Créer Votre PayString est une information qui vous est propre, comme un numéro de téléphone, une adresse du courrier électronique ou un ABN. - PayString a été créé avec succès et copié dans le presse-papiers Mauvaise réponse lors de la création de PayString Ce PayString existe déjà. Essayez une autre variante. Adresse ou PayString @@ -65,7 +50,6 @@ Priorité Inclure les commissions Sera envoyé %s %s - ≈ %s (incl. les commissions : %s) Sera envoyé %s %s et %s %s Solde : %s %s La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps @@ -75,33 +59,21 @@ Signé %s hashes Monnaie de l\'application - Paramètres - Carte - Valider la carte - Gestion de la sécurité - Annuler le portefeuille Cette action est irréversible. Si, après avoir annulé le portefeuille, quelqu\'un lui envoie des fonds, vous ne pourrez pas les retirer. - Si vous oubliez le code, vous perdrez la possibilité d\'utiliser la carte. Si vous perdez le code, il sera impossible de le restaurer ou modifier. - Gestion de la sécurité Tenez la carte fermement Ce mécanisme protège contre les attaques sans contact sur la carte. Il y a un délai entre la réception et l\'exécution de la commande. Après la première transaction signée, ce téléphone sera associé à la carte et les transactions seront signées immédiatement. Mot de passe Avant d\'exécuter une commande qui modifie l\'état de la carte, vous devrez entrer un mot de passe. Code d\'accès Vous devrez entrer le mot de passe correct avant de scanner la carte - Vous pouvez rencontrer des problèmes NFC avec certains iPhone 7/7 + lors de l\'extraction Cette carte a déjà été rechargée et a signé des transactions avant. Envisagez la possibilité de retirer tous les fonds immédiatement si vous avez reçu cette carte d\'une source non fiable. Attention Cette carte n\'est pas conçue pour fonctionner avec Tangem La carte que vous avez scannée est une carte de développement. Ne l\'acceptez pas comme paiement - Les cartes Tangem émises avant septembre 2019 ne peuvent actuellement pas être extraites à l\'aide de l\'iPhone. Nous travaillons activement avec Apple pour rendre cela possible dans les futures versions d\'iOS. Touchez pour signer - Appuyez votre carte au téléphone comme indiqué ci-dessus - Touchez, pour annuler un portefeuille Touchez, pour créer un portefeuille Touchez, pour modifier le code d\'accès Touchez, pour modifier le mot de passe - PayString incorrect @@ -157,7 +129,6 @@ Avertissement Conditions d\'utilisation Pas de connexion internet - La vérification de PayString a échoué PayString non pris en charge par la blockchain PayString non enregistré La demande de PayString a échoué @@ -166,7 +137,6 @@ Avertissement Saisissez d\'abord le PayString requis Posez pour scanner Posez la carte - Cette option est déjà active diff --git a/app/src/main/res/values-fr/strings_blockchain.xml b/app/src/main/res/values-fr/strings_blockchain.xml index 197f42551c..380232b81b 100644 --- a/app/src/main/res/values-fr/strings_blockchain.xml +++ b/app/src/main/res/values-fr/strings_blockchain.xml @@ -6,7 +6,6 @@ Le montant minimal est de %s Le reste est trop petit Le compte cible n\'a pas été créé. Le montant à envoyer doit être de %s + commissions ou plus - Pour créer un compte, envoyez 1+ XLM à cette adresse Échec de réception des commissions Erreur inconnue Adresse incorrecte diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b9b5539110..28789234b2 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -7,22 +7,11 @@ Fatto Annulla Rimuovere - Accesso alla videocamera negato Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy - Impostazioni - Benvenuti in Tangem. Hai una delle nostre schede?> - Benevenuti di nuovo su Tangem. Scansiona la tua carta per iniziare. - Si! Scansiona carta. - Shop - Preparati ad avvicinare la tua carta sul retro del telefono. - Avvicina Scansiona carta - Scansiona Invia Crea portafoglio Tangem - Scegli l\'opzione del portafoglio - Token Token Saldo verificato Il saldo sta per essere caricato… @@ -40,14 +29,10 @@ da %s Cerca indirizzo Crea PayString - portafoglio %s Crea PayString - Carta: %s Nome PayString $payid.tangem.com - Crea Il tuo PayString – informazioni per te uniche, come il tuo numero di telefono, indirizzo email o ABN. - PayString creato con successo e copiato negli appunti Errore di risposta durante la creazione del PayString Questo PayString esiste già. Scegline un altro. Indirizzo o PayString @@ -64,7 +49,6 @@ Prioritario Includi commissione Sarà inviato %s %s - ≈ %s (inc. commissione: %s) Sarà inviato %s %s e %s %s Saldo: %s %s La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo @@ -74,33 +58,21 @@ Firmato Hash %s Valuta dell\'applicazione - Impostazioni - Carta - Convalidare la carta - Gestione del portafoglio - Cancella il portafoglio Questa azione è irreversibile. Se dopo aver eliminato il portafoglio qualcuno invia dei fondi, non potrai ritirarli. - Se dimentichi il codice, perderai la possibilità di utilizzare la carta. Non è possibile in alcun modo ripristinare o modificare il codice se lo perdi. - Gestione della sicurezza Mantenimento della carta Questo meccanismo protegge dagli avvicinamenti senza contatto sulla carta. Attiva un ritardo tra la ricezione e l\'esecuzione di un comando. Dopo la prima transazione firmata, questo telefono verrà associato alla carta e le transazioni verranno firmate immediatamente. Password Dovrai inserire una password prima di eseguire qualsiasi comando che modifichi lo stato della carta. Codice di accesso Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto - Potresti riscontrare problemi con l\'NFC su alcuni iPhone 7/7 + durante la rimozione Questa carta è già stata ricaricata e ha firmato transazioni in passato. Valuta la possibilità di prelevare immediatamente tutti i fondi se hai ricevuto questa carta da una fonte inaffidabile. Attenzione Questa carta non è progettata per funzionare con Tangem La carta che hai scansionato è una carta di sviluppo. Non utilizzarla come strumento di pagamento - Le schede emesse prima di settembre 2019 non possono attualmente essere utilizzate con un iPhone. Stiamo lavorando a stretto contatto con Apple per renderle disponibili nelle future versioni di iOS. Avvicina per firmare - Avvicina la carta al telefono come mostrato sopra - Avvicina per cancellare il portafoglio Avvicina per creare il portafoglio Avvicina per modificare il codice di accesso Avvicina per modificare la password - PayString non valido @@ -159,7 +131,6 @@ Questa nota legale è stata modificata l\'ultima volta 01.10.2020. Termini del servizio Nessuna connessione a Internet - Verifica del PayString fallita PayString non supportato dalla blockchain PayString non registrato Richiesta PayString fallita @@ -174,6 +145,5 @@ Questa nota legale è stata modificata l\'ultima volta 01.10.2020. Avvicina per scansionare Avvicina la carta - E\' l\'opzione attualmente attiva diff --git a/app/src/main/res/values-it/strings_blockchain.xml b/app/src/main/res/values-it/strings_blockchain.xml index 92df7b9703..c9842eb6dd 100644 --- a/app/src/main/res/values-it/strings_blockchain.xml +++ b/app/src/main/res/values-it/strings_blockchain.xml @@ -7,9 +7,8 @@ L\'importo residuo è molto basso Errore sconosciuto L\'importo supera il saldo - Per creare un account, invia %s a questo indirizzo + Il conto totale non è stato creato L\'importo da inviare deve essere %s + commissione o maggiore Impossibile ottenere la commissione - Per creare un account, invia 1+ XLM a questo indirizzo Indirizzo non valido Importo non valido Il totale supera il saldo diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 2e55d1156d..172380e133 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -9,22 +9,11 @@ Принять Удалить %s - Доступ к камере запрещен Вы не предоставили доступ к своей камере, измените настройки конфиденциальности - Настройки - Добро пожаловать в Tangem.\nУ Вас есть одна из наших карт? - Добро пожаловать обратно в Тангем. \nОтсканируйте карту, чтобы начать. - Магазин - Приготовьтесь приложить свою\nкарту к задней панели\nтелефона - Нажмите на Сканировать карту - Да! Отсканировать. - Сканировать Отправить Создать кошелек Tangem - Выбрите опцию - Токен Токены Подтвержденный баланс Загрузка баланса… @@ -42,14 +31,10 @@ Отправка Проверить адрес Создать PayString - %s кошелек Создать PayString - Карта: %s PayString имя $payid.tangem.com - Создать Ваша PayString — это уникальная для вас информация, такая как номер телефона, электронная почта или ABN - PayString успешно создан и скопирован в буфер обмена Ошибка при создании PayString Такой PayString уже существует. Попробуйте другой. Адрес или PayString @@ -65,7 +50,6 @@ Приоритетный Включить коммисию %s %s будет отправлено - ≈ %s (включая комиссию: %s) %s %s и %s %s будут отправлены Баланс: %s %s Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время @@ -75,36 +59,23 @@ Подписано %s хэшей Валюта приложения - Настройки - Карта - Подтвердить карту - Управление безопасностью - Удалить кошелек Это действие необратимо. Если после удаления кошелька кто-то отправит на него средства, то Вы не сможете их вывести. - Если вы забудете код, Вы потеряете возможность использовать карту. Невозможно восстановить или изменить код, если вы его потеряете. - Управление безопасностью Длительное удержание Этот механизм защищает карту от бесконтактных атак. Между сканированием карты и выполнением команды будет добавлена задержка. Пароль Перед выполнением любой команды, влекущей за собой изменение состояния карты, вам необходимо будет ввести пароль Код доступа Перед сканированием карты вам нужно будет ввести правильный код доступа - У Вас могут возникнуть проблемы с NFC на некоторых iPhone 7/7+ во время извлечения Эта карта ранее пополнялась и подписывала транзакции. Выведите средства как можно быстрее, если вы получили эту карту из ненадежного источника. Если это ваша карта, то не о чем беспокоиться. Предупреждение Эта карта не предназначена для работы с этим приложением Карта, которую Вы отсканировали, является картой для разработчиков. Не принимайте ее в качестве оплаты. - Карты Tangem, выпущенные до сентября 2019 года, в настоящее время не могут быть извлечены с помощью iPhone. Мы усердно работаем с Apple, чтобы сделать это возможным в будущих версиях iOS. Нажмите, чтобы подписать - Чтобы подписать транзакцию, соедините телефон и карту в точности, как показано выше. - Чтобы сбросить настройки до заводских, соедините телефон и карту в точности, как показано выше. Приложите, чтобы создать кошелек Чтобы изменить код доступа, соедините телефон и карту в точности, как показано выше. Чтобы изменить пароль, соедините телефон и карту в точности, как показано выше. - Неверный PayString Условия использования Нет соединения с интернетом - Ошибка проверки PayString PayString не поддерживается блокчейном PayString не зарегистрирован Не удалось выполнить запрос PayString @@ -114,21 +85,16 @@ Сначала введите желаемую PayString Приложите, чтобы отсканировать Приложите карту - Это текущая активная опция Ошибка сканирования карты. Попробуй снова. Купить Продать Торговать По умолчанию - Совместимость Устаревший - Создать кошелек Создать кошелек Сгенерируйте ключи кошелька на обеих картах, чтобы начать использовать свои Twin карты - Tangem Twin Один кошелек. Две карты. Одна, которую Вы держите в руках, и вторая карта с номером #%s. - Пересоздать Twin кошелек Условия использования карты Tangem Twin Это действие необратимо. У Вас не будет доступа к старому кошельку. @@ -136,58 +102,34 @@ Подготовка карты Создание кошелька Сканировать карту #%s - Вы уже начали процесс пересоздания кошелька. Если Вы прервете его, вы не сможете использовать свои карты, пока не запустите его снова и не завершите пересоздание кошелька. Вы приложили ту же карту. Чтобы создать кошелек, вам нужно приложить другу Twin карту. - Адрес кошелька успешно создан Начать - Назад Добавить - Удалить Поиск - Приложение Отправить отзыв - Успешно отправлено - Спасибо за ваш отзыв - Ваши предложения отправлены - Спасибо за ваш отзыв. Мы ответим как можно скорее. - Не удалось отправить электронное письмо - Причина: %s Не могу отправить транзакцию Причина: %s У Вас возникли трудности со сканированием вашей карты? - Пожалуйста, попробуйте приложить карту точно так, как показано на анимации, или обратитесь в поддержку Отмена - Попробуйте снова Обратиться в поддержку Отправить отзыв Очень круто! Может быть лучше Один вопрос Вам нравится приложение Tangem? - Токенов пока нет - Добавленные токены - Удалить токен %s - Вы уверены, что хотите удалить этот токен? Добавить токены - Добавить токен Начните поиск - Управление токенами - Добавить пользовательский - Популярные токены - Добавлен Значок токена Добавить токен Адрес контракта Обязательное поле Пожалуйста, выберите сеть - Пожалуйста, заполните все поля Количество знаков после запятой должно быть корректным числом не больше %d Адрес контракта некорректен Путь деривации некорректен Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить Этот токен/сеть уже находится в вашем списке Сеть - Токен Знаков после запятой Сеть Не выбрано @@ -197,13 +139,8 @@ Символ токена Деривация по BIP44 По-умолчанию - Добавить токен - Блокчейн - Управление токенами - Блокчейны Копировать Поделиться - + Добавить токены Пожалуйста, дождитесь завершения транзакции %s, чтобы иметь возможность отправить средства Проверка подлинности не удалась Эта карта может быть производственным образцом или подделкой @@ -219,9 +156,6 @@ Это тестовая карта. Не принимайте ее в качестве оплаты. Она должна использоваться только в целях тестирования и разработки. Вы хотите купить или продать криптовалюту? К сожалению, текущая версия приложения не готова к работе с этой картой, проверьте наличие обновлений - Добро пожаловать в Tangem - Самый безопасный способ покупать, использовать и\n хранить криптовалюту - Получить новую карту Активация карты Как это работает? Получить криптовалюту @@ -233,7 +167,6 @@ Чтобы начать, просто пополните кошелек более чем на %s %s Купить криптовалюту Показать адрес кошелька - Отсканируйте адрес, чтобы пополнить кошелек Успешно! Ваша криптокарта активирована и готова к использованию Продолжить @@ -242,32 +175,25 @@ Отправляйте только %s (%s) из сети %s на этот адрес. Иначе это может привести к утрате средств. Если процесс создания кошелька каким-либо образом прервется, вам придется начать все сначала. Процесс связывания карт частично завершен. Вы не можете выйти из него сейчас. - Внутренняя ошибка: не удается создать менеджер кошельков Блокчейн недоступен. Попробуй позже. Хорошо, понял! - Успешно! Начнем Выполнить резервное копирование - Получить резервную карту Оставить на потом Завершить процесс резервного копирования + Добавить резервную карту Сканировать карту #%d Сканировать основную карту Добавить больше карт - Сделайте резервную копию вашего кошелька Нет резервных карт Подготовьте свою карту Добавлена одна резервная карта Добавлены две резервные карты Резервная карта #%d - Вы можете привязать до трех карт к одному кошельку Чтобы начать процесс резервного копирования, добавьте одну или две резервные карты. - Подготовьте основную карту с номером %s Подготовьте основную карту Вы можете добавить еще одну карту или завершить процесс резервного копирования Добавлено максимальное количество карт - Если процесс резервного копирования будет прерван каким-либо образом, вам придется начать все сначала Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить? Создать код доступа Введите код доступа повторно @@ -282,7 +208,6 @@ Введенный код доступа не соответствует исходному коду доступа Создать Подтвердить - Вы создали 2 резервные карты, и теперь эти карты готовы к использованию Создание резервной копии Процесс резервного копирования частично завершен. Вы не можете выйти из него сейчас. Резервный кошелек @@ -294,13 +219,6 @@ Вы сможете установить код доступа для защиты своих кошельков Код доступа можно восстановить с помощью одной из резервных карт Ваша карта настроена и готова к использованию. - Вы создали 1 резервную карту, и теперь эта карта готова к использованию - Основная карта - Я понимаю - Статус резервного копирования - Активный. Количество резервных карт: %d - Связано. Количество резервных карт: %d - Нет резервной копии У Вас есть прерванное резервное копирование. Хотите возобновить? Да, возобновить Отказаться @@ -315,20 +233,15 @@ Подготовьте резервную карту с номером %s Подготовьте основную карту с номером %s Сброс до заводских настроек - Код доступа доступен только для карт с резервной копией Сеть Solana взымает арендную плату в размере %s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %s, чтобы использовать его бесплатно. - %s (%s) Эта карта не является векселем на предъявителя. В настоящее время мы не можем сопоставить количество подписей на карте с информацией в блокчейне. Это нормально, но в редких случаях может означать, что предыдущий владелец скрывает автономную подпись, что является проблемой безопасности.\n\nНе принимайте эту карту в качестве физического платежа от кого-то, кому Вы не доверяете.\n\nВо всех остальных отношениях - это совершенно безопасно.\n\nTangem — единственный аппаратный кошелек, предлагающий защиту от подсчета подписей. WalletConnect Запрос на создание транзакции для %s\n%s\n\nСумма: %s\nКомиссия: %s\nВсего: %s\nБаланс: %s - Запрос на запуск сеанса для %s\n\nURL: %s + Запрос на открытие сессии для\n%s\n\nСЕТЬ: %s\n\nURL: %s Сессии WalletConnect - Сеанс WalletConnect открыт с %s Упс. Нет сессий. Нет открытых сессий WalletConnect Открытая сессия - Карта: %s - Коснитесь карты, чтобы привязать ее WalletConnect Не удается отправить транзакцию. Недостаточно средств. Просьба подписать сообщение\n Сообщение для %s:\n%s @@ -336,24 +249,20 @@ Вставить из буфера обмена Сканировать новый код Эту карту нельзя использовать для установки сеанса WalletConnect. - Операция не может быть завершена Операция не может быть завершена. \n\nВы уже установили сеанс WalletConnect с этими параметрами. Не удалось установить сеанс WalletConnect: ошибка времени выполнения. Пожалуйста, повторите попытку позже. - Транзакция BNB успешно подписана и отправлена в DApp DApp %s, запрашивает\nподпись транзакции BNB с\n%s Сведения о транзакции:\nОт: %s\nКому: %s\nСумма: %s Торговый ордер на %s\nЦена: %s\nСумма к получению: %s\nСумма к оплате: %s Мои предложения Не могу отсканировать карту Не могу отправить транзакцию - Не могу протолкнуть транзакцию Обращение в поддержку Tangem Обращение в поддержку Расскажите, каких функций Вам не хватает, и мы постараемся Вам помочь Скажите, пожалуйста, какая у Вас карта? Пожалуйста, расскажите нам больше о Вашей проблеме. Каждая маленькая деталь может помочь. Привет, команда поддержки, - Пожалуйста, расскажите нам больше о Вашей проблеме. Каждая деталь может быть полезной. Информация ниже не является обязательной. Вы можете стереть её, если хотите. Нет diff --git a/app/src/main/res/values-ru/strings_blockchain.xml b/app/src/main/res/values-ru/strings_blockchain.xml index 5baeca3d33..f3be360857 100644 --- a/app/src/main/res/values-ru/strings_blockchain.xml +++ b/app/src/main/res/values-ru/strings_blockchain.xml @@ -5,9 +5,8 @@ Неверная комиссия Минимальная сумма: %s Сдача слишком мала - Для создания учетной записи отправьте 1+ XLM на этот адрес Не удалось получить комиссию - Целевая учетная запись не создана. Сумма для отправки должна быть %s + плата за создание или больше + Аккаунт получателя не создан. Сумма для отправки должна быть %s + комиссия или больше Неизвестная ошибка Неверный адрес Недопустимая сумма diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6ea8e27b1c..af231b0713 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,25 +11,14 @@ Delete %s - Camera access denied You have not given access to your camera, please adjust your privacy settings - Settings - Welcome to Tangem.\nDo you have one of our cards? - Welcome back to Tangem. \nScan your card to start. - Shop - Get ready to tap your \ncard to the back of the \nphone. - Tap in Scan card - Yes! Scan it. - Scan Send Create wallet Tangem - Choice wallet option - Token Tokens Verified Balance Balance is loading… @@ -50,15 +39,11 @@ Explore address Create PayString - %s wallet Create PayString - Card: %s PayString name $payid.tangem.com - Create Your PayString is information unique to you, like your phone number, email or ABN. - PayString created successfully and copied to clipboard Error response while creating PayString This PayString already exists. Try a different one. @@ -75,7 +60,6 @@ Priority Include fee %s %s will be sent - ≈ %s (inc. fee: %s) %s %s and %s %s will be sent Balance: %s %s Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while @@ -86,14 +70,7 @@ Signed %s hashes App currency - Settings - Card - Validate card - Manage Security - Erase wallet This action is irreversible. If, after deleting the wallet, someone sends funds to it, then you will not be able to withdraw them. - If you forget the code, you will lose the ability to use the card. There is no way to recover or change your code if you lose it. - Manage Security Long Tap This mechanism protects against proximity attacks on a card. It will enforce a delay between reception and execution of a command. Passcode @@ -101,21 +78,16 @@ Access code You will have to submit the correct access code before scanning the card - You may experience NFC problems with some iPhone 7/7+ during the extraction This card has been already topped up and signed transactions in the past. Consider immediate withdrawal of all funds if you have received this card from an untrusted source. If it\'s your card, there is nothing to worry about. Warning This card is not designed to work with this app The card you scanned is a development card. Don\'t accept it as a payment - Tangem cards manufactured before September 2019 cannot currently be extracted with an iPhone. We\'re working hard with Apple to make it possible in future versions of iOS. Tap to sign - To sign, connect your phone and the card exactly as it shown above - To reset to factory settings, connect your phone and the card exactly as it shown above Tap to create wallet To change the access code, connect your phone and the card exactly as it shown above To change the passcode, connect your phone and the card exactly as it shown above - Invalid PayString Legal Disclaimer \n @@ -170,7 +142,6 @@ No internet connection - PayString verification failed PayString unsupported by blockchain PayString not registered PayString request failed @@ -185,5 +156,4 @@ Tap to scan Tap the card - This is the currently active option diff --git a/app/src/main/res/values/strings_blockchain.xml b/app/src/main/res/values/strings_blockchain.xml index f19a487b14..b2ebd2e2d9 100644 --- a/app/src/main/res/values/strings_blockchain.xml +++ b/app/src/main/res/values/strings_blockchain.xml @@ -5,9 +5,8 @@ Invalid Fee Minimum amount is %s Change is too small - To create account send 1+ XLM to this address Failed to get fee - Target account is not created. Amount to send should be %s + fee or more to create + Target account is not created. Amount to send should be %s + fee or more Unknown error Invalid address Invalid amount diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index 676c8cc848..f82c05ea0e 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -5,18 +5,14 @@ Sell Trade Default - Compatibility Legacy - Create twin wallet Create twin wallet Generate wallet keys on both cards to start using your Twins - Tangem Twin One wallet. Two cards. This one that you are holding in your hands and the other one with number #%s. - Re-create twin wallet Card terms of use Tangem Twin @@ -25,32 +21,19 @@ Preparing card Creating wallet Scan the card #%s - You have already started the process of recreating twin wallet. If you interrupt it, you won\'t be able to use your twin cards until you start it again and complete recreating the wallet. You\'ve tapped the same card. To create twin wallet you need to tap a second twin card. - The twin address was successfully created //Common Start - Back Add - Remove Search - App Send feedback - Sent successfully - Thank you for your feedback - Your suggestions were sent - Thank your for your feedback. We will response as soon as possible - Failed to send email - Reason: %s Can\'t send a transaction Reason: %s Are you having difficulty scanning your card? - Please try to tap the card exactly as shown in the animation or request support. I\'m okay - Try again Request support Send feedback Really cool! @@ -59,31 +42,20 @@ How do you like Tangem? //Manage tokens - No tokens yet - Added tokens - Remove %s token - Are you sure you want to remove this token? Add Tokens - Add Token - Manage Tokens - Add Custom Custom tokens - Popular tokens - Added Token icon Add Token Contract address Required field Please select the network - Please fill in all the fields Decimal number must be a valid integer, no higher than %d Contract address is invalid Derivation path is invalid Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing. This token/network has already been added to your list Network - Token Decimals Network Not selected @@ -93,17 +65,12 @@ Token symbol BIP44 coin type Default - Add token //Details - Blockchain - Manage tokens - Blockchains %s tokens Copy Share - + Add tokens Please wait for %s transaction to complete to be able to send funds Authenticity check failed @@ -124,18 +91,15 @@ //Wallet Connect WalletConnect Wallet Connect Sessions - WalletConnect session opened with %s Ooops. No Sessions. No opened WalletConnect sessions Open session - Card: %s - Tap card to bind to wallet connect Request to create transaction for %s\n%s\n\nAmount: %s\nFee: %s\nTotal: %s\nBalance: %s Can\'t send transaction. Not enough funds. - Request to start a session for %s\n\nURL: %s + Request to start a session for\n%s\n\nNETWORK: %s\n\nURL: %s Requesting to sign a message\n Message for %s:\n%s @@ -150,7 +114,6 @@ Paste from clipboard Scan new code This card can\'t be used to establish WalletConnect session - The operation couldn\'t be completed The operation couldn\'t be completed. \n\nYou have already established a WalletConnect session with this parameters. Failed to establish WalletConnect session: timeout error. Please, try again later. @@ -162,9 +125,6 @@ Oops, the current version of the application is not ready to work with this card, please check for updates. - Welcome to Tangem - The safest way to buy, use and\n store cryptocurrency - Get a new card Activating card How it works? @@ -179,7 +139,6 @@ To get started, simply top up the wallet with more than %s %s. Buy crypto Show the wallet\'s address - Scan the address to top up your wallet Success! Your crypto card is activated and ready to be used @@ -191,17 +150,14 @@ If the process of creating the wallet gets interrupted in any way, you\'ll have to start over. The twinning process is partly complete. You can\'t exit it now. - Internal error: can\'t create wallet manager Blockchain is unreachable. Try later Ok, Got it! - Success! Getting started Backup now - Get a backup card Skip for later Finalize the backup process + Add backup card @@ -209,20 +165,16 @@ Scan primary card Add more cards - Backup your wallet No backup cards Prepare your card One backup card added Two backup cards added Backup card #%d - You can backup your keys up to two other blank wallet cards To start the backup process add up to two backup cards. - Prepare the primary card with number %s Prepare the primary card You can add one more card or finalize the backup process Max number of cards added. Finalize the backup process - If the process of backup gets interrupted in any way you\'ll have to start over You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it in backup, otherwise you can buy it in shop. Do you like to continue backup process? @@ -240,7 +192,6 @@ Create Submit - You have created 2 backup cards, and now these cards are ready for use. Creating a backup The backup process is partly complete. You can\'t exit it now. @@ -255,14 +206,7 @@ Access code can be restored with one of backup cards. Your wallet card is configured and ready for use. - You have created 1 backup card, and now this card are ready for use. - Primary Card - I understand - Backup status - Active. Backup cards count: %d - Linked. Backup cards count: %d - No backup You have an interrupted backup. Do you want to resume? Yes, resume Discard @@ -278,11 +222,9 @@ Prepare the primary card with number %s Reset to factory settings - Access code is available only for cards with a backup. - The BNB transaction has been successfully signed and sent to the Dapp Dapp %s, requesting to\n sign BNB transaction \n @@ -298,20 +240,17 @@ Amount to pay: %s Solana network charges a rent of %s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %s to use it for free. - %s (%s) Type here to search My suggestions Can\'t scan a card Can\'t send a transaction - Can\'t push a transaction Tangem feedback Feedback Tell us what functions you are missing, and we will try to help you. Please tell us what card do you have? Please tell us more about your issue. Every small detail can help. Hi support team, - Please tell us more about your issue. Every small detail can help. Following information is optional. You can erase it if you don’t want to share it. diff --git a/core/ui/src/main/res/values-de/strings_final.xml b/core/ui/src/main/res/values-de/strings_final.xml index b7e64d53df..665f17302a 100644 --- a/core/ui/src/main/res/values-de/strings_final.xml +++ b/core/ui/src/main/res/values-de/strings_final.xml @@ -9,8 +9,6 @@ Total Other payment methods Buy now - Placing your order - Your order Meet\nTangem Buy Store @@ -19,7 +17,6 @@ Exchange Lend Borrow - Stake Revolutionary Hardware Wallet Store your crypto assets secure while keeping private keys contained in your card Ultra Secure Backup @@ -33,7 +30,6 @@ The Wallet for Everyone Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. Order - Search tokens Search tokens You are currently running in Demo mode. All funds are not real. This feature is disabled in Demo mode @@ -41,11 +37,8 @@ Available networks Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds. Tokens in Solana network are not supported by this card due to firmware limitation. - Do not transfer your funds to tokens in this blockchain, otherwise it may lead to their irretrievable loss. Contract address copied! If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds - Custom - Notice Attention The server is not available, please try again later Total balance @@ -72,15 +65,12 @@ Tangem Bot Card Settings Security Mode - Selected application protection method Change access code Access code will be changed on this card only Get your card ready! Scan the card to change its settings. The changes will impact only the card you’ve scanned and will not affect other cards tied to your wallet. Scan Card App settings - Enable biometric authentication - Go to settings to enable biometric authentication in the Tangem app Keep the wallet in the app Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card. Save Access Code @@ -90,7 +80,6 @@ WalletConnect Connect to Dapps Link More Cards - You can synchronize up to three cards into one wallet. It can only be done once. Privacy policy Reset to factory settings This action will completely remove the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. @@ -107,7 +96,6 @@ Invalid Memo. It won\'t be added to the transaction. Invalid Tag. It won\'t be added to the transaction. Connection with this Dapp cannot be established due to its technical implementation. - To start working with the card scan the first card and make a backup Attention Tap the card with the visa logo No funds for activation @@ -118,7 +106,6 @@ Connect KYC Verify your identity - In progress Set PIN code Register Verify via Utorg @@ -132,7 +119,6 @@ PIN Code Set PIN code for your SaltPay card Chat with support - This card is not designed to SaltPay backup Please hold the card until the operation complete To start the backup process you have to add Tangem card as your backup No backup card diff --git a/core/ui/src/main/res/values-fr/strings_final.xml b/core/ui/src/main/res/values-fr/strings_final.xml index b7e64d53df..665f17302a 100644 --- a/core/ui/src/main/res/values-fr/strings_final.xml +++ b/core/ui/src/main/res/values-fr/strings_final.xml @@ -9,8 +9,6 @@ Total Other payment methods Buy now - Placing your order - Your order Meet\nTangem Buy Store @@ -19,7 +17,6 @@ Exchange Lend Borrow - Stake Revolutionary Hardware Wallet Store your crypto assets secure while keeping private keys contained in your card Ultra Secure Backup @@ -33,7 +30,6 @@ The Wallet for Everyone Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. Order - Search tokens Search tokens You are currently running in Demo mode. All funds are not real. This feature is disabled in Demo mode @@ -41,11 +37,8 @@ Available networks Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds. Tokens in Solana network are not supported by this card due to firmware limitation. - Do not transfer your funds to tokens in this blockchain, otherwise it may lead to their irretrievable loss. Contract address copied! If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds - Custom - Notice Attention The server is not available, please try again later Total balance @@ -72,15 +65,12 @@ Tangem Bot Card Settings Security Mode - Selected application protection method Change access code Access code will be changed on this card only Get your card ready! Scan the card to change its settings. The changes will impact only the card you’ve scanned and will not affect other cards tied to your wallet. Scan Card App settings - Enable biometric authentication - Go to settings to enable biometric authentication in the Tangem app Keep the wallet in the app Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card. Save Access Code @@ -90,7 +80,6 @@ WalletConnect Connect to Dapps Link More Cards - You can synchronize up to three cards into one wallet. It can only be done once. Privacy policy Reset to factory settings This action will completely remove the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. @@ -107,7 +96,6 @@ Invalid Memo. It won\'t be added to the transaction. Invalid Tag. It won\'t be added to the transaction. Connection with this Dapp cannot be established due to its technical implementation. - To start working with the card scan the first card and make a backup Attention Tap the card with the visa logo No funds for activation @@ -118,7 +106,6 @@ Connect KYC Verify your identity - In progress Set PIN code Register Verify via Utorg @@ -132,7 +119,6 @@ PIN Code Set PIN code for your SaltPay card Chat with support - This card is not designed to SaltPay backup Please hold the card until the operation complete To start the backup process you have to add Tangem card as your backup No backup card diff --git a/core/ui/src/main/res/values-it/strings_final.xml b/core/ui/src/main/res/values-it/strings_final.xml index b7e64d53df..665f17302a 100644 --- a/core/ui/src/main/res/values-it/strings_final.xml +++ b/core/ui/src/main/res/values-it/strings_final.xml @@ -9,8 +9,6 @@ Total Other payment methods Buy now - Placing your order - Your order Meet\nTangem Buy Store @@ -19,7 +17,6 @@ Exchange Lend Borrow - Stake Revolutionary Hardware Wallet Store your crypto assets secure while keeping private keys contained in your card Ultra Secure Backup @@ -33,7 +30,6 @@ The Wallet for Everyone Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. Order - Search tokens Search tokens You are currently running in Demo mode. All funds are not real. This feature is disabled in Demo mode @@ -41,11 +37,8 @@ Available networks Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds. Tokens in Solana network are not supported by this card due to firmware limitation. - Do not transfer your funds to tokens in this blockchain, otherwise it may lead to their irretrievable loss. Contract address copied! If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds - Custom - Notice Attention The server is not available, please try again later Total balance @@ -72,15 +65,12 @@ Tangem Bot Card Settings Security Mode - Selected application protection method Change access code Access code will be changed on this card only Get your card ready! Scan the card to change its settings. The changes will impact only the card you’ve scanned and will not affect other cards tied to your wallet. Scan Card App settings - Enable biometric authentication - Go to settings to enable biometric authentication in the Tangem app Keep the wallet in the app Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card. Save Access Code @@ -90,7 +80,6 @@ WalletConnect Connect to Dapps Link More Cards - You can synchronize up to three cards into one wallet. It can only be done once. Privacy policy Reset to factory settings This action will completely remove the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. @@ -107,7 +96,6 @@ Invalid Memo. It won\'t be added to the transaction. Invalid Tag. It won\'t be added to the transaction. Connection with this Dapp cannot be established due to its technical implementation. - To start working with the card scan the first card and make a backup Attention Tap the card with the visa logo No funds for activation @@ -118,7 +106,6 @@ Connect KYC Verify your identity - In progress Set PIN code Register Verify via Utorg @@ -132,7 +119,6 @@ PIN Code Set PIN code for your SaltPay card Chat with support - This card is not designed to SaltPay backup Please hold the card until the operation complete To start the backup process you have to add Tangem card as your backup No backup card diff --git a/core/ui/src/main/res/values-ru/strings_final.xml b/core/ui/src/main/res/values-ru/strings_final.xml index dc387d3f71..5b98f5cc07 100644 --- a/core/ui/src/main/res/values-ru/strings_final.xml +++ b/core/ui/src/main/res/values-ru/strings_final.xml @@ -9,8 +9,6 @@ Итого Другие способы оплаты Купить сейчас - Оформление заказа - Ваш заказ Встречайте\nTangem Покупайте Храните @@ -19,7 +17,6 @@ Обменивайте Вкладывайте Занимайте - Стейкайте Революционный аппаратный кошелек Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Все ключи в безопасности @@ -33,7 +30,6 @@ Кошелек для каждого Используйте его на ходу, в любом месте, в любое время. Без проводов и батареек. Как только понадобится крипта, просто приложите карту к телефону. Купить - Поиск токенов Поиск токенов Приложение работает в демонстрационном режиме. Средства на всех счетах ненастоящие. Эта функция недоступна в демонстрационном режиме @@ -41,11 +37,8 @@ Доступные сети Внимание! Валюты на разных сетях имеют разные адреса. Убедитесь, что адрес соответствует сети, в которой вы отправляете средства. Токены в сети Solana не поддерживаются этой картой из-за ограничений прошивки. - Не осуществляйте перевод на токены в данной сети, иначе это может привести к их безвозвратной утере. Адрес контракта скопирован! Если вы совершили ошибку с выбором сети при переводе средств с биржи, эта инструкция поможет вам восстановить средства - Пользовательский - Уведомление Внимание Сервер недоступен, повторите попытку позднее Баланс @@ -70,15 +63,12 @@ Tangem Bot Настройки карты Тип безопасности - Выбранный способ защиты приложения Смена кода доступа Код доступа будет изменен только на данной карте Приготовьте свою карту Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку. Сканировать Настройки приложения - Включите биометрическую аутентификацию - Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem Cохранение кошелька Если эта опция включена, кошельки всех отсканированных карт будут привязываться к приложению и при открытии приложения будет необходимо пройти биометрическую аутентификацию. Подпись транзакций все так же потребует карту. Сохранение кода доступа @@ -88,7 +78,6 @@ WalletConnect Подключение к Dapps Добавить еще карты - Вы можете объединить до трех карт в одном кошельке. Это можно сделать только один раз. Privacy policy Сброс к заводским настройкам Это действие приведет к полному удалению кошелька на этой карте. Кошелек невозможно будет восстановить или использовать данную карту для восстановления кода доступа @@ -105,7 +94,6 @@ Недопустимый Tag. Он не будет добавлен в транзакцию. Не удалось установить сессию WalletConnect Cоединение с этим Dapp сервисом не может быть установлено из-за его технической реализации. - Для начала работы с картой сначала отсканируйте первую карту и сделайте бэкап Внимание Приложите карту с логотипом Visa Недостаточно средств для активации @@ -116,7 +104,6 @@ Подключиться Верификация клиента Подтвердите свою личность - В обработке Установить Код доступа Зарегистрироваться Верифицировать (Utorg) @@ -130,7 +117,6 @@ Код доступа Установите Код доступа для вашей SaltPay карты Чат поддержки - Данная карта не подходит для бэкапа SaltPay Пожалуйста, удерживайте карту до завершения операции Для начала процесса бэкапа вам необходимо добавить Tangem карту Бэкап карта не добавлена diff --git a/core/ui/src/main/res/values/strings_final.xml b/core/ui/src/main/res/values/strings_final.xml index e434fc002d..8b0c3fcc01 100644 --- a/core/ui/src/main/res/values/strings_final.xml +++ b/core/ui/src/main/res/values/strings_final.xml @@ -9,8 +9,6 @@ Total Other payment methods Buy now - Placing your order - Your order Meet\nTangem Buy Store @@ -19,7 +17,6 @@ Exchange Lend Borrow - Stake Revolutionary Hardware Wallet Store your crypto assets secure while keeping private keys contained in your card Ultra Secure Backup @@ -33,7 +30,6 @@ The Wallet for Everyone Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. Order - Search tokens Search tokens You are currently running in Demo mode. All funds are not real. This feature is disabled in Demo mode @@ -41,11 +37,8 @@ Available networks Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds. Tokens in Solana network are not supported by this card due to firmware limitation. - Do not transfer your funds to tokens in this blockchain, otherwise it may lead to their irretrievable loss. Contract address copied! If you chose the wrong network during your crypto transfer from the exchange, this guide will help you recover your funds - Custom - Notice Attention The server is not available, please try again later Total balance @@ -72,15 +65,12 @@ Tangem Bot Card Settings Security Mode - Selected application protection method Change access code Access code will be changed on this card only Get your card ready! Scan the card to change its settings. The changes will impact only the card you’ve scanned and will not affect other cards tied to your wallet. Scan Card App settings - Enable biometric authentication - Go to settings to enable biometric authentication in the Tangem app Keep the wallet in the app Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card. Save Access Code @@ -90,7 +80,6 @@ WalletConnect Connect to Dapps Link More Cards - You can synchronize up to three cards into one wallet. It can only be done once. Privacy policy Reset to factory settings This action will completely remove the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. @@ -107,7 +96,6 @@ Invalid Memo. It won\'t be added to the transaction. Invalid Tag. It won\'t be added to the transaction. Connection with this Dapp cannot be established due to its technical implementation. - To start working with the card scan the first card and make a backup Attention Tap the card with the visa logo No funds for activation @@ -118,7 +106,6 @@ Connect KYC Verify your identity - In progress Set PIN code Register Verify via Utorg @@ -132,7 +119,6 @@ PIN Code Set PIN code for your SaltPay card Chat with support - This card is not designed to SaltPay backup Please hold the card until the operation complete To start the backup process you have to add Tangem card as your backup No backup card @@ -154,5 +140,5 @@ Do you want to exit the activation process? In this case, you will need to start from the beginning. You have used a card from another wallet. Tap the card associated with this wallet - Internal error: wallet manager not found + Internal error: wallet manager not found diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt index cbdcdaeb50..1e2ceefc51 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementText.kt @@ -27,7 +27,7 @@ import com.tangem.feature.referral.presentation.R fun Preview_AgreementText_InLightTheme() { TangemTheme(isDark = false) { Box(modifier = Modifier.background(MaterialTheme.colors.primary)) { - AgreementText(firstPartResId = R.string.referral_tou_not_enroled_prefix, onClicked = {}) + AgreementText(firstPartResId = R.string.referral_tos_not_enroled_prefix, onClicked = {}) } } } @@ -37,7 +37,7 @@ fun Preview_AgreementText_InLightTheme() { fun Preview_AgreementText_InDarkTheme() { TangemTheme(isDark = true) { Box(modifier = Modifier.background(MaterialTheme.colors.primary)) { - AgreementText(firstPartResId = R.string.referral_tou_not_enroled_prefix, onClicked = {}) + AgreementText(firstPartResId = R.string.referral_tos_not_enroled_prefix, onClicked = {}) } } } @@ -71,7 +71,7 @@ private fun annotatedAgreementString(firstPart: String): AnnotatedString { append(stringResource(id = R.string.common_terms_and_conditions)) } withStyle(SpanStyle(color = MaterialTheme.colors.textColor(type = TextColorType.TERTIARY))) { - append(" ${stringResource(id = R.string.referral_tou_suffix)}") + append(" ${stringResource(id = R.string.referral_tos_suffix)}") } } } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt index 2706490417..f202bd4ef6 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/NonParticipateBottomBlock.kt @@ -81,7 +81,7 @@ fun NonParticipateBottomBlock( ) PersonalCodeCard(code = code) AdditionalButtons(onCopyClicked = onCopyClicked, onShareClicked = onShareClicked) - AgreementText(firstPartResId = R.string.referral_tou_enroled_prefix, onClicked = onAgreementClicked) + AgreementText(firstPartResId = R.string.referral_tos_enroled_prefix, onClicked = onAgreementClicked) } } diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt index f6c6eced81..d58527d894 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt @@ -37,7 +37,7 @@ fun Preview_ParticipateBottomBlock_InDarkTheme() { fun ParticipateBottomBlock(onAgreementClicked: () -> Unit, onParticipateClicked: () -> Unit) { Column { AgreementText( - firstPartResId = R.string.referral_tou_not_enroled_prefix, + firstPartResId = R.string.referral_tos_not_enroled_prefix, onClicked = onAgreementClicked, ) PrimaryEndIconButton( diff --git a/features/referral/presentation/src/main/res/values-ru/string.xml b/features/referral/presentation/src/main/res/values-ru/string.xml index 4a6e997f8e..5e6ef22607 100644 --- a/features/referral/presentation/src/main/res/values-ru/string.xml +++ b/features/referral/presentation/src/main/res/values-ru/string.xml @@ -9,11 +9,11 @@ Получит %s скидку при покупке карточки на сайте tangem.com - Нажимая на эту кнопку вы принимаете + Нажимая на эту кнопку вы принимаете условия участия - в реферальной программе + в реферальной программе Участвовать - Вы приняли + Вы приняли Ваш персональный код %d кошельков Ваши друзья купили diff --git a/features/referral/presentation/src/main/res/values/string.xml b/features/referral/presentation/src/main/res/values/string.xml index 7042ed2411..e429fe37e1 100644 --- a/features/referral/presentation/src/main/res/values/string.xml +++ b/features/referral/presentation/src/main/res/values/string.xml @@ -9,11 +9,11 @@ Will get a %s discount when buying a card on tangem.com - By tapping this button you accept + By tapping this button you accept terms and conditions - of the referral program + of the referral program Participate - You\'ve accepted + You\'ve accepted Your personal code %d wallets Your friends bought From 9eab9e1a6e83a879b53fb0b130d0a8fb5b9bb010 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 30 Nov 2022 11:29:27 +0300 Subject: [PATCH 075/372] Updated on 2026-08-14 --- .../ui/walletconnect/dialogs/ApproveWcSessionDialog.kt | 4 +++- .../features/send/ui/dialogs/RequestFeeErrorDialog.kt | 2 +- .../com/tangem/tap/features/wallet/ui/BalanceWidget.kt | 2 +- .../tap/features/wallet/ui/WalletDetailsFragment.kt | 2 +- .../wallet/ui/dialogs/SignedHashesWarningDialog.kt | 2 +- .../layout_onboarding_container_bottom.xml | 2 +- app/src/main/res/layout/layout_address.xml | 4 ++-- .../res/layout/layout_onboarding_container_bottom.xml | 2 +- app/src/main/res/layout/layout_wallet_details.xml | 8 ++++---- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-de/strings_blockchain.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-fr/strings_blockchain.xml | 2 +- app/src/main/res/values-it/strings.xml | 2 +- app/src/main/res/values-it/strings_blockchain.xml | 2 +- app/src/main/res/values-ru/strings.xml | 10 ++++------ app/src/main/res/values-ru/strings_blockchain.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- app/src/main/res/values/strings_blockchain.xml | 2 +- app/src/main/res/values/strings_untranslated.xml | 10 ++++------ core/ui/src/main/res/values-de/strings_final.xml | 4 ++-- core/ui/src/main/res/values-fr/strings_final.xml | 4 ++-- core/ui/src/main/res/values-it/strings_final.xml | 4 ++-- core/ui/src/main/res/values/string_common.xml | 4 ++-- core/ui/src/main/res/values/strings_final.xml | 6 +++--- 25 files changed, 43 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt index ab95ef6ef8..f0917ecf17 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ApproveWcSessionDialog.kt @@ -12,9 +12,11 @@ import com.tangem.wallet.R class ApproveWcSessionDialog { companion object { fun create(session: WalletConnectSession, networks: List, context: Context): AlertDialog { + val sessionBlockchain = requireNotNull(session.wallet.blockchain) { "session network is null" } val message = context.getString( R.string.wallet_connect_request_session_start, session.peerMeta.name, + sessionBlockchain.fullName, session.peerMeta.url, ) return AlertDialog.Builder(context).apply { @@ -22,7 +24,7 @@ class ApproveWcSessionDialog { setMessage(message) setPositiveButton(context.getText(R.string.common_start)) { _, _ -> store.dispatch(GlobalAction.HideDialog) - store.dispatch(WalletConnectAction.ChooseNetwork(session.wallet.blockchain!!)) + store.dispatch(WalletConnectAction.ChooseNetwork(sessionBlockchain)) } if (networks.size > 1) { setNeutralButton(context.getText(R.string.wallet_connect_select_network)) { _, _ -> diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt index 669ec571f0..353989e76c 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt @@ -18,7 +18,7 @@ class RequestFeeErrorDialog { val errorMessage = dialog.error.customMessage return AlertDialog.Builder(context).apply { - setTitle(R.string.send_error_fee_request_failed) + setTitle(R.string.common_fee_error) setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage)) setNegativeButton(R.string.alert_button_send_feedback) { _, _ -> store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage))) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index 34ca1f10c1..4b9b5cb18e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -128,7 +128,7 @@ class BalanceWidget( tvErrorTitle.text = fragment.getText(R.string.wallet_error_no_account) tvErrorDescriptions.text = fragment.getString( - R.string.wallet_error_no_account_subtitle_format, + R.string.no_account_generic, data.amountToCreateAccount, data.currencySymbol ) } 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 f40bf97ea5..d938b8966d 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 @@ -340,7 +340,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), lBalanceError.tvErrorTitle.text = getText(R.string.wallet_error_no_account) lBalanceError.tvErrorDescriptions.text = getString( - R.string.wallet_error_no_account_subtitle_format, + R.string.no_account_generic, data.amountToCreateAccount, data.currencySymbol, ) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SignedHashesWarningDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SignedHashesWarningDialog.kt index 609537bbaf..c8db3c3d23 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SignedHashesWarningDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/SignedHashesWarningDialog.kt @@ -14,7 +14,7 @@ class SignedHashesWarningDialog { return AlertDialog.Builder(context).apply { setTitle(context.getString(R.string.warning_important_security_info)) setMessage(R.string.alert_signed_hashes_message) - setPositiveButton(R.string.alert_button_i_understand) { _, _ -> + setPositiveButton(R.string.common_understand) { _, _ -> store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId) store.dispatch( GlobalAction.HideWarningMessage( diff --git a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml index b14f8571de..e0f2965300 100644 --- a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml +++ b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml @@ -51,7 +51,7 @@ android:id="@+id/chb_understand" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/alert_button_i_understand" + android:text="@string/common_understand" android:textStyle="bold" android:visibility="gone" app:buttonTint="@color/accent" diff --git a/app/src/main/res/layout/layout_address.xml b/app/src/main/res/layout/layout_address.xml index abf039bab7..79341aa867 100644 --- a/app/src/main/res/layout/layout_address.xml +++ b/app/src/main/res/layout/layout_address.xml @@ -43,14 +43,14 @@ style="@style/TapChip" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/wallet_address_chip_default" /> + android:text="@string/address_type_default" /> + android:text="@string/address_type_legacy" /> diff --git a/app/src/main/res/layout/layout_onboarding_container_bottom.xml b/app/src/main/res/layout/layout_onboarding_container_bottom.xml index 9ee7ea6489..6cbb49b0fa 100644 --- a/app/src/main/res/layout/layout_onboarding_container_bottom.xml +++ b/app/src/main/res/layout/layout_onboarding_container_bottom.xml @@ -51,7 +51,7 @@ android:id="@+id/chb_understand" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/alert_button_i_understand" + android:text="@string/common_understand" android:textStyle="bold" android:visibility="gone" app:buttonTint="@color/accent" diff --git a/app/src/main/res/layout/layout_wallet_details.xml b/app/src/main/res/layout/layout_wallet_details.xml index 8e33a5cead..85cb90977c 100644 --- a/app/src/main/res/layout/layout_wallet_details.xml +++ b/app/src/main/res/layout/layout_wallet_details.xml @@ -60,14 +60,14 @@ style="@style/TapChip" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/wallet_address_chip_default" /> + android:text="@string/address_type_default" /> + android:text="@string/address_type_legacy" /> @@ -127,7 +127,7 @@ android:layout_height="wrap_content" android:layout_marginEnd="4dp" android:src="@drawable/ic_inactive" - android:text="@string/wallet_address_button_copy" + android:text="@string/common_copy" android:textAlignment="center" android:textColor="@color/colorSecondary" app:chipBackgroundColor="@color/blue_pale" @@ -148,7 +148,7 @@ android:layout_height="wrap_content" android:layout_marginStart="4dp" android:src="@drawable/ic_inactive" - android:text="@string/wallet_address_button_share" + android:text="@string/common_share" android:textAlignment="center" android:textColor="@color/colorSecondary" app:chipBackgroundColor="@color/blue_pale" diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 967b2ab32f..82b5ef487f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -18,7 +18,7 @@ Die Bilanz wird aufgeladen… Blockchain ist nicht erreichbar Das Konto ist nicht erstellt - %s+ %s aufladen um ein Konto zu erstellen + Laden Sie %s+ %s auf um ein Konto zu erstellen Leere Karte Erstellen Sie ein Wallet um die Tangem-Karte verwenden zu können Diese Karte wird nicht unterstützt diff --git a/app/src/main/res/values-de/strings_blockchain.xml b/app/src/main/res/values-de/strings_blockchain.xml index 8b9715a9e6..910ae90ce2 100644 --- a/app/src/main/res/values-de/strings_blockchain.xml +++ b/app/src/main/res/values-de/strings_blockchain.xml @@ -6,7 +6,7 @@ Minimaler Betrag ist %s Restbestand zu klein Das Zielkonto ist nicht erstellt. Der abzusendende Betrag soll %s + Gebühr oder mehr sein - Erhalt der Gebühr fehlgeschlagen + Erhalt der Gebühr fehlgeschlagen Unbekannter Fehler Damit Sie beim nächsten Aufladen Ihrer Brieftasche keine erhöhte Provision zahlen, soll der Betrag um %s XTZ reduziert werden Um %s XTZ reduzieren diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0694d102a5..8ec21b0632 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -19,7 +19,7 @@ Solde est en cours de téléchargement … Blockchain est inaccessible Compte n\'est pas créé - Téléchargez %s +%s pour créer un compte + Pour créer un compte, téléchargez %s+ %s Carte vide Créez un portefeuille pour commencer à utiliser votre carte Tangem Cette carte n\'est pas prise en charge diff --git a/app/src/main/res/values-fr/strings_blockchain.xml b/app/src/main/res/values-fr/strings_blockchain.xml index 380232b81b..60b04a9489 100644 --- a/app/src/main/res/values-fr/strings_blockchain.xml +++ b/app/src/main/res/values-fr/strings_blockchain.xml @@ -6,7 +6,7 @@ Le montant minimal est de %s Le reste est trop petit Le compte cible n\'a pas été créé. Le montant à envoyer doit être de %s + commissions ou plus - Échec de réception des commissions + Échec de réception des commissions Erreur inconnue Adresse incorrecte Somme incorrecte diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 28789234b2..ba083b4b39 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -17,7 +17,7 @@ Il saldo sta per essere caricato… Blockchain non raggiungibile Conto non creato - Carica %s+ %s per creare un conto + Scarica %s+ %s per creare un account Carta vuota Crea un portafoglio per iniziare ad utilizzare la tua carta Tangem Questa carta non è supportata diff --git a/app/src/main/res/values-it/strings_blockchain.xml b/app/src/main/res/values-it/strings_blockchain.xml index c9842eb6dd..d51ca39197 100644 --- a/app/src/main/res/values-it/strings_blockchain.xml +++ b/app/src/main/res/values-it/strings_blockchain.xml @@ -8,7 +8,7 @@ Errore sconosciuto L\'importo supera il saldo Il conto totale non è stato creato L\'importo da inviare deve essere %s + commissione o maggiore - Impossibile ottenere la commissione + Impossibile ottenere la commissione Indirizzo non valido Importo non valido Il totale supera il saldo diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 172380e133..a709a41b26 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -19,7 +19,7 @@ Загрузка баланса… Блокчейн недоступен Аккаунт не создан - Загрузите %s+ %s для создания учетной записи + Пополните счет на %s+ %s, чтобы создать аккаунт Карта пуста Создайте кошелек, чтобы начать пользоваться картой Tangem Эта карта не поддерживается @@ -89,8 +89,8 @@ Купить Продать Торговать - По умолчанию - Устаревший + По умолчанию + Устаревший Создать кошелек Сгенерируйте ключи кошелька на обеих картах, чтобы начать использовать свои Twin карты Один кошелек. Две карты. @@ -139,15 +139,13 @@ Символ токена Деривация по BIP44 По-умолчанию - Копировать - Поделиться Пожалуйста, дождитесь завершения транзакции %s, чтобы иметь возможность отправить средства Проверка подлинности не удалась Эта карта может быть производственным образцом или подделкой Важная информация о безопасности \u26A0 Эта карта ранее подписывала транзакции Узнать подробнее - Я понял + Я понял На этой карте доступно только %s подписей. Вы должны вывести все свои средства. Отключить Подписать diff --git a/app/src/main/res/values-ru/strings_blockchain.xml b/app/src/main/res/values-ru/strings_blockchain.xml index f3be360857..ee2464671d 100644 --- a/app/src/main/res/values-ru/strings_blockchain.xml +++ b/app/src/main/res/values-ru/strings_blockchain.xml @@ -5,7 +5,7 @@ Неверная комиссия Минимальная сумма: %s Сдача слишком мала - Не удалось получить комиссию + Не удалось получить комиссию Аккаунт получателя не создан. Сумма для отправки должна быть %s + комиссия или больше Неизвестная ошибка Неверный адрес diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index af231b0713..ab83f1c625 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,7 +25,7 @@ Blockchain is unreachable Account is not created - Load %s+ %s to create account + Load %s+ %s to create account Empty card Create wallet to start using Tangem card This card is not supported diff --git a/app/src/main/res/values/strings_blockchain.xml b/app/src/main/res/values/strings_blockchain.xml index b2ebd2e2d9..a286c1a5f8 100644 --- a/app/src/main/res/values/strings_blockchain.xml +++ b/app/src/main/res/values/strings_blockchain.xml @@ -5,7 +5,7 @@ Invalid Fee Minimum amount is %s Change is too small - Failed to get fee + Failed to get fee Target account is not created. Amount to send should be %s + fee or more Unknown error Invalid address diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index f82c05ea0e..59a680c908 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -4,8 +4,8 @@ Buy Sell Trade - Default - Legacy + Default + Legacy Create twin wallet Generate wallet keys on both cards to start using your Twins @@ -69,8 +69,6 @@ //Details %s tokens - Copy - Share Please wait for %s transaction to complete to be able to send funds Authenticity check failed @@ -84,7 +82,7 @@ \n\nDo not accept this card as physical payment from someone you don\'t trust. \n\nIt\'s perfectly safe in all other respects. \n\nTangem is the only hardware wallet to offer signature count protection. - I understand + I understand There are only %s signatures available on this card. You must withdraw all of your funds. @@ -251,6 +249,6 @@ Amount to pay: %s Please tell us what card do you have? Please tell us more about your issue. Every small detail can help. Hi support team, - Following information is optional. You can erase it if you don’t want to share it. + Following information is optional. You can erase it if you don\'t want to share it. diff --git a/core/ui/src/main/res/values-de/strings_final.xml b/core/ui/src/main/res/values-de/strings_final.xml index 665f17302a..99b3e6b5b6 100644 --- a/core/ui/src/main/res/values-de/strings_final.xml +++ b/core/ui/src/main/res/values-de/strings_final.xml @@ -26,7 +26,7 @@ Thousands of Currencies A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card DeFi Compatible - Exchange, buy NFT’s, make loans and deposits in more than 100 different decentralized services + Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services The Wallet for Everyone Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. Order @@ -68,7 +68,7 @@ Change access code Access code will be changed on this card only Get your card ready! - Scan the card to change its settings. The changes will impact only the card you’ve scanned and will not affect other cards tied to your wallet. + Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet. Scan Card App settings Keep the wallet in the app diff --git a/core/ui/src/main/res/values-fr/strings_final.xml b/core/ui/src/main/res/values-fr/strings_final.xml index 665f17302a..99b3e6b5b6 100644 --- a/core/ui/src/main/res/values-fr/strings_final.xml +++ b/core/ui/src/main/res/values-fr/strings_final.xml @@ -26,7 +26,7 @@ Thousands of Currencies A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card DeFi Compatible - Exchange, buy NFT’s, make loans and deposits in more than 100 different decentralized services + Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services The Wallet for Everyone Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. Order @@ -68,7 +68,7 @@ Change access code Access code will be changed on this card only Get your card ready! - Scan the card to change its settings. The changes will impact only the card you’ve scanned and will not affect other cards tied to your wallet. + Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet. Scan Card App settings Keep the wallet in the app diff --git a/core/ui/src/main/res/values-it/strings_final.xml b/core/ui/src/main/res/values-it/strings_final.xml index 665f17302a..99b3e6b5b6 100644 --- a/core/ui/src/main/res/values-it/strings_final.xml +++ b/core/ui/src/main/res/values-it/strings_final.xml @@ -26,7 +26,7 @@ Thousands of Currencies A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card DeFi Compatible - Exchange, buy NFT’s, make loans and deposits in more than 100 different decentralized services + Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services The Wallet for Everyone Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. Order @@ -68,7 +68,7 @@ Change access code Access code will be changed on this card only Get your card ready! - Scan the card to change its settings. The changes will impact only the card you’ve scanned and will not affect other cards tied to your wallet. + Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet. Scan Card App settings Keep the wallet in the app diff --git a/core/ui/src/main/res/values/string_common.xml b/core/ui/src/main/res/values/string_common.xml index ad2cf0d79f..85e2e9ef97 100644 --- a/core/ui/src/main/res/values/string_common.xml +++ b/core/ui/src/main/res/values/string_common.xml @@ -1,5 +1,5 @@ - Share - Copy + Share + Copy \ No newline at end of file diff --git a/core/ui/src/main/res/values/strings_final.xml b/core/ui/src/main/res/values/strings_final.xml index 8b0c3fcc01..3460b046ce 100644 --- a/core/ui/src/main/res/values/strings_final.xml +++ b/core/ui/src/main/res/values/strings_final.xml @@ -26,7 +26,7 @@ Thousands of Currencies A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card DeFi Compatible - Exchange, buy NFT’s, make loans and deposits in more than 100 different decentralized services + Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services The Wallet for Everyone Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. Order @@ -60,7 +60,7 @@ Russian bank cards are not accepted at the moment Do you have a bank card of another country or a UnionPay card? Yes - No + No Chat Tangem Bot Card Settings @@ -68,7 +68,7 @@ Change access code Access code will be changed on this card only Get your card ready! - Scan the card to change its settings. The changes will impact only the card you’ve scanned and will not affect other cards tied to your wallet. + Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet. Scan Card App settings Keep the wallet in the app From 7f8c5cd23ad952807fc7cfab933ccc6f69c3438e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 30 Nov 2022 16:43:27 +0400 Subject: [PATCH 076/372] Updated on 2026-08-14 --- app/libs/walletconnect-1.5.5.aar | Bin 145525 -> 0 bytes app/libs/walletconnect-1.5.6.aar | Bin 0 -> 140873 bytes .../walletconnect/WalletConnectManager.kt | 34 ------------------ .../walletconnect/WalletConnectSdkHelper.kt | 30 ++++++++++------ 4 files changed, 20 insertions(+), 44 deletions(-) delete mode 100644 app/libs/walletconnect-1.5.5.aar create mode 100644 app/libs/walletconnect-1.5.6.aar diff --git a/app/libs/walletconnect-1.5.5.aar b/app/libs/walletconnect-1.5.5.aar deleted file mode 100644 index 4e2f1b4fb70b95f8fff0e8727a5a73285666b1d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 145525 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 z zpMFRwTrX7WUp>&H@O@Qa+sP64>^8EBsCuYecn2htAdY-+9 zYJ*7bwA}s42K>1Et?8CG_djr=jniv+-4xzNr^vR8Uf1n$wdb-dR;?=$uo#HN^UC(Z zNyhcGvv6P2QOX7fem;~TX5)?ODFF2M9s}{M;7$f;tispfmifu~tomHZ1*}6*_;>B^ zL(fxBtLC-E8?T{eZHU)4I2!L*jvpQ25DUKdyPFv{!c(;q=b&+7Fa;Z&3~jtx#0tRc z0qe%Qi>l%u7GamvDW(Im2-N5|awr_+bgqKuEa#pAipVa}g{z8?Ad4u#jIUJ4TI?eP zA;d-)VXn~oKn*0t$O$Ua^2fi_cQz%?$VDKF=!XIW@d5cz=3Cy2=a4 zBT!yi;fG?P6Qk#p6trur5HCkiwJfYq@=&E15iw~adyvOB&Jo1vPu7-srdMv`O2=y> z$2lJliMJY0=mnVPA~%rdmk$@8Jg<+ATRNVfpX9-As`^g9tMZ9MD;D~!wlmbmQL9VR z=p1?KRy<@xHrYQdlH8zd@W(e5w4h5UAW1u@tJerEfOVZI$?FQX&AHcH(e2( z{FxD(RkP55g_Zj77|p}~m9U`5sDS;{M;bcg>=Ly-HlzH*Pn`Tu@}`CXYx}7*Rz^6? zVMJXYm~v@r77@r3`VXCnat61e`*FA~Gq9O?KR>UGE2QJf6BU>1H|l79^!Xs{(N^Ra zS;KH?-N{!R14E%Gk6Un5i1boz~uzSW;u0No=vKs z0F8{ba&Dt*YFITOBOp`>VsO#Qa;g--2`eW)3D=A(1!ytOW7ilhnDUQ=>gbY*e8?4=$Eu@&jC;N$Vi@FCm zx_>Z5E6S}BrB8S)D#(sz#{~t6$YlSxqKQ$4w&PqWDvswx;NrJ!zQW#1VxEO-qYK1h79~J z#$T7Y5{X$SG&W*N9(+HdgYcx&AcYvb}AU$Nbjk2M?M4WO77QfYr>5MX`K$QaAdvpZHC5+3lx0doK%4& z#Zn-8=GQ_vcnK#z{~9_%Xe2&^H3K#Di&m8drsm=>;B=mmxB zIrww$o?u1!zm-NgM}J-gyd{q7&4K!?VflS;5=kRi{%jtJnKV(D7fCT{Yv+|HYR=p^nziw76Ej5wKch-eXFlYh)VM?%($yBV>bJc+DmBl=QBoD$Q^Q5e3nT}As$o#p2b^aiRld}!{8fPMLI@1 z34nVwU_fXa`K6opX--U=@?H_vrW5d!jib?7B}cX$2cj2t_DKWcnmS!SQ3Xab#h8L5 zH)|?w(*Xu%+LP3Z^aWKhyYd-VwuL4OSOcW#|50@c~5>%P*# zMt~O6?!|`DG9Zb2=%IJ2b%>3lQI0`yA`CYs_g7s+49Uwxx5l(wiDcT=M$X>hnc~c_ zcGYn2zUT9;*|?SoT~Vtjx1f8tqdkko7nMno1W-!YE3}r-MgS%&4?M07ec$=2zT&8q zA9rw4zih5>ah#Cd@Lj2R%fp#YWzt(_9iVEVF~5YdA*rrdCQxvQR@=xRd@Vul+kNg=k-fr%_XJMIm*80(JY|`&J?!YaWs))TB>Xj~tei>OSaomT z)<79#5kXG5yv0ZL)qpo{wl8-aS+uk(GdeGDdy6Rjk9^R)G(durY#ewp+LwiqpNHD%*Vm7dtkN!v5h2Gcs%JN18qpPI;K+1@DP9r-(-G8P>Ln7k5jep1Qpdk1&YwD+NCCaDO z5#3y&(m+?7Y^)*icQ26tuKdOWLXOvjkOoQ!!hc8kqK;xlC?~WOuH4c*LzJ+T<**=SJ}E}-Sc3x%|*~T`exp@u!~tXupEJum}4G1!&WQD1f`dNVEZ8h@Imtz3}m8 z`JvxYA#X