Updated on 2026-08-14
This commit is contained in:
commit
b0d5ab72e1
50 changed files with 378 additions and 137 deletions
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue