Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-02 10:24:41 +03:00
commit af09ce9ca4
877 changed files with 16091 additions and 6613 deletions

View file

@ -28,6 +28,7 @@ import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanFailsRequester
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
@ -151,4 +152,6 @@ interface ApplicationEntryPoint {
fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory
fun getCustomerIoFeatureToggles(): CustomerIoFeatureToggles
fun getScanFailsRequester(): ScanFailsRequester
}

View file

@ -4,7 +4,7 @@ import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import kotlin.reflect.KClass
object ForegroundActivityObserver {
@ -14,7 +14,7 @@ object ForegroundActivityObserver {
val foregroundActivity: AppCompatActivity?
get() = activities.entries
.firstOrNull { entry ->
Timber.i("foregroundActivity: ${entry.key} | ${entry.value.isDestroyed}")
TangemLogger.i("foregroundActivity: ${entry.key} | ${entry.value.isDestroyed}")
entry.value.isDestroyed == false
}
?.value
@ -27,15 +27,15 @@ object ForegroundActivityObserver {
}
override fun onActivityResumed(activity: Activity) {
Timber.i("onActivityResumed ${activity::class}")
TangemLogger.i("onActivityResumed ${activity::class}")
if (activity is AppCompatActivity) {
Timber.i("onActivityResumed store activity")
TangemLogger.i("onActivityResumed store activity")
activities[activity::class] = activity
}
}
override fun onActivityDestroyed(activity: Activity) {
Timber.i("onActivityDestroyed")
TangemLogger.i("onActivityDestroyed")
activities.remove(activity::class)
}

View file

@ -6,9 +6,9 @@ import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import timber.log.Timber
@HiltWorker
class LockTimerWorker @AssistedInject constructor(
@ -19,11 +19,11 @@ class LockTimerWorker @AssistedInject constructor(
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
Timber.i("onStart job")
TangemLogger.i("onStart job")
userWalletsListRepository.lockAllWallets().onRight {
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
}
Timber.i("onStart job complete")
TangemLogger.i("onStart job complete")
return Result.success()
}

View file

@ -13,11 +13,11 @@ import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase
import com.tangem.tap.LockTimerWorker.Companion.TAG
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.TimeUnit
import kotlin.time.Duration
@ -47,7 +47,7 @@ internal class LockUserWalletsTimer(
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
coroutineScope.launch {
val shouldOpenWelcomeScreenOnResume = settingsRepository.shouldOpenWelcomeScreenOnResume()
Timber.i(
TangemLogger.i(
"""
Owner resumed
|- Need to open welcome screen: $shouldOpenWelcomeScreenOnResume
@ -68,7 +68,7 @@ internal class LockUserWalletsTimer(
}
override fun onStop(owner: LifecycleOwner) {
Timber.i("Owner stopped")
TangemLogger.i("Owner stopped")
delayJob = null
startTimerWorker()
@ -76,7 +76,7 @@ internal class LockUserWalletsTimer(
fun restart() {
if (delayJob == null) return
Timber.i(
TangemLogger.i(
"""
Timer restart
|- Duration millis: ${duration.inWholeMilliseconds}
@ -96,7 +96,7 @@ internal class LockUserWalletsTimer(
private fun start(log: Boolean = true) {
if (log) {
Timber.i(
TangemLogger.i(
"""
Timer start
|- Duration millis: ${duration.inWholeMilliseconds}

View file

@ -54,7 +54,6 @@ import com.tangem.operations.backup.BackupService
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.ActivityResultCallbackHolder
import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.analytics.events.Push
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
@ -67,11 +66,11 @@ import com.tangem.tap.routing.utils.DeepLinkFactory
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import com.tangem.utils.extensions.uriValidate
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration.Companion.seconds
@ -171,16 +170,15 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
private val dialogManager = DialogManager()
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
override fun onCreate(savedInstanceState: Bundle?) {
Timber.i("onCreate")
TangemLogger.i("onCreate")
// We need to call it before onCreate to prevent unnecessary activity recreation
installAppTheme()
val splashScreen = installSplashScreen()
TangemLogger.i("Splash screen installed")
enableEdgeToEdge(
navigationBarStyle = SystemBarStyle.auto(
@ -322,18 +320,16 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
override fun onStart() {
super.onStart()
Timber.i("onStart")
dialogManager.onStart(this)
TangemLogger.i("onStart")
}
override fun onStop() {
dialogManager.onStop()
super.onStop()
Timber.i("onStop")
TangemLogger.i("onStop")
}
override fun onDestroy() {
Timber.i("onDestroy")
TangemLogger.i("onDestroy")
// workaround: kill process when activity destroy to avoid state when lock() wallets
// and navigation to unlock screen was skipped because system kills activity but not process
if (BuildConfig.BUILD_TYPE != MOCKED_BUILD_TYPE) {
@ -366,6 +362,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
TangemLogger.i("onNewIntent: data=${intent.data}, extras=${intent.extras?.keySet()}")
val isFromPush = intent.extras?.containsKey(OPENED_FROM_GCM_PUSH) == true
if (isFromPush) {
@ -432,8 +429,8 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
private fun sendStakingUnsubmittedHashes() {
lifecycleScope.launch {
sendUnsubmittedHashesUseCase.invoke()
.onLeft { Timber.e(it.toString()) }
.onRight { Timber.d("Submitting hashes succeeded") }
.onLeft { TangemLogger.e(it.toString()) }
.onRight { TangemLogger.d("Submitting hashes succeeded") }
}
}

View file

@ -75,13 +75,13 @@ import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import org.rekotlin.Store
import timber.log.Timber
lateinit var store: Store<AppState>
@ -239,6 +239,9 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
private val customerIoFeatureToggles: CustomerIoFeatureToggles
get() = entryPoint.getCustomerIoFeatureToggles()
private val scanFailsRequester
get() = entryPoint.getScanFailsRequester()
// endregion
private val appScope = MainScope()
@ -270,22 +273,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
}
}
private fun updateLogFiles() {
appLogsStore.deleteOldLogsFile()
if (!BuildConfig.TESTER_MENU_ENABLED) {
appLogsStore.deleteLastLogFile()
}
// Temporally logs are not saved
// scope.launch {
// if (!appPreferencesStore.getSyncOrDefault(WAS_LOG_FILE_CLEARED, false)) {
// appLogsStore.deleteLastLogFile()
// appPreferencesStore.store(WAS_LOG_FILE_CLEARED, true)
// }
// }
}
/**
* Initialize components that need to be initialized before [super.onCreate] is called
*/
@ -299,10 +286,10 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
store = createReduxStore()
Timber.i("APP STARTED")
TangemLogger.i("APP STARTED")
if (BuildConfig.TESTER_MENU_ENABLED) {
Timber.i(featureTogglesManager.toString())
Timber.i(excludedBlockchainsManager.toString())
TangemLogger.i(featureTogglesManager.toString())
TangemLogger.i(excludedBlockchainsManager.toString())
}
initWithConfigDependency(environmentConfig = environmentConfig)
@ -384,11 +371,28 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration.
userWalletsListRepository = userWalletsListRepository,
tangemHotSdk = tangemHotSdk,
trackingContextProxy = trackingContextProxy,
scanFailsRequester = scanFailsRequester,
),
),
)
}
private fun updateLogFiles() {
appLogsStore.deleteOldLogsFile()
if (!BuildConfig.TESTER_MENU_ENABLED) {
appLogsStore.deleteLastLogFile()
}
// Temporarily logs are not saved
// scope.launch {
// if (!appPreferencesStore.getSyncOrDefault(WAS_LOG_FILE_CLEARED, false)) {
// appLogsStore.deleteLastLogFile()
// appPreferencesStore.store(WAS_LOG_FILE_CLEARED, true)
// }
// }
}
override fun newImageLoader(): ImageLoader {
return createCoilImageLoader(
context = this,

View file

@ -7,7 +7,7 @@ import androidx.lifecycle.LifecycleOwner
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.event.TechAnalyticsEvent
import com.tangem.core.analytics.models.event.TechAnalyticsEvent.WindowObscured.ObscuredState
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
internal object WindowObscurationObserver : DefaultLifecycleObserver {
@ -36,7 +36,7 @@ internal object WindowObscurationObserver : DefaultLifecycleObserver {
}
if (isPartiallyObscured) {
Timber.d("Window is partially obscured")
TangemLogger.d("Window is partially obscured")
if (!isWindowPartiallyObscuredAlreadySent) {
analyticsEventHandler.send(
@ -50,7 +50,7 @@ internal object WindowObscurationObserver : DefaultLifecycleObserver {
val isFullyObscured = event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0
if (isFullyObscured) {
Timber.d("Window is partially or fully obscured")
TangemLogger.d("Window is partially or fully obscured")
if (!isWindowFullyObscuredAlreadySent) {
analyticsEventHandler.send(

View file

@ -1,98 +0,0 @@
package com.tangem.tap.common
import android.app.Dialog
import android.content.Context
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.ui.ScanFailsDialog
import com.tangem.tap.common.ui.SimpleAlertDialog
import com.tangem.tap.common.ui.SimpleCancelableAlertDialog
import com.tangem.tap.common.ui.SimpleOkDialog
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletActivationErrorDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletAlreadyWasUsedDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
class DialogManager : StoreSubscriber<GlobalState> {
var context: Context? = null
private var dialog: Dialog? = null
fun onStart(context: Context) {
this.context = context
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.globalState == newState.globalState
}.select { it.globalState }
}
}
fun onStop() {
this.context = null
store.unsubscribe(this)
}
@Suppress("LongMethod", "ComplexMethod")
override fun newState(state: GlobalState) {
if (state.dialog == null) {
dialog?.dismiss()
dialog = null
return
}
val context = context ?: return
if (dialog != null) return
dialog = when (state.dialog) {
is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context)
is StateDialog.ScanFailsDialog -> ScanFailsDialog.create(
context = context,
source = state.dialog.source,
onTryAgain = state.dialog.onTryAgain,
)
is StateDialog.NfcFeatureIsUnavailable -> SimpleAlertDialog.create(
titleRes = R.string.common_error,
messageRes = R.string.nfc_error_unavailable,
context = context,
)
is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog)
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(
context = context,
scanResponse = state.dialog.scanResponse,
)
is BackupDialog.ConfirmDiscardingBackup -> ConfirmDiscardingBackupDialog.create(
context = context,
unfinishedBackupScanResponse = state.dialog.scanResponse,
)
is AppDialog.TokensAreLinkedDialog -> SimpleAlertDialog.create(
title = context.getString(state.dialog.titleRes, state.dialog.currencySymbol),
message = context.getString(
state.dialog.messageRes,
state.dialog.currencyTitle,
state.dialog.currencySymbol,
state.dialog.networkName,
),
context = context,
)
is AppDialog.WalletAlreadyWasUsedDialog -> WalletAlreadyWasUsedDialog.create(
context = context,
onOk = state.dialog.onOk,
onSupport = state.dialog.onSupportClick,
onCancel = state.dialog.onCancel,
)
is AppDialog.RemoveWalletDialog -> SimpleCancelableAlertDialog.create(
title = context.getString(state.dialog.titleRes, state.dialog.currencyTitle),
messageRes = state.dialog.messageRes,
context = context,
primaryButtonRes = state.dialog.primaryButtonRes,
primaryButtonAction = state.dialog.onOk,
)
else -> null
}
dialog?.show()
}
}

View file

@ -1,16 +0,0 @@
package com.tangem.tap.common
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
private val uiHandler = Handler(Looper.getMainLooper())
private val backgroundHandler = Handler(HandlerThread("AppMainHandlerThread").apply { start() }.looper)
fun postUi(ms: Long = 0, func: Runnable) {
if (ms == 0L) uiHandler.post { func.run() } else uiHandler.postDelayed(func, ms)
}
fun postUiDelayBg(ms: Long, func: Runnable) {
backgroundHandler.postDelayed({ uiHandler.post(func) }, ms)
}

View file

@ -1,9 +1,9 @@
package com.tangem.tap.common.analytics
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.core.analytics.api.ExceptionLogger
import com.tangem.core.analytics.api.EventLogger
import timber.log.Timber
import com.tangem.core.analytics.api.ExceptionLogger
import com.tangem.utils.logging.TangemLogger
class AnalyticsEventsLogger(
private val name: String,
@ -11,11 +11,11 @@ class AnalyticsEventsLogger(
) : EventLogger, ExceptionLogger {
override fun logEvent(event: String, params: Map<String, String>) {
Timber.d(jsonConverter.prettyPrint(PrintEventModel(name, event, params)))
TangemLogger.d(jsonConverter.prettyPrint(PrintEventModel(name, event, params)))
}
override fun logException(error: Throwable, params: Map<String, String>) {
Timber.e(error, jsonConverter.prettyPrint(PrintEventModel(name, "error", params)))
TangemLogger.e(jsonConverter.prettyPrint(PrintEventModel(name, "error", params)), error)
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.tap.common.analytics.appsflyer
import com.appsflyer.deeplink.DeepLinkListener
import com.appsflyer.deeplink.DeepLinkResult
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
import javax.inject.Singleton
@ -17,10 +17,10 @@ class AppsFlyerDeepLinkListener @Inject constructor(
referralParamsHandler.handle(deepLink = p0.deepLink)
}
DeepLinkResult.Status.NOT_FOUND -> {
Timber.i("No deep link found")
TangemLogger.i("No deep link found")
}
DeepLinkResult.Status.ERROR -> {
Timber.e("Deep link error: ${p0.error}")
TangemLogger.e("Deep link error: ${p0.error}")
}
}
}

View file

@ -2,13 +2,13 @@ package com.tangem.tap.common.analytics.appsflyer
import com.appsflyer.deeplink.DeepLink
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.wallets.models.AppsFlyerConversionData
import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.contracts.ExperimentalContracts
@ -41,15 +41,15 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
private fun handle(deepLinkValue: String?, deepLinkSub1: String?, deepLinkSub2: String?) {
if (deepLinkValue != REFERRAL_DEEP_LINK_VALUE) {
Timber.i("Ignoring deep link with value: ${deepLinkValue ?: "null"}")
TangemLogger.i("Ignoring deep link with value: ${deepLinkValue ?: "null"}")
return
}
@Suppress("NullableToStringCall")
Timber.i("refcode=$deepLinkSub1\ncampaign=$deepLinkSub2")
TangemLogger.i("refcode=$deepLinkSub1\ncampaign=$deepLinkSub2")
if (!isValidParam(deepLinkSub1)) {
Timber.e("Deeplink conversion data is invalid")
TangemLogger.e("Deeplink conversion data is invalid")
return
}
@ -69,7 +69,7 @@ class AppsFlyerReferralParamsHandler @Inject constructor(
coroutineScope.launch {
mutex.withLock {
setShouldShowMobileWalletPromoUseCase(true)
.onLeft { Timber.e(it) }
.onLeft { TangemLogger.e("Error", it) }
appsFlyerStore.storeIfAbsent(
value = AppsFlyerConversionData(refcode = refcode, campaign = campaign),
)

View file

@ -1,7 +1,7 @@
package com.tangem.tap.common.analytics.appsflyer
import com.appsflyer.AppsFlyerConversionListener
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import javax.inject.Inject
import javax.inject.Singleton
@ -11,7 +11,7 @@ class TangemAFConversionListener @Inject constructor(
) : AppsFlyerConversionListener {
override fun onConversionDataSuccess(p0: Map<String?, Any?>?) {
Timber.i("AppsFlyer conversion data success: ${p0.orEmpty()}")
TangemLogger.i("AppsFlyer conversion data success: ${p0.orEmpty()}")
if (p0 == null) return
@ -19,14 +19,14 @@ class TangemAFConversionListener @Inject constructor(
}
override fun onConversionDataFail(p0: String?) {
Timber.e("AppsFlyer conversion data failure: ${p0.orEmpty()}")
TangemLogger.e("AppsFlyer conversion data failure: ${p0.orEmpty()}")
}
override fun onAppOpenAttribution(p0: Map<String?, String?>?) {
Timber.i("AppsFlyer app open attribution: ${p0.orEmpty()}")
TangemLogger.i("AppsFlyer app open attribution: ${p0.orEmpty()}")
}
override fun onAttributionFailure(p0: String?) {
Timber.e("AppsFlyer attribution failure: ${p0.orEmpty()}")
TangemLogger.e("AppsFlyer attribution failure: ${p0.orEmpty()}")
}
}

View file

@ -11,7 +11,7 @@ class ScanFailsDialogAnalytics(button: Buttons, source: AnalyticsParam.ScreensSo
),
) {
enum class Buttons(val event: String) {
TRY_AGAIN("Try again button"),
HOW_TO_SCAN("Button blog"),
TRY_AGAIN("Button try again"),
}
}

View file

@ -6,16 +6,16 @@ import com.appsflyer.attribution.AppsFlyerRequestListener
import com.tangem.core.analytics.api.EventLogger
import com.tangem.core.analytics.api.UserIdHolder
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.tap.common.analytics.appsflyer.AppsFlyerDeepLinkListener
import com.tangem.tap.common.analytics.appsflyer.TangemAFConversionListener
import com.tangem.tap.common.analytics.handlers.firebase.UnderscoreAnalyticsEventConverter
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.launch
import timber.log.Timber
interface AppsFlyerAnalyticsClient : EventLogger, UserIdHolder
@ -40,9 +40,9 @@ class AppsFlyerClient @AssistedInject constructor(
init(apiKey, tangemAFConversionListener, context)
Timber.i("Starting AppsFlyer SDK")
TangemLogger.i("Starting AppsFlyer SDK")
start(context, apiKey, InitializationListener)
Timber.i("AppsFlyer SDK started")
TangemLogger.i("AppsFlyer SDK started")
saveUID()
}
@ -57,7 +57,7 @@ class AppsFlyerClient @AssistedInject constructor(
}
override fun logEvent(event: String, params: Map<String, String>) {
Timber.tag("AppsFlyer").i("Logging event: $event with params: $params")
TangemLogger.withTag("AppsFlyer").i("Logging event: $event with params: $params")
appsFlyerLib.logEvent(
context,
event,
@ -78,21 +78,21 @@ class AppsFlyerClient @AssistedInject constructor(
private object InitializationListener : AppsFlyerRequestListener {
override fun onSuccess() {
Timber.d("AppsFlyer initialized successfully")
TangemLogger.d("AppsFlyer initialized successfully")
}
override fun onError(p0: Int, p1: String) {
Timber.e("AppsFlyer initialization error: $p0, $p1")
TangemLogger.e("AppsFlyer initialization error: $p0, $p1")
}
}
private object LogEventListener : AppsFlyerRequestListener {
override fun onSuccess() {
Timber.tag("AppsFlyerClient").i("AppsFlyerRequestListener send")
TangemLogger.withTag("AppsFlyerClient").i("AppsFlyerRequestListener send")
}
override fun onError(p0: Int, p1: String) {
Timber.tag("AppsFlyerClient").e("AppsFlyerRequestListener onError: $p0, $p1")
TangemLogger.withTag("AppsFlyerClient").e("AppsFlyerRequestListener onError: $p0, $p1")
}
}

View file

@ -6,14 +6,14 @@ import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AppsFlyerIncludedEvent
import com.tangem.core.analytics.models.AppsFlyerOnlyEvent
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
class AppsFlyerAnalyticsHandler(
private val client: AppsFlyerAnalyticsClient,
) : AnalyticsHandler, AnalyticsUserIdHandler {
init {
Timber.tag("AppsFlyer").i("AppsFlyer Analytics Handler created")
TangemLogger.withTag("AppsFlyer").i("AppsFlyer Analytics Handler created")
}
override fun id(): String = ID
@ -21,11 +21,15 @@ class AppsFlyerAnalyticsHandler(
override fun send(event: AnalyticsEvent) {
when (event) {
is AppsFlyerOnlyEvent -> {
Timber.tag("AppsFlyer").i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}")
TangemLogger.withTag(
"AppsFlyer",
).i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}")
client.logEvent(event.id, event.params)
}
is AppsFlyerIncludedEvent -> {
Timber.tag("AppsFlyer").i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}")
TangemLogger.withTag(
"AppsFlyer",
).i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}")
val replacedEvent = event.appsFlyerReplacedEvent ?: event.event
client.logEvent(
event = AnalyticsEvent(category = event.category, event = replacedEvent).id,
@ -52,15 +56,15 @@ class AppsFlyerAnalyticsHandler(
) : AnalyticsHandlerBuilder {
init {
Timber.tag("AppsFlyer").i("AppsFlyer Analytics Handler Builder created")
TangemLogger.withTag("AppsFlyer").i("AppsFlyer Analytics Handler Builder created")
}
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler = AppsFlyerAnalyticsHandler(
client = if (data.logConfig.isAppsflyerLogEnabled) {
Timber.tag("AppsFlyer").i("AppsFlyer log enabled, mock client created")
TangemLogger.withTag("AppsFlyer").i("AppsFlyer log enabled, mock client created")
AppsFlyerLogClient(data.jsonConverter)
} else {
Timber.tag("AppsFlyer").i("AppsFlyer log disabled, real client created")
TangemLogger.withTag("AppsFlyer").i("AppsFlyer log disabled, real client created")
appsFlyerClientFactory.create(apiKey = data.config.appsFlyerApiKey)
},
)

View file

@ -1,11 +1,11 @@
package com.tangem.tap.common.analytics.handlers.customerio
import android.app.Application
import com.tangem.utils.logging.TangemLogger
import io.customer.messagingpush.ModuleMessagingPushFCM
import io.customer.sdk.CustomerIO
import io.customer.sdk.CustomerIOBuilder
import io.customer.sdk.data.model.Region
import timber.log.Timber
/**
* Real Customer.io SDK client.
@ -32,7 +32,7 @@ internal class CustomerIoClient(
.addCustomerIOModule(ModuleMessagingPushFCM())
.build()
Timber.d("CustomerIO SDK initialized")
TangemLogger.d("CustomerIO SDK initialized")
}
override fun setUserId(userId: String) {

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics.handlers.customerio
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
/**
* Log client for Customer.io (used in debug mode).
@ -13,11 +13,11 @@ internal class CustomerIoLogClient : CustomerIoAnalyticsClient {
override fun setUserId(userId: String) {
this.userId = userId
Timber.tag(CustomerIoAnalyticsHandler.ID).d("identify: userId=$userId")
TangemLogger.withTag(CustomerIoAnalyticsHandler.ID).d("identify: userId=$userId")
}
override fun clearUserId() {
Timber.tag(CustomerIoAnalyticsHandler.ID).d("clearIdentify: previous userId=$userId")
TangemLogger.withTag(CustomerIoAnalyticsHandler.ID).d("clearIdentify: previous userId=$userId")
this.userId = null
}
}

View file

@ -3,8 +3,8 @@ package com.tangem.tap.common.analytics.handlers.firebase
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
import com.tangem.core.analytics.AppInstanceIdProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import kotlin.coroutines.resume
internal class FirebaseAppInstanceIdProvider : AppInstanceIdProvider {
@ -13,7 +13,7 @@ internal class FirebaseAppInstanceIdProvider : AppInstanceIdProvider {
Firebase.analytics.appInstanceId
.addOnSuccessListener { continuation.resume(it) }
.addOnFailureListener {
Timber.w("Fail to get appInstanceId")
TangemLogger.w("Fail to get appInstanceId")
continuation.resume(null)
}
}
@ -22,7 +22,7 @@ internal class FirebaseAppInstanceIdProvider : AppInstanceIdProvider {
return try {
Firebase.analytics.appInstanceId.result
} catch (e: IllegalStateException) {
Timber.e(e, "getAppInstanceIdSync")
TangemLogger.e("getAppInstanceIdSync", e)
null
}
}

View file

@ -6,7 +6,7 @@ import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN
import android.os.Build
import android.os.PersistableBundle
import com.tangem.core.ui.clipboard.ClipboardManager
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
import android.content.ClipboardManager as AndroidClipboardManager
internal class DefaultClipboardManager(private val clipboardManager: AndroidClipboardManager) : ClipboardManager {
@ -24,13 +24,13 @@ internal class DefaultClipboardManager(private val clipboardManager: AndroidClip
val clip = clipboardManager.primaryClip
if (clip == null || clip.itemCount == 0) {
Timber.d("Clipboard is empty")
TangemLogger.d("Clipboard is empty")
return default
}
val clipDescription = clipboardManager.primaryClipDescription
if (clipDescription?.hasMimeType(MIMETYPE_TEXT_PLAIN) == false) {
Timber.d("Clipboard doesn't contain text")
TangemLogger.d("Clipboard doesn't contain text")
return default
}

View file

@ -1,12 +1,12 @@
package com.tangem.tap.common.clipboard
import com.tangem.core.ui.clipboard.ClipboardManager
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
internal object MockClipboardManager : ClipboardManager {
override fun setText(text: String, isSensitive: Boolean, label: String) {
Timber.w("Clipboard Manager not available")
TangemLogger.w("Clipboard Manager not available")
}
override fun getText(default: String?): String? = null

View file

@ -0,0 +1,56 @@
package com.tangem.tap.common.deeplink
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import com.tangem.common.routing.DeepLinkScheme
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.utils.logging.TangemLogger
/**
* [DeeplinkLauncher] implementation that launches deep links as intents to the current Activity
* and opens web URLs in the browser via [UrlOpener].
*/
internal class DefaultDeeplinkLauncher(
private val context: Context,
private val urlOpener: UrlOpener,
) : DeeplinkLauncher {
override fun launch(link: String) {
val deeplinkUri = link.toUri()
when (deeplinkUri.scheme) {
DeepLinkScheme.Tangem.scheme,
DeepLinkScheme.WalletConnect.scheme,
-> launchDeepLink(deeplinkUri)
DeepLinkScheme.Https.scheme -> launchDeeplinkOrOpenBrowser(deeplinkUri, link)
else -> {
TangemLogger.i(
"""
No match found for deep link
|- Received URI: $deeplinkUri
""".trimIndent(),
)
}
}
}
private fun launchDeeplinkOrOpenBrowser(uri: Uri, link: String) {
val intent = createDeepLinkIntent(uri)
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
} else {
urlOpener.openUrl(link)
}
}
private fun launchDeepLink(uri: Uri) {
context.startActivity(createDeepLinkIntent(uri))
}
private fun createDeepLinkIntent(uri: Uri): Intent = Intent(Intent.ACTION_VIEW, uri).apply {
setPackage(context.packageName)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
}

View file

@ -1,5 +0,0 @@
package com.tangem.tap.common.entities
import com.tangem.tap.common.toggleWidget.WidgetState
enum class ProgressState : WidgetState { Loading, Done, Error }

View file

@ -1,14 +1,9 @@
package com.tangem.tap.common.extensions
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
@ -45,26 +40,6 @@ suspend fun Store<AppState>.onUserWalletSelected(userWallet: UserWallet) {
state.globalState.tapWalletManager.onWalletSelected(userWallet)
}
/**
* @param fatal used to indicate errors that should not normally occur
*/
fun Store<AppState>.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) {
val prefix = if (fatal) "FATAL ERROR: " else "DEBUG ERROR: "
dispatchDebugErrorNotification(TapError.CustomError("$prefix $message"))
}
fun Store<AppState>.dispatchDebugErrorNotification(error: TapError) {
inject(DaggerGraphState::uiMessageSender).send(SnackbarMessage(stringReference(error.message ?: "debug error")))
}
fun Store<*>.dispatchDialogShow(dialog: StateDialog) {
dispatchOnMain(GlobalAction.ShowDialog(dialog))
}
fun Store<*>.dispatchDialogHide() {
dispatchOnMain(GlobalAction.HideDialog)
}
/**
* Dispatch action inside a coroutine with the Main dispatcher
*/
@ -76,14 +51,6 @@ suspend fun dispatchOnMain(vararg actions: Action) {
withMainContext { actions.forEach { store.dispatch(it) } }
}
fun Store<AppState>.dispatchOpenUrl(url: String) {
inject(DaggerGraphState::urlOpener).openUrl(url)
}
fun Store<AppState>.dispatchShare(url: String) {
inject(DaggerGraphState::shareManager).shareText(url)
}
fun Store<AppState>.dispatchNavigationAction(action: AppRouter.() -> Unit) {
inject(DaggerGraphState::appRouter).action()
}

View file

@ -1,19 +0,0 @@
package com.tangem.tap.common.extensions
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.transition.AutoTransition
import androidx.transition.Transition
import androidx.transition.TransitionManager
/**
[REDACTED_AUTHOR]
*/
fun ViewGroup.inflate(viewToInflate: Int, attachToRoot: Boolean = false): View {
return LayoutInflater.from(context).inflate(viewToInflate, this, attachToRoot)
}
fun ViewGroup.beginDelayedTransition(transition: Transition = AutoTransition()) {
TransitionManager.beginDelayedTransition(this, transition)
}

View file

@ -10,8 +10,8 @@ import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.delay
import timber.log.Timber
/**
[REDACTED_AUTHOR]
@ -31,7 +31,7 @@ suspend fun WalletManager.safeUpdate(isDemoCard: Boolean): Result<Wallet> = try
Result.Success(wallet)
}
} catch (exception: Exception) {
Timber.e(exception)
TangemLogger.e("Error", exception)
val networkConnectionManager = store.inject(DaggerGraphState::networkConnectionManager)
if (!networkConnectionManager.isOnline) {

View file

@ -9,10 +9,10 @@ import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder
import coil.memory.MemoryCache
import coil.request.CachePolicy
import coil.util.Logger
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.utils.logging.TangemLogger
import okhttp3.OkHttpClient
import timber.log.Timber
import coil.util.Logger as CoilLogger
private const val COIL_LOG_TAG = "COIL"
private const val COIL_MEMORY_CACHE_SIZE = 0.25
@ -22,7 +22,7 @@ fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageL
.apply {
if (!logEnabled) return@apply
logger(CoilTimberLogger())
logger(CoilKermitLogger())
okHttpClient {
OkHttpClient.Builder()
.addNetworkInterceptor(createNetworkLoggingInterceptor())
@ -48,14 +48,16 @@ fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageL
.build()
}
private class CoilTimberLogger : Logger {
private class CoilKermitLogger : CoilLogger {
override var level: Int = Log.DEBUG
private val logger = TangemLogger.withTag(COIL_LOG_TAG)
override fun log(tag: String, priority: Int, message: String?, throwable: Throwable?) {
with(Timber.tag(COIL_LOG_TAG)) {
if (throwable != null) e(throwable, message)
if (message != null) d(message)
if (throwable != null) {
logger.e(message ?: "<EMPTY>", throwable)
} else if (message != null) {
logger.d(message)
}
}
}

View file

@ -8,8 +8,8 @@ import co.touchlab.kermit.Logger
import co.touchlab.kermit.Severity
import com.orhanobut.logger.AndroidLogAdapter
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.BuildConfig
import timber.log.Timber
import java.util.regex.Pattern
import com.orhanobut.logger.Logger as PrettyLogger
@ -30,18 +30,9 @@ class TangemAppLoggerInitializer(
PrettyLogger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy()))
}
Timber.plant(tree = createTimberTree())
Logger.setLogWriters(KermitLogWriter(::finalLogOutput))
}
private fun createTimberTree(): Timber.Tree {
return object : Timber.DebugTree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
finalLogOutput(priority = priority, tag = tag, message = message, t = t)
}
}
}
private fun finalLogOutput(priority: Int, tag: String?, message: String, t: Throwable?) {
if (IS_LOG_ENABLED) {
PrettyLogger.log(priority, tag, message, t)
@ -71,6 +62,8 @@ private class KermitLogWriter(
KermitLogWriter::class.java.name,
BaseLogger::class.java.name,
Logger::class.java.name,
TangemLogger::class.java.name,
TangemLogger.TaggedLogger::class.java.name,
)
override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) {
@ -87,7 +80,7 @@ private class KermitLogWriter(
tag
} else {
/**
* like in [Timber.DebugTree.tag]
* like in [Logger.debugTree.tag]
*/
@Suppress("UnnecessaryLet", "ThrowingExceptionsWithoutMessageOrCause")
Throwable().stackTrace
@ -99,7 +92,7 @@ private class KermitLogWriter(
}
/**
* copy from [Timber.DebugTree.createStackElementTag]
* copy from [Logger.debugTree.createStackElementTag]
*/
@Suppress("MagicNumber")
private fun createStackElementTag(element: StackTraceElement): String? {
@ -120,7 +113,7 @@ private class KermitLogWriter(
private const val KERMIT_LOGGER_DEFAULT_TAG = ""
/**
* copy from [Timber.DebugTree.Companion]
* copy from [Logger.debugTree.Companion]
*/
private const val MAX_TAG_LENGTH = 23
private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$")

View file

@ -4,9 +4,9 @@ import android.annotation.SuppressLint
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.tangem.tap.common.analytics.CustomerIoFeatureToggles
import com.tangem.utils.logging.TangemLogger
import dagger.hilt.android.AndroidEntryPoint
import io.customer.messagingpush.CustomerIOFirebaseMessagingService
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
@ -22,7 +22,7 @@ internal class TangemPushNotificationService : FirebaseMessagingService() {
override fun onNewToken(token: String) {
super.onNewToken(token)
Timber.d("New FCM token received: $token")
TangemLogger.d("New FCM token received: $token")
if (customerIoFeatureToggles.isFeatureEnabled) {
CustomerIOFirebaseMessagingService.onNewToken(applicationContext, token)

View file

@ -1,41 +0,0 @@
package com.tangem.tap.common.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.redux.StateDialog
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
sealed class AppDialog : StateDialog {
data class SimpleOkDialogRes(
val headerId: Int,
val messageId: Int,
val args: List<String> = emptyList(),
val onOk: VoidCallback? = null,
) : AppDialog()
data class RemoveWalletDialog(
val currencyTitle: String,
val onOk: () -> Unit,
) : AppDialog() {
val messageRes: Int = R.string.token_details_hide_alert_message
val titleRes: Int = R.string.token_details_hide_alert_title
val primaryButtonRes: Int = R.string.token_details_hide_alert_hide
}
data class TokensAreLinkedDialog(
val currencyTitle: String,
val currencySymbol: String,
val networkName: String,
) : AppDialog() {
val messageRes: Int = R.string.token_details_unable_hide_alert_message
val titleRes: Int = R.string.token_details_unable_hide_alert_title
}
data class WalletAlreadyWasUsedDialog(
val onOk: () -> Unit,
val onSupportClick: () -> Unit,
val onCancel: () -> Unit,
) : AppDialog()
}

View file

@ -5,7 +5,6 @@ import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.legacy.LegacyMiddleware
import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
import com.tangem.tap.proxy.redux.DaggerGraphMiddleware
import com.tangem.tap.proxy.redux.DaggerGraphState
import org.rekotlin.Middleware
@ -23,7 +22,6 @@ data class AppState(
logMiddleware,
GlobalMiddleware.handler,
DetailsMiddleware().detailsMiddleware,
BackupMiddleware().backupMiddleware,
LockUserWalletsTimerMiddleware().middleware,
AccessCodeRequestPolicyMiddleware().middleware,
DaggerGraphMiddleware.daggerGraphMiddleware,

View file

@ -1,7 +1,7 @@
package com.tangem.tap.common.redux
import com.tangem.utils.logging.TangemLogger
import org.rekotlin.Middleware
import timber.log.Timber
/**
[REDACTED_AUTHOR]
@ -9,7 +9,7 @@ import timber.log.Timber
val logMiddleware: Middleware<AppState> = { _, _ ->
{ nextDispatch ->
{ action ->
Timber.i("Dispatch action: ${action::class.java.simpleName}")
TangemLogger.i("Dispatch action: ${action::class.java.simpleName}")
nextDispatch(action)
}
}

View file

@ -1,28 +1,11 @@
package com.tangem.tap.common.redux.global
import com.tangem.common.CompletionResult
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import org.rekotlin.Action
sealed class GlobalAction : Action {
// dialogs
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
object HideDialog : GlobalAction()
object ScanFailsCounter {
data class ChooseBehavior(
val result: CompletionResult<ScanResponse>,
val analyticsSource: AnalyticsParam.ScreensSources,
) : GlobalAction()
object Reset : GlobalAction()
object Increment : GlobalAction()
}
data class SaveScanResponse(val scanResponse: ScanResponse) : GlobalAction()
data class ChangeAppCurrency(val appCurrency: AppCurrency) : GlobalAction()

View file

@ -1,12 +1,6 @@
package com.tangem.tap.common.redux.global
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
@ -31,41 +25,12 @@ private val globalMiddlewareHandler: Middleware<AppState> = { _, _ ->
}
}
@Suppress("LongMethod", "ComplexMethod")
private fun handleAction(action: Action) {
when (action) {
is GlobalAction.ScanFailsCounter.ChooseBehavior -> {
when (action.result) {
is CompletionResult.Success -> store.dispatch(GlobalAction.ScanFailsCounter.Reset)
is CompletionResult.Failure -> {
handleFailureChooseBehaviour(action.result, action.analyticsSource)
}
}
}
is GlobalAction.RestoreAppCurrency -> restoreAppCurrency()
}
}
private fun handleFailureChooseBehaviour(
result: CompletionResult.Failure<ScanResponse>,
analyticsSource: AnalyticsParam.ScreensSources,
) {
if (result.error is TangemSdkError.UserCancelled) {
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
if (store.state.globalState.scanCardFailsCounter >= 2) {
val scanFailsSource = when (analyticsSource) {
is AnalyticsParam.ScreensSources.SignIn -> StateDialog.ScanFailsSource.SIGN_IN
is AnalyticsParam.ScreensSources.Settings -> StateDialog.ScanFailsSource.SETTINGS
is AnalyticsParam.ScreensSources.Intro -> StateDialog.ScanFailsSource.INTRO
else -> StateDialog.ScanFailsSource.MAIN
}
store.dispatchDialogShow(StateDialog.ScanFailsDialog(scanFailsSource))
}
} else {
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
}
}
private fun restoreAppCurrency() {
scope.launch {
val currency = store.inject(DaggerGraphState::appCurrencyRepository)

View file

@ -10,12 +10,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
val globalState = state.globalState
return when (action) {
is GlobalAction.ScanFailsCounter.Increment -> {
globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1)
}
is GlobalAction.ScanFailsCounter.Reset -> {
globalState.copy(scanCardFailsCounter = 0)
}
is GlobalAction.SaveScanResponse -> {
globalState.copy(scanResponse = action.scanResponse)
}
@ -26,12 +20,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
globalState.copy(appCurrency = action.appCurrency)
}
is GlobalAction.IsSignWithRing -> globalState.copy(isLastSignWithRing = action.isSignWithRing)
is GlobalAction.ShowDialog -> {
globalState.copy(dialog = action.stateDialog)
}
is GlobalAction.HideDialog -> {
globalState.copy(dialog = null)
}
else -> globalState
}
}

View file

@ -2,24 +2,15 @@ package com.tangem.tap.common.redux.global
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.domain.TapWalletManager
import org.rekotlin.StateType
data class GlobalState(
@Deprecated("Use scan response from selected user wallet")
val scanResponse: ScanResponse? = null,
val onboardingState: OnboardingState = OnboardingState(),
val tapWalletManager: TapWalletManager = TapWalletManager(),
val appCurrency: AppCurrency = AppCurrency.Default,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val isLastSignWithRing: Boolean = false,
) : StateType
typealias CryptoCurrencyName = String
data class OnboardingState(
val isOnboardingStarted: Boolean = false,
val shouldResetOnCreate: Boolean = false,
)
typealias CryptoCurrencyName = String

View file

@ -1,13 +0,0 @@
package com.tangem.tap.common.toggleWidget
import android.view.View
/**
[REDACTED_AUTHOR]
*/
interface WidgetState
interface ViewStateWidget {
val mainView: View
fun changeState(state: WidgetState)
}

View file

@ -1,24 +0,0 @@
package com.tangem.tap.common.transitions
import androidx.transition.ChangeBounds
import androidx.transition.ChangeTransform
import androidx.transition.Fade
import androidx.transition.TransitionSet
class HomeToOnboardingTransition : TransitionSet() {
init {
ordering = ORDERING_TOGETHER
addTransition(Fade())
addTransition(ChangeTransform())
addTransition(ChangeBounds())
}
}
class InternalNoteLayoutTransition : TransitionSet() {
init {
ordering = ORDERING_TOGETHER
addTransition(ChangeTransform())
addTransition(ChangeBounds())
addTransition(Fade())
}
}

View file

@ -1,84 +0,0 @@
package com.tangem.tap.common.ui
import android.content.Context
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.compose.ui.text.intl.Locale
import androidx.core.view.isVisible
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.coroutines.launch
/**
[REDACTED_AUTHOR]
*/
internal object ScanFailsDialog {
private const val HOW_TO_SCAN_RU_LINK = "https://tangem.com/ru/blog/post/scan-tangem-card/"
private const val HOW_TO_SCAN_LINK = "https://tangem.com/en/blog/post/scan-tangem-card/"
private const val RUSSIA_LOCALE = "ru"
fun create(context: Context, source: StateDialog.ScanFailsSource, onTryAgain: (() -> Unit)? = null): AlertDialog {
return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply {
val customView = View.inflate(context, R.layout.dialog_scan_fails, null)
val sourceAnalytics = when (source) {
StateDialog.ScanFailsSource.MAIN -> AnalyticsParam.ScreensSources.Main
StateDialog.ScanFailsSource.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn
StateDialog.ScanFailsSource.SETTINGS -> AnalyticsParam.ScreensSources.Settings
StateDialog.ScanFailsSource.INTRO -> AnalyticsParam.ScreensSources.Intro
}
val tryAgainBtn: TextView? = customView.findViewById(R.id.try_again_button)
if (onTryAgain != null) {
tryAgainBtn?.isVisible = true
tryAgainBtn?.setOnClickListener {
store.dispatchDialogHide()
Analytics.send(
ScanFailsDialogAnalytics(
button = ScanFailsDialogAnalytics.Buttons.TRY_AGAIN,
source = sourceAnalytics,
),
)
onTryAgain()
}
} else {
tryAgainBtn?.isVisible = false
}
customView.findViewById<TextView>(R.id.how_to_scan_button)?.setOnClickListener {
Analytics.send(
ScanFailsDialogAnalytics(
button = ScanFailsDialogAnalytics.Buttons.HOW_TO_SCAN,
source = sourceAnalytics,
),
)
val locale = Locale.current.region
val link = if (locale.lowercase() == RUSSIA_LOCALE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK
store.dispatchOpenUrl(link)
}
customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener {
Analytics.send(Basic.ButtonSupport(sourceAnalytics))
scope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
.invoke(type = FeedbackEmailType.ScanningProblem)
}
}
customView.findViewById<TextView>(R.id.cancel_button)?.setOnClickListener {
store.dispatchDialogHide()
}
setView(customView)
setOnDismissListener { store.dispatchDialogHide() }
}.create()
}
}

View file

@ -1,55 +0,0 @@
package com.tangem.tap.common.ui
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.wallet.R
object SimpleAlertDialog {
fun create(
titleRes: Int? = null,
messageRes: Int? = null,
title: String? = null,
message: String? = null,
primaryButtonRes: Int = R.string.common_ok,
context: Context,
): AlertDialog {
return SimpleCancelableAlertDialog.create(
titleRes = titleRes,
messageRes = messageRes,
title = title,
message = message,
primaryButtonRes = primaryButtonRes,
secondaryButtonRes = null,
context = context,
)
}
}
object SimpleCancelableAlertDialog {
fun create(
titleRes: Int? = null,
messageRes: Int? = null,
title: String? = null,
message: String? = null,
primaryButtonRes: Int = R.string.common_ok,
secondaryButtonRes: Int? = R.string.common_cancel,
primaryButtonAction: () -> Unit = {},
secondaryButtonAction: () -> Unit = {},
context: Context,
): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(if (titleRes != null) context.getString(titleRes) else title)
setMessage(if (messageRes != null) context.getString(messageRes) else message)
setPositiveButton(context.getText(primaryButtonRes)) { _, _ -> primaryButtonAction() }
if (secondaryButtonRes != null) {
setNegativeButton(context.getText(secondaryButtonRes)) { _, _ -> secondaryButtonAction() }
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -1,31 +0,0 @@
package com.tangem.tap.common.ui
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
object SimpleOkDialog {
fun create(dialog: AppDialog.SimpleOkDialogRes, context: Context): AlertDialog {
val message = if (dialog.args.isEmpty()) {
context.getString(dialog.messageId)
} else {
context.getString(dialog.messageId, *dialog.args.toTypedArray())
}
return AlertDialog.Builder(context).apply {
setTitle(context.getString(dialog.headerId))
setMessage(message)
setPositiveButton(R.string.common_ok) { _, _ -> }
setOnDismissListener {
store.dispatchDialogHide()
dialog.onOk?.invoke()
}
}.create()
}
}

View file

@ -14,8 +14,8 @@ import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.getColorCompat
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.withForegroundActivity
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.R
import timber.log.Timber
internal class CustomTabsUrlOpener : UrlOpener {
@ -55,7 +55,7 @@ internal class CustomTabsUrlOpener : UrlOpener {
customTabsIntent.launchUrl(context, url.toUri())
}
}.onFailure {
Timber.e(it)
TangemLogger.e("Error", it)
}
}

View file

@ -1,10 +1,10 @@
package com.tangem.tap.core
import co.touchlab.kermit.Logger
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.SupervisorJob
@ -28,7 +28,7 @@ internal class DefaultAppCoroutineScope @Inject constructor(
}
private fun logError(throwable: Throwable, coroutineName: String) {
Logger.withTag(tag).e(
TangemLogger.withTag(tag).e(
messageString = "CoroutineName $coroutineName",
throwable = throwable,
)

View file

@ -8,7 +8,7 @@ import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.tangem.core.navigation.email.EmailSender
import com.tangem.tap.foregroundActivityObserver
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
/**
* Implementation of email sender for Android
@ -21,7 +21,7 @@ internal class AndroidEmailSender : EmailSender {
val activity = foregroundActivityObserver.foregroundActivity
if (activity == null) {
Timber.e("Foreground activity not found")
TangemLogger.e("Foreground activity not found")
return
}
@ -50,7 +50,7 @@ internal class AndroidEmailSender : EmailSender {
ContextCompat.startActivity(activity, chooserIntent, null)
} catch (ex: Exception) {
Timber.e("Failed to send email: $ex")
TangemLogger.e("Failed to send email: $ex")
}
}

View file

@ -1,8 +1,9 @@
package com.tangem.tap.core.security
import android.os.Build
import com.dexprotector.rtc.RtcStatus
import com.tangem.security.DeviceSecurityInfoProvider
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider {
override val isRooted: Boolean
@ -12,12 +13,86 @@ internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider {
override val isXposed: Boolean
get() = getRtcStatusSafely()?.xposed == true
override val isVulnerableToMediaTekExploit: Boolean by lazy {
val isAffected by lazy { isAffectedMediaTekDevice() }
val isPatched by lazy { hasSecurityPatch() }
val isVulnerable = isAffected && !isPatched
TangemLogger.i(
"CVE-2026-20435 check: isAffectedMediaTek=$isAffected, " +
"isPatched=$isPatched, isVulnerable=$isVulnerable",
)
isVulnerable
}
private fun isAffectedMediaTekDevice(): Boolean {
val socModel = resolveMediaTekSocModel()
val isAffected = socModel != null && socModel in AFFECTED_MEDIATEK_SOCS
TangemLogger.i("CVE-2026-20435 SoC result: model=$socModel, isAffected=$isAffected")
return isAffected
}
private fun resolveMediaTekSocModel(): String? {
// Layer 1: API 31+ provides direct SoC info (public API, most reliable)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val manufacturer = Build.SOC_MANUFACTURER
val model = Build.SOC_MODEL
TangemLogger.i("CVE-2026-20435 Layer 1: SOC_MANUFACTURER=$manufacturer, SOC_MODEL=$model")
if (manufacturer.equals("MediaTek", ignoreCase = true)) {
extractSocModel(model)?.let { return it }
}
}
// Layer 2: Build.HARDWARE often contains "mtXXXX" on MediaTek devices (public API)
val hardware = Build.HARDWARE
TangemLogger.i("CVE-2026-20435 Layer 2: HARDWARE=$hardware")
extractSocModel(hardware)?.let { return it }
return null
}
private fun extractSocModel(value: String): String? {
val match = MEDIATEK_SOC_PATTERN.find(value.uppercase()) ?: return null
return match.value
}
private fun hasSecurityPatch(): Boolean {
val patch = Build.VERSION.SECURITY_PATCH
val isPatched = try {
patch >= MEDIATEK_CVE_FIX_PATCH_LEVEL
} catch (e: Exception) {
TangemLogger.w("CVE-2026-20435 patch check: failed to parse SECURITY_PATCH=$patch", e)
false // fail-safe: treat unknown patch level as unpatched
}
TangemLogger.i(
"CVE-2026-20435 patch check: SECURITY_PATCH=$patch, " +
"required=$MEDIATEK_CVE_FIX_PATCH_LEVEL, isPatched=$isPatched",
)
return isPatched
}
private fun getRtcStatusSafely(): RtcStatus? {
return try {
RtcStatus.getRtcStatus()
} catch (e: Throwable) {
Timber.e(e)
TangemLogger.e("Error", e)
null
}
}
private companion object {
/** Android security patch level that includes the fix for CVE-2026-20435 */
const val MEDIATEK_CVE_FIX_PATCH_LEVEL = "2026-03-05"
/** Regex to extract MediaTek SoC model number (e.g., MT6789) */
val MEDIATEK_SOC_PATTERN = Regex("MT\\d{4}")
/** Affected MediaTek SoC models per Ledger Donjon disclosure */
val AFFECTED_MEDIATEK_SOCS = setOf(
"MT6739", "MT6761", "MT6765", "MT6768", "MT6781",
"MT6789", "MT6813", "MT6833", "MT6853", "MT6855",
"MT6877", "MT6878", "MT6879", "MT6880", "MT6885",
"MT6886", "MT6890", "MT6893", "MT6895", "MT6897",
"MT6983", "MT6985", "MT6989", "MT6990", "MT6993",
)
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.tap.data
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.datasource.local.logs.AppLogsStore
import timber.log.Timber
import com.tangem.utils.logging.TangemLogger
/**
* BlockchainSDK logger implementation
@ -16,7 +16,7 @@ internal class TangemBlockchainSDKLogger(
) : BlockchainSDKLogger {
override fun log(level: BlockchainSDKLogger.Level, message: String) {
Timber.d(message)
TangemLogger.d(message)
appLogsStore.saveLogMessage(tag = "BlockchainSDK_${level.name}", message)
}
}

View file

@ -1,8 +1,8 @@
package com.tangem.tap.di
import android.content.Context
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.LinkHandler
import com.tangem.tap.common.deeplink.DefaultDeeplinkLauncher
import com.tangem.core.navigation.deeplink.DeeplinkLauncher
import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.navigation.share.ShareManager
@ -49,6 +49,7 @@ internal interface UtilsModule {
@Provides
@Singleton
fun provideLinkHandler(appRouter: AppRouter): LinkHandler = LinkHandler(appRouter)
fun provideDeeplinkLauncher(@ApplicationContext context: Context, urlOpener: UrlOpener): DeeplinkLauncher =
DefaultDeeplinkLauncher(context, urlOpener)
}
}

View file

@ -4,7 +4,7 @@ import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.tokensync.repository.TokenSyncRepository
import com.tangem.domain.tokensync.usecase.AcknowledgeTokenSyncCompletionUseCase
import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase
import com.tangem.domain.tokensync.usecase.SyncTokensUseCase
import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase
import com.tangem.utils.coroutines.AppCoroutineScope
import dagger.Module
import dagger.Provides
@ -36,12 +36,12 @@ internal object TokenSyncDomainModule {
@Provides
@Singleton
fun provideSyncTokensUseCase(
fun provideStartTokenSyncUseCase(
tokenSyncRepository: TokenSyncRepository,
manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
appCoroutineScope: AppCoroutineScope,
): SyncTokensUseCase {
return SyncTokensUseCase(
): StartTokenSyncUseCase {
return StartTokenSyncUseCase(
tokenSyncRepository = tokenSyncRepository,
manageCryptoCurrenciesUseCase = manageCryptoCurrenciesUseCase,
appCoroutineScope = appCoroutineScope,

View file

@ -4,7 +4,6 @@ import com.tangem.data.wallets.hot.TangemHotWalletSigner
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
@ -12,13 +11,11 @@ import com.tangem.domain.notifications.repository.PushNotificationsRepository
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.GaslessTransactionRepository
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.WalletAddressServiceRepository
import com.tangem.domain.transaction.*
import com.tangem.domain.transaction.usecase.*
import com.tangem.domain.transaction.usecase.gasless.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.AppCoroutineScope
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -175,8 +172,14 @@ internal object TransactionDomainModule {
@Provides
@Singleton
fun provideGetAllowanceUseCase(transactionRepository: TransactionRepository): GetAllowanceUseCase {
return GetAllowanceUseCase(transactionRepository)
fun provideGetAllowanceUseCase(allowanceRepository: AllowanceRepository): GetAllowanceUseCase {
return GetAllowanceUseCase(allowanceRepository)
}
@Provides
@Singleton
fun provideGetAllowanceInfoUseCase(allowanceRepository: AllowanceRepository): GetAllowanceInfoUseCase {
return GetAllowanceInfoUseCase(allowanceRepository)
}
@Provides

View file

@ -222,12 +222,6 @@ internal object WalletsDomainModule {
)
}
@Provides
@Singleton
fun providesSeedPhraseNotificationUseCase(walletsRepository: WalletsRepository): SeedPhraseNotificationUseCase {
return SeedPhraseNotificationUseCase(walletsRepository = walletsRepository)
}
@Provides
@Singleton
fun provideGetCardImageUseCase(cardArtworksProvider: CardArtworksProvider): GetCardImageUseCase {

View file

@ -1,6 +1,5 @@
package com.tangem.tap.domain.model
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.blockchain.common.Blockchain as SdkBlockchain
import com.tangem.blockchain.common.Token as SdkToken
@ -29,20 +28,4 @@ sealed interface Currency {
) : Currency {
override val currencySymbol: CryptoCurrencyName = blockchain.currency
}
companion object {
fun fromCustomCurrency(customCurrency: CustomCurrency): Currency {
return when (customCurrency) {
is CustomCurrency.CustomBlockchain -> Blockchain(
blockchain = customCurrency.network,
derivationPath = customCurrency.derivationPath?.rawPath,
)
is CustomCurrency.CustomToken -> Token(
token = customCurrency.token,
blockchain = customCurrency.network,
derivationPath = customCurrency.derivationPath?.rawPath,
)
}
}
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.tap.domain.scanCard
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.card.ScanFailsCounter
import com.tangem.domain.card.ScanFailsRequester
import com.tangem.utils.coroutines.AppCoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class DefaultScanFailsCounter @Inject constructor(
private val scanFailsRequester: ScanFailsRequester,
private val appScope: AppCoroutineScope,
) : ScanFailsCounter {
private var counter: Int = 0
override fun reset() {
counter = 0
}
override fun onScanFailure(isUserCancelled: Boolean, source: AnalyticsParam.ScreensSources) {
if (isUserCancelled) {
counter++
if (counter >= THRESHOLD) {
appScope.launch { scanFailsRequester.show(source) }
}
} else {
counter = 0
}
}
private companion object {
const val THRESHOLD = 2
}
}

View file

@ -19,6 +19,7 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.domain.card.ScanFailsCounter
import com.tangem.domain.card.common.util.twinsIsTwinned
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.feedback.models.FeedbackEmailType
@ -26,9 +27,7 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.sdk.extensions.localizedDescriptionRes
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.mainScope
@ -49,6 +48,7 @@ internal class LegacyScanProcessor @Inject constructor(
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
private val analyticsEventHandler: AnalyticsEventHandler,
private val trackingContextProxy: TrackingContextProxy,
private val scanFailsCounter: ScanFailsCounter,
) {
suspend fun scan(
@ -89,10 +89,13 @@ internal class LegacyScanProcessor @Inject constructor(
)
val analyticsEvent = Basic.CardWasScanned(analyticsSource)
store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result, analyticsSource))
result
.doOnFailure { error ->
scanFailsCounter.onScanFailure(
isUserCancelled = error is TangemSdkError.UserCancelled,
source = analyticsSource,
)
onScanFailure(
analyticsSource = analyticsSource,
error = error,
@ -106,6 +109,7 @@ internal class LegacyScanProcessor @Inject constructor(
)
}
.doOnSuccess { scanResponse ->
scanFailsCounter.reset()
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
sendAnalytics(analyticsEvent, scanResponse)

View file

@ -10,17 +10,17 @@ import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.scanCard.chains.*
import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
internal object UseCaseScanProcessor {
private val scanCardExceptionConverter = ScanCardExceptionConverter()
@ -57,7 +57,7 @@ internal object UseCaseScanProcessor {
val chains = buildList {
add(
FailedScansCounterChain(
{ showMaxUnsuccessfulScansReachedDialog(analyticsSource) },
{ showScanFailsDialog(analyticsSource) },
),
)
add(AnalyticsChain(Basic.CardWasScanned(analyticsSource)))
@ -71,14 +71,10 @@ internal object UseCaseScanProcessor {
)
}
private fun showMaxUnsuccessfulScansReachedDialog(source: AnalyticsParam.ScreensSources) {
val scanFailsSource = when (source) {
is AnalyticsParam.ScreensSources.SignIn -> StateDialog.ScanFailsSource.SIGN_IN
is AnalyticsParam.ScreensSources.Settings -> StateDialog.ScanFailsSource.SETTINGS
is AnalyticsParam.ScreensSources.Intro -> StateDialog.ScanFailsSource.INTRO
else -> StateDialog.ScanFailsSource.MAIN
private fun showScanFailsDialog(source: AnalyticsParam.ScreensSources) {
scope.launch {
store.inject(DaggerGraphState::scanFailsRequester).show(source)
}
store.dispatchDialogShow(StateDialog.ScanFailsDialog(scanFailsSource))
}
private suspend fun proceedWithException(

View file

@ -19,10 +19,10 @@ import com.tangem.datasource.local.visa.VisaOtpData
import com.tangem.datasource.local.visa.hasSavedOTP
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.operations.GenerateOTPCommand
import com.tangem.operations.attestation.AttestCardKeyCommand
import com.tangem.operations.pins.SetUserCodeCommand
@ -31,11 +31,11 @@ import com.tangem.operations.sign.SignHashResponse
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.sdk.api.visa.VisaCardActivationResponse
import com.tangem.sdk.api.visa.VisaCardActivationTaskMode
import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.*
import timber.log.Timber
import kotlin.coroutines.resume
import kotlin.time.measureTimedValue
@ -94,7 +94,7 @@ class VisaCardActivationTask @AssistedInject constructor(
}
}
}
Timber.i("VisaCardActivationTask all time: ${timedResult.duration}")
TangemLogger.i("VisaCardActivationTask all time: ${timedResult.duration}")
return timedResult.value
}
@ -109,11 +109,11 @@ class VisaCardActivationTask @AssistedInject constructor(
}
}
}
Timber.i("AttestCardKeyCommand time: ${timedResult.duration}")
TangemLogger.i("AttestCardKeyCommand time: ${timedResult.duration}")
return when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("AttestCardKeyCommand success")
TangemLogger.i("AttestCardKeyCommand success")
processSignedAuthorizationChallenge(
signedChallenge = challengeToSign.toSignedChallenge(
signedChallenge = result.data.cardSignature.toHexString(),
@ -122,7 +122,7 @@ class VisaCardActivationTask @AssistedInject constructor(
)
}
is CompletionResult.Failure -> {
Timber.e("AttestCardKeyCommand failure ${result.error}")
TangemLogger.e("AttestCardKeyCommand failure ${result.error}")
CompletionResult.Failure(result.error)
}
}
@ -206,15 +206,15 @@ class VisaCardActivationTask @AssistedInject constructor(
}
}
Timber.i("CreateWalletTask time: ${timedResult.duration}")
TangemLogger.i("CreateWalletTask time: ${timedResult.duration}")
when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("CreateWalletTask success")
TangemLogger.i("CreateWalletTask success")
CompletionResult.Success(Unit)
}
is CompletionResult.Failure -> {
Timber.e("CreateWalletTask failure ${result.error}")
TangemLogger.e("CreateWalletTask failure ${result.error}")
CompletionResult.Failure(result.error)
}
}
@ -237,11 +237,11 @@ class VisaCardActivationTask @AssistedInject constructor(
}
}
Timber.i("GenerateOTPCommand time: ${timedResult.duration}")
TangemLogger.i("GenerateOTPCommand time: ${timedResult.duration}")
return when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("GenerateOTPCommand success")
TangemLogger.i("GenerateOTPCommand success")
otpStorage.saveOTP(
cardId = cardId,
data = VisaOtpData(result.data.rootOTP, result.data.rootOTPCounter),
@ -249,7 +249,7 @@ class VisaCardActivationTask @AssistedInject constructor(
CompletionResult.Success(Unit)
}
is CompletionResult.Failure -> {
Timber.e("GenerateOTPCommand failure ${result.error}")
TangemLogger.e("GenerateOTPCommand failure ${result.error}")
CompletionResult.Failure(result.error)
}
}
@ -278,11 +278,11 @@ class VisaCardActivationTask @AssistedInject constructor(
}
}
Timber.i("SignHashCommand time: ${timedResult.duration}")
TangemLogger.i("SignHashCommand time: ${timedResult.duration}")
return when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("SignHashCommand success")
TangemLogger.i("SignHashCommand success")
handleSignedData(
dataToSign = dataToSign,
response = result.data,
@ -290,7 +290,7 @@ class VisaCardActivationTask @AssistedInject constructor(
)
}
is CompletionResult.Failure -> {
Timber.e("SignHashCommand failure ${result.error}")
TangemLogger.e("SignHashCommand failure ${result.error}")
CompletionResult.Failure(result.error)
}
}
@ -336,7 +336,7 @@ class VisaCardActivationTask @AssistedInject constructor(
return CompletionResult.Success(Unit)
}
Timber.i("Setting access code")
TangemLogger.i("Setting access code")
val task = SetUserCodeCommand.changeAccessCode(mode.accessCode)
@ -348,15 +348,15 @@ class VisaCardActivationTask @AssistedInject constructor(
}
}
Timber.i("SetUserCodeCommand time: ${timedResult.duration}")
TangemLogger.i("SetUserCodeCommand time: ${timedResult.duration}")
return when (val result = timedResult.value) {
is CompletionResult.Success -> {
Timber.i("SetUserCodeCommand success")
TangemLogger.i("SetUserCodeCommand success")
CompletionResult.Success(Unit)
}
is CompletionResult.Failure -> {
Timber.i("SetUserCodeCommand failure ${result.error}")
TangemLogger.i("SetUserCodeCommand failure ${result.error}")
CompletionResult.Failure(result.error)
}
}

View file

@ -1,13 +1,9 @@
package com.tangem.tap.domain.tokens
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.tokens.TokensFeatureToggles
@Suppress("UnusedPrivateProperty")
internal class DefaultTokensFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : TokensFeatureToggles {
override val isMultiAddressUtxoEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.MULTI_ADDRESS_UTXO_ENABLED)
}
) : TokensFeatureToggles

View file

@ -41,7 +41,7 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object UserWalletsListManagerModule {
internal object UserWalletsListRepositoryModule {
@Provides
@Singleton

View file

@ -14,9 +14,9 @@ import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey
import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation
import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository
import com.tangem.tap.domain.userWalletList.utils.sensitiveInformation
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.crypto.spec.SecretKeySpec
internal class DefaultUserWalletsSensitiveInformationRepository(
@ -123,7 +123,7 @@ internal class DefaultUserWalletsSensitiveInformationRepository(
this@decodeToSensitiveInformation.decodeToString(throwOnInvalidSequence = true),
)
} catch (e: CharacterCodingException) {
Timber.e(e, "Unable to decode sensitive information")
TangemLogger.e("Unable to decode sensitive information", e)
null
}

View file

@ -10,19 +10,18 @@ import com.tangem.common.extensions.toHexString
import com.tangem.core.error.ext.tangemError
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.error.VisaCardScanError
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.operations.attestation.AttestCardKeyCommand
import com.tangem.operations.attestation.AttestCardKeyResponse
import com.tangem.operations.attestation.AttestWalletKeyResponse
import com.tangem.operations.attestation.AttestWalletKeyTask
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.suspendCancellableCoroutine
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.resume
@ -38,10 +37,10 @@ internal class VisaCardScanHandler @Inject constructor(
)
suspend fun handleVisaCardScan(session: CardSession): CompletionResult<VisaCardActivationStatus> {
Timber.i("Attempting to handle Visa card scan")
TangemLogger.i("Attempting to handle Visa card scan")
val card = session.environment.card ?: run {
Timber.e("Card is null")
TangemLogger.e("Card is null")
return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
}
@ -68,12 +67,12 @@ internal class VisaCardScanHandler @Inject constructor(
}
private suspend fun SessionContext.handleWalletAuthorization(): CompletionResult<VisaCardActivationStatus> {
Timber.i("Started handling authorization using Visa wallet")
TangemLogger.i("Started handling authorization using Visa wallet")
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run {
Timber.e("Failed to find extended public key while handling wallet authorization")
TangemLogger.e("Failed to find extended public key while handling wallet authorization")
return CompletionResult.Failure(VisaCardScanError.FailedToFindWallet.tangemError)
}
@ -82,14 +81,14 @@ internal class VisaCardScanHandler @Inject constructor(
return CompletionResult.Failure(it.tangemError)
}
Timber.i("Requesting challenge for wallet authorization")
TangemLogger.i("Requesting challenge for wallet authorization")
val challengeResponse = visaAuthRemoteDataSource.getCardWalletAuthChallenge(
cardId = card.cardId,
// This is the wallet public key, not the address and it's alright, as the API expects it in this format
cardWalletAddress = wallet.publicKey.toHexString(),
).getOrElse { error ->
Timber.i("Failed to get Access token for Wallet public key authorization")
TangemLogger.i("Failed to get Access token for Wallet public key authorization")
return CompletionResult.Failure(error.tangemError)
}
@ -103,7 +102,7 @@ internal class VisaCardScanHandler @Inject constructor(
val signature = signChallengeResult.data.walletSignature
val salt = signChallengeResult.data.salt
Timber.i("Challenge signed with Wallet public key")
TangemLogger.i("Challenge signed with Wallet public key")
handleWalletAuthorizationTokens(
cardWalletAddress = walletAddress.value,
signedChallenge = challengeResponse.toSignedChallenge(
@ -113,7 +112,9 @@ internal class VisaCardScanHandler @Inject constructor(
)
}
is CompletionResult.Failure -> {
Timber.e("Error during Wallet authorization process. Tangem Sdk Error: ${signChallengeResult.error}")
TangemLogger.e(
"Error during Wallet authorization process. Tangem Sdk Error: ${signChallengeResult.error}",
)
CompletionResult.Failure(signChallengeResult.error)
}
}
@ -125,19 +126,19 @@ internal class VisaCardScanHandler @Inject constructor(
): CompletionResult<VisaCardActivationStatus> {
val authorizationTokensResponse = visaAuthRemoteDataSource.getAccessTokens(signedChallenge = signedChallenge)
.getOrElse { error ->
Timber.i("Failed to get Access token for Wallet public key authorization.")
TangemLogger.i("Failed to get Access token for Wallet public key authorization.")
return if (
error is VisaApiError.ProductInstanceIsNotActivated ||
error is VisaApiError.ProductInstanceNotFoundActivationRequired
) {
Timber.i("Proceeding with card authorization.")
TangemLogger.i("Proceeding with card authorization.")
handleCardAuthorization(cardWalletAddress = cardWalletAddress)
} else {
CompletionResult.Failure(error.tangemError)
}
}
Timber.i("Authorized using Wallet public key successfully")
TangemLogger.i("Authorized using Wallet public key successfully")
return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse))
}
@ -148,27 +149,27 @@ internal class VisaCardScanHandler @Inject constructor(
): CompletionResult<VisaCardActivationStatus> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
Timber.i("Requesting authorization challenge to sign")
TangemLogger.i("Requesting authorization challenge to sign")
val challengeResponse = visaAuthRemoteDataSource.getCardAuthChallenge(
cardId = card.cardId,
cardPublicKey = card.cardPublicKey.toHexString(),
).getOrElse { error ->
Timber.e("Failed to get challenge for Card authorization. Plain error: ${error.errorCode}")
TangemLogger.e("Failed to get challenge for Card authorization. Plain error: ${error.errorCode}")
return CompletionResult.Failure(error.tangemError)
}
Timber.i("Received challenge to sign: ${challengeResponse.challenge}")
TangemLogger.i("Received challenge to sign: ${challengeResponse.challenge}")
val signChallengeResult = signChallengeWithCard(challenge = challengeResponse.challenge)
val attestCardKeyResponse = when (signChallengeResult) {
is CompletionResult.Success -> {
Timber.i("Challenged signed.")
TangemLogger.i("Challenged signed.")
signChallengeResult.data
}
is CompletionResult.Failure -> {
Timber.e(
TangemLogger.e(
"Failed to sign challenge with Card public key. Tangem Sdk Error: ${signChallengeResult.error}",
)
return CompletionResult.Failure(signChallengeResult.error)
@ -181,7 +182,7 @@ internal class VisaCardScanHandler @Inject constructor(
salt = attestCardKeyResponse.salt.toHexString(),
),
).getOrElse { error ->
Timber.e("Failed to sign challenge with Card public key. Plain error: ${error.errorCode}")
TangemLogger.e("Failed to sign challenge with Card public key. Plain error: ${error.errorCode}")
return CompletionResult.Failure(error.tangemError)
}
@ -191,7 +192,7 @@ internal class VisaCardScanHandler @Inject constructor(
)
val activationRemoteState = visaActivationRepository.getActivationRemoteState().getOrElse { error ->
Timber.e("Failed to sign challenge with Card public key. Plain error: ${error.errorCode}")
TangemLogger.e("Failed to sign challenge with Card public key. Plain error: ${error.errorCode}")
return CompletionResult.Failure(error.tangemError)
}

View file

@ -20,6 +20,7 @@ import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
@ -29,7 +30,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
@Suppress("MemberNameEqualsClassName")
class DetailsMiddleware {
@ -228,7 +228,7 @@ class DetailsMiddleware {
)
}
.doOnFailure { error ->
Timber.e(error, "Unable to delete saved access codes")
TangemLogger.e("Unable to delete saved access codes", error)
}
}
}

View file

@ -10,6 +10,8 @@ import com.tangem.core.analytics.Analytics
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.message.dialog.Dialogs
import com.tangem.domain.card.CardTypesResolver
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.common.util.cardTypesResolver
@ -25,9 +27,7 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.features.details.ui.cardsettings.CardInfo
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsScreenState
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
@ -36,11 +36,10 @@ import com.tangem.tap.features.details.ui.common.utils.*
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addIf
import com.tangem.wallet.R
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ -56,6 +55,7 @@ internal class CardSettingsModel @Inject constructor(
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val settingsRepository: SettingsRepository,
private val onboardingRepository: OnboardingRepository,
private val uiMessageSender: UiMessageSender,
) : Model() {
private val params = paramsContainer.require<CardSettingsComponent.Params>()
@ -115,12 +115,7 @@ internal class CardSettingsModel @Inject constructor(
if (userWalletId == scannedUserWalletId || scannedUserWalletId == null) {
cardSettingsInteractor.initialize(scanResponse)
} else {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
headerId = R.string.common_warning,
messageId = R.string.error_wrong_wallet_tapped,
),
)
uiMessageSender.send(Dialogs.wrongWalletTapped())
}
}
}
@ -244,7 +239,7 @@ internal class CardSettingsModel @Inject constructor(
when (val result = tangemSdkManager.setAccessCode(scanResponse.card.cardId)) {
is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged())
is CompletionResult.Failure -> {
Timber.e("Failed to change access code: ${result.error}")
TangemLogger.e("Failed to change access code: ${result.error}")
}
}
}

View file

@ -28,6 +28,7 @@ import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -35,7 +36,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ -190,7 +190,7 @@ internal class ResetCardModel @Inject constructor(
resetCardUseCase(cardId = primaryCardId, params = currentUserCodeParams).onRight {
deleteSavedAccessCodesUseCase(cardId = primaryCardId)
val hasUserWallets = deleteWalletUseCase(userWalletId = currentUserWalletId).getOrElse { error ->
Timber.e("Unable to delete user wallet: $error")
TangemLogger.e("Unable to delete user wallet: $error")
return@launch
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.main
import android.os.Build
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.blockchainsdk.BlockchainSDKFactory
@ -39,16 +40,17 @@ import com.tangem.domain.wallets.usecase.GetSavedWalletsCountUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase
import com.tangem.feature.swap.analytics.StoriesEvents
import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList", "LargeClass")
@ -81,6 +83,7 @@ internal class MainViewModel @Inject constructor(
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val appRouterConfig: AppRouterConfig,
private val sellService: SellService,
private val deviceSecurityInfoProvider: DeviceSecurityInfoProvider,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() {
@ -121,6 +124,7 @@ internal class MainViewModel @Inject constructor(
deleteDeprecatedLogsUseCase()
sendKeyboardIdentifierEvent()
sendMediaTekVulnerabilityEvent()
preloadImages()
}
@ -144,13 +148,14 @@ internal class MainViewModel @Inject constructor(
// await while initial route stack is initialized
appRouterConfig.initializedState.first { it }
TangemLogger.withTag("MainActivity").i("Splash screen dismissed")
isSplashScreenShown = false
}
}
private suspend fun fetchUserCountry() {
fetchUserCountryUseCase().onLeft {
Timber.e("Unable to fetch the user country code $it")
TangemLogger.e("Unable to fetch the user country code $it")
}
}
@ -184,8 +189,8 @@ internal class MainViewModel @Inject constructor(
private suspend fun fetchStakingOptions() {
fetchStakingOptionsUseCase()
.onLeft { Timber.e(it.toString(), "Unable to fetch staking options") }
.onRight { Timber.d("Staking options were fetched successfully") }
.onLeft { TangemLogger.e("Unable to fetch staking options: $it") }
.onRight { TangemLogger.d("Staking options were fetched successfully") }
}
private fun initializeOffRamp() {
@ -335,13 +340,14 @@ internal class MainViewModel @Inject constructor(
listenToFlipsUseCase.changeUpdateEnabled(isUpdateEnabled = true)
}
@Suppress("NullableToStringCall")
private fun sendKeyboardIdentifierEvent() {
viewModelScope.launch {
val keyboardId = keyboardValidator.getKeyboardId()
if (keyboardId != null) {
Timber.d("Keyboard ID: https://play.google.com/store/apps/details?id=${keyboardId.getPackageName()}")
TangemLogger.d(
"Keyboard ID: https://play.google.com/store/apps/details?id=${keyboardId.getPackageName()}",
)
analyticsEventHandler.send(
event = TechAnalyticsEvent.KeyboardIdentifier(
@ -351,7 +357,28 @@ internal class MainViewModel @Inject constructor(
),
)
} else {
Timber.e("Unable to get keyboard identifier")
TangemLogger.e("Unable to get keyboard identifier")
}
}
}
private fun sendMediaTekVulnerabilityEvent() {
viewModelScope.launch(dispatchers.io) {
val isVulnerable = deviceSecurityInfoProvider.isVulnerableToMediaTekExploit
if (isVulnerable) {
analyticsEventHandler.send(
event = TechAnalyticsEvent.MediaTekVulnerability(
model = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Build.SOC_MODEL else "unknown",
manufacturer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Build.SOC_MANUFACTURER
} else {
"unknown"
},
hardware = Build.HARDWARE,
patch = Build.VERSION.SECURITY_PATCH,
),
)
}
}
}
@ -365,7 +392,7 @@ internal class MainViewModel @Inject constructor(
refresh = true,
).fold(
ifLeft = { error ->
Timber.e(error)
TangemLogger.e("Error", error)
analyticsEventHandler.send(
StoriesEvents.Error(
type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType,
@ -382,7 +409,7 @@ internal class MainViewModel @Inject constructor(
)
}
} catch (ex: Exception) {
Timber.e(ex)
TangemLogger.e("Error", ex)
analyticsEventHandler.send(
StoriesEvents.Error(
type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType,
@ -407,7 +434,7 @@ internal class MainViewModel @Inject constructor(
associateAndUpdateWallets(applicationId = applicationId)
}
}
.onLeft(Timber::e)
.onLeft { TangemLogger.e("Error", it) }
}
private suspend fun associateAndUpdateWallets(applicationId: ApplicationId) {

View file

@ -1,10 +0,0 @@
package com.tangem.tap.features.onboarding
import com.tangem.domain.redux.StateDialog
/**
[REDACTED_AUTHOR]
*/
sealed class OnboardingDialog : StateDialog {
data class WalletActivationError(val onConfirm: () -> Unit) : OnboardingDialog()
}

View file

@ -1,14 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
sealed class BackupDialog : StateDialog {
data class UnfinishedBackupFound(
val scanResponse: ScanResponse? = null,
) : BackupDialog()
data class ConfirmDiscardingBackup(
val scanResponse: ScanResponse? = null,
) : BackupDialog()
}

View file

@ -1,63 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.tap.backupService
import com.tangem.tap.common.analytics.events.Onboarding.Finished
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@Suppress("MemberNameEqualsClassName")
class BackupMiddleware {
val backupMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
if (action is BackupAction) handleBackupAction(state, action)
next(action)
}
}
}
}
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) {
if (DemoHelper.tryHandle(appState)) return
when (action) {
is BackupAction.DiscardBackup -> {
backupService.discardSavedBackup()
}
is BackupAction.DiscardSavedBackup -> {
mainScope.launch {
backupService.discardSavedBackup()
val onboardingRepository = store.inject(DaggerGraphState::onboardingRepository)
val cardRepository = store.inject(DaggerGraphState::cardRepository)
val unfinishedBackup = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
cardRepository.finishCardActivation(unfinishedBackup.card.cardId)
onboardingRepository.clearUnfinishedFinalizeOnboarding()
Analytics.send(Finished())
}
}
is BackupAction.ResumeFoundUnfinishedBackup -> {
if (action.unfinishedBackupScanResponse != null) {
store.dispatchNavigationAction {
replaceAll(
AppRoute.Onboarding(
scanResponse = action.unfinishedBackupScanResponse,
mode = AppRoute.Onboarding.Mode.ContinueFinalize,
),
)
}
}
}
}
}

View file

@ -1,19 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
import org.rekotlin.Action
sealed class OnboardingWalletAction : Action {
data class WalletSaved(val userWalletId: UserWalletId) : OnboardingWalletAction()
}
sealed class BackupAction : Action {
data object DiscardBackup : BackupAction()
data object DiscardSavedBackup : BackupAction()
data class ResumeFoundUnfinishedBackup(
val unfinishedBackupScanResponse: ScanResponse?,
) : BackupAction()
}

View file

@ -1,29 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
import com.tangem.wallet.R
object ConfirmDiscardingBackupDialog {
fun create(context: Context, unfinishedBackupScanResponse: ScanResponse? = null): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.welcome_interrupted_backup_discard_title)
setMessage(R.string.welcome_interrupted_backup_discard_message)
setPositiveButton(R.string.welcome_interrupted_backup_discard_resume) { _, _ ->
store.dispatch(BackupAction.ResumeFoundUnfinishedBackup(unfinishedBackupScanResponse))
}
setNegativeButton(R.string.welcome_interrupted_backup_discard_discard) { _, _ ->
store.dispatch(BackupAction.DiscardSavedBackup)
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
setCancelable(false)
}.create()
}
}

View file

@ -1,33 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.store
import com.tangem.wallet.R
object UnfinishedBackupFoundDialog {
fun create(context: Context, scanResponse: ScanResponse? = null): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.common_warning)
setMessage(R.string.welcome_interrupted_backup_alert_message)
setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ ->
Analytics.send(OnboardingEvent.Backup.ResumeInterruptedBackup())
store.dispatch(GlobalAction.HideDialog)
store.dispatch(BackupAction.ResumeFoundUnfinishedBackup(scanResponse))
}
setNegativeButton(R.string.welcome_interrupted_backup_alert_discard) { _, _ ->
Analytics.send(OnboardingEvent.Backup.CancelInterruptedBackup())
store.dispatch(GlobalAction.HideDialog)
store.dispatch(GlobalAction.ShowDialog(BackupDialog.ConfirmDiscardingBackup(scanResponse)))
}
setCancelable(false)
}.create()
}
}

View file

@ -1,45 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.app.Dialog
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.coroutines.launch
object WalletActivationErrorDialog {
fun create(context: Context, dialog: OnboardingDialog.WalletActivationError): Dialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(context.getString(R.string.onboarding_activation_error_title))
setMessage(context.getString(R.string.onboarding_activation_error_message))
setPositiveButton(R.string.common_ok) { _, _ -> dialog.onConfirm() }
setNegativeButton(R.string.common_support) { _, _ ->
// changed on email support [REDACTED_TASK_KEY]
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
val scanResponse = store.state.globalState.scanResponse
?: error("ScanResponse must be not null")
val cardInfo = store.inject(DaggerGraphState::getWalletMetaInfoUseCase).invoke(scanResponse).getOrNull()
?: error("CardInfo must be not null")
scope.launch {
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
.invoke(type = FeedbackEmailType.DirectUserRequest(cardInfo))
}
}
setOnDismissListener { store.dispatchDialogHide() }
setCancelable(false)
}.create()
}
}

View file

@ -1,33 +0,0 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.wallet.R
object WalletAlreadyWasUsedDialog {
fun create(context: Context, onOk: () -> Unit, onCancel: () -> Unit, onSupport: () -> Unit): AlertDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.security_alert_title)
setMessage(R.string.wallet_been_activated_message)
setPositiveButton(R.string.this_is_my_wallet_title) { dialog, _ ->
onOk()
dialog.dismiss()
}
setNeutralButton(R.string.common_cancel) { dialog, _ ->
onCancel()
dialog.dismiss()
}
setNegativeButton(R.string.alert_button_request_support) { dialog, _ ->
onSupport()
dialog.dismiss()
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.tap.features.scanfails
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.card.ScanFailsRequester
import com.tangem.tap.features.scanfails.ui.ScanFailsDialogContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class ScanFailsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
) : AppComponentContext by appComponentContext, ComposableContentComponent, ScanFailsRequester {
private val model: ScanFailsModel = getOrCreateModel(params)
override suspend fun show(source: AnalyticsParam.ScreensSources): ScanFailsRequester.Result {
model.show(source)
return model.waitResult()
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
if (state.isShown) {
ScanFailsDialogContent(state = state)
}
}
@AssistedFactory
interface Factory : ComponentFactory<Unit, ScanFailsComponent> {
override fun create(context: AppComponentContext, params: Unit): ScanFailsComponent
}
}

View file

@ -0,0 +1,72 @@
package com.tangem.tap.features.scanfails
import com.tangem.common.TangemBlogUrlBuilder
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.card.ScanFailsRequester
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics
import com.tangem.tap.features.scanfails.ui.ScanFailsUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@ModelScoped
internal class ScanFailsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val urlOpener: UrlOpener,
) : Model() {
private val result = MutableStateFlow<ScanFailsRequester.Result?>(null)
val uiState: StateFlow<ScanFailsUM>
field = MutableStateFlow(ScanFailsUM(onDismiss = ::dismiss))
fun show(source: AnalyticsParam.ScreensSources) {
result.value = null
uiState.update {
ScanFailsUM(
isShown = true,
onHowToScan = { onHowToScan(source) },
onRequestSupport = { onRequestSupport(source) },
onDismiss = ::dismiss,
)
}
}
suspend fun waitResult(): ScanFailsRequester.Result {
return result.filterNotNull().first().also { result.value = null }
}
fun dismiss() {
result.value = ScanFailsRequester.Result.Dismissed
uiState.update { it.copy(isShown = false) }
}
private fun onHowToScan(source: AnalyticsParam.ScreensSources) {
analyticsEventHandler.send(
ScanFailsDialogAnalytics(
button = ScanFailsDialogAnalytics.Buttons.HOW_TO_SCAN,
source = source,
),
)
modelScope.launch {
urlOpener.openUrl(TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.HowToScan))
}
}
private fun onRequestSupport(source: AnalyticsParam.ScreensSources) {
analyticsEventHandler.send(Basic.ButtonSupport(source))
modelScope.launch {
sendFeedbackEmailUseCase(type = FeedbackEmailType.ScanningProblem)
}
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.tap.features.scanfails
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.card.ScanFailsRequester
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ScanFailsRequesterProxy @Inject constructor() : ScanFailsRequester {
val componentRequester = MutableStateFlow<ScanFailsRequester?>(null)
override suspend fun show(source: AnalyticsParam.ScreensSources): ScanFailsRequester.Result {
return withTimeout(timeMillis = 1000) {
componentRequester.filterNotNull().first()
}.show(source)
}
}

View file

@ -0,0 +1,38 @@
package com.tangem.tap.features.scanfails.di
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.decompose.model.Model
import com.tangem.domain.card.ScanFailsCounter
import com.tangem.domain.card.ScanFailsRequester
import com.tangem.tap.domain.scanCard.DefaultScanFailsCounter
import com.tangem.tap.features.scanfails.ScanFailsComponent
import com.tangem.tap.features.scanfails.ScanFailsModel
import com.tangem.tap.features.scanfails.ScanFailsRequesterProxy
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ScanFailsModule {
@Binds
fun bindComponentFactory(impl: ScanFailsComponent.Factory): ComponentFactory<Unit, ScanFailsComponent>
@Binds
@IntoMap
@ClassKey(ScanFailsModel::class)
fun bindModel(model: ScanFailsModel): Model
@Binds
@Singleton
fun bindRequester(impl: ScanFailsRequesterProxy): ScanFailsRequester
@Binds
@Singleton
fun bindScanFailsCounter(impl: DefaultScanFailsCounter): ScanFailsCounter
}

View file

@ -0,0 +1,119 @@
package com.tangem.tap.features.scanfails.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.tangem.core.ui.extensions.LocalUserInteractionTracker
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.trackUserInteraction
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.wallet.R
@Composable
internal fun ScanFailsDialogContent(state: ScanFailsUM) {
val userInteractionTracker = LocalUserInteractionTracker.current
Dialog(
onDismissRequest = state.onDismiss,
properties = DialogProperties(dismissOnClickOutside = true),
) {
Column(
modifier = Modifier
.trackUserInteraction(userInteractionTracker)
.background(
color = TangemTheme.colors.background.primary,
shape = RoundedCornerShape(16.dp),
)
.padding(vertical = 16.dp, horizontal = 38.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
Text(
text = stringResourceSafe(R.string.common_warning),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h3,
textAlign = TextAlign.Center,
)
Text(
text = stringResourceSafe(R.string.alert_troubleshooting_scan_card_title),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body2,
textAlign = TextAlign.Center,
)
DialogTextButton(
text = stringResourceSafe(R.string.alert_button_how_to_scan),
onClick = state.onHowToScan,
color = TangemTheme.colors.text.accent,
modifier = Modifier.testTag(ScanFailsDialogTestTags.HOW_TO_SCAN_BUTTON),
)
DialogTextButton(
text = stringResourceSafe(R.string.alert_button_request_support),
onClick = state.onRequestSupport,
color = TangemTheme.colors.text.accent,
modifier = Modifier.testTag(ScanFailsDialogTestTags.REQUEST_SUPPORT_BUTTON),
)
DialogTextButton(
text = stringResourceSafe(R.string.common_cancel),
onClick = state.onDismiss,
color = TangemTheme.colors.text.warning,
modifier = Modifier.testTag(ScanFailsDialogTestTags.CANCEL_BUTTON),
)
}
}
}
@Composable
private fun DialogTextButton(text: String, onClick: () -> Unit, color: Color, modifier: Modifier = Modifier) {
Text(
text = text.uppercase(),
color = color,
style = TangemTheme.typography.button,
textAlign = TextAlign.Center,
modifier = modifier.clickable(onClick = onClick),
)
}
object ScanFailsDialogTestTags {
const val HOW_TO_SCAN_BUTTON = "scan_fails_how_to_scan_button"
const val REQUEST_SUPPORT_BUTTON = "scan_fails_request_support_button"
const val CANCEL_BUTTON = "scan_fails_cancel_button"
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(showBackground = true, widthDp = 360, fontScale = 2f)
@Composable
private fun ScanFailsDialogContentPreview() {
TangemThemePreview {
ScanFailsDialogContent(
state = ScanFailsUM(
isShown = true,
onHowToScan = {},
onRequestSupport = {},
onDismiss = {},
),
)
}
}
// endregion Preview

View file

@ -0,0 +1,8 @@
package com.tangem.tap.features.scanfails.ui
internal data class ScanFailsUM(
val isShown: Boolean = false,
val onHowToScan: () -> Unit = {},
val onRequestSupport: () -> Unit = {},
val onDismiss: () -> Unit = {},
)

View file

@ -19,9 +19,9 @@ import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.SellService
import com.tangem.tap.network.exchangeServices.SellServiceInitializationStatus
import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import timber.log.Timber
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
@ -42,13 +42,13 @@ class MoonPayService(
override suspend fun update() {
withIOContext {
Timber.i("Start updating")
TangemLogger.i("Start updating")
_initializationStatus.value = lceLoading()
performRequest {
val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) {
is Result.Failure -> {
Timber.e(result.error, "Failed to load user status")
TangemLogger.e("Failed to load user status", result.error)
_initializationStatus.value = result.error.lceError()
return@performRequest
}
@ -57,7 +57,7 @@ class MoonPayService(
val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) {
is Result.Failure -> {
Timber.e(result.error, "Failed to load currencies")
TangemLogger.e("Failed to load currencies", result.error)
_initializationStatus.value = result.error.lceError()
return@performRequest
}
@ -77,7 +77,7 @@ class MoonPayService(
)
}
Timber.i("Successfully updated")
TangemLogger.i("Successfully updated")
_initializationStatus.value = lceContent()
status = MoonPayStatus(currenciesToSell, userStatus, currencies)
}

View file

@ -2,8 +2,6 @@ package com.tangem.tap.proxy
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
@ -32,8 +30,4 @@ class AppStateHolder @Inject constructor() : ReduxStateHolder {
override suspend fun onUserWalletSelected(userWallet: UserWallet) {
mainStore?.onUserWalletSelected(userWallet)
}
override fun dispatchDialogShow(dialog: StateDialog) {
mainStore?.dispatchDialogShow(dialog)
}
}

View file

@ -18,6 +18,7 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.ScanFailsRequester
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
@ -73,4 +74,5 @@ data class DaggerGraphState(
val userWalletsListRepository: UserWalletsListRepository? = null,
val tangemHotSdk: TangemHotSdk? = null,
val trackingContextProxy: TrackingContextProxy? = null,
val scanFailsRequester: ScanFailsRequester? = null,
) : StateType

View file

@ -7,10 +7,11 @@ import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
import com.tangem.core.decompose.navigation.Router
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.reflect.KClass
internal class ProxyAppRouter(
@ -19,6 +20,8 @@ internal class ProxyAppRouter(
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
) : AppRouter {
private val logger = TangemLogger.withTag("AppRouter")
private val routerScope: CoroutineScope
get() = requireNotNull(config.routerScope) {
"Router scope is not set in config"
@ -47,12 +50,8 @@ internal class ProxyAppRouter(
}
override fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) {
safeNavigate(onComplete, message = "Replace all routes with $routes") {
runCatching {
innerRouter.replaceAll(*routes, onComplete = onComplete)
}.getOrElse {
Timber.e(it)
}
safeNavigate(onComplete, message = "Replace all routes with ${routes.toList().ifEmpty { "<empty>" }}") {
innerRouter.replaceAll(*routes, onComplete = onComplete)
}
}
@ -76,21 +75,20 @@ internal class ProxyAppRouter(
private fun safeNavigate(onComplete: (isSuccess: Boolean) -> Unit, message: String, block: () -> Unit) {
routerScope.launch(dispatchers.mainImmediate) {
Timber.i(message)
logger.i(message)
try {
block()
} catch (e: Throwable) {
Timber.e(e)
onComplete(false)
}
runSuspendCatching(block = { block() })
.onFailure { throwable ->
logger.e(messageString = "Error", throwable = throwable)
onComplete(false)
}
}
}
override fun defaultCompletionHandler(isSuccess: Boolean, errorMessage: String) {
if (!isSuccess) {
analyticsExceptionHandler.sendException(ExceptionAnalyticsEvent(RuntimeException(errorMessage)))
Timber.w(errorMessage)
logger.w(errorMessage)
with(receiver = config.snackbarHandler ?: return) {
showSnackbar(

View file

@ -52,6 +52,7 @@ internal fun RootContent(
wcContent: @Composable (modifier: Modifier) -> Unit,
hotAccessCodeContent: @Composable (modifier: Modifier) -> Unit,
rootDetectedWarningContent: @Composable (modifier: Modifier) -> Unit,
scanFailsContent: @Composable (modifier: Modifier) -> Unit,
) {
val context = LocalContext.current
@ -89,6 +90,8 @@ internal fun RootContent(
rootDetectedWarningContent(Modifier.fillMaxSize())
scanFailsContent(Modifier.fillMaxSize())
TangemSnackbarHost(
modifier = Modifier
.align(Alignment.BottomCenter)

View file

@ -23,20 +23,27 @@ import com.tangem.core.decompose.navigation.getOrCreateTyped
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
import com.tangem.features.walletconnect.components.WcRoutingComponent
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.android.create
import com.tangem.sdk.api.BackupServiceHolder
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.features.scanfails.ScanFailsComponent
import com.tangem.tap.features.scanfails.ScanFailsRequesterProxy
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.hot.TangemHotSDKProxy
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.root.RootDetectedWarningComponent
import com.tangem.tap.routing.RootContent
import com.tangem.tap.routing.component.RoutingComponent
@ -45,11 +52,12 @@ import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.tap.routing.utils.ChildFactory
import com.tangem.tap.routing.utils.DeepLinkFactory
import com.tangem.tap.store
import com.tangem.utils.logging.TangemLogger
import com.tangem.wallet.R
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.launch
import timber.log.Timber
@Suppress("LongParameterList")
internal class DefaultRoutingComponent @AssistedInject constructor(
@ -69,8 +77,11 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private val cardRepository: CardRepository,
private val onboardingRepository: OnboardingRepository,
private val trackingContextProxy: TrackingContextProxy,
private val scanFailsComponentFactory: ScanFailsComponent.Factory,
private val scanFailsRequesterProxy: ScanFailsRequesterProxy,
private val analyticsEventHandler: AnalyticsEventHandler,
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
private val backupServiceHolder: BackupServiceHolder,
) : RoutingComponent,
AppComponentContext by context,
SnackbarHandler {
@ -90,6 +101,11 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
.create(child("rootDetectedWarningComponent"), Unit)
}
private val scanFailsComponent: ScanFailsComponent by lazy {
scanFailsComponentFactory
.create(child("scanFailsComponent"), Unit)
}
private val navigation = navigationProvider.getOrCreateTyped<AppRoute>()
private val stack: Value<ChildStack<AppRoute, Child>> = childStack(
@ -101,7 +117,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
try {
childFactory.createChild(route, childByContext(childContext))
} catch (e: Exception) {
Timber.e(e, "App Router Failed")
TangemLogger.e("App Router Failed", e)
analyticsExceptionHandler.sendException(
ExceptionAnalyticsEvent(exception = e, params = mapOf("Category" to "App Routing")),
)
@ -190,6 +206,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
wcContent = { wcRoutingComponent.Content(it) },
hotAccessCodeContent = { hotAccessCodeRequestComponent.Content(it) },
rootDetectedWarningContent = { rootDetectedWarningComponent.Content(it) },
scanFailsContent = { scanFailsComponent.Content(it) },
)
}
@ -233,10 +250,12 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
onCreate = {
tangemHotSDKProxy.sdkState.value = TangemHotSdk.create(activity)
hotAccessCodeRequesterProxy.componentRequester.value = hotAccessCodeRequestComponent
scanFailsRequesterProxy.componentRequester.value = scanFailsComponent
},
onDestroy = {
tangemHotSDKProxy.sdkState.value = null
hotAccessCodeRequesterProxy.componentRequester.value = null
scanFailsRequesterProxy.componentRequester.value = null
},
)
}
@ -251,9 +270,71 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
}
private fun checkForUnfinishedBackup() {
if (DemoHelper.tryHandle { store.state }) return
componentScope.launch(dispatchers.main) {
val onboardingScanResponse = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound(onboardingScanResponse)))
val scanResponse = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
messageSender.send(unfinishedBackupFoundDialog(scanResponse))
}
}
private fun unfinishedBackupFoundDialog(scanResponse: ScanResponse): DialogMessage = DialogMessage(
title = resourceReference(R.string.common_warning),
message = resourceReference(R.string.welcome_interrupted_backup_alert_message),
isDismissable = false,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.welcome_interrupted_backup_alert_resume),
onClick = {
analyticsEventHandler.send(OnboardingEvent.Backup.ResumeInterruptedBackup())
resumeUnfinishedBackup(scanResponse)
},
)
},
secondActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.welcome_interrupted_backup_alert_discard),
onClick = {
analyticsEventHandler.send(OnboardingEvent.Backup.CancelInterruptedBackup())
messageSender.send(confirmDiscardingBackupDialog(scanResponse))
},
)
},
)
private fun confirmDiscardingBackupDialog(scanResponse: ScanResponse): DialogMessage = DialogMessage(
title = resourceReference(R.string.welcome_interrupted_backup_discard_title),
message = resourceReference(R.string.welcome_interrupted_backup_discard_message),
isDismissable = false,
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.welcome_interrupted_backup_discard_resume),
onClick = { resumeUnfinishedBackup(scanResponse) },
)
},
secondActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.welcome_interrupted_backup_discard_discard),
onClick = { discardSavedBackup() },
)
},
)
private fun resumeUnfinishedBackup(scanResponse: ScanResponse) {
router.replaceAll(
AppRoute.Onboarding(
scanResponse = scanResponse,
mode = AppRoute.Onboarding.Mode.ContinueFinalize,
),
)
}
private fun discardSavedBackup() {
componentScope.launch(dispatchers.main) {
backupServiceHolder.backupService.get()?.discardSavedBackup()
val unfinishedBackup = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch
cardRepository.finishCardActivation(unfinishedBackup.card.cardId)
onboardingRepository.clearUnfinishedFinalizeOnboarding()
analyticsEventHandler.send(Onboarding.Finished())
}
}

View file

@ -138,7 +138,6 @@ internal class ChildFactory @Inject constructor(
AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS
AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES
AppRoute.ManageTokens.Source.ACCOUNT -> ManageTokensSource.ACCOUNT
AppRoute.ManageTokens.Source.TOKEN_SYNC_BANNER -> ManageTokensSource.TOKEN_SYNC_BANNER
}
val mode = route.accountId?.let { ManageTokensMode.Account(it) } ?: ManageTokensMode.None

View file

@ -23,6 +23,7 @@ import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLi
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.extensions.uriValidate
import com.tangem.utils.logging.TangemLogger
import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -30,7 +31,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.transformLatest
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ -62,7 +62,7 @@ internal class DeepLinkFactory @Inject constructor(
fun handleDeeplink(deeplinkUri: Uri, coroutineScope: CoroutineScope, isFromOnNewIntent: Boolean) {
lastDeepLink = deeplinkUri
Timber.i(
TangemLogger.i(
"""
Received deep link intent
|- Received URI: $deeplinkUri
@ -108,7 +108,7 @@ internal class DeepLinkFactory @Inject constructor(
DeepLinkScheme.Tangem.scheme -> handleTangemDeepLinks(deeplinkUri, coroutineScope, isFromOnNewIntent)
DeepLinkScheme.WalletConnect.scheme -> walletConnectDeepLink.create(deeplinkUri)
else -> {
Timber.i(
TangemLogger.i(
"""
No match found for deep link
|- Received URI: $deeplinkUri
@ -157,7 +157,7 @@ internal class DeepLinkFactory @Inject constructor(
DeepLinkRoute.Promo.host -> promoDeepLink.create(coroutineScope, queryParams)
DeepLinkRoute.OnboardVisa.host -> onboardVisaDeepLink.create(deeplinkUri)
else -> {
Timber.i(
TangemLogger.i(
"""
No match found for deep link
|- Received URI: $deeplinkUri