Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-29 13:07:57 +03:00
commit b0d5ab72e1
50 changed files with 378 additions and 137 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,4 +16,6 @@ interface WalletConnectEventsHandler {
fun onSessionRequest(request: WcPreparedRequest)
fun onUnsupportedRequest()
fun onPairConnectError(error: Throwable)
}

View file

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

View file

@ -26,4 +26,6 @@ sealed interface WalletConnectEvents {
val metaUrl: String,
val method: String,
) : WalletConnectEvents
data class PairConnectError(val error: Throwable) : WalletConnectEvents
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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