Updated on 2026-08-14
This commit is contained in:
commit
b0d5ab72e1
50 changed files with 378 additions and 137 deletions
|
|
@ -20,6 +20,7 @@
|
|||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID"/>
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
|
|
|
|||
|
|
@ -2,13 +2,10 @@ package com.tangem.tap
|
|||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.nfc.NfcAdapter
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
|
|
@ -298,31 +295,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
|
|||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val nfcAdapter = NfcAdapter.getDefaultAdapter(this)
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP),
|
||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val intentFilters = arrayOf(
|
||||
IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED),
|
||||
IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED),
|
||||
IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED),
|
||||
)
|
||||
nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, null)
|
||||
// TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop
|
||||
notificationsHandler = NotificationsHandler(binding.fragmentContainer)
|
||||
|
||||
navigateToInitialScreenIfNeeded(intent)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
val nfcAdapter = NfcAdapter.getDefaultAdapter(this)
|
||||
nfcAdapter.disableForegroundDispatch(this)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
notificationsHandler = null
|
||||
dialogManager.onStop()
|
||||
|
|
|
|||
|
|
@ -126,6 +126,11 @@ class DialogManager : StoreSubscriber<GlobalState> {
|
|||
preparedData = state.dialog.data,
|
||||
context = context,
|
||||
)
|
||||
is WalletConnectDialog.PairConnectErrorDialog -> SimpleAlertDialog.create(
|
||||
titleRes = R.string.wallet_connect_title,
|
||||
message = state.dialog.error.message,
|
||||
context = context,
|
||||
)
|
||||
is BackupDialog.AttestationFailed -> AttestationFailedDialog.create(context)
|
||||
is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context)
|
||||
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)
|
||||
|
|
@ -139,8 +144,9 @@ class DialogManager : StoreSubscriber<GlobalState> {
|
|||
title = context.getString(state.dialog.titleRes, state.dialog.currencySymbol),
|
||||
message = context.getString(
|
||||
state.dialog.messageRes,
|
||||
state.dialog.currencySymbol,
|
||||
state.dialog.currencyTitle,
|
||||
state.dialog.currencySymbol,
|
||||
state.dialog.networkName,
|
||||
),
|
||||
context = context,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ sealed class AppDialog : StateDialog {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.common.redux.legacy
|
|||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.domain.tokens.utils.convertToAmount
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.feedback.FeedbackEmail
|
||||
import com.tangem.tap.common.feedback.RateCanBeBetterEmail
|
||||
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
|
|
@ -21,6 +22,9 @@ internal object LegacyMiddleware {
|
|||
is LegacyAction.SendEmailRateCanBeBetter -> {
|
||||
store.state.globalState.feedbackManager?.sendEmail(RateCanBeBetterEmail())
|
||||
}
|
||||
is LegacyAction.SendEmailSupport -> {
|
||||
store.state.globalState.feedbackManager?.sendEmail(FeedbackEmail())
|
||||
}
|
||||
is LegacyAction.StartOnboardingProcess -> {
|
||||
store.dispatch(
|
||||
GlobalAction.Onboarding.Start(action.scanResponse, canSkipBackup = action.canSkipBackup),
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ private fun internalReduce(action: Action, state: AppState): NavigationState {
|
|||
when {
|
||||
// Destroy the activity if it invoked for the same activity. Prevents overwriting to null if there is a
|
||||
// new scan from the background [REDACTED_TASK_KEY]
|
||||
navState.activity?.get() == navigationAction.activity.get() -> navState.copy(activity = null)
|
||||
navState.activity?.get() == navigationAction.activity.get() -> NavigationState()
|
||||
else -> navState
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import com.tangem.sdk.extensions.*
|
|||
import com.tangem.sdk.nfc.NfcManager
|
||||
import com.tangem.sdk.storage.create
|
||||
import com.tangem.tap.foregroundActivityObserver
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -30,6 +32,7 @@ import javax.inject.Singleton
|
|||
@Singleton
|
||||
internal class DefaultCardSdkProvider @Inject constructor(
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : CardSdkProvider, CardSdkOwner {
|
||||
|
||||
private val observer = Observer()
|
||||
|
|
@ -39,12 +42,12 @@ internal class DefaultCardSdkProvider @Inject constructor(
|
|||
override val sdk: TangemSdk
|
||||
get() = holder?.sdk ?: tryToRegisterWithForegroundActivity()
|
||||
|
||||
override fun register(activity: FragmentActivity) {
|
||||
override fun register(activity: FragmentActivity) = runBlocking(dispatchers.mainImmediate) {
|
||||
if (activity.isDestroyed || activity.isFinishing || activity.isChangingConfigurations) {
|
||||
val message = "Tangem SDK owner registration skipped: activity is destroyed or finishing"
|
||||
analyticsEventHandler.send(TangemSdkWarningEvent(message))
|
||||
Log.info { message }
|
||||
return
|
||||
return@runBlocking
|
||||
}
|
||||
|
||||
if (holder != null) {
|
||||
|
|
@ -58,7 +61,7 @@ internal class DefaultCardSdkProvider @Inject constructor(
|
|||
Log.info { "Tangem SDK owner registered" }
|
||||
}
|
||||
|
||||
private fun tryToRegisterWithForegroundActivity(): TangemSdk {
|
||||
private fun tryToRegisterWithForegroundActivity(): TangemSdk = runBlocking(dispatchers.mainImmediate) {
|
||||
val warning = "Tangem SDK holder is null, trying to recreate it with foreground activity"
|
||||
analyticsEventHandler.send(TangemSdkWarningEvent(warning))
|
||||
Log.warning { warning }
|
||||
|
|
@ -83,7 +86,7 @@ internal class DefaultCardSdkProvider @Inject constructor(
|
|||
error(error)
|
||||
}
|
||||
|
||||
return sdk
|
||||
return@runBlocking sdk
|
||||
}
|
||||
|
||||
private fun initialize(activity: FragmentActivity) {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ interface TangemSdkManager {
|
|||
|
||||
val userCodeRequestPolicy: UserCodeRequestPolicy
|
||||
|
||||
suspend fun checkCanUseBiometry(awaitInitialization: Boolean = true): Boolean
|
||||
|
||||
suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean = true): Boolean
|
||||
|
||||
suspend fun scanProduct(
|
||||
cardId: String? = null,
|
||||
messageRes: Int? = null,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ package com.tangem.tap.domain.sdk.impl
|
|||
import android.content.res.Resources
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.tangem.Log
|
||||
import com.tangem.Message
|
||||
import com.tangem.TangemSdk
|
||||
import com.tangem.common.*
|
||||
import com.tangem.common.authentication.AuthenticationManager
|
||||
import com.tangem.common.authentication.keystore.KeystoreManager
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.common.core.*
|
||||
|
|
@ -38,16 +40,21 @@ import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
|
|||
import com.tangem.tap.domain.twins.FinalizeTwinTask
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
@Suppress("TooManyFunctions", "LargeClass")
|
||||
class DefaultTangemSdkManager(
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val resources: Resources,
|
||||
) : TangemSdkManager {
|
||||
|
||||
private val awaitInitializationMutex = Mutex()
|
||||
|
||||
private val tangemSdk: TangemSdk
|
||||
get() = cardSdkConfigRepository.sdk
|
||||
|
||||
|
|
@ -73,6 +80,42 @@ class DefaultTangemSdkManager(
|
|||
override val userCodeRequestPolicy: UserCodeRequestPolicy
|
||||
get() = tangemSdk.config.userCodeRequestPolicy
|
||||
|
||||
override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean {
|
||||
return try {
|
||||
needEnrollBiometrics
|
||||
} catch (e: TangemSdkError.AuthenticationNotInitialized) {
|
||||
Log.error {
|
||||
"Trying to access `needEnrollBiometrics` flag when authentication manager is not initialized: " +
|
||||
if (awaitInitialization) "awaiting initialization" else "failing"
|
||||
}
|
||||
|
||||
if (awaitInitialization) {
|
||||
awaitAuthenticationManagerInitialization().needEnrollBiometrics
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun checkCanUseBiometry(awaitInitialization: Boolean): Boolean {
|
||||
return try {
|
||||
canUseBiometry
|
||||
} catch (e: TangemSdkError.AuthenticationNotInitialized) {
|
||||
Log.error {
|
||||
"Trying to access `canUseBiometry` flag when authentication manager is not initialized: " +
|
||||
if (awaitInitialization) "awaiting initialization" else "failing"
|
||||
}
|
||||
|
||||
if (awaitInitialization) {
|
||||
val manager = awaitAuthenticationManagerInitialization()
|
||||
|
||||
manager.canAuthenticate || manager.needEnrollBiometrics
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun scanProduct(
|
||||
cardId: String?,
|
||||
messageRes: Int?,
|
||||
|
|
@ -288,6 +331,26 @@ class DefaultTangemSdkManager(
|
|||
tangemSdk.config.userCodeRequestPolicy = policy
|
||||
}
|
||||
|
||||
private suspend fun awaitAuthenticationManagerInitialization(): AuthenticationManager {
|
||||
return awaitInitializationMutex.withLock {
|
||||
var attemps = 0
|
||||
|
||||
do {
|
||||
if (tangemSdk.authenticationManager.isInitialized) {
|
||||
break
|
||||
} else {
|
||||
if (attemps++ >= MAX_INITIALIZE_ATTEMPTS) {
|
||||
error("Can't initialize authentication manager after $MAX_INITIALIZE_ATTEMPTS attempts")
|
||||
} else {
|
||||
delay(timeMillis = 200)
|
||||
}
|
||||
}
|
||||
} while (true)
|
||||
|
||||
tangemSdk.authenticationManager
|
||||
}
|
||||
}
|
||||
|
||||
// region Twin-specific
|
||||
|
||||
override suspend fun createFirstTwinWallet(
|
||||
|
|
@ -335,6 +398,8 @@ class DefaultTangemSdkManager(
|
|||
// endregion
|
||||
|
||||
companion object {
|
||||
private const val MAX_INITIALIZE_ATTEMPTS = 10
|
||||
|
||||
@Deprecated("Use [DefaultCardSdkProvider] instead")
|
||||
val config = Config(
|
||||
linkedTerminal = true,
|
||||
|
|
|
|||
|
|
@ -28,9 +28,11 @@ class MockTangemSdkManager(
|
|||
private val resources: Resources,
|
||||
) : TangemSdkManager {
|
||||
|
||||
override val canUseBiometry = false
|
||||
private var userCodeRequestPolicyInternal: UserCodeRequestPolicy = UserCodeRequestPolicy.Default
|
||||
|
||||
override val needEnrollBiometrics = false
|
||||
override val canUseBiometry: Boolean = false
|
||||
|
||||
override val needEnrollBiometrics: Boolean = false
|
||||
|
||||
override val keystoreManager = DummyKeystoreManager()
|
||||
|
||||
|
|
@ -39,7 +41,9 @@ class MockTangemSdkManager(
|
|||
override val userCodeRequestPolicy: UserCodeRequestPolicy
|
||||
get() = userCodeRequestPolicyInternal
|
||||
|
||||
private var userCodeRequestPolicyInternal: UserCodeRequestPolicy = UserCodeRequestPolicy.Default
|
||||
override suspend fun checkCanUseBiometry(awaitInitialization: Boolean): Boolean = canUseBiometry
|
||||
|
||||
override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean = needEnrollBiometrics
|
||||
|
||||
override suspend fun scanProduct(
|
||||
cardId: String?,
|
||||
|
|
|
|||
|
|
@ -7,5 +7,5 @@ internal class DefaultLegacySettingsRepository(
|
|||
private val tangemSdkManager: TangemSdkManager,
|
||||
) : LegacySettingsRepository {
|
||||
|
||||
override fun canUseBiometry(): Boolean = tangemSdkManager.canUseBiometry
|
||||
override suspend fun canUseBiometry(): Boolean = tangemSdkManager.checkCanUseBiometry()
|
||||
}
|
||||
|
|
@ -48,21 +48,33 @@ internal class GeneralUserWalletsListManager(
|
|||
get() = requireImplementation.isLockable
|
||||
|
||||
override val userWallets: Flow<List<UserWallet>>
|
||||
get() = implementation.transformLatest { impl ->
|
||||
if (impl != null && impl.hasUserWallets) {
|
||||
emitAll(impl.userWallets)
|
||||
get() = implementation
|
||||
.transformLatest { impl ->
|
||||
if (impl != null) {
|
||||
emitAll(impl.userWallets)
|
||||
}
|
||||
}
|
||||
}
|
||||
// To avoid returning empty flow to subscriber while implementation and userWallets are null
|
||||
// Flow is called first time when implementation is null and then when its assigned with implementation
|
||||
// that may have not user wallets (null or empty).
|
||||
// As a result subscription occurs on empty flow, than will not change if user wallets are available
|
||||
.filter { requireImplementation.hasUserWallets }
|
||||
|
||||
override val userWalletsSync: List<UserWallet>
|
||||
get() = requireImplementation.userWalletsSync
|
||||
|
||||
override val selectedUserWallet: Flow<UserWallet>
|
||||
get() = implementation.transformLatest { impl ->
|
||||
if (impl != null && impl.hasUserWallets) {
|
||||
emitAll(impl.selectedUserWallet)
|
||||
get() = implementation
|
||||
.transformLatest { impl ->
|
||||
if (impl != null) {
|
||||
emitAll(impl.selectedUserWallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
// To avoid returning empty flow to subscriber while implementation and userWallets are null
|
||||
// Flow is called first time when implementation is null and then when its assigned with implementation
|
||||
// that may have not user wallets (null or empty).
|
||||
// As a result subscription occurs on empty flow, than will not change if user wallets are available
|
||||
.filter { requireImplementation.hasUserWallets }
|
||||
|
||||
override val selectedUserWalletSync: UserWallet?
|
||||
get() = requireImplementation.selectedUserWalletSync
|
||||
|
|
|
|||
|
|
@ -46,4 +46,8 @@ internal class WalletConnectEventsHandlerImpl : WalletConnectEventsHandler {
|
|||
override fun onUnsupportedRequest() {
|
||||
store.dispatchOnMain(WalletConnectAction.RejectUnsupportedRequest)
|
||||
}
|
||||
|
||||
override fun onPairConnectError(error: Throwable) {
|
||||
store.dispatchOnMain(WalletConnectAction.PairConnectErrorAction(error))
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +35,8 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
private val _activeSessions: MutableSharedFlow<List<WalletConnectSession>> = MutableSharedFlow()
|
||||
override val activeSessions: Flow<List<WalletConnectSession>> = _activeSessions
|
||||
|
||||
private var currentSessions: List<WalletConnectSession> = emptyList()
|
||||
override var currentSessions: List<WalletConnectSession> = emptyList()
|
||||
private set
|
||||
|
||||
/**
|
||||
* @param projectId Project ID at https://cloud.walletconnect.com/
|
||||
|
|
@ -246,7 +247,20 @@ internal class DefaultLegacyWalletConnectRepository(
|
|||
}
|
||||
|
||||
override fun pair(uri: String) {
|
||||
Web3Wallet.pair(Wallet.Params.Pair(uri))
|
||||
Web3Wallet.pair(
|
||||
params = Wallet.Params.Pair(uri),
|
||||
onSuccess = {
|
||||
Timber.i("Paired successfully: $it")
|
||||
},
|
||||
onError = {
|
||||
Timber.e("Error while pairing: $it")
|
||||
scope.launch {
|
||||
_events.emit(
|
||||
WalletConnectEvents.PairConnectError(it.throwable),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun approve(userNamespaces: Map<NetworkNamespace, List<Account>>) {
|
||||
|
|
|
|||
|
|
@ -14,26 +14,24 @@ import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
|
|||
import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl
|
||||
import com.tangem.tap.domain.walletconnect2.data.DefaultLegacyWalletConnectRepository
|
||||
import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectSessionsRepository
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
|
||||
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
|
||||
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
|
||||
import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
|
||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(ActivityComponent::class)
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object WalletConnectInteractorModule {
|
||||
|
||||
@Provides
|
||||
@ActivityScoped
|
||||
@Singleton
|
||||
fun provideWalletConnectInteractor(
|
||||
wcRepository: LegacyWalletConnectRepository,
|
||||
wcSessionsRepository: WalletConnectSessionsRepository,
|
||||
|
|
@ -41,6 +39,7 @@ internal object WalletConnectInteractorModule {
|
|||
currenciesRepository: CurrenciesRepository,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
userWalletsListManager: UserWalletsListManager,
|
||||
coroutineDispatcherProvider: CoroutineDispatcherProvider,
|
||||
): WalletConnectInteractor {
|
||||
return WalletConnectInteractor(
|
||||
handler = WalletConnectEventsHandlerImpl(),
|
||||
|
|
@ -51,7 +50,7 @@ internal object WalletConnectInteractorModule {
|
|||
currenciesRepository = currenciesRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
userWalletsListManager = userWalletsListManager,
|
||||
dispatchers = AppCoroutineDispatcherProvider(),
|
||||
dispatchers = coroutineDispatcherProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ interface LegacyWalletConnectRepository {
|
|||
|
||||
val activeSessions: Flow<List<WalletConnectSession>>
|
||||
|
||||
val currentSessions: List<WalletConnectSession>
|
||||
|
||||
fun init(projectId: String)
|
||||
|
||||
fun setUserNamespaces(userNamespaces: Map<NetworkNamespace, List<Account>>)
|
||||
|
|
|
|||
|
|
@ -16,4 +16,6 @@ interface WalletConnectEventsHandler {
|
|||
fun onSessionRequest(request: WcPreparedRequest)
|
||||
|
||||
fun onUnsupportedRequest()
|
||||
|
||||
fun onPairConnectError(error: Throwable)
|
||||
}
|
||||
|
|
@ -9,18 +9,18 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
|
|||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.filterNotNull
|
||||
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
|
||||
import com.tangem.tap.domain.walletconnect2.domain.models.*
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.Stack
|
||||
|
||||
@Suppress("LargeClass", "LongParameterList")
|
||||
class WalletConnectInteractor(
|
||||
|
|
@ -35,18 +35,27 @@ class WalletConnectInteractor(
|
|||
val blockchainHelper: WcBlockchainHelper,
|
||||
) {
|
||||
|
||||
var isWalletConnectReadyForDeepLinks = false
|
||||
|
||||
private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) {
|
||||
GetSelectedWalletUseCase(userWalletsListManager)
|
||||
}
|
||||
|
||||
private val wcScope = CoroutineScope(
|
||||
Job() + dispatchers.io + FeatureCoroutineExceptionHandler.create("wcScope"),
|
||||
SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable ->
|
||||
Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable")
|
||||
},
|
||||
)
|
||||
|
||||
private val listenerScope = CoroutineScope(
|
||||
Job() + dispatchers.io + FeatureCoroutineExceptionHandler.create("listenScope"),
|
||||
SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable ->
|
||||
Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable")
|
||||
},
|
||||
)
|
||||
|
||||
/** Stack of deeplinks to handle if user wallet is not selected or cryptocurrency statuses are not available */
|
||||
private val deeplinkStack: Stack<String> = Stack()
|
||||
|
||||
private val events = walletConnectRepository.events
|
||||
private val sessions = walletConnectRepository.activeSessions
|
||||
|
||||
|
|
@ -96,6 +105,7 @@ class WalletConnectInteractor(
|
|||
networks = currencies.map { it.network },
|
||||
)
|
||||
setUserChains(accounts)
|
||||
handleDeeplinkStack(accounts)
|
||||
}
|
||||
|
||||
private suspend fun startListeningWc(userWalletId: String, cardId: String?) {
|
||||
|
|
@ -118,6 +128,18 @@ class WalletConnectInteractor(
|
|||
walletConnectRepository.setUserNamespaces(userNamespaces)
|
||||
}
|
||||
|
||||
private fun handleDeeplinkStack(accounts: List<Account>) {
|
||||
runCatching {
|
||||
if (accounts.isEmpty()) return
|
||||
isWalletConnectReadyForDeepLinks = true
|
||||
if (deeplinkStack.empty()) return
|
||||
val lastDeeplink = deeplinkStack.pop()
|
||||
store.dispatchOnMain(WalletConnectAction.OpenSession(lastDeeplink))
|
||||
}.onFailure {
|
||||
Timber.e("WC deeplink handling failed. $it")
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun subscribeToEvents() {
|
||||
events
|
||||
.onEach { wcEvent ->
|
||||
|
|
@ -167,6 +189,9 @@ class WalletConnectInteractor(
|
|||
is WalletConnectEvents.SessionRequest -> {
|
||||
handleRequest(wcEvent)
|
||||
}
|
||||
is WalletConnectEvents.PairConnectError -> {
|
||||
handler.onPairConnectError(wcEvent.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
|
|
@ -329,6 +354,34 @@ class WalletConnectInteractor(
|
|||
return uri.lowercase().startsWith(WC_SCHEME)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Wallet Connect deep links.
|
||||
* If wallet connect is able to handle the deeplink, session is started with deeplink.
|
||||
* Otherwise, deeplink is stored until wallet connect is ready to handle it.
|
||||
*
|
||||
* @param deeplink deeplink to handle
|
||||
*/
|
||||
fun addDeeplink(deeplink: String) {
|
||||
val deeplinkRegex = Regex(WC_PARAM_REGEX)
|
||||
val matched = deeplinkRegex.findAll(deeplink)
|
||||
val sessionTopic = matched.firstOrNull { it.value.contains(WC_TOPIC_QUERY_NAME) }?.groupValues?.lastOrNull()
|
||||
|
||||
val isAlreadyActiveSessionTopic = walletConnectRepository.currentSessions.any { session ->
|
||||
session.topic == sessionTopic
|
||||
}
|
||||
|
||||
if (isAlreadyActiveSessionTopic && sessionTopic != null) {
|
||||
Timber.i("WC already has an active session topic: $deeplink")
|
||||
return
|
||||
}
|
||||
|
||||
if (isWalletConnectReadyForDeepLinks) {
|
||||
store.dispatchOnMain(WalletConnectAction.OpenSession(deeplink))
|
||||
} else {
|
||||
deeplinkStack.push(deeplink)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCardId(userWallet: UserWallet): String? {
|
||||
return if (userWallet.scanResponse.card.backupStatus?.isActive != true) {
|
||||
userWallet.cardId
|
||||
|
|
@ -367,7 +420,9 @@ class WalletConnectInteractor(
|
|||
return sessionRequestConverter.prepareRequest(sessionRequest, userWalletId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val WC_SCHEME = "wc"
|
||||
private companion object {
|
||||
const val WC_SCHEME = "wc"
|
||||
const val WC_TOPIC_QUERY_NAME = "sessionTopic"
|
||||
const val WC_PARAM_REGEX = "([a-zA-Z\\d-]+)=([a-zA-Z\\d]+)"
|
||||
}
|
||||
}
|
||||
|
|
@ -26,4 +26,6 @@ sealed interface WalletConnectEvents {
|
|||
val metaUrl: String,
|
||||
val method: String,
|
||||
) : WalletConnectEvents
|
||||
|
||||
data class PairConnectError(val error: Throwable) : WalletConnectEvents
|
||||
}
|
||||
|
|
@ -37,7 +37,10 @@ import com.tangem.utils.coroutines.saveIn
|
|||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.rekotlin.Action
|
||||
|
|
@ -265,7 +268,6 @@ class DetailsMiddleware {
|
|||
.onEach { needEnrollBiometrics ->
|
||||
store.dispatchWithMain(DetailsAction.AppSettings.BiometricsStatusChanged(needEnrollBiometrics))
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.launchIn(lifecycleScope)
|
||||
.saveIn(checkBiometricsStatusJobHolder)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,9 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppS
|
|||
},
|
||||
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
|
||||
appSettingsState = AppSettingsState(
|
||||
isBiometricsAvailable = tangemSdkManager.canUseBiometry,
|
||||
isBiometricsAvailable = runBlocking {
|
||||
tangemSdkManager.checkCanUseBiometry()
|
||||
},
|
||||
saveWallets = action.shouldSaveUserWallets,
|
||||
saveAccessCodes = runBlocking {
|
||||
store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
|
||||
|
|
|
|||
|
|
@ -22,17 +22,19 @@ sealed class WalletConnectAction : Action {
|
|||
|
||||
data class ShowClipboardOrScanQrDialog(val wcUri: String) : WalletConnectAction()
|
||||
//region WalletConnect 2.0
|
||||
object ApproveProposal : WalletConnectAction()
|
||||
object RejectProposal : WalletConnectAction()
|
||||
data object ApproveProposal : WalletConnectAction()
|
||||
data object RejectProposal : WalletConnectAction()
|
||||
|
||||
object SessionEstablished : WalletConnectAction()
|
||||
data object SessionEstablished : WalletConnectAction()
|
||||
data class SessionRejected(val error: WalletConnectError) : WalletConnectAction()
|
||||
data class SessionListUpdated(val sessions: List<WcSessionForScreen>) : WalletConnectAction()
|
||||
|
||||
data class ShowSessionRequest(val sessionRequest: WcPreparedRequest) : WalletConnectAction()
|
||||
|
||||
object RejectUnsupportedRequest : WalletConnectAction()
|
||||
data object RejectUnsupportedRequest : WalletConnectAction()
|
||||
|
||||
data class PerformRequestedAction(val sessionRequest: WcPreparedRequest) : WalletConnectAction()
|
||||
|
||||
data class PairConnectErrorAction(val throwable: Throwable) : WalletConnectAction()
|
||||
//endregion WalletConnect 2.0
|
||||
}
|
||||
|
|
@ -49,8 +49,11 @@ class WalletConnectMiddleware {
|
|||
|
||||
when (action) {
|
||||
is WalletConnectAction.HandleDeepLink -> {
|
||||
if (!action.wcUri.isNullOrBlank()) {
|
||||
store.dispatchOnMain(WalletConnectAction.OpenSession(action.wcUri))
|
||||
val wsUrl = action.wcUri
|
||||
Timber.i("WC deeplink: $wsUrl")
|
||||
if (!wsUrl.isNullOrBlank()) {
|
||||
Timber.i("WC deeplink added to stack: $wsUrl")
|
||||
walletConnectInteractor.addDeeplink(wsUrl)
|
||||
}
|
||||
}
|
||||
is WalletConnectAction.DisconnectSession -> {
|
||||
|
|
@ -181,6 +184,9 @@ class WalletConnectMiddleware {
|
|||
is WalletConnectAction.RejectUnsupportedRequest -> {
|
||||
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork()))
|
||||
}
|
||||
is WalletConnectAction.PairConnectErrorAction -> {
|
||||
store.dispatch(GlobalAction.ShowDialog(WalletConnectDialog.PairConnectErrorDialog(action.throwable)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ object WalletConnectReducer {
|
|||
is WalletConnectAction.RejectProposal,
|
||||
is WalletConnectAction.SessionEstablished,
|
||||
is WalletConnectAction.SessionRejected,
|
||||
is WalletConnectAction.PairConnectErrorAction,
|
||||
-> state.copy(loading = false)
|
||||
is WalletConnectAction.SessionListUpdated -> state.copy(
|
||||
wc2Sessions = action.sessions,
|
||||
|
|
|
|||
|
|
@ -117,6 +117,8 @@ sealed class WalletConnectDialog : StateDialog {
|
|||
data class SignTransactionDialog(
|
||||
val data: WcPreparedRequest.SignTransaction,
|
||||
) : WalletConnectDialog()
|
||||
|
||||
data class PairConnectErrorDialog(val error: Throwable) : WalletConnectDialog()
|
||||
}
|
||||
|
||||
data class WcTransactionData(
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ object OnboardingHelper {
|
|||
}
|
||||
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
|
||||
// then open save wallet screen
|
||||
tangemSdkManager.canUseBiometry && settingsRepository.shouldShowSaveUserWalletScreen() -> {
|
||||
tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen() -> {
|
||||
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
|
||||
|
||||
delay(timeMillis = 1_200)
|
||||
|
|
|
|||
|
|
@ -483,7 +483,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
val backupValidator = BackupValidator()
|
||||
if (!backupValidator.isValid(CardDTO(result.data))) {
|
||||
if (!backupValidator.isValidBackupStatus(CardDTO(result.data))) {
|
||||
store.dispatchOnMain(BackupAction.ErrorInBackupCard)
|
||||
}
|
||||
if (backupService.currentState == BackupService.State.Finished) {
|
||||
|
|
|
|||
|
|
@ -91,10 +91,10 @@ internal class SaveWalletMiddleware {
|
|||
store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
|
||||
}
|
||||
|
||||
private fun allowToUseBiometrics(state: SaveWalletState) {
|
||||
if (tangemSdkManager.needEnrollBiometrics) {
|
||||
store.dispatchOnMain(SaveWalletAction.EnrollBiometrics)
|
||||
return
|
||||
private fun allowToUseBiometrics(state: SaveWalletState) = scope.launch {
|
||||
if (tangemSdkManager.checkNeedEnrollBiometrics()) {
|
||||
store.dispatchWithMain(SaveWalletAction.EnrollBiometrics)
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (state.backupInfo != null) {
|
||||
|
|
@ -104,24 +104,22 @@ internal class SaveWalletMiddleware {
|
|||
Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On))
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
/*
|
||||
/*
|
||||
|
||||
* because it will be automatically saved on UserWalletsListManager switch
|
||||
*/
|
||||
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
|
||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
|
||||
val error = IllegalStateException("No selected user wallet")
|
||||
Timber.e(error, "Unable to save user wallet")
|
||||
store.dispatchWithMain(
|
||||
SaveWalletAction.AllowToUseBiometrics.Error(TangemSdkError.ExceptionError(error)),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
* because it will be automatically saved on UserWalletsListManager switch
|
||||
*/
|
||||
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
|
||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
|
||||
val error = IllegalStateException("No selected user wallet")
|
||||
Timber.e(error, "Unable to save user wallet")
|
||||
store.dispatchWithMain(
|
||||
SaveWalletAction.AllowToUseBiometrics.Error(TangemSdkError.ExceptionError(error)),
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
handleSuccessAllowing(selectedUserWallet)
|
||||
}.saveIn(saveWalletJobHolder)
|
||||
}
|
||||
handleSuccessAllowing(selectedUserWallet)
|
||||
}.saveIn(saveWalletJobHolder)
|
||||
|
||||
private suspend fun handleSuccessAllowing(userWallet: UserWallet) {
|
||||
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true)
|
||||
|
|
|
|||
|
|
@ -29,9 +29,13 @@ internal class DefaultTokensListRouter : TokensListRouter {
|
|||
store.dispatchNotification(R.string.contract_address_copied_message)
|
||||
}
|
||||
|
||||
override fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String) {
|
||||
override fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String, networkName: String) {
|
||||
store.dispatchDialogShow(
|
||||
dialog = AppDialog.TokensAreLinkedDialog(currencyTitle = tokenName, currencySymbol = tokenSymbol),
|
||||
dialog = AppDialog.TokensAreLinkedDialog(
|
||||
currencyTitle = tokenName,
|
||||
currencySymbol = tokenSymbol,
|
||||
networkName = networkName,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ internal interface TokensListRouter {
|
|||
*
|
||||
* @param tokenName token name
|
||||
* @param tokenSymbol token brief name
|
||||
* @param networkName blockchain network full name
|
||||
*/
|
||||
fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String)
|
||||
fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String, networkName: String)
|
||||
|
||||
/**
|
||||
* Open alert to remove wallet
|
||||
|
|
|
|||
|
|
@ -365,6 +365,7 @@ internal class TokensListViewModel @Inject constructor(
|
|||
router.openUnableHideMainTokenAlert(
|
||||
tokenName = blockchain.name,
|
||||
tokenSymbol = blockchain.currency,
|
||||
networkName = blockchain.fullName,
|
||||
)
|
||||
} else if (isAddedOnMainScreen) {
|
||||
router.openRemoveWalletAlert(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import okio.Timeout
|
|||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import timber.log.Timber
|
||||
|
||||
internal class ApiResponseCallDelegate<T : Any>(
|
||||
private val wrappedCall: Call<T>,
|
||||
|
|
@ -20,7 +21,9 @@ internal class ApiResponseCallDelegate<T : Any>(
|
|||
override fun timeout(): Timeout = wrappedCall.timeout()
|
||||
override fun isExecuted(): Boolean = wrappedCall.isExecuted
|
||||
override fun isCanceled(): Boolean = wrappedCall.isCanceled
|
||||
override fun cancel() { wrappedCall.cancel() }
|
||||
override fun cancel() {
|
||||
wrappedCall.cancel()
|
||||
}
|
||||
|
||||
private inner class ApiResponseCallback(
|
||||
private val responseCallback: Callback<ApiResponse<T>>,
|
||||
|
|
@ -33,7 +36,15 @@ internal class ApiResponseCallDelegate<T : Any>(
|
|||
}
|
||||
|
||||
override fun onFailure(call: Call<T>, t: Throwable) {
|
||||
val error = t.toApiError()
|
||||
val error = try {
|
||||
t.toApiError()
|
||||
} catch (e: ApiResponseError) {
|
||||
Timber.e(e, "error map toApiError")
|
||||
e
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "onFailure UnknownException")
|
||||
ApiResponseError.UnknownException(e)
|
||||
}
|
||||
val safeResponse = apiError<T>(error)
|
||||
|
||||
responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse))
|
||||
|
|
|
|||
|
|
@ -73,10 +73,16 @@ sealed class ApiResponseError : Exception() {
|
|||
}
|
||||
|
||||
/** Represents a network error, typically when there's no connectivity. */
|
||||
data object NetworkException : ApiResponseError()
|
||||
@Suppress("UnusedPrivateMember")
|
||||
data object NetworkException : ApiResponseError() {
|
||||
private fun readResolve(): Any = NetworkException
|
||||
}
|
||||
|
||||
/** Represents a timeout error, typically when the server takes too long to respond. */
|
||||
data object TimeoutException : ApiResponseError()
|
||||
@Suppress("UnusedPrivateMember")
|
||||
data object TimeoutException : ApiResponseError() {
|
||||
private fun readResolve(): Any = TimeoutException
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an unexpected exception that doesn't fall into one of the other categories.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.response
|
|||
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import retrofit2.Response
|
||||
import timber.log.Timber
|
||||
import java.net.ConnectException
|
||||
import java.net.SocketTimeoutException
|
||||
import java.net.UnknownHostException
|
||||
|
|
@ -16,10 +17,15 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(): ApiResponse<T> {
|
|||
} else {
|
||||
val code = ApiResponseError.HttpException.Code.values
|
||||
.firstOrNull { it.code == code() }
|
||||
val e = if (code == null) {
|
||||
ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}"))
|
||||
} else {
|
||||
ApiResponseError.HttpException(code, message(), errorBody()?.string())
|
||||
val e = try {
|
||||
if (code == null) {
|
||||
ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}"))
|
||||
} else {
|
||||
ApiResponseError.HttpException(code, message(), errorBody()?.string())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "UnknownException occured")
|
||||
ApiResponseError.UnknownException(e)
|
||||
}
|
||||
|
||||
apiError(e)
|
||||
|
|
|
|||
|
|
@ -673,7 +673,7 @@
|
|||
<string name="wallet_settings_title">Настройки кошелька</string>
|
||||
<string name="wallet_title">Tangem</string>
|
||||
<string name="warning_access_denied_message">Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку</string>
|
||||
<string name="warning_backup_errors_message">Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян.</string>
|
||||
<string name="warning_backup_errors_message">Похоже, что процесс активации карт не был завершен корректно. Это могло быть вызвано проблемой взаимодействия с модулем NFC либо некорректным прикладыванием карты к телефону. Пожалуйста, обратитесь в нашу службу поддержки для уточнения деталей.</string>
|
||||
<string name="warning_backup_errors_title">Ошибка активации</string>
|
||||
<string name="warning_beacon_chain_retirement_content">По решению разработчиков сети BNB стандарт BEP-2 перестанет поддерживаться в июне 2024 года. Чтобы не потерять активы, их необходимо преобразовать в стандарт BEP-20. Используйте функцию обмена в приложении или сторонние сервисы, чтобы перевести средства в cеть BNB Smart Chain.</string>
|
||||
<string name="warning_beacon_chain_retirement_title">Отключение сети BNB Beacon Chain</string>
|
||||
|
|
|
|||
|
|
@ -666,7 +666,7 @@
|
|||
<string name="wallet_settings_title">Wallet settings</string>
|
||||
<string name="wallet_title">Tangem</string>
|
||||
<string name="warning_access_denied_message">Use %s or scan a card to unlock access to your wallet</string>
|
||||
<string name="warning_backup_errors_message">Please withdraw all funds from this wallet, reset it to factory settings, and create a new one. Access to the current wallet will be lost.</string>
|
||||
<string name="warning_backup_errors_message">It seems that the card activation was not completed correctly. This could be due to an issue with your device\'s NFC module or incorrect tapping of the card to your device. Please contact our Support team for assistance.</string>
|
||||
<string name="warning_backup_errors_title">Activation error</string>
|
||||
<string name="warning_beacon_chain_retirement_content">According to BNB network developers, support for the BEP-2 standard will end in June 2024. To avoid losing assets with this standard, please convert them to the BEP-20 standard. Use our swap service or third-party services to transfer funds to the BNB Smart Chain network.</string>
|
||||
<string name="warning_beacon_chain_retirement_title">BNB Beacon Chain will shut down</string>
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvid
|
|||
title = resourceReference(id = R.string.main_swap_changelly_promotion_title),
|
||||
subtitle = resourceReference(
|
||||
id = R.string.main_swap_changelly_promotion_message,
|
||||
formatArgs = wrappedList("1", "2"),
|
||||
formatArgs = wrappedList("1", "2", "3"),
|
||||
),
|
||||
iconResId = R.drawable.img_swap_promo,
|
||||
backgroundResId = R.drawable.img_swap_promo_blue_banner_background,
|
||||
|
|
@ -180,7 +180,7 @@ private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvid
|
|||
NotificationConfig(
|
||||
title = resourceReference(
|
||||
id = R.string.token_swap_changelly_promotion_title,
|
||||
formatArgs = wrappedList("1", "2"),
|
||||
formatArgs = wrappedList("1"),
|
||||
),
|
||||
subtitle = stringReference(
|
||||
"Swap multiple currencies between any chains you wish. Swap multiple " +
|
||||
|
|
|
|||
|
|
@ -82,7 +82,9 @@ private fun LazyListScope.contentItems(
|
|||
when (item) {
|
||||
is TxHistoryState.TxHistoryItemState.GroupTitle -> item.itemKey
|
||||
is TxHistoryState.TxHistoryItemState.Title -> item.onExploreClick.hashCode()
|
||||
is TxHistoryState.TxHistoryItemState.Transaction -> item.state.txHash
|
||||
is TxHistoryState.TxHistoryItemState.Transaction ->
|
||||
item.state.txHash +
|
||||
((item.state as? TransactionState.Content)?.hashCode() ?: "")
|
||||
}
|
||||
},
|
||||
contentType = txHistoryItems.itemContentType { it::class.java },
|
||||
|
|
|
|||
|
|
@ -52,19 +52,23 @@ internal class CoinsPagingSource(
|
|||
suffix = CryptoCurrency.ID.Suffix.RawID(coin.id),
|
||||
)
|
||||
}
|
||||
val quotes = quotesRepository.getQuotesSync(currenciesIds = coinsIds.toSet(), refresh = false)
|
||||
|
||||
LoadResult.Page(
|
||||
data = CoinsResponseConverter.convert(
|
||||
CoinsData(
|
||||
response.coins,
|
||||
response.imageHost,
|
||||
quotes,
|
||||
try {
|
||||
val quotes = quotesRepository.getQuotesSync(currenciesIds = coinsIds.toSet(), refresh = false)
|
||||
LoadResult.Page(
|
||||
data = CoinsResponseConverter.convert(
|
||||
CoinsData(
|
||||
response.coins,
|
||||
response.imageHost,
|
||||
quotes,
|
||||
),
|
||||
),
|
||||
),
|
||||
prevKey = if (page == 0) null else page.minus(other = 1),
|
||||
nextKey = if (response.coins.isEmpty()) null else page.plus(other = 1),
|
||||
)
|
||||
prevKey = if (page == 0) null else page.minus(other = 1),
|
||||
nextKey = if (response.coins.isEmpty()) null else page.plus(other = 1),
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
LoadResult.Error(t)
|
||||
}
|
||||
},
|
||||
onFailure = { LoadResult.Error(it) },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ import java.math.BigDecimal
|
|||
|
||||
sealed interface LegacyAction : Action {
|
||||
|
||||
object SendEmailRateCanBeBetter : LegacyAction
|
||||
data object SendEmailSupport : LegacyAction
|
||||
|
||||
data object SendEmailRateCanBeBetter : LegacyAction
|
||||
|
||||
/**
|
||||
* Initiate an onboarding process.
|
||||
|
|
|
|||
|
|
@ -4,5 +4,5 @@ import com.tangem.domain.settings.repositories.LegacySettingsRepository
|
|||
|
||||
class CanUseBiometryUseCase(private val legacySettingsRepository: LegacySettingsRepository) {
|
||||
|
||||
operator fun invoke(): Boolean = legacySettingsRepository.canUseBiometry()
|
||||
suspend operator fun invoke(): Boolean = legacySettingsRepository.canUseBiometry()
|
||||
}
|
||||
|
|
@ -2,5 +2,5 @@ package com.tangem.domain.settings.repositories
|
|||
|
||||
interface LegacySettingsRepository {
|
||||
|
||||
fun canUseBiometry(): Boolean
|
||||
suspend fun canUseBiometry(): Boolean
|
||||
}
|
||||
|
|
@ -23,12 +23,8 @@ platform :android do
|
|||
|
||||
desc "Runs all the tests"
|
||||
lane :test do
|
||||
gradle(
|
||||
task: "clean assemble",
|
||||
build_type: "Debug"
|
||||
)
|
||||
gradle(task: "detekt")
|
||||
gradle(task: "test")
|
||||
gradle(task: "testDebugUnitTest")
|
||||
end
|
||||
|
||||
desc "Build a signed release APK"
|
||||
|
|
|
|||
|
|
@ -1949,13 +1949,21 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map<CryptoCurrency.ID, Quote> {
|
||||
val set = quotesRepository.getQuotesSync(ids.toSet(), false)
|
||||
val set = ids.toSet().getQuotesOrEmpty(false)
|
||||
|
||||
return ids
|
||||
.mapNotNull { id -> set.find { it.rawCurrencyId == id.rawCurrencyId }?.let { id to it } }
|
||||
.toMap()
|
||||
}
|
||||
|
||||
private suspend fun Set<CryptoCurrency.ID>.getQuotesOrEmpty(refresh: Boolean): Set<Quote> {
|
||||
return try {
|
||||
quotesRepository.getQuotesSync(this, refresh)
|
||||
} catch (t: Throwable) {
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDemoFees(cryptoCurrency: CryptoCurrency): ProxyFees.MultipleFees {
|
||||
val demoFee = ProxyAmount(
|
||||
currencySymbol = cryptoCurrency.symbol,
|
||||
|
|
|
|||
|
|
@ -73,7 +73,8 @@ internal class ExchangeStatusFactory(
|
|||
.map { savedTransactions ->
|
||||
val quotes = savedTransactions
|
||||
?.flatMap { setOf(it.fromCryptoCurrency.id, it.toCryptoCurrency.id) }
|
||||
?.let { quotesRepository.getQuotesSync(it.toSet(), true) }
|
||||
?.toSet()
|
||||
?.getQuotesOrEmpty(true)
|
||||
?: emptySet()
|
||||
|
||||
getExchangeStatusState(
|
||||
|
|
@ -216,4 +217,12 @@ internal class ExchangeStatusFactory(
|
|||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun Set<CryptoCurrency.ID>.getQuotesOrEmpty(refresh: Boolean): Set<Quote> {
|
||||
return try {
|
||||
quotesRepository.getQuotesSync(this, refresh)
|
||||
} catch (t: Throwable) {
|
||||
emptySet()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,10 +7,14 @@ import javax.inject.Inject
|
|||
|
||||
class BackupValidator @Inject constructor() {
|
||||
|
||||
fun isValid(cardDTO: CardDTO): Boolean {
|
||||
fun isValidFull(cardDTO: CardDTO): Boolean {
|
||||
return validateBackupStatus(cardDTO) && validateCurves(cardDTO)
|
||||
}
|
||||
|
||||
fun isValidBackupStatus(cardDTO: CardDTO): Boolean {
|
||||
return validateBackupStatus(cardDTO)
|
||||
}
|
||||
|
||||
private fun validateCurves(cardDTO: CardDTO): Boolean {
|
||||
val config = CardConfig.createConfig(cardDTO)
|
||||
// / Since the curve `bls12381_G2_AUG` was added later into first generation of wallets,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
buildList {
|
||||
addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents)
|
||||
|
||||
addCriticalNotifications(userWallet)
|
||||
addCriticalNotifications(userWallet, clickIntents)
|
||||
|
||||
addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents)
|
||||
|
||||
|
|
@ -86,11 +86,14 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addCriticalNotifications(userWallet: UserWallet) {
|
||||
private fun MutableList<WalletNotification>.addCriticalNotifications(
|
||||
userWallet: UserWallet,
|
||||
clickIntents: WalletClickIntents,
|
||||
) {
|
||||
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
|
||||
addIf(
|
||||
element = WalletNotification.Critical.BackupError,
|
||||
condition = !backupValidator.isValid(userWallet.scanResponse.card) || userWallet.hasBackupError,
|
||||
element = WalletNotification.Critical.BackupError { clickIntents.onSupportClick() },
|
||||
condition = !backupValidator.isValidBackupStatus(userWallet.scanResponse.card) || userWallet.hasBackupError,
|
||||
)
|
||||
|
||||
addIf(
|
||||
|
|
|
|||
|
|
@ -20,11 +20,16 @@ import org.joda.time.DateTime
|
|||
@Immutable
|
||||
sealed class WalletNotification(val config: NotificationConfig) {
|
||||
|
||||
sealed class Critical(title: TextReference, subtitle: TextReference) : WalletNotification(
|
||||
sealed class Critical(
|
||||
title: TextReference,
|
||||
subtitle: TextReference,
|
||||
buttonsState: NotificationConfig.ButtonsState? = null,
|
||||
) : WalletNotification(
|
||||
config = NotificationConfig(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
buttonsState = buttonsState,
|
||||
),
|
||||
) {
|
||||
|
||||
|
|
@ -38,9 +43,13 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
|||
subtitle = resourceReference(id = R.string.warning_failed_to_verify_card_message),
|
||||
)
|
||||
|
||||
data object BackupError : Critical(
|
||||
data class BackupError(val onSupportClick: () -> Unit) : Critical(
|
||||
title = resourceReference(R.string.warning_backup_errors_title),
|
||||
subtitle = resourceReference(R.string.warning_backup_errors_message),
|
||||
buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig(
|
||||
text = resourceReference(id = R.string.details_row_title_contact_to_support),
|
||||
onClick = onSupportClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ internal interface WalletWarningsClickIntents {
|
|||
fun onTravalaPromoClick(link: String?)
|
||||
|
||||
fun onCloseTravalaPromoClick()
|
||||
|
||||
fun onSupportClick()
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -254,7 +256,11 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun getSelectedUserWallet(): UserWallet? {
|
||||
override fun onSupportClick() {
|
||||
reduxStateHolder.dispatch(LegacyAction.SendEmailSupport)
|
||||
}
|
||||
|
||||
private fun getSelectedUserWallet(): UserWallet? {
|
||||
val userWalletId = stateHolder.getSelectedWalletId()
|
||||
return getUserWalletUseCase(userWalletId).getOrElse {
|
||||
Timber.e(
|
||||
|
|
|
|||
|
|
@ -87,9 +87,9 @@ markdown = "0.7.2"
|
|||
# endregion Other libraries
|
||||
|
||||
# region Tangem
|
||||
tangemBlockchainSdk = "release-app_5.12-698"
|
||||
tangemBlockchainSdk = "release-app_5.12-701"
|
||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||
tangemCardSdk = "release-app_5.12-369"
|
||||
tangemCardSdk = "release-app_5.12-373"
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
# endregion Tangem
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue