Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-09 14:43:19 +03:00
commit 9ffebc7634
566 changed files with 22478 additions and 5796 deletions

View file

@ -0,0 +1,8 @@
package com.tangem.tap
import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
interface ActivityResultCaller {
val activityResultLauncher: ActivityResultLauncher<Intent>?
}

View file

@ -2,11 +2,18 @@ package com.tangem.tap
import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.content.Intent
import android.os.Bundle
import java.util.WeakHashMap
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import java.util.*
import kotlin.reflect.KClass
class ForegroundActivityObserver {
class ForegroundActivityObserver : ActivityResultCaller {
override var activityResultLauncher: ActivityResultLauncher<Intent>? = null
private set
private val activities = WeakHashMap<KClass<out Activity>, Activity>()
val foregroundActivity: Activity?
@ -15,25 +22,38 @@ class ForegroundActivityObserver {
.firstOrNull()
?.value
val callbacks get() = Callbacks()
internal val callbacks: ActivityLifecycleCallbacks
get() = Callbacks()
internal inner class Callbacks : ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
activityResultLauncher = (activity as? AppCompatActivity)?.registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
) {
/* no-op */
}
}
inner class Callbacks : ActivityLifecycleCallbacks {
override fun onActivityResumed(activity: Activity) {
activities[activity::class] = activity
}
override fun onActivityDestroyed(activity: Activity) {
activities.remove(activity::class)
if (activities.isEmpty()) {
activityResultLauncher = null
}
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}
}

View file

@ -0,0 +1,117 @@
package com.tangem.tap
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.time.Duration
internal class LockUserWalletsTimer(
owner: LifecycleOwner,
private val duration: Duration = with(Duration) { 10.minutes },
) : LifecycleOwner by owner,
DefaultLifecycleObserver {
private var delayJob: Job? = null
set(value) {
field?.cancel()
field = value
}
init {
lifecycle.addObserver(this)
}
override fun onResume(owner: LifecycleOwner) {
Timber.d(
"""
Owner resumed
|- Was stopped: ${preferencesStorage.wasApplicationStopped}
|- Need to open welcome screen: ${preferencesStorage.shouldOpenWelcomeScreenOnResume}
""".trimIndent(),
)
preferencesStorage.wasApplicationStopped = false
start()
if (preferencesStorage.shouldOpenWelcomeScreenOnResume) {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
preferencesStorage.shouldOpenWelcomeScreenOnResume = false
}
}
override fun onStop(owner: LifecycleOwner) {
Timber.d("Owner stopped")
preferencesStorage.wasApplicationStopped = true
}
override fun onDestroy(owner: LifecycleOwner) {
Timber.d("Owner destroyed")
stop()
}
fun restart() {
if (delayJob == null) return
Timber.d(
"""
Timer restart
|- Duration millis: ${duration.inWholeMilliseconds}
""".trimIndent(),
)
start(log = false)
}
private fun start(log: Boolean = true) {
if (log) {
Timber.d(
"""
Timer start
|- Duration millis: ${duration.inWholeMilliseconds}
""".trimIndent(),
)
}
delayJob = createDelayJob()
}
private fun stop(log: Boolean = true) {
if (log) {
Timber.d(
"""
Timer stop
|- Was started: ${delayJob?.isActive ?: false}
""".trimIndent(),
)
}
delayJob = null
}
private fun createDelayJob(): Job = lifecycleScope.launch(Dispatchers.Default) {
val startTime = System.currentTimeMillis()
delay(duration)
if (isActive) {
val userWalletsListManager = userWalletsListManagerSafe ?: return@launch
if (userWalletsListManager.hasSavedUserWallets) {
val currentTime = System.currentTimeMillis()
Timber.d(
"""
Finished
|- App is stopped: ${preferencesStorage.wasApplicationStopped}
|- Millis passed: ${currentTime - startTime}
""".trimIndent(),
)
userWalletsListManager.lock()
if (preferencesStorage.wasApplicationStopped) {
preferencesStorage.shouldOpenWelcomeScreenOnResume = true
} else {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
}
}
}
}
}

View file

@ -15,30 +15,39 @@ import com.tangem.operations.backup.BackupService
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tap.common.ActivityResultCallbackHolder
import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.IntentHandler
import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.extensions.dispatchOnMain
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
import com.tangem.tap.common.shop.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE
import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.proxy.AppStateHolder
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
import java.lang.ref.WeakReference
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
lateinit var tangemSdk: TangemSdk
lateinit var tangemSdkManager: TangemSdkManager
lateinit var backupService: BackupService
lateinit var userWalletsListManager: UserWalletsListManager
internal var lockUserWalletsTimer: LockUserWalletsTimer? = null
private set
var userWalletsListManagerSafe: UserWalletsListManager? = null
private set
var notificationsHandler: NotificationsHandler? = null
private val coroutineContext: CoroutineContext
@ -49,11 +58,14 @@ private val mainCoroutineContext: CoroutineContext
get() = Job() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope")
val mainScope = CoroutineScope(mainCoroutineContext)
@AndroidEntryPoint
class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder {
@Inject
lateinit var appStateHolder: AppStateHolder
private var snackbar: Snackbar? = null
private val dialogManager = DialogManager()
private val intentHandler = IntentHandler()
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
@ -66,9 +78,15 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
tangemSdk = TangemSdk.init(this, TangemSdkManager.config)
tangemSdkManager = TangemSdkManager(tangemSdk, this)
appStateHolder.tangemSdkManager = tangemSdkManager
backupService = BackupService.init(tangemSdk, this)
userWalletsListManager = UserWalletsListManager.provideBiometricImplementation(
context = applicationContext,
tangemSdkManager = tangemSdkManager,
)
userWalletsListManagerSafe = userWalletsListManager
lockUserWalletsTimer = LockUserWalletsTimer(owner = this)
store.dispatch(GlobalAction.SetResources(getAndroidResources()))
store.dispatch(
ShopAction.CheckIfGooglePayAvailable(
GooglePayService(createPaymentsClient(this), this),
@ -76,17 +94,7 @@ 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)
val windowInsetsController = WindowInsetsControllerCompat(window, binding.root)
@ -110,14 +118,23 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
val isOnboardingServiceActive = store.state.globalState.onboardingState.onboardingStarted
val shopOpened = store.state.shopState.total != null
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore && !shopOpened)) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
if (userWalletsListManager.hasSavedUserWallets) {
store.dispatchOnMain(WelcomeAction.HandleDeepLink(intent))
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Welcome))
} else {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Home))
intentHandler.handleWalletConnectLink(intent)
}
}
intentHandler.handleIntent(intent)
intentHandler.handleBackgroundScan(intent)
intentHandler.handleSellCurrencyCallback(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intentHandler.handleIntent(intent)
intentHandler.handleBackgroundScan(intent)
intentHandler.handleSellCurrencyCallback(intent)
intentHandler.handleWalletConnectLink(intent)
}
override fun onStart() {
@ -174,4 +191,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
override fun removeOnActivityResultCallback(callback: OnActivityResultCallback) {
onActivityResultCallbacks.remove(callback)
}
override fun onUserInteraction() {
super.onUserInteraction()
lockUserWalletsTimer?.restart()
}
}

View file

@ -7,18 +7,30 @@ import coil.ImageLoader
import coil.ImageLoaderFactory
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.domain.DomainLayer
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.IntentHandler
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.filters.BasicSignInFilter
import com.tangem.tap.common.analytics.filters.BasicTopUpFilter
import com.tangem.tap.common.analytics.filters.ShopPurchasedEventFilter
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.common.moshi.BigDecimalAdapter
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.common.redux.global.GlobalAction
@ -28,47 +40,112 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
import com.tangem.tap.domain.totalBalance.di.provideDefaultImplementation
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
import com.tangem.tap.domain.walletCurrencies.di.provideDefaultImplementation
import com.tangem.tap.domain.walletStores.WalletStoresManager
import com.tangem.tap.domain.walletStores.di.provideDefaultImplementation
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation
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.tap.proxy.AppStateHolder
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
import javax.inject.Inject
val store = Store(
reducer = ::appReducer,
middleware = AppState.getMiddleware(),
state = AppState(),
)
lateinit var store: Store<AppState>
lateinit var foregroundActivityObserver: ForegroundActivityObserver
lateinit var activityResultCaller: ActivityResultCaller
lateinit var preferencesStorage: PreferencesStorage
lateinit var walletConnectRepository: WalletConnectRepository
lateinit var shopService: TangemShopService
lateinit var assetReader: AssetReader
lateinit var userTokensRepository: UserTokensRepository
private val walletStoresRepository by lazy { WalletStoresRepository.provideDefaultImplementation() }
private val walletManagersRepository by lazy {
WalletManagersRepository.provideDefaultImplementation(
walletManagerFactory = WalletManagerFactory(
blockchainSdkConfig = store.state.globalState.configManager
?.config
?.blockchainSdkConfig
?: BlockchainSdkConfig(),
),
)
}
private val walletAmountsRepository by lazy {
WalletAmountsRepository.provideDefaultImplementation(
tangemTechService = store.state.domainNetworks.tangemTechService,
)
}
val walletStoresManager by lazy {
WalletStoresManager.provideDefaultImplementation(
userTokensRepository = userTokensRepository,
walletStoresRepository = walletStoresRepository,
walletManagersRepository = walletManagersRepository,
walletAmountsRepository = walletAmountsRepository,
appCurrencyProvider = { store.state.globalState.appCurrency },
)
}
val walletCurrenciesManager by lazy {
WalletCurrenciesManager.provideDefaultImplementation(
userTokensRepository = userTokensRepository,
walletStoresRepository = walletStoresRepository,
walletManagersRepository = walletManagersRepository,
walletAmountsRepository = walletAmountsRepository,
appCurrencyProvider = { store.state.globalState.appCurrency },
)
}
val totalFiatBalanceCalculator by lazy {
TotalFiatBalanceCalculator.provideDefaultImplementation()
}
val intentHandler by lazy { IntentHandler() }
@HiltAndroidApp
class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var appStateHolder: AppStateHolder
override fun onCreate() {
super.onCreate()
store = Store(
reducer = { action, state ->
appReducer(action, state, appStateHolder)
},
middleware = AppState.getMiddleware(),
state = AppState(),
)
appStateHolder.mainStore = store
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
foregroundActivityObserver = ForegroundActivityObserver()
activityResultCaller = 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()
@ -78,6 +155,19 @@ class TapApplication : Application(), ImageLoaderFactory {
context = this,
tangemTechService = store.state.domainNetworks.tangemTechService,
)
appStateHolder.userTokensRepository = userTokensRepository
}
//todo refactor: move to datasource and provide via DI
private fun initMoshiConverter() {
fun appAdapters(): List<Any> = listOf(
BigDecimalAdapter(),
CardBalanceStateAdapter(),
)
MoshiConverter.reInitInstance(
adapters = appAdapters() + MoshiJsonConverter.getTangemSdkAdapters(),
typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters(),
)
}
override fun newImageLoader(): ImageLoader {
@ -102,14 +192,23 @@ class TapApplication : Application(), ImageLoaderFactory {
}
private fun initAnalytics(application: Application, config: Config) {
val globalAnalyticsHandler = GlobalAnalyticsEventHandlerBuilder(
val factory = AnalyticsFactory()
factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder())
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder())
factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
factory.addFilter(ShopPurchasedEventFilter())
factory.addFilter(BasicSignInFilter())
factory.addFilter(BasicTopUpFilter(preferencesStorage.toppedUpWalletStorage))
val buildData = AnalyticsHandlerBuilder.Data(
application = application,
config = config,
isDebug = BuildConfig.DEBUG,
logConfig = LogConfig.analyticsHandlers,
jsonConverter = MoshiJsonConverter.INSTANCE,
).default()
store.dispatch(GlobalAction.SetGlobalAnalyticsHandler(globalAnalyticsHandler))
jsonConverter = MoshiConverter.INSTANCE,
)
factory.build(Analytics, buildData)
}
private fun initFeedbackManager(context: Context, preferencesStorage: PreferencesStorage, config: Config) {

View file

@ -76,13 +76,13 @@ class DialogManager : StoreSubscriber<GlobalState> {
CreateWalletInterruptDialog.create(state.dialog, context)
is WalletConnectDialog.UnsupportedCard ->
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_scanner_error_no_ethereum_wallet,
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_scanner_error_not_valid_card,
context = context,
)
is WalletConnectDialog.AddNetwork ->
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
titleRes = R.string.wallet_connect_title,
message = context.getString(
R.string.wallet_connect_network_not_found_format,
state.dialog.network,
@ -91,14 +91,14 @@ class DialogManager : StoreSubscriber<GlobalState> {
)
is WalletConnectDialog.OpeningSessionRejected -> {
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_same_wcuri,
context = context,
)
}
is WalletConnectDialog.SessionTimeout -> {
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_error_timeout,
context = context,
)
@ -123,7 +123,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
)
is WalletConnectDialog.UnsupportedNetwork ->
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_scanner_error_unsupported_network,
context = context,
)
@ -153,7 +153,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
primaryButtonAction = state.dialog.onOk,
)
is WalletDialog.RussianCardholdersWarningDialog ->
RussianCardholdersWarningBottomSheetDialog(context, state.dialog.topUpUrl)
RussianCardholdersWarningBottomSheetDialog(context, state.dialog.data)
else -> null
}
dialog?.show()

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common
import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
import com.tangem.tap.common.extensions.removePrefixOrNull
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.walletconnect.WalletConnectManager
@ -14,18 +15,24 @@ import timber.log.Timber
class IntentHandler {
private val TRANSACTION_ID_PARAM = "transactionId"
private val CURRENCY_CODE_PARAM = "baseCurrencyCode"
private val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
private val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
fun handleIntent(intent: Intent?) {
handleBackgroundScan(intent)
handleWalletConnectLink(intent)
handleSellCurrencyCallback(intent)
fun handleWalletConnectLink(intent: Intent?) {
val wcUri = when (intent?.scheme) {
WalletConnectManager.WC_SCHEME -> {
intent.data?.toString()
}
TANGEM_SCHEME -> {
intent.data?.toString()?.removePrefixOrNull(TANGEM_WC_PREFIX)
}
else -> {
null
}
}
if (wcUri != null) {
store.dispatch(WalletConnectAction.HandleDeepLink(wcUri))
}
}
private fun handleBackgroundScan(intent: Intent?) {
fun handleBackgroundScan(intent: Intent?) {
if (intent != null && (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action ||
NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action
)
@ -40,13 +47,7 @@ class IntentHandler {
}
}
private fun handleWalletConnectLink(intent: Intent?) {
if (intent?.scheme == WalletConnectManager.WC_SCHEME) {
store.dispatch(WalletConnectAction.HandleDeepLink(intent.data?.toString()))
}
}
private fun handleSellCurrencyCallback(intent: Intent?) {
fun handleSellCurrencyCallback(intent: Intent?) {
try {
val transactionID =
intent?.data?.getQueryParameter(TRANSACTION_ID_PARAM) ?: return
@ -60,16 +61,25 @@ class IntentHandler {
Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
store.dispatch(WalletAction.TradeCryptoAction.SendCrypto(
currencyId = currency,
amount = amount,
destinationAddress = destinationAddress,
transactionId = transactionID
))
store.dispatch(
WalletAction.TradeCryptoAction.SendCrypto(
currencyId = currency,
amount = amount,
destinationAddress = destinationAddress,
transactionId = transactionID,
),
)
} catch (exception: Exception) {
Timber.d("Not MoonPay URL")
}
}
companion object {
private const val TRANSACTION_ID_PARAM = "transactionId"
private const val CURRENCY_CODE_PARAM = "baseCurrencyCode"
private const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
private const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
private const val TANGEM_SCHEME = "tangem"
private const val TANGEM_WC_PREFIX = "tangem://wc?uri="
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -74,7 +74,7 @@ object TangemSdk {
is TangemSdkError.UnsupportedWalletConfig -> TangemSdkError.UnsupportedWalletConfig()
is TangemSdkError.CryptoUtilsError -> TangemSdkError.CryptoUtilsError(error.customMessage)
is TangemSdkError.NetworkError -> TangemSdkError.NetworkError(error.customMessage)
is TangemSdkError.ExceptionError -> TangemSdkError.ExceptionError(error.throwable)
is TangemSdkError.ExceptionError -> TangemSdkError.ExceptionError(error.cause)
is TangemSdkError.TooMuchBackupCards -> TangemSdkError.TooMuchBackupCards()
is TangemSdkError.BackupCardRequired -> TangemSdkError.BackupCardRequired()
is TangemSdkError.CertificateSignatureRequired -> TangemSdkError.CertificateSignatureRequired()
@ -107,6 +107,8 @@ object TangemSdk {
is TangemSdkError.BackupFailedFirmware -> TangemSdkError.BackupFailedFirmware()
is TangemSdkError.UserForgotTheCode -> TangemSdkError.UserForgotTheCode()
is TangemSdkError.BackupFailedIncompatibleBatch -> TangemSdkError.BackupFailedIncompatibleBatch()
is TangemSdkError.BiometricsUnavailable -> error
is TangemSdkError.BiometricsAuthenticationFailed -> error
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,12 +14,17 @@ sealed class MainScreen(
class CardWasScanned : MainScreen("Card Was Scanned")
class ButtonMyWallets : MainScreen("Button - My Wallets")
class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen(
event = "Enable Biometric",
params = mapOf("State" to state.value),
)
class MainCurrencyChanged(currencyType: AnalyticsParam.CurrencyType) : MainScreen(
event = "Main Currency Changed",
params = mapOf("Currency Type" to currencyType.value),
)
sealed class NoticeRateAppButton(result: AnalyticsParam.RateApp) : MainScreen(
class NoticeRateAppButton(result: AnalyticsParam.RateApp) : MainScreen(
event = "Notice - Rate The App Button Tapped",
params = mapOf("Result" to result.value),
)

View file

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

View file

@ -0,0 +1,18 @@
package com.tangem.tap.common.analytics.events
sealed class MyWallets(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("My Wallets", event, params) {
object MyWalletsScreenOpened : MyWallets(event = "My Wallets Screen Opened")
object CardWasScanned : MyWallets(event = "Card Was Scanned")
object WalletUnlockTapped : MyWallets(event = "Wallet Unlock Tapped")
object Button {
object ScanNewCard : MyWallets(event = "Button - Scan New Card")
object UnlockWithBiometrics : MyWallets(event = "Button - Unlock all with Face ID")
object EditWalletTapped : MyWallets(event = "Button - Edit Wallet Tapped")
object DeleteWalletTapped : MyWallets(event = "Button - Delete Wallet Tapped")
}
}

View file

@ -34,9 +34,9 @@ sealed class Onboarding(
class AccessCodeEntered : Backup("Access Code Entered")
class AccessCodeReEntered : Backup("Access Code Re-entered")
class Finished(cardsCount: String) : Backup(
class Finished(cardsCount: Int) : Backup(
event = "Backup Finished",
params = mapOf("Cards count" to cardsCount),
params = mapOf("Cards count" to "$cardsCount"),
)
}
@ -65,12 +65,17 @@ 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")
class EnableBiometrics(state: AnalyticsParam.OnOffState) : Onboarding(
category = "Onboarding / Biometric",
event = "Enable Biometric",
params = mapOf("State" to state.value),
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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") {}

View file

@ -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 <T> SimpleDialog(
modifier = Modifier.padding(22.dp)
) {
DialogTitle(title = title)
LazyColumn() {
LazyColumn {
items(items) { item ->
Row(
modifier = Modifier
@ -154,4 +155,4 @@ fun ErrorDialog(
}
}
)
}
}

View file

@ -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)
}

View file

@ -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,

View file

@ -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

View file

@ -1,10 +1,12 @@
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
import com.tangem.common.extensions.VoidCallback
import com.tangem.feature.referral.ReferralFragment
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
@ -20,21 +22,22 @@ import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment
import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment
import com.tangem.tap.features.onboarding.products.twins.ui.TwinsCardsFragment
import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment
import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.tokens.addCustomToken.AddCustomTokenFragment
import com.tangem.tap.features.tokens.ui.AddTokensFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.walletSelector.ui.WalletSelectorBottomSheetFragment
import com.tangem.tap.features.welcome.ui.WelcomeFragment
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 +51,18 @@ 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) {
transaction.addToBackStack(screen.name)
}
} else {
transaction.replace(R.id.fragment_container, fragment, screen.name)
if (addToBackstack) {
transaction.addToBackStack(screen.name)
}
transaction.commitAllowingStateLoss()
}
}
fun FragmentActivity.popBackTo(screen: AppScreen?, inclusive: Boolean = false) {
@ -104,5 +116,9 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.WalletDetails -> WalletDetailsFragment()
AppScreen.WalletConnectSessions -> WalletConnectFragment()
AppScreen.QrScan -> QrScanFragment()
AppScreen.ReferralProgram -> ReferralFragment()
AppScreen.Welcome -> WelcomeFragment()
AppScreen.SaveWallet -> SaveWalletBottomSheetFragment()
AppScreen.WalletSelector -> WalletSelectorBottomSheetFragment()
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
@ -26,6 +27,11 @@ fun Store<*>.dispatchNotification(resId: Int) {
dispatchOnMain(GlobalAction.ShowNotification(resId))
}
@Suppress("UnusedReceiverParameter")
suspend fun Store<*>.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) {
store.state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh)
}
fun Store<*>.dispatchToastNotification(resId: Int) {
dispatchOnMain(GlobalAction.ShowToastNotification(resId))
}
@ -71,7 +77,6 @@ suspend fun Store<*>.onCardScanned(scanResponse: ScanResponse) {
fun Store<*>.dispatchOpenUrl(url: String) {
store.dispatch(NavigationAction.OpenUrl(url))
}
fun Store<*>.dispatchShare(url: String) {
store.dispatch(NavigationAction.Share(url))
}

View file

@ -62,4 +62,9 @@ fun String.toQrCode(): Bitmap {
return bmp
}
fun String.urlEncode(): String = Uri.encode(this)
fun String.urlEncode(): String = Uri.encode(this)
fun String.removePrefixOrNull(prefix: String): String? = when {
startsWith(prefix) -> substring(prefix.length)
else -> null
}

View file

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

View file

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

View file

@ -8,9 +8,10 @@ import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.Address
import com.tangem.common.card.CardWallet
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
class AdditionalFeedbackInfo {
class EmailWalletInfo(
@ -29,6 +30,7 @@ class AdditionalFeedbackInfo {
var cardFirmwareVersion: String = ""
var cardIssuer: String = ""
var cardBlockchain: String = ""
var userWalletId: String = ""
// wallets
internal val walletsInfo = mutableListOf<EmailWalletInfo>()
@ -52,6 +54,7 @@ class AdditionalFeedbackInfo {
cardFirmwareVersion = data.card.firmwareVersion.stringValue
cardIssuer = data.card.issuer.name
signedHashesCount = formatSignedHashes(data.card.wallets)
userWalletId = UserWalletIdBuilder.scanResponse(data).build()?.stringValue ?: ""
}
fun setWalletsInfo(walletManagers: List<WalletManager>) {
@ -89,7 +92,7 @@ class AdditionalFeedbackInfo {
)
}
private fun formatSignedHashes(wallets: List<CardWallet>): String {
private fun formatSignedHashes(wallets: List<CardDTO.Wallet>): String {
return wallets.joinToString("\n") { "Signed hashes: ${it.curve.curve} - ${it.totalSignedHashes}" }
}

View file

@ -96,8 +96,8 @@ class FeedbackEmail : FeedbackData {
}
class SupportInfo : FeedbackData {
override val subjectResId: Int = R.string.details_ask_a_question
override val mainMessageResId: Int = R.string.details_ask_a_question
override val subjectResId: Int = R.string.details_chat
override val mainMessageResId: Int = R.string.details_chat
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String {
return FeedbackDataBuilder(infoHolder)

View file

@ -22,6 +22,7 @@ class FeedbackDataBuilder(
builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
builder.appendKeyValue("Card Blockchain", infoHolder.cardBlockchain)
builder.appendKeyValue("", infoHolder.signedHashesCount)
builder.appendKeyValue("User Wallet ID", infoHolder.userWalletId)
return this
}

View file

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

View file

@ -0,0 +1,27 @@
package com.tangem.tap.common.redux
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.tangemSdkManager
import org.rekotlin.Middleware
class AccessCodeRequestPolicyMiddleware {
val middleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action ->
if (action is GlobalAction.SaveScanResponse) {
updateAccessCodeRequestPolicy(action.scanResponse)
}
next(action)
}
}
}
private fun updateAccessCodeRequestPolicy(scanResponse: ScanResponse) {
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes &&
scanResponse.card.isAccessCodeSet,
)
}
}

View file

@ -10,24 +10,28 @@ import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteRedu
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsReducer
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.tokens.redux.TokensReducer
import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer
import org.rekotlin.Action
fun appReducer(action: Action, state: AppState?): AppState {
fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder): AppState {
requireNotNull(state)
if (action is AppAction.RestoreState) return action.state
return AppState(
navigationState = NavigationReducer.reduce(action, state),
globalState = globalReducer(action, state),
globalState = globalReducer(action, state, appStateHolder),
homeState = HomeReducer.reduce(action, state),
onboardingNoteState = OnboardingNoteReducer.reduce(action, state),
onboardingWalletState = OnboardingWalletReducer.reduce(action, state),
onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state),
walletState = WalletReducer.reduce(action, state),
walletState = WalletReducer.reduce(action, state, appStateHolder),
twinCardsState = TwinCardsReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState),
detailsState = DetailsReducer.reduce(action, state),
@ -35,6 +39,9 @@ fun appReducer(action: Action, state: AppState?): AppState {
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
shopState = ShopReducer.reduce(action, state.shopState),
welcomeState = WelcomeReducer.reduce(action, state),
saveWalletState = SaveWalletReducer.reduce(action, state),
walletSelectorState = WalletSelectorReducer.reduce(action, state),
)
}

View file

@ -25,6 +25,8 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayMiddleware
import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware
import com.tangem.tap.features.saveWallet.redux.SaveWalletState
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.shop.redux.ShopMiddleware
@ -33,6 +35,10 @@ import com.tangem.tap.features.tokens.redux.TokensMiddleware
import com.tangem.tap.features.tokens.redux.TokensState
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.middlewares.WalletMiddleware
import com.tangem.tap.features.walletSelector.redux.WalletSelectorMiddleware
import com.tangem.tap.features.walletSelector.redux.WalletSelectorState
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeState
import org.rekotlin.Middleware
import org.rekotlin.StateType
@ -51,6 +57,9 @@ data class AppState(
val tokensState: TokensState = TokensState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
val shopState: ShopState = ShopState(),
val welcomeState: WelcomeState = WelcomeState(),
val saveWalletState: SaveWalletState = SaveWalletState(),
val walletSelectorState: WalletSelectorState = WalletSelectorState(),
) : StateType {
val domainState: DomainState
@ -62,7 +71,9 @@ data class AppState(
companion object {
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(
logMiddleware, navigationMiddleware, notificationsMiddleware,
logMiddleware,
navigationMiddleware,
notificationsMiddleware,
GlobalMiddleware.handler,
HomeMiddleware.handler,
OnboardingNoteMiddleware.handler,
@ -78,8 +89,12 @@ data class AppState(
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
ShopMiddleware().shopMiddleware,
WelcomeMiddleware().middleware,
SaveWalletMiddleware().middleware,
WalletSelectorMiddleware().middleware,
LockUserWalletsTimerMiddleware().middleware,
AccessCodeRequestPolicyMiddleware().middleware,
)
}
}
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.tap.common.redux
import com.tangem.tap.lockUserWalletsTimer
import org.rekotlin.Middleware
class LockUserWalletsTimerMiddleware {
val middleware: Middleware<AppState> = { _, _ ->
{ nextDispatch ->
{ action ->
lockUserWalletsTimer?.restart()
nextDispatch(action)
}
}
}
}

View file

@ -5,7 +5,6 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackData
import com.tangem.tap.common.feedback.FeedbackManager
@ -30,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()
@ -68,7 +64,7 @@ sealed class GlobalAction : Action {
object Increment : GlobalAction()
}
data class SaveScanNoteResponse(val scanResponse: ScanResponse) : GlobalAction()
data class SaveScanResponse(val scanResponse: ScanResponse) : GlobalAction()
data class SetIfCardVerifiedOnline(val verified: Boolean) : GlobalAction()
@ -89,7 +85,6 @@ sealed class GlobalAction : Action {
data class SetConfigManager(val configManager: ConfigManager) : GlobalAction()
data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction()
data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction()
data class SetGlobalAnalyticsHandler(val analyticsHandler: GlobalAnalyticsEventHandler) : GlobalAction()
data class SendEmail(val feedbackData: FeedbackData) : GlobalAction()
data class OpenChat(val feedbackData: FeedbackData, val zendeskConfig: ZendeskConfig? = null) : GlobalAction()

View file

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

View file

@ -2,21 +2,20 @@ package com.tangem.tap.common.redux.global
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.tap.common.extensions.replaceBy
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.AppStateHolder
import org.rekotlin.Action
fun globalReducer(action: Action, state: AppState): GlobalState {
fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolder): GlobalState {
if (action !is GlobalAction) return state.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 +33,8 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.ScanFailsCounter.Reset -> {
globalState.copy(scanCardFailsCounter = 0)
}
is GlobalAction.SaveScanNoteResponse ->{
is GlobalAction.SaveScanResponse -> {
appStateHolder.scanResponse = action.scanResponse
domainStore.dispatch(DomainGlobalAction.SaveScanNoteResponse(action.scanResponse))
globalState.copy(scanResponse = action.scanResponse)
}
@ -48,15 +48,22 @@ 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
val wallet = card.wallets
.firstOrNull { it.publicKey.contentEquals(action.walletPublicKey) }
?: return globalState
val newCardInstance = card.updateWallet(wallet.copy(
totalSignedHashes = action.walletSignedHashes,
remainingSignatures = action.remainingSignatures
))
val newCardInstance = card.copy(
wallets = card.wallets.toMutableList().also { walletsMutable ->
walletsMutable.replaceBy(
item = wallet.copy(
totalSignedHashes = action.walletSignedHashes,
remainingSignatures = action.remainingSignatures,
),
) { it.index == wallet.index }
},
)
globalState.copy(scanResponse = globalState.scanResponse.copy(card = newCardInstance))
}
is GlobalAction.SetFeedbackManager -> {
@ -74,7 +81,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
userCountryCode = action.countryCode,
)
else -> globalState
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.common.redux.global
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.StateDialog
@ -26,20 +25,9 @@ data class GlobalState(
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(),
val resources: AndroidResources = AndroidResources(),
val analyticsHandler: GlobalAnalyticsEventHandler = GlobalAnalyticsEventHandler.stub(),
val userCountryCode: String? = null,
) : StateType
data class AndroidResources(
val strings: RString = RString(),
) {
data class RString(
val addressWasCopied: Int = -1,
val walletIsNotEmpty: Int = -1,
)
}
typealias CryptoCurrencyName = String
data class OnboardingState(

View file

@ -12,12 +12,17 @@ sealed class NavigationAction : Action {
val addToBackstack: Boolean = true,
) : NavigationAction()
data class PopBackTo(val screen: AppScreen? = null) : NavigationAction()
data class PopBackTo(
val screen: AppScreen? = null,
val inclusive: Boolean = false,
) : NavigationAction()
data class OpenUrl(val url: String) : NavigationAction()
data class OpenDocument(val url: Uri) : NavigationAction()
object OpenBiometricsSettings : NavigationAction()
data class Share(val data: String) : NavigationAction()
data class ActivityCreated(val activity: WeakReference<AppCompatActivity>) : NavigationAction()

View file

@ -1,14 +1,20 @@
package com.tangem.tap.common.redux.navigation
import android.content.Intent
import android.hardware.biometrics.BiometricManager
import android.os.Build
import android.provider.Settings
import com.tangem.tap.activityResultCaller
import com.tangem.tap.common.CustomTabsManager
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.openFragment
import com.tangem.tap.common.extensions.popBackTo
import com.tangem.tap.common.extensions.shareText
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.store
import org.rekotlin.Middleware
val navigationMiddleware: Middleware<AppState> = { dispatch, state ->
val navigationMiddleware: Middleware<AppState> = { _, state ->
{ next ->
{ action ->
if (action is NavigationAction) {
@ -16,16 +22,24 @@ val navigationMiddleware: Middleware<AppState> = { dispatch, state ->
when (action) {
is NavigationAction.NavigateTo -> {
navState?.activity?.get()?.openFragment(
action.screen,
action.addToBackstack,
action.fragmentShareTransition
screen = action.screen,
addToBackstack = action.addToBackstack,
fgShareTransition = action.fragmentShareTransition,
)
}
is NavigationAction.PopBackTo -> {
if (action.screen == AppScreen.Home) {
navState?.activity?.get()?.popBackTo(null, true)
} else {
navState?.activity?.get()?.popBackTo(action.screen)
if (navState?.backStack?.lastOrNull() != action.screen) {
when (val screen = action.screen) {
AppScreen.Home,
AppScreen.Welcome,
-> {
navState?.activity?.get()?.popBackTo(screen, inclusive = true)
store.dispatchOnMain(NavigationAction.NavigateTo(screen))
}
else -> {
navState?.activity?.get()?.popBackTo(screen, action.inclusive)
}
}
}
}
is NavigationAction.OpenUrl -> {
@ -38,11 +52,31 @@ val navigationMiddleware: Middleware<AppState> = { dispatch, state ->
intent.data = action.url
navState?.activity?.get()?.startActivity(intent)
}
is NavigationAction.Share -> {
navState?.activity?.get()?.let {
it.shareText(action.data)
is NavigationAction.OpenBiometricsSettings -> {
val settingsAction = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
Settings.ACTION_BIOMETRIC_ENROLL
}
else -> {
Settings.ACTION_SECURITY_SETTINGS
}
}
val intent = Intent(settingsAction).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
putExtra(
Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
BiometricManager.Authenticators.BIOMETRIC_STRONG,
)
}
}
activityResultCaller.activityResultLauncher?.launch(intent)
}
is NavigationAction.Share -> {
navState?.activity?.get()?.shareText(action.data)
}
is NavigationAction.ActivityCreated,
is NavigationAction.ActivityDestroyed,
-> Unit
}
}
next(action)

View file

@ -20,6 +20,8 @@ private fun internalReduce(action: Action, state: AppState): NavigationState {
navState.copy(backStack = navState.backStack + navigationAction.screen)
}
is NavigationAction.PopBackTo -> {
if (navState.backStack.lastOrNull() == navigationAction.screen) return navState
val screen = navigationAction.screen ?: navState.activity?.get()?.getPreviousScreen()
val index = navState.backStack.lastIndexOf(screen) + 1
state.navigationState.copy(backStack = navState.backStack.subList(0, index))

View file

@ -5,11 +5,13 @@ import org.rekotlin.StateType
import java.lang.ref.WeakReference
data class NavigationState(
val backStack: List<AppScreen> = listOf(AppScreen.Home),
val backStack: List<AppScreen> = emptyList(),
val activity: WeakReference<AppCompatActivity>? = null,
) : StateType
enum class AppScreen {
enum class AppScreen(
val isDialogFragment: Boolean = false,
) {
Home,
Shop,
Disclaimer,
@ -19,5 +21,9 @@ enum class AppScreen {
Details, DetailsSecurity, CardSettings, AppSettings, ResetToFactory,
AddTokens, AddCustomToken,
WalletConnectSessions,
QrScan
QrScan,
ReferralProgram,
Welcome,
SaveWallet(isDialogFragment = true),
WalletSelector(isDialogFragment = true),
}

View file

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

View file

@ -2,7 +2,7 @@ package com.tangem.tap.domain
import com.tangem.common.services.Result
import com.tangem.domain.common.ThrottlerWithValues
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.store
import java.math.BigDecimal

View file

@ -1,7 +1,5 @@
package com.tangem.tap.domain
import CreateProductWalletTask
import CreateProductWalletTaskResponse
import android.content.Context
import androidx.annotation.StringRes
import com.tangem.Message
@ -10,14 +8,20 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CardFilter
import com.tangem.common.CompletionResult
import com.tangem.common.SuccessResponse
import com.tangem.common.card.Card
import com.tangem.common.UserCode
import com.tangem.common.UserCodeType
import com.tangem.common.biometric.BiometricManager
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.Config
import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.common.map
import com.tangem.common.usersCode.UserCodeRepository
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.operations.CommandResponse
import com.tangem.operations.ScanTask
@ -26,15 +30,14 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.pins.CheckUserCodesCommand
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.tap.common.analytics.AnalyticsAnOld
import com.tangem.tap.common.analytics.AnalyticsEventAnOld
import com.tangem.tap.common.analytics.AnalyticsParamAnOld
import com.tangem.tap.common.analytics.GlobalAnalyticsEventHandler
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
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
@ -44,19 +47,38 @@ import kotlin.coroutines.suspendCoroutine
class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) {
private val userCodeRepository by lazy {
UserCodeRepository(
biometricManager = tangemSdk.biometricManager,
secureStorage = tangemSdk.secureStorage,
)
}
val canUseBiometry: Boolean
get() = tangemSdk.biometricManager.canAuthenticate || needEnrollBiometrics
val needEnrollBiometrics: Boolean
get() = tangemSdk.biometricManager.canEnrollBiometrics
val biometricManager: BiometricManager
get() = tangemSdk.biometricManager
suspend fun scanProduct(
analyticsHandler: GlobalAnalyticsEventHandler?,
userTokensRepository: UserTokensRepository,
cardId: String? = null,
additionalBlockchainsToDerive: Collection<Blockchain>? = null,
messageRes: Int? = null,
): CompletionResult<ScanResponse> {
analyticsHandler?.handleAnalyticsEvent(AnalyticsEventAnOld.READY_TO_SCAN, card = null)
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(
runnable = ScanProductTask(null, userTokensRepository, additionalBlockchainsToDerive),
cardId = null, initialMessage = message,
).also { sendScanResultsToAnalytics(analyticsHandler, it) }
runnable = ScanProductTask(
card = null,
userTokensRepository = userTokensRepository,
additionalBlockchainsToDerive = additionalBlockchainsToDerive,
),
cardId = cardId,
initialMessage = message,
).also { sendScanResultsToAnalytics(it) }
}
suspend fun createProductWallet(
@ -70,41 +92,22 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
}
private fun sendScanResultsToAnalytics(
analyticsHandler: GlobalAnalyticsEventHandler?,
result: CompletionResult<ScanResponse>,
) {
when (result) {
is CompletionResult.Success -> {
analyticsHandler?.handleAnalyticsEvent(
event = AnalyticsEventAnOld.CARD_IS_SCANNED,
card = result.data.card,
blockchain = result.data.walletData?.blockchain,
)
if (DemoHelper.isDemoCard(result.data)) {
analyticsHandler?.handleAnalyticsEvent(
event = AnalyticsEventAnOld.DEMO_MODE_ACTIVATED,
params = mapOf(AnalyticsParamAnOld.CARD_ID.param to result.data.card.cardId),
card = result.data.card,
blockchain = result.data.walletData?.blockchain,
)
}
if (result is CompletionResult.Failure) {
(result.error as? TangemSdkError)?.let { error ->
Analytics.send(Basic.ScanError(error))
}
is CompletionResult.Failure ->
(result.error as? TangemSdkError)?.let { error ->
analyticsHandler?.handleCardSdkErrorEvent(
error = error,
action = AnalyticsAnOld.ActionToLog.Scan,
)
}
}
}
suspend fun createWallet(cardId: String?): CompletionResult<Card> {
suspend fun createWallet(cardId: String?): CompletionResult<CardDTO> {
return runTaskAsyncReturnOnMain(
CreateWalletAndRescanTask(),
cardId,
initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body)),
)
.map { CardDTO(it) }
}
suspend fun derivePublicKeys(
@ -114,12 +117,31 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId)
}
suspend fun resetToFactorySettings(card: Card): CompletionResult<Card> {
suspend fun resetToFactorySettings(cardId: String): CompletionResult<CardDTO> {
return runTaskAsyncReturnOnMain(
ResetToFactorySettingsTask(),
card.cardId,
initialMessage = Message(context.getString(R.string.details_row_title_reset_factory_settings)),
runnable = ResetToFactorySettingsTask(),
cardId = cardId,
initialMessage = Message(context.getString(R.string.card_settings_reset_card_to_factory)),
)
.map { CardDTO(it) }
}
suspend fun saveAccessCode(accessCode: String, cardsIds: Set<String>): CompletionResult<Unit> {
return userCodeRepository.save(
cardsIds = cardsIds,
userCode = UserCode(
type = UserCodeType.AccessCode,
stringValue = accessCode,
),
)
}
suspend fun deleteSavedUserCodes(cardsIds: Set<String>): CompletionResult<Unit> {
return userCodeRepository.delete(cardsIds.toSet())
}
suspend fun clearSavedUserCodes(): CompletionResult<Unit> {
return userCodeRepository.clear()
}
suspend fun setPasscode(cardId: String?): CompletionResult<SuccessResponse> {
@ -154,11 +176,16 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
)
}
suspend fun scanCard(): CompletionResult<Card> {
suspend fun scanCard(
cardId: String? = null,
allowRequestAccessCodeFromRepository: Boolean = false,
): CompletionResult<CardDTO> {
return runTaskAsyncReturnOnMain(
ScanTask(),
runnable = ScanTask(allowRequestAccessCodeFromRepository),
cardId = cardId,
initialMessage = Message(context.getString(R.string.initial_message_tap_header)),
)
.map { CardDTO(it) }
}
suspend fun <T : CommandResponse> runTaskAsync(
@ -176,7 +203,9 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
}
private suspend fun <T : CommandResponse> runTaskAsyncReturnOnMain(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
runnable: CardSessionRunnable<T>,
cardId: String? = null,
initialMessage: Message? = null,
): CompletionResult<T> {
val result = runTaskAsync(runnable, cardId, initialMessage)
return withContext(Dispatchers.Main) { result }
@ -192,7 +221,22 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
}
fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String {
return context.getString(stringResId, formatArgs)
return context.getString(stringResId, *formatArgs)
}
fun setAccessCodeRequestPolicy(
useBiometricsForAccessCode: Boolean,
) {
tangemSdk.config.userCodeRequestPolicy = if (useBiometricsForAccessCode) {
UserCodeRequestPolicy.AlwaysWithBiometrics(codeType = UserCodeType.AccessCode)
} else {
UserCodeRequestPolicy.Default
}
}
fun useBiometricsForAccessCode(): Boolean {
val policy = tangemSdk.config.userCodeRequestPolicy
return policy is UserCodeRequestPolicy.AlwaysWithBiometrics && policy.codeType == UserCodeType.AccessCode
}
companion object {

View file

@ -5,13 +5,13 @@ import com.tangem.TangemSdk
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.domain.common.CardDTO
import com.tangem.tap.domain.tasks.SignHashesTask
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class TangemSigner(
private val card: Card,
private val card: CardDTO,
private val tangemSdk: TangemSdk,
private val initialMessage: Message,
private val accessCode: String? = null,

View file

@ -2,7 +2,7 @@ package com.tangem.tap.domain
import androidx.annotation.StringRes
import com.tangem.common.core.TangemError
import com.tangem.network.api.tangemTech.TangemTechError
import com.tangem.datasource.api.tangemTech.TangemTechError
import com.tangem.wallet.R
interface TapErrors
@ -64,8 +64,7 @@ sealed class TapError(
) : TapError(-1), MultiMessageError
}
sealed class TapSdkError(override val messageResId: Int?) : Throwable(), TangemError {
final override val code: Int = 50100
sealed class TapSdkError(override val messageResId: Int?) : TangemError(code = 50100) {
override var customMessage: String = code.toString()
object CardForDifferentApp : TapSdkError(R.string.alert_unsupported_card)
@ -88,4 +87,4 @@ fun TangemTechError.toTapError(): TapError {
}
}
class NoDataError(message: String) : TapError.CustomError(customMessage = message)
class NoDataError(message: String) : TapError.CustomError(customMessage = message)

View file

@ -6,13 +6,16 @@ import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.card.Card
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.services.Result
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.ThrottlerWithValues
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.attestation.Attestation
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.GlobalAction
@ -20,16 +23,22 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.walletStores.WalletStoresError
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import com.tangem.tap.walletStoresManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
class TapWalletManager {
val walletManagerFactory: WalletManagerFactory
@ -81,22 +90,75 @@ class TapWalletManager {
}
}
suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean) {
val scanResponse = userWallet.scanResponse
val card = scanResponse.card
val attestationFailed = card.attestation.status == Attestation.Status.Failed
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(scanResponse)
updateConfigManager(scanResponse)
withMainContext {
store.dispatch(WalletAction.UserWalletChanged(userWallet))
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
store.dispatch(WalletConnectAction.ResetState)
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
store.dispatch(WalletConnectAction.RestoreSessions(scanResponse))
store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed))
store.dispatch(WalletAction.Warnings.CheckIfNeeded)
}
loadData(userWallet, refresh)
}
suspend fun loadData(userWallet: UserWallet, refresh: Boolean = false) {
walletStoresManager.fetch(userWallet, refresh)
.doOnSuccess {
Timber.d("Wallet stores fetched for ${userWallet.walletId}")
store.dispatchOnMain(WalletAction.LoadData.Success)
}
.doOnFailure { error ->
val errorAction = when (error) {
is WalletStoresError -> when (error) {
is WalletStoresError.FetchFiatRatesError,
is WalletStoresError.UpdateWalletManagerError,
-> WalletAction.LoadData.Failure(error = null)
is WalletStoresError.WalletManagerNotCreated -> WalletAction.LoadData.Failure(
error = TapError.WalletManager.CreationError,
)
is WalletStoresError.UnknownBlockchain -> WalletAction.LoadData.Failure(
error = TapError.UnknownBlockchain,
)
is WalletStoresError.NoInternetConnection -> WalletAction.LoadData.Failure(
error = TapError.NoInternetConnection,
)
}
else -> WalletAction.LoadData.Failure(error = null)
}
Timber.e(error, "Wallet stores fetching failed for ${userWallet.walletId}")
store.dispatchOnMain(errorAction)
}
}
suspend fun onCardScanned(data: ScanResponse) {
walletManagersThrottler.clear()
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(data)
updateConfigManager(data)
withMainContext {
store.dispatch(WalletAction.ResetState(data.card.cardId))
store.dispatch(WalletAction.ResetState(data.card))
store.dispatch(WalletConnectAction.ResetState)
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
store.dispatch(GlobalAction.SaveScanResponse(data))
store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard))
store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed))
store.dispatch(WalletConnectAction.RestoreSessions(data))
store.dispatch(
WalletAction.MultiWallet.ShowWalletBackupWarning(
show = data.card.settings.isBackupAllowed
&& data.card.backupStatus == Card.BackupStatus.NoBackup,
&& data.card.backupStatus == CardDTO.BackupStatus.NoBackup,
),
)
loadData(data)
@ -143,7 +205,7 @@ class TapWalletManager {
private fun checkIfDerivationsAreMissing(blockchainNetworks: List<BlockchainNetwork>, scanResponse: ScanResponse) {
blockchainNetworks.map {
if (it.tokens.isNotEmpty()) {
WalletAction.MultiWallet.AddTokens(it.tokens, it, false)
WalletAction.MultiWallet.AddTokens(it.tokens, it)
}
}
val missingDerivations = blockchainNetworks
@ -171,7 +233,6 @@ class TapWalletManager {
WalletAction.MultiWallet.AddBlockchains(
blockchains = listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
walletManagers = listOf(primaryWalletManager),
save = false,
),
WalletAction.LoadFiatRate(),
)
@ -187,7 +248,6 @@ class TapWalletManager {
WalletAction.MultiWallet.AddBlockchains(
blockchains = blockchainNetworks,
walletManagers = walletManagers,
save = false,
),
)
@ -197,7 +257,6 @@ class TapWalletManager {
WalletAction.MultiWallet.AddTokens(
tokens = it.tokens,
blockchain = it,
save = false,
),
)
}

View file

@ -77,7 +77,7 @@ class WarningMessagesManager {
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.alert_title,
R.string.common_warning,
R.string.alert_developer_card,
WarningMessage.Origin.Local,
)
@ -89,7 +89,7 @@ class WarningMessagesManager {
priority = WarningMessage.Priority.Info,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.alert_title,
R.string.common_warning,
R.string.alert_card_signed_transactions,
WarningMessage.Origin.Local,
)
@ -142,7 +142,7 @@ class WarningMessagesManager {
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.alert_title,
titleResId = R.string.common_warning,
messageResId = R.string.warning_low_signatures_format,
origin = WarningMessage.Origin.Local,
messageFormatArg = remainingSignatures.toString(),
@ -155,7 +155,7 @@ class WarningMessagesManager {
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen, WarningMessage.Location.SendScreen),
null,
R.string.alert_title,
R.string.common_warning,
R.string.warning_testnet_card_message,
WarningMessage.Origin.Local,
)
@ -167,7 +167,7 @@ class WarningMessagesManager {
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.alert_title,
R.string.common_warning,
R.string.alert_demo_message,
WarningMessage.Origin.Local,
)

View file

@ -1,58 +1,45 @@
package com.tangem.tap.domain.extensions
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTangemNote
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.extensions.calculateHmacSha256
import com.tangem.domain.common.getTwinCardNumber
import com.tangem.domain.common.isTangemTwin
import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.features.wallet.redux.Artwork
val Card.remainingSignatures: Int?
get() = this.getSingleWallet()?.remainingSignatures
val CardDTO.remainingSignatures: Int?
get() = this.wallets.firstOrNull()?.remainingSignatures
val Card.isWalletDataSupported: Boolean
val CardDTO.isWalletDataSupported: Boolean
get() = this.firmwareVersion.major >= 4
val Card.isMultiwalletAllowed: Boolean
val CardDTO.isMultiwalletAllowed: Boolean
get() {
return !isTangemTwin() && !isStart2Coin && !isTangemNote && !isSaltPay
&& (firmwareVersion >= FirmwareVersion.MultiWalletAvailable ||
getSingleWallet()?.curve == EllipticCurve.Secp256k1)
return !isTangemTwin() && !isStart2Coin && !isTangemNote && !isSaltPay &&
(firmwareVersion >= FirmwareVersion.MultiWalletAvailable ||
wallets.firstOrNull()?.curve == EllipticCurve.Secp256k1)
}
val Card.isHdWalletAllowedByApp: Boolean
val CardDTO.isHdWalletAllowedByApp: Boolean
get() = settings.isHDWalletAllowed && !isSaltPay
fun Card.getSingleWallet(): CardWallet? {
return wallets.firstOrNull()
fun CardDTO.hasSignedHashes(): Boolean {
return wallets.any { (it.totalSignedHashes ?: 0) > 0 }
}
fun Card.hasWallets(): Boolean = wallets.isNotEmpty()
fun Card.hasNoWallets(): Boolean = wallets.isEmpty()
fun Card.hasSingleWallet(): Boolean = wallets.size == 1
fun Card.hasSignedHashes(): Boolean {
return wallets.any { it.totalSignedHashes ?: 0 > 0 }
fun CardDTO.signedHashesCount(): Int {
return wallets.sumOf { it.totalSignedHashes ?: 0 }
}
fun Card.signedHashesCount(): Int {
return wallets.map { it.totalSignedHashes ?: 0 }.sum()
}
suspend fun Card.getOrLoadCardArtworkUrl(cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null): String {
suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null): String {
fun ifAnyError(): String {
return when {
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
@ -67,42 +54,28 @@ suspend fun Card.getOrLoadCardArtworkUrl(cardInfo: Result<CardVerifyAndGetInfo.R
}
}
val cardInfoResult = cardInfo ?: OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)
return when (cardInfoResult) {
return when (val cardInfoResult = cardInfo ?: OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)) {
is Result.Success -> {
val artworkId = cardInfoResult.data.artwork?.id
if (artworkId == null || artworkId.isEmpty()) {
if (artworkId.isNullOrEmpty()) {
ifAnyError()
} else {
OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
}
is Result.Failure -> ifAnyError()
}
}
fun Card.getArtworkUrl(artworkId: String?): String? {
fun CardDTO.getArtworkUrl(artworkId: String?): String? {
return when {
artworkId != null -> {
OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
else -> null
}
}
fun Card.getUserWalletId(): String {
val walletPublicKey = this.wallets.firstOrNull()?.publicKey ?: return ""
return UserWalletId(walletPublicKey).stringValue
}
class UserWalletId(val walletPublicKey: ByteArray) {
val stringValue: String = calculateUserId(walletPublicKey)
private fun calculateUserId(walletPublicKey: ByteArray): String {
val message = "UserWalletID".toByteArray()
val keyHash = walletPublicKey.calculateSha256()
return message.calculateHmacSha256(keyHash).toHexString()
}
}

View file

@ -7,13 +7,12 @@ import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
@ -82,7 +81,7 @@ fun WalletManagerFactory.makeWalletManagerForApp(
)
}
private fun getDerivationParams(derivationPath: String?, card: Card): DerivationParams? {
private fun getDerivationParams(derivationPath: String?, card: CardDTO): DerivationParams? {
return derivationPath?.let {
DerivationParams.Custom(
DerivationPath(it),
@ -132,7 +131,7 @@ fun WalletManagerFactory.makePrimaryWalletManager(
)
}
private fun selectWallet(wallets: List<CardWallet>): CardWallet? {
private fun selectWallet(wallets: List<CardDTO.Wallet>): CardDTO.Wallet? {
return when (wallets.size) {
0 -> null
1 -> wallets[0]

View file

@ -0,0 +1,21 @@
package com.tangem.tap.domain.model
import java.math.BigDecimal
/**
* Represents fiat balance of [WalletStoreModel] list
* @property amount Amount of the total balance
* */
sealed class TotalFiatBalance {
open val amount: BigDecimal = BigDecimal.ZERO
object Loading : TotalFiatBalance()
data class Error(
override val amount: BigDecimal,
) : TotalFiatBalance()
data class Loaded(
override val amount: BigDecimal,
) : TotalFiatBalance()
}

View file

@ -0,0 +1,35 @@
package com.tangem.tap.domain.model
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.util.UserWalletId
/**
* Represents user's wallet which stored in app persistence
* @param name User wallet name
* @param walletId User wallet [UserWalletId]
* @param artworkUrl User wallet card artwork URL
* @param cardsInWallet List of cards IDs assigned with this user's wallet
* @param isMultiCurrency Indicates whether this user wallet can work with more than one currency
* @param scanResponse [ScanResponse] of primary user's wallet card.
* TODO: Replace with [com.tangem.domain.common.CardDTO]
* @property cardId ID of user's wallet primary card
* @property hasAccessCode Indicates if the user's wallet primary card has access code
* @property isLocked Indicates if this primary card has no currency wallets
* */
data class UserWallet(
val name: String,
val walletId: UserWalletId,
val artworkUrl: String,
val cardsInWallet: Set<String>,
val isMultiCurrency: Boolean,
val scanResponse: ScanResponse,
) {
val cardId: String
get() = scanResponse.card.cardId
val hasAccessCode: Boolean
get() = scanResponse.card.isAccessCodeSet
val isLocked: Boolean
get() = scanResponse.card.wallets.isEmpty()
}

View file

@ -0,0 +1,71 @@
package com.tangem.tap.domain.model
import com.tangem.tap.domain.model.WalletDataModel.Status
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.AddressData
import java.math.BigDecimal
/**
* Contains info about wallet store currency
* @param currency Wallet's [Currency]
* @param status Wallet's [Status], represents current status of that currency
* @param walletAddresses List of wallet data [AddressData]
* @param existentialDeposit Amount that must be held on currency's balance, if balance is below that amount all
* founds will be destroyed. Null if currency don't have existential deposit
* @param fiatRate Wallet's fiat rate, used to calculate fiat balance. Null if not provided
* */
data class WalletDataModel(
val currency: Currency,
val status: Status,
val walletAddresses: List<AddressData>,
val existentialDeposit: BigDecimal?,
val fiatRate: BigDecimal?,
) {
/**
* Represent current status of currency
* @property amount Currency amount
* @property pendingTransactions List of currency [PendingTransaction] sent in currency's blockchain
* @property errorMessage Status error message, null if not provided
* @property isErrorStatus true if current status is error status, false otherwise
* */
sealed class Status {
open val amount: BigDecimal = BigDecimal.ZERO
open val pendingTransactions: List<PendingTransaction> = emptyList()
open val errorMessage: String? = null
open val isErrorStatus: Boolean = false
}
object Loading : Status()
data class VerifiedOnline(
override val amount: BigDecimal,
) : Status()
data class TransactionInProgress(
override val amount: BigDecimal,
override val pendingTransactions: List<PendingTransaction>,
) : Status()
data class SameCurrencyTransactionInProgress(
override val amount: BigDecimal,
override val pendingTransactions: List<PendingTransaction>,
) : Status()
data class NoAccount(
val amountToCreateAccount: BigDecimal?,
) : Status() {
override val isErrorStatus: Boolean = true
}
data class Unreachable(
override val errorMessage: String?,
) : Status() {
override val isErrorStatus: Boolean = true
}
object MissedDerivation : Status() {
override val isErrorStatus: Boolean = true
}
}

View file

@ -0,0 +1,77 @@
package com.tangem.tap.domain.model
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.domain.model.WalletStoreModel.WalletRent
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import java.math.BigDecimal
/**
* Contains info about the blockchain and its currencies
* @param userWalletId ID of the associated [UserWallet]
* @param blockchain [Blockchain] of this WalletStore
* @param derivationPath [DerivationPath] of this store, null if the card does not support the
* [HD Wallet](https://coinsutra.com/hd-wallets-deterministic-wallet/)
* @param walletsData List of [WalletDataModel] which represents store's blockchain currency and tokens currencies
* @param walletRent [WalletRent], null if store has no rent or currency balance is greater then
* [WalletRent.exemptionAmount]
* @param blockchainNetwork [BlockchainNetwork].
* TODO: Remove after WalletMiddleware refactoring
* @param walletManager [WalletManager], may be null if it fails to create this manager.
* TODO: Remove after WalletMiddleware refactoring
* */
data class WalletStoreModel(
val userWalletId: UserWalletId,
val blockchain: Blockchain,
val derivationPath: DerivationPath?,
val walletsData: List<WalletDataModel>,
val walletRent: WalletRent?,
@Deprecated("Don't use it, will be removed")
val blockchainNetwork: BlockchainNetwork,
@Deprecated("Don't use it, will be removed")
val walletManager: WalletManager?,
) {
/**
* Represents wallet blockchain rent
* @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than
* the [WalletRent.exemptionAmount]
* @param exemptionAmount Amount that should be on the blockchain balance not to pay rent
* */
data class WalletRent(
val rent: BigDecimal,
val exemptionAmount: BigDecimal,
)
// TODO: Remove the generated methods after blockchainNetwork and walletManager are removed from the model
// region Generated
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is WalletStoreModel) return false
if (userWalletId != other.userWalletId) return false
if (blockchain != other.blockchain) return false
if (derivationPath != other.derivationPath) return false
if (walletsData != other.walletsData) return false
if (walletRent != other.walletRent) return false
return true
}
override fun hashCode(): Int {
var result = userWalletId.hashCode()
result = 31 * result + blockchain.hashCode()
result = 31 * result + (derivationPath?.hashCode() ?: 0)
result = 31 * result + walletsData.hashCode()
result = 31 * result + (walletRent?.hashCode() ?: 0)
return result
}
override fun toString(): String {
return "WalletStoreModel(userWalletId=$userWalletId, blockchain=$blockchain, derivationPath=$derivationPath, " +
"walletsData=$walletsData, walletRent=$walletRent)"
}
// endregion Generated
}

View file

@ -0,0 +1,110 @@
package com.tangem.tap.domain.model.builders
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.TwinsHelper
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.attestation.TangemApi
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.features.wallet.redux.Artwork
class UserWalletBuilder(
private val scanResponse: ScanResponse,
private val onlineCardVerifier: OnlineCardVerifier = OnlineCardVerifier(),
) {
private var backupCardsIds: Set<String> = emptySet()
private val CardDTO.isBackupNotAllowed: Boolean
get() = !this.settings.isBackupAllowed
private val ScanResponse.userWalletName: String
get() = when (productType) {
ProductType.Note -> "Note"
ProductType.Twins -> "Twin"
ProductType.SaltPay -> "SaltPay"
ProductType.Wallet -> when {
card.isBackupNotAllowed -> "Tangem card"
card.isStart2Coin -> "Start2Coin"
else -> "Wallet"
}
}
private val ScanResponse.isMultiCurrency: Boolean
get() = when (productType) {
ProductType.Note -> false
ProductType.Twins -> false
ProductType.SaltPay -> false
ProductType.Wallet -> when {
card.isStart2Coin -> false
card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable -> true
else -> {
val cardWallets = card.wallets
require(cardWallets.isNotEmpty()) { "Card wallets must not be empty" }
cardWallets.first().curve == EllipticCurve.Secp256k1
}
}
}
fun backupCardsIds(backupCardsIds: Set<String>?) = this.apply {
if (backupCardsIds != null) {
this.backupCardsIds = backupCardsIds
}
}
suspend fun build(): UserWallet? {
return with(scanResponse) {
UserWalletIdBuilder.scanResponse(scanResponse)
.build()
?.let {
UserWallet(
walletId = it,
name = userWalletName,
artworkUrl = loadArtworkUrl(card.cardId, card.cardPublicKey),
cardsInWallet = backupCardsIds.plus(card.cardId),
scanResponse = this,
isMultiCurrency = isMultiCurrency,
)
}
}
}
private suspend fun loadArtworkUrl(cardId: String, cardPublicKey: ByteArray): String {
return when (val result = onlineCardVerifier.getCardInfo(cardId, cardPublicKey)) {
is Result.Success -> {
val artworkId = result.data.artwork?.id
if (artworkId.isNullOrEmpty()) {
getFallbackArtworkUrl(cardId)
} else {
getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
}
is Result.Failure -> getFallbackArtworkUrl(cardId)
}
}
private fun getFallbackArtworkUrl(cardId: String): String {
return when {
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
else -> when (TwinsHelper.getTwinCardNumber(cardId)) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1
TwinCardNumber.Second -> Artwork.TWIN_CARD_2
else -> Artwork.DEFAULT_IMG_URL
}
}
}
private fun getUrlForArtwork(cardId: String, cardPublicKeyHex: String, artworkId: String): String {
return TangemApi.Companion.BaseUrl.VERIFY.url + TangemApi.ARTWORK +
"?artworkId=${artworkId}&CID=${cardId}&publicKey=$cardPublicKeyHex"
}
}

View file

@ -0,0 +1,70 @@
package com.tangem.tap.domain.model.builders
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.crypto.Secp256k1
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.extensions.calculateHmacSha256
import com.tangem.domain.common.util.UserWalletId
class UserWalletIdBuilder private constructor(
private val publicKey: ByteArray?,
private val pairTwinPublicKey: ByteArray? = null,
) {
fun build(): UserWalletId? {
val seed = if (publicKey != null) {
if (pairTwinPublicKey != null) {
Secp256k1.sum(publicKey, pairTwinPublicKey)
} else {
publicKey
}
} else null
return seed?.let {
UserWalletId(value = calculateUserWalletId(it))
}
}
private fun calculateUserWalletId(seed: ByteArray?): ByteArray? {
val message = MESSAGE_FOR_WALLET_ID.toByteArray()
val keyHash = seed?.calculateSha256()
return if (keyHash != null) {
message.calculateHmacSha256(keyHash)
} else null
}
companion object {
private const val MESSAGE_FOR_WALLET_ID = "UserWalletID"
@Throws(IllegalArgumentException::class)
fun card(card: CardDTO): UserWalletIdBuilder {
require(!card.isTangemTwins) {
"For twin cards use scanResponse to ID calculation"
}
return UserWalletIdBuilder(findPublicKey(card.wallets))
}
fun scanResponse(scanResponse: ScanResponse): UserWalletIdBuilder {
return UserWalletIdBuilder(
publicKey = findPublicKey(scanResponse.card.wallets),
pairTwinPublicKey = when (scanResponse.productType) {
ProductType.Twins -> scanResponse.secondTwinPublicKey?.hexToBytes()
ProductType.Note,
ProductType.Wallet,
ProductType.SaltPay,
-> null
},
)
}
private fun findPublicKey(wallets: List<CardDTO.Wallet>): ByteArray? {
return wallets.firstOrNull()
?.publicKey
}
}
}

View file

@ -0,0 +1,151 @@
package com.tangem.tap.domain.model.builders
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
import java.math.BigDecimal
interface WalletStoreBuilder {
fun build(): WalletStoreModel
interface BlockchainNetworkWalletStoreBuilder : WalletStoreBuilder {
fun walletManager(walletManager: WalletManager?): WalletStoreBuilder
}
interface WalletMangerWalletStoreBuilder : WalletStoreBuilder
companion object {
operator fun invoke(
userWalletId: UserWalletId,
blockchainNetwork: BlockchainNetwork,
): BlockchainNetworkWalletStoreBuilder {
return BlockchainNetworkWalletStoreBuilderImpl(userWalletId, blockchainNetwork)
}
operator fun invoke(
userWalletId: UserWalletId,
walletManager: WalletManager,
): WalletMangerWalletStoreBuilder {
return WalletMangerWalletStoreBuilderImpl(userWalletId, walletManager)
}
}
}
private class BlockchainNetworkWalletStoreBuilderImpl(
private val userWalletId: UserWalletId,
private val blockchainNetwork: BlockchainNetwork,
) : WalletStoreBuilder.BlockchainNetworkWalletStoreBuilder {
private var walletManager: WalletManager? = null
override fun walletManager(walletManager: WalletManager?) = this.apply {
this.walletManager = walletManager
}
override fun build(): WalletStoreModel {
val blockchainWalletData = blockchainNetwork.getBlockchainWalletData(walletManager)
val tokensWalletsData = blockchainNetwork.getTokensWalletsData(walletManager)
return WalletStoreModel(
userWalletId = userWalletId,
blockchain = blockchainNetwork.blockchain,
derivationPath = blockchainNetwork.derivationPath?.let { DerivationPath(it) },
walletsData = (listOf(blockchainWalletData) + tokensWalletsData),
walletRent = null,
walletManager = walletManager,
blockchainNetwork = blockchainNetwork,
)
}
}
private class WalletMangerWalletStoreBuilderImpl(
private val userWalletId: UserWalletId,
private val walletManager: WalletManager,
) : WalletStoreBuilder.WalletMangerWalletStoreBuilder {
override fun build(): WalletStoreModel {
val wallet = walletManager.wallet
val blockchainWalletData = wallet.blockchain.toBlockchainWalletData(walletManager)
val tokenWalletsData = wallet.getTokens().firstOrNull()?.toTokenWalletData(walletManager)
return WalletStoreModel(
userWalletId = userWalletId,
blockchain = wallet.blockchain,
derivationPath = wallet.publicKey.derivationPath,
walletsData = (listOf(blockchainWalletData) + listOfNotNull(tokenWalletsData)),
walletRent = null,
walletManager = walletManager,
blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager),
)
}
}
private fun BlockchainNetwork.getBlockchainWalletData(walletManager: WalletManager?): WalletDataModel {
return WalletDataModel(
currency = Currency.Blockchain(
blockchain = blockchain,
derivationPath = derivationPath,
),
status = WalletDataModel.Loading,
walletAddresses = walletManager?.wallet?.createAddressesData().orEmpty(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
)
}
private fun BlockchainNetwork.getTokensWalletsData(walletManager: WalletManager?): List<WalletDataModel> {
return this.tokens
.map { token ->
WalletDataModel(
currency = Currency.Token(
token = token,
blockchain = blockchain,
derivationPath = derivationPath,
),
status = WalletDataModel.Loading,
walletAddresses = walletManager?.wallet?.createAddressesData().orEmpty(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
)
}
}
private fun Blockchain.toBlockchainWalletData(walletManager: WalletManager): WalletDataModel {
val wallet = walletManager.wallet
return WalletDataModel(
currency = Currency.Blockchain(
blockchain = this,
derivationPath = wallet.publicKey.derivationPath?.rawPath,
),
status = WalletDataModel.Loading,
walletAddresses = wallet.createAddressesData(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
)
}
private fun Token.toTokenWalletData(walletManager: WalletManager): WalletDataModel {
val wallet = walletManager.wallet
return WalletDataModel(
currency = Currency.Token(
token = this,
blockchain = wallet.blockchain,
derivationPath = wallet.publicKey.derivationPath?.rawPath,
),
status = WalletDataModel.Loading,
walletAddresses = wallet.createAddressesData(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
)
}
private fun getExistentialDeposit(walletManager: WalletManager?): BigDecimal? {
return (walletManager as? ExistentialDepositProvider)?.getExistentialDeposit()
}

View file

@ -0,0 +1,250 @@
package com.tangem.tap.domain.scanCard
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.services.Result
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.backup.BackupService
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.backupService
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.paramsInterceptor.BatchIdParamsInterceptor
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa
import com.tangem.tap.common.redux.AppDialog
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.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerType
import com.tangem.tap.features.disclaimer.redux.isAccepted
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
// TODO: Create repository for that
object ScanCardProcessor {
suspend fun scan(
additionalBlockchainsToDerive: Collection<Blockchain>? = null,
cardId: String? = null,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {},
onScanStateChange: suspend (scanInProgress: Boolean) -> Unit = {},
onWalletNotCreated: suspend (() -> Unit) = {},
onFailure: suspend (error: TangemError) -> Unit = {},
onSuccess: suspend (scanResponse: ScanResponse) -> Unit = {},
) = withMainContext {
onProgressStateChange(true)
onScanStateChange(true)
tangemSdkManager.changeDisplayedCardIdNumbersCount(null)
val result = tangemSdkManager.scanProduct(
userTokensRepository = userTokensRepository,
cardId = cardId,
additionalBlockchainsToDerive = additionalBlockchainsToDerive,
)
store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
result
.doOnFailure { error ->
onProgressStateChange(false)
onScanStateChange(false)
onFailure(error)
}
.doOnSuccess { scanResponse ->
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
onScanStateChange(false)
checkForUnfinishedBackupForSaltPay(
backupService = backupService,
scanResponse = scanResponse,
onProgressStateChange = { onProgressStateChange(it) },
nextHandler = { scanResponse1 ->
showDisclaimerIfNeed(
scanResponse = scanResponse1,
onProgressStateChange = onProgressStateChange,
nextHandler = { scanResponse2 ->
onScanSuccess(
scanResponse = scanResponse2,
onProgressStateChange = onProgressStateChange,
onSuccess = onSuccess,
onWalletNotCreated = onWalletNotCreated,
onFailure = onFailure,
)
},
)
},
)
}
}
/**
* It checks only the SaltPay cards. To check for unfinished backups for the standard Wallet cards
* see BackupAction.CheckForUnfinishedBackup
* If user touches card other than Visa SaltPay - show dialog and block next processing
*/
private inline fun checkForUnfinishedBackupForSaltPay(
backupService: BackupService,
scanResponse: ScanResponse,
onProgressStateChange: (showProgress: Boolean) -> Unit,
nextHandler: (ScanResponse) -> Unit,
) {
if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) {
nextHandler(scanResponse)
return
}
val isTheSamePrimaryCard = backupService.primaryCardId
?.let { it == scanResponse.card.cardId }
?: false
if (scanResponse.isSaltPayWallet() || !isTheSamePrimaryCard) {
onProgressStateChange(false)
showSaltPayTapVisaLogoCardDialog()
} else {
nextHandler(scanResponse)
}
}
private suspend inline fun showDisclaimerIfNeed(
scanResponse: ScanResponse,
crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
crossinline nextHandler: suspend (ScanResponse) -> Unit,
) {
val disclaimerType = DisclaimerType.get(scanResponse)
store.dispatchOnMain(DisclaimerAction.SetDisclaimerType(disclaimerType))
if (disclaimerType.isAccepted()) {
nextHandler((scanResponse))
} else scope.launch(Dispatchers.Main) {
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatchOnMain(
DisclaimerAction.Show(
onAcceptCallback = {
scope.launch(Dispatchers.Main) {
nextHandler(scanResponse)
}
},
onDismissCallback = {
scope.launch(Dispatchers.Main) {
onProgressStateChange(false)
}
},
),
)
}
}
private suspend inline fun onScanSuccess(
scanResponse: ScanResponse,
crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
crossinline onWalletNotCreated: suspend () -> Unit,
crossinline onSuccess: suspend (ScanResponse) -> Unit,
crossinline onFailure: suspend (error: TangemError) -> Unit,
) {
Analytics.send(IntroductionProcess.CardWasScanned())
val globalState = store.state.globalState
val tapWalletManager = globalState.tapWalletManager
tapWalletManager.updateConfigManager(scanResponse)
Analytics.addParamsInterceptor(BatchIdParamsInterceptor(scanResponse.card.batchId))
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(scanResponse))
if (scanResponse.isSaltPay()) {
if (scanResponse.isSaltPayVisa()) {
val (manager, config) = OnboardingSaltPayState.initDependency(scanResponse)
val result = OnboardingSaltPayHelper.isOnboardingCase(scanResponse, manager)
delay(500)
withMainContext {
when (result) {
is Result.Success -> {
val isOnboardingCase = result.data
if (isOnboardingCase) {
onWalletNotCreated()
store.dispatchOnMain(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
store.dispatchOnMain(OnboardingSaltPayAction.SetDependencies(manager, config))
store.dispatchOnMain(OnboardingSaltPayAction.Update)
navigateTo(AppScreen.OnboardingWallet) { onProgressStateChange(it) }
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)
onProgressStateChange(false)
}
}
is Result.Failure -> {
SaltPayExceptionHandler.handle(result.error)
delay(DELAY_SDK_DIALOG_CLOSE)
onFailure(TangemSdkError.ExceptionError(result.error))
onProgressStateChange(false)
}
}
}
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
if (scanResponse.card.backupStatus?.isActive == false) {
showSaltPayTapVisaLogoCardDialog()
} else {
onSuccess(scanResponse)
}
onProgressStateChange(false)
}
} else {
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
onWalletNotCreated()
store.dispatchOnMain(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = true))
val appScreen = OnboardingHelper.whereToNavigate(scanResponse)
navigateTo(appScreen) { onProgressStateChange(it) }
} else {
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
onWalletNotCreated()
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse)))
navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) }
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)
onProgressStateChange(false)
}
}
}
}
private fun showSaltPayTapVisaLogoCardDialog() {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
headerId = R.string.saltpay_error_empty_backup_title,
messageId = R.string.saltpay_error_empty_backup_message,
),
)
}
private suspend inline fun navigateTo(
screen: AppScreen,
onProgressStateChange: (showProgress: Boolean) -> Unit,
) {
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatchOnMain(NavigationAction.NavigateTo(screen))
onProgressStateChange(false)
}
}

View file

@ -1,3 +1,5 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
@ -9,6 +11,8 @@ import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.common.map
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.KeyWalletPublicKey
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.TapWorkarounds.derivationStyle
@ -19,49 +23,72 @@ import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.backup.StartPrimaryCardLinkingTask
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.tap.domain.tasks.product.CreateWalletsTask
import com.tangem.tap.domain.tasks.product.ProductCommandProcessor
import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.operations.wallet.CreateWalletResponse as SdkCreateWalletResponse
data class CreateProductWalletTaskResponse(
val card: Card,
val card: CardDTO,
val derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap> = mapOf(),
val primaryCard: PrimaryCard? = null
) : CommandResponse
val primaryCard: PrimaryCard? = null,
) : CommandResponse {
constructor(
card: Card,
derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap> = mapOf(),
primaryCard: PrimaryCard? = null,
) : this(
card = CardDTO(card),
derivedKeys = derivedKeys,
primaryCard = primaryCard,
)
}
private data class CreateWalletResponse(
val cardId: String,
val wallet: CardDTO.Wallet,
) {
constructor(
sdkResponse: SdkCreateWalletResponse,
) : this(
cardId = sdkResponse.cardId,
wallet = CardDTO.Wallet(sdkResponse.wallet),
)
}
class CreateProductWalletTask(
private val type: ProductType,
) : CardSessionRunnable<CreateProductWalletTaskResponse> {
override val allowsRequestAccessCodeFromRepository: Boolean = false
override fun run(
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
val card = session.environment.card.guard {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
val cardDto = CardDTO(card)
val commandProcessor = when (type) {
ProductType.Note -> CreateWalletTangemNote()
ProductType.Twins -> throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet")
else -> CreateWalletTangemWallet()
}
commandProcessor.proceed(card, session) {
commandProcessor.proceed(cardDto, session) {
when (it) {
is CompletionResult.Success -> {
val result = when (commandProcessor) {
is CreateWalletTangemWallet -> {
it.data as CreateProductWalletTaskResponse
}
else -> CreateProductWalletTaskResponse(card = session.environment.card!!)
}
callback(CompletionResult.Success(result))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(it.error))
}
}
@ -70,7 +97,7 @@ class CreateProductWalletTask(
private class CreateWalletTangemNote : ProductCommandProcessor<CreateWalletResponse> {
override fun proceed(
card: Card,
card: CardDTO,
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit,
) {
@ -94,52 +121,65 @@ private class CreateWalletTangemNote : ProductCommandProcessor<CreateWalletRespo
} else {
intersectCurves[0]
}
CreateWalletTask(curve).run(session, callback)
CreateWalletTask(curve).run(session) { result ->
callback(result.map { CreateWalletResponse(it) })
}
}
}
}
private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWalletTaskResponse> {
private lateinit var card: Card
private var primaryCard: PrimaryCard? = null
override fun proceed(
card: Card,
card: CardDTO,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
this.card = card
val curves = card.getCurvesForNonCreatedWallets()
if (curves.isEmpty()) {
val createWalletResponses = card.wallets.map { CreateWalletResponse(card.cardId, it) }
proceedWithCreatedWallets(createWalletResponses, session, callback)
val createWalletResponses = card.wallets.map { wallet ->
CreateWalletResponse(card.cardId, wallet)
}
proceedWithCreatedWallets(card, createWalletResponses, session, callback)
return
}
CreateWalletsTask(curves).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
proceedWithCreatedWallets(result.data.createWalletResponses, session, callback)
proceedWithCreatedWallets(
card = card,
createWalletResponses = result.data.createWalletResponses.map { CreateWalletResponse(it) },
session = session,
callback = callback,
)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun proceedWithCreatedWallets(
card: CardDTO,
createWalletResponses: List<CreateWalletResponse>,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
when {
card.settings.isBackupAllowed -> {
linkPrimaryCard(createWalletResponses, session, callback)
linkPrimaryCard(card, createWalletResponses, session, callback)
}
card.settings.isHDWalletAllowed -> {
deriveKeys(createWalletResponses, session, callback)
deriveKeys(card, createWalletResponses, session, callback)
}
else -> {
callback(
CompletionResult.Success(
@ -151,6 +191,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
}
private fun linkPrimaryCard(
card: CardDTO,
createWalletResponse: List<CreateWalletResponse>,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
@ -161,8 +202,9 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
primaryCard = result.data
when {
card.settings.isHDWalletAllowed -> {
deriveKeys(createWalletResponse, session, callback)
deriveKeys(card, createWalletResponse, session, callback)
}
else -> {
callback(
CompletionResult.Success(
@ -174,6 +216,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
}
}
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
@ -182,13 +225,14 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
}
private fun deriveKeys(
card: CardDTO,
createWalletResponse: List<CreateWalletResponse>,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
val map = mutableMapOf<ByteArrayKey, List<DerivationPath>>()
createWalletResponse.forEach { response ->
val blockchainsForCurve = getBlockchains(response.cardId).filter {
val blockchainsForCurve = getBlockchains(response.cardId, card).filter {
it.getSupportedCurves().contains(response.wallet.curve)
}
val derivationPaths = blockchainsForCurve.mapNotNull { it.derivationPath(card.derivationStyle) }
@ -210,17 +254,21 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
CreateProductWalletTaskResponse(
card = session.environment.card!!,
derivedKeys = result.data.entries,
primaryCard = primaryCard
)
)
primaryCard = primaryCard,
),
),
)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun getBlockchains(cardId: String): List<Blockchain> {
private fun getBlockchains(
cardId: String,
card: CardDTO,
): List<Blockchain> {
return when {
DemoHelper.isDemoCardId(cardId) -> DemoHelper.config.demoBlockchains
card.isTestCard -> listOf(Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet)

View file

@ -1,15 +1,15 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.core.CardSession
import com.tangem.domain.common.CardDTO
/**
[REDACTED_AUTHOR]
*/
interface ProductCommandProcessor<T> {
fun proceed(
card: Card,
card: CardDTO,
session: CardSession,
callback: (result: CompletionResult<T>) -> Unit,
)

View file

@ -13,6 +13,7 @@ import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toHexString
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isExcluded
@ -32,7 +33,6 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.extensions.getPrimaryCurve
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
@ -46,6 +46,9 @@ class ScanProductTask(
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null,
) : CardSessionRunnable<ScanResponse> {
override val allowsRequestAccessCodeFromRepository: Boolean
get() = !additionalBlockchainsToDerive.isNullOrEmpty()
override fun run(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
@ -54,19 +57,20 @@ class ScanProductTask(
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
val cardDto = CardDTO(card)
val error = getErrorIfExcludedCard(card)
val error = getErrorIfExcludedCard(cardDto)
if (error != null) {
callback(CompletionResult.Failure(error))
return
}
val commandProcessor = when {
card.isTangemNote -> ScanNoteProcessor()
card.isTangemTwins -> ScanTwinProcessor()
cardDto.isTangemNote -> ScanNoteProcessor()
cardDto.isTangemTwins -> ScanTwinProcessor()
else -> ScanWalletProcessor(userTokensRepository, additionalBlockchainsToDerive)
}
commandProcessor.proceed(card, session) { processorResult ->
commandProcessor.proceed(cardDto, session) { processorResult ->
when (processorResult) {
is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult ->
when (scanTaskResult) {
@ -74,7 +78,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 = CardDTO(scanTaskResult.data),
)
callback(CompletionResult.Success(processorScanResponseWithNewCard))
}
@ -86,7 +90,7 @@ class ScanProductTask(
}
}
private fun getErrorIfExcludedCard(card: Card): TangemError? {
private fun getErrorIfExcludedCard(card: CardDTO): TangemError? {
if (card.isExcluded) return TapSdkError.CardForDifferentApp
if (card.isNotSupportedInThatRelease) return TapSdkError.CardNotSupportedByRelease
return null
@ -95,7 +99,7 @@ class ScanProductTask(
private class ScanNoteProcessor : ProductCommandProcessor<ScanResponse> {
override fun proceed(
card: Card,
card: CardDTO,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
@ -118,7 +122,7 @@ private class ScanWalletProcessor(
var primaryCard: PrimaryCard? = null
override fun proceed(
card: Card,
card: CardDTO,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
@ -126,7 +130,7 @@ private class ScanWalletProcessor(
}
private fun createMissingWalletsIfNeeded(
card: Card,
card: CardDTO,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
@ -160,13 +164,15 @@ private class ScanWalletProcessor(
}
private fun startLinkingForBackupIfNeeded(
card: Card,
card: CardDTO,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val activationIsFinished = preferencesStorage.usedCardsPrefStorage.isActivationFinished(card.cardId)
val activationInProgress = preferencesStorage.usedCardsPrefStorage.isActivationInProgress(card.cardId)
if (card.backupStatus == Card.BackupStatus.NoBackup && !activationIsFinished && card.wallets.isNotEmpty()) {
if ((card.backupStatus == CardDTO.BackupStatus.NoBackup && card.wallets.isNotEmpty())
&& (activationInProgress || card.isSaltPay)
) {
StartPrimaryCardLinkingTask().run(session) { linkingResult ->
when (linkingResult) {
is CompletionResult.Success -> {
@ -184,7 +190,7 @@ private class ScanWalletProcessor(
}
private fun deriveKeysIfNeeded(
card: Card,
card: CardDTO,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
@ -227,25 +233,41 @@ private class ScanWalletProcessor(
}
}
private suspend fun getBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
private suspend fun getBlockchainsToDerive(card: CardDTO): List<BlockchainNetwork> {
val userTokensRepository = userTokensRepository ?: return emptyList()
val blockchainsToDerive = userTokensRepository.loadBlockchainsToDerive(card).toMutableList().ifEmpty {
mutableListOf(
BlockchainNetwork(Blockchain.Bitcoin, card),
BlockchainNetwork(Blockchain.Ethereum, card),
)
}
val blockchainsToDerive = userTokensRepository.loadBlockchainsToDerive(card)
.toMutableList()
.ifEmpty {
mutableListOf(
BlockchainNetwork(
blockchain = Blockchain.Bitcoin,
card = card,
),
BlockchainNetwork(
blockchain = Blockchain.Ethereum,
card = card,
),
)
}
if (card.settings.isHDWalletAllowed) {
blockchainsToDerive.addAll(
listOf(
BlockchainNetwork(Blockchain.Ethereum, card),
BlockchainNetwork(Blockchain.EthereumTestnet, card),
BlockchainNetwork(
blockchain = Blockchain.Ethereum,
card = card,
),
BlockchainNetwork(
blockchain = Blockchain.EthereumTestnet,
card = card,
),
),
)
}
if (additionalBlockchainsToDerive != null) {
blockchainsToDerive.addAll(additionalBlockchainsToDerive.map { BlockchainNetwork(it, card) })
blockchainsToDerive.addAll(
additionalBlockchainsToDerive.map { BlockchainNetwork(it, card) }
)
}
if (!card.useOldStyleDerivation) {
blockchainsToDerive.removeAll(
@ -261,7 +283,7 @@ private class ScanWalletProcessor(
return blockchainsToDerive.distinct()
}
private suspend fun collectDerivations(card: Card): Map<ByteArrayKey, List<DerivationPath>> {
private suspend fun collectDerivations(card: CardDTO): Map<ByteArrayKey, List<DerivationPath>> {
val blockchains = getBlockchainsToDerive(card)
val derivations = mutableMapOf<ByteArrayKey, List<DerivationPath>>()
@ -287,14 +309,14 @@ private class ScanWalletProcessor(
private class ScanTwinProcessor : ProductCommandProcessor<ScanResponse> {
override fun proceed(
card: Card,
card: CardDTO,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
ReadIssuerDataCommand().run(session) { readDataResult ->
when (readDataResult) {
is CompletionResult.Success -> {
val publicKey = card.getSingleWallet()?.publicKey
val publicKey = card.wallets.firstOrNull()?.publicKey
if (publicKey == null) {
val response = ScanResponse(
card = card,
@ -332,7 +354,7 @@ private class ScanTwinProcessor : ProductCommandProcessor<ScanResponse> {
}
}
fun Card.getCurvesForNonCreatedWallets(): List<EllipticCurve> {
fun CardDTO.getCurvesForNonCreatedWallets(): List<EllipticCurve> {
val curvesPresent = wallets.map { it.curve }.toSet()
val curvesForNonCreatedWallets = supportedCurves.subtract(curvesPresent + EllipticCurve.Secp256r1)
return curvesForNonCreatedWallets.toList()

View file

@ -3,8 +3,8 @@ package com.tangem.tap.domain.termsOfUse
import android.content.res.Resources
import android.net.Uri
import androidx.core.os.ConfigurationCompat
import com.tangem.common.card.Card
import java.util.Locale
import com.tangem.domain.common.CardDTO
import java.util.*
/**
[REDACTED_AUTHOR]
@ -13,7 +13,7 @@ class CardTou {
private val locale: Locale =
ConfigurationCompat.getLocales(Resources.getSystem().configuration).get(0)!!
fun getUrl(card: Card): Uri? {
fun getUrl(card: CardDTO): Uri? {
val issuerName = card.issuer.name
if (issuerName.lowercase(Locale.getDefault()) != "start2coin") return null

View file

@ -2,10 +2,11 @@ package com.tangem.tap.domain.tokens
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.CardDTO
object CurrenciesRepository {
fun getBlockchains(
cardFirmware: FirmwareVersion,
cardFirmware: CardDTO.FirmwareVersion,
isTestNet: Boolean = false,
): List<Blockchain> {
val blockchains = if (cardFirmware < FirmwareVersion.MultiWalletAvailable) {
@ -26,6 +27,4 @@ object CurrenciesRepository {
)
}
}
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.network.api.tangemTech.CoinsResponse
import com.tangem.datasource.api.tangemTech.CoinsResponse
@JsonClass(generateAdapter = true)
data class CurrencyFromJson(

View file

@ -6,9 +6,9 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.getListOfCoins
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.network.api.tangemTech.CoinsResponse
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.common.MoshiConverter
import com.tangem.datasource.api.tangemTech.CoinsResponse
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.tap.common.AssetReader
class LoadAvailableCoinsService(

View file

@ -7,8 +7,8 @@ import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.getTokens
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.common.MoshiConverter
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.tap.common.FileReader
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.TokenDao

View file

@ -2,8 +2,8 @@ package com.tangem.tap.domain.tokens
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.api.tangemTech.UserTokensResponse
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.datasource.api.tangemTech.UserTokensResponse
import com.tangem.tap.domain.NoDataError
class UserTokensNetworkService(private val tangemTechService: TangemTechService) {

View file

@ -2,13 +2,13 @@ package com.tangem.tap.domain.tokens
import android.content.Context
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.card.Card
import com.tangem.common.services.Result
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.api.tangemTech.UserTokensResponse
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.datasource.api.tangemTech.UserTokensResponse
import com.tangem.domain.common.CardDTO
import com.tangem.tap.common.AndroidFileReader
import com.tangem.tap.domain.NoDataError
import com.tangem.tap.domain.extensions.getUserWalletId
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.models.Currency
@ -23,8 +23,8 @@ class UserTokensRepository(
private val storageService: UserTokensStorageService,
private val networkService: UserTokensNetworkService,
) {
suspend fun getUserTokens(card: Card): List<Currency> {
val userId = card.getUserWalletId()
suspend fun getUserTokens(card: CardDTO): List<Currency> {
val userId = getUserWalletId(card) ?: return emptyList()
if (DemoHelper.isDemoCardId(card.cardId)) {
return loadTokensOffline(card, userId).ifEmpty { loadDemoCurrencies() }
}
@ -36,25 +36,28 @@ class UserTokensRepository(
return when (val networkResult = networkService.getUserTokens(userId)) {
is Result.Success -> {
val tokens = networkResult.data.tokens.mapNotNull { Currency.fromTokenResponse(it) }
storageService.saveUserTokens(card.getUserWalletId(), tokens.toUserTokensResponse())
storageService.saveUserTokens(userId, tokens.toUserTokensResponse())
tokens.distinct()
}
is Result.Failure -> {
handleGetUserTokensFailure(card = card, userId = userId, error = networkResult.error)
}
}
}
suspend fun saveUserTokens(card: Card, tokens: List<Currency>) {
suspend fun saveUserTokens(card: CardDTO, tokens: List<Currency>) {
val userId = getUserWalletId(card) ?: return
val userTokens = tokens.toUserTokensResponse()
networkService.saveUserTokens(card.getUserWalletId(), userTokens)
storageService.saveUserTokens(card.getUserWalletId(), userTokens)
networkService.saveUserTokens(userId, userTokens)
storageService.saveUserTokens(userId, userTokens)
}
suspend fun removeUserTokens(card: Card) {
suspend fun removeUserTokens(card: CardDTO) {
val userId = getUserWalletId(card) ?: return
val userTokens = emptyList<Currency>().toUserTokensResponse()
networkService.saveUserTokens(card.getUserWalletId(), userTokens)
storageService.saveUserTokens(card.getUserWalletId(), userTokens)
networkService.saveUserTokens(userId, userTokens)
storageService.saveUserTokens(userId, userTokens)
}
private fun List<Currency>.toUserTokensResponse(): UserTokensResponse {
@ -66,8 +69,8 @@ class UserTokensRepository(
)
}
suspend fun loadBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
val userId = card.getUserWalletId()
suspend fun loadBlockchainsToDerive(card: CardDTO): List<BlockchainNetwork> {
val userId = getUserWalletId(card) ?: return emptyList()
val blockchainNetworks = loadTokensOffline(card, userId).toBlockchainNetworks()
if (DemoHelper.isDemoCardId(card.cardId)) {
@ -89,7 +92,7 @@ class UserTokensRepository(
}
private suspend fun handleGetUserTokensFailure(
card: Card,
card: CardDTO,
userId: String,
error: Throwable,
): List<Currency> {
@ -100,6 +103,7 @@ class UserTokensRepository(
coroutineScope { launch { networkService.saveUserTokens(userId = userId, tokens = userTokens) } }
tokens
}
else -> {
val tokens = storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
tokens.distinct()
@ -107,10 +111,15 @@ class UserTokensRepository(
}
}
private suspend fun loadTokensOffline(card: Card, userId: String): List<Currency> {
private suspend fun loadTokensOffline(card: CardDTO, userId: String): List<Currency> {
return storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
}
private fun getUserWalletId(card: CardDTO): String? {
return UserWalletIdBuilder.card(card).build()
?.stringValue
}
companion object {
const val SORT_DEFAULT_VALUE = "manual"
const val GROUP_DEFAULT_VALUE = "none"

View file

@ -2,9 +2,9 @@ package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonAdapter
import com.tangem.Log
import com.tangem.common.card.Card
import com.tangem.network.api.tangemTech.UserTokensResponse
import com.tangem.network.common.MoshiConverter
import com.tangem.datasource.api.tangemTech.UserTokensResponse
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.domain.common.CardDTO
import com.tangem.tap.common.FileReader
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.toCurrencies
@ -28,7 +28,7 @@ class UserTokensStorageService(
}
@Deprecated("")
suspend fun getUserTokens(card: Card): List<Currency> {
suspend fun getUserTokens(card: CardDTO): List<Currency> {
val blockchainNetworks =
oldUserTokensRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
return blockchainNetworks.flatMap { it.toCurrencies() }

View file

@ -4,8 +4,8 @@ import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.card.Card
import com.tangem.common.extensions.calculateHashCode
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.derivationStyle
@JsonClass(generateAdapter = true)
@ -15,16 +15,18 @@ data class BlockchainNetwork(
val tokens: List<Token>
) {
constructor(blockchain: Blockchain, card: Card) : this(
constructor(
blockchain: Blockchain,
card: CardDTO,
) : this(
blockchain = blockchain,
derivationPath = if (card.settings.isHDWalletAllowed) blockchain.derivationPath(card.derivationStyle)?.rawPath else null,
tokens = emptyList()
tokens = emptyList(),
)
fun updateTokens(tokens: List<Token>): BlockchainNetwork {
return copy(
tokens = (this.tokens + tokens).distinct()
tokens = (this.tokens + tokens).distinct(),
)
}

View file

@ -0,0 +1,34 @@
package com.tangem.tap.domain.totalBalance
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.domain.model.WalletStoreModel
import java.math.BigDecimal
interface TotalFiatBalanceCalculator {
/**
* Calculate total fiat balance for list of [WalletStoreModel]
* @param prevAmount Previous amount, used in [TotalFiatBalance.Refreshing]
* @param walletStores List of [WalletStoreModel] to calculate fiat amount
* @param initial Initial [TotalFiatBalance] state, used when list of [WalletStoreModel] is empty
* @return [TotalFiatBalance] with state found with the [WalletStoreModel] list
* */
suspend fun calculate(
prevAmount: BigDecimal?,
walletStores: List<WalletStoreModel>,
initial: TotalFiatBalance,
): TotalFiatBalance
/**
* Same as [TotalFiatBalanceCalculator.calculate] but returns null if list of [WalletStoreModel] is empty
* @param prevAmount Previous amount, used in [TotalFiatBalance.Refreshing]
* @param walletStores List of [WalletStoreModel] to calculate fiat amount
* @return [TotalFiatBalance] with state found with the [WalletStoreModel] list
* */
suspend fun calculateOrNull(
prevAmount: BigDecimal?,
walletStores: List<WalletStoreModel>,
): TotalFiatBalance?
companion object
}

View file

@ -0,0 +1,8 @@
package com.tangem.tap.domain.totalBalance.di
import com.tangem.tap.domain.totalBalance.TotalFiatBalanceCalculator
import com.tangem.tap.domain.totalBalance.implementation.DefaultTotalFiatBalanceCalculator
fun TotalFiatBalanceCalculator.Companion.provideDefaultImplementation(): TotalFiatBalanceCalculator {
return DefaultTotalFiatBalanceCalculator()
}

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