Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-05 18:54:08 +04:00
parent fd7bc522f2
commit 194e750c93
74 changed files with 52 additions and 4627 deletions

View file

@ -1,44 +1,22 @@
package com.tangem.tap.features.demo
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.Action
object DemoHelper {
val config = DemoConfig()
private val disabledActionFeatures = listOf(
WalletConnectAction.StartWalletConnect::class.java,
)
fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId)
fun isTestDemoCard(scanResponse: ScanResponse): Boolean = config.isTestDemoCardId(scanResponse.card.cardId)
fun isDemoCardId(cardId: String): Boolean = config.isDemoCardId(cardId)
fun tryHandle(appState: () -> AppState?, action: Action): Boolean {
fun tryHandle(appState: () -> AppState?): Boolean {
val scanResponse = getScanResponse(appState) ?: return false
if (!scanResponse.isDemoCard()) return false
disabledActionFeatures.firstOrNull { it == action::class.java }?.let {
val uiMessageSender = store.inject(DaggerGraphState::uiMessageSender)
uiMessageSender.send(
DialogMessage(
message = resourceReference(R.string.alert_demo_feature_disabled),
),
)
return true
}
return false
}

View file

@ -39,7 +39,7 @@ class DetailsMiddleware {
val detailsMiddleware: Middleware<AppState> = { _, stateProvider ->
{ next ->
{ action ->
if (!DemoHelper.tryHandle(stateProvider, action)) {
if (!DemoHelper.tryHandle(stateProvider)) {
val detailsState = stateProvider()?.detailsState
if (detailsState != null) {
handleAction(detailsState, action)

View file

@ -1,45 +0,0 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
import org.rekotlin.Action
sealed class WalletConnectAction : Action {
data class HandleDeepLink(val wcUri: String?) : WalletConnectAction()
data object StartWalletConnect : WalletConnectAction()
data class OpenSession(
val wcUri: String,
val source: SourceType,
val userWalletId: UserWalletId,
) : WalletConnectAction() {
enum class SourceType { QR, DEEPLINK, CLIPBOARD, ETC }
}
data class DisconnectSession(val topic: String) : WalletConnectAction()
data class RejectRequest(val topic: String, val id: Long) : WalletConnectAction()
//region WalletConnect 2.0
data class ApproveProposal(val proposal: WalletConnectEvents.SessionProposal) : WalletConnectAction()
data object RejectProposal : 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()
data object RejectUnsupportedRequest : WalletConnectAction()
data class PerformRequestedAction(val sessionRequest: WcPreparedRequest) : WalletConnectAction()
data class PairConnectErrorAction(val throwable: Throwable) : WalletConnectAction()
data object UnsupportedDappRequest : WalletConnectAction()
//endregion WalletConnect 2.0
}

View file

@ -1,202 +0,0 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.common.routing.AppRoute
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
@Suppress("LargeClass")
class WalletConnectMiddleware {
private val walletConnectInteractor: WalletConnectInteractor
get() = store.inject(DaggerGraphState::walletConnectInteractor)
private val walletConnectRepository: LegacyWalletConnectRepository
get() = store.inject(DaggerGraphState::walletConnectRepository)
val walletConnectMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handle(state, action)
next(action)
}
}
}
@Suppress("ComplexMethod", "LongMethod")
private fun handle(state: () -> AppState?, action: Action) {
if (DemoHelper.tryHandle(state, action)) return
when (action) {
is WalletConnectAction.HandleDeepLink -> {
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 -> {
walletConnectInteractor.disconnectSession(action.topic)
}
is WalletConnectAction.StartWalletConnect -> {
store.dispatchNavigationAction {
push(AppRoute.QrScanning(AppRoute.QrScanning.Source.WalletConnect))
}
}
is WalletConnectAction.OpenSession -> {
val index = action.wcUri.indexOf("@")
when (action.wcUri[index + 1]) {
'2' -> walletConnectRepository.pair(
uri = action.wcUri,
source = action.source,
userWalletId = action.userWalletId,
)
'1' -> {
store.dispatchOnMain(WalletConnectAction.UnsupportedDappRequest)
store.dispatchOnMain(
GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedWcVersion),
)
}
}
}
is WalletConnectAction.RejectRequest -> {
walletConnectInteractor.cancelRequest(action.topic, action.id)
}
is WalletConnectAction.ApproveProposal -> {
scope.launch {
val proposalChainIds = action.proposal.requiredChainIds + action.proposal.optionalChainIds
val proposalBlockchains =
walletConnectInteractor.blockchainHelper.chainIdsToBlockchains(proposalChainIds)
val accounts = getWalletManagers()
.filter { walletManager -> proposalBlockchains.contains(walletManager.wallet.blockchain) }
.flatMap {
val wallet = it.wallet
val chainIds = walletConnectInteractor.blockchainHelper.networkIdToChainIdOrNull(
wallet.blockchain.toNetworkId(),
)
walletConnectInteractor.blockchainHelper.chainIdsToAccounts(
walletAddress = wallet.address,
chainIds = chainIds,
derivationPath = wallet.publicKey.derivationPath?.rawPath,
)
}
walletConnectInteractor.approveSessionProposal(accounts)
}
}
is WalletConnectAction.RejectProposal -> {
walletConnectInteractor.rejectSessionProposal()
}
is WalletConnectAction.SessionEstablished -> {
}
is WalletConnectAction.SessionRejected -> {
when (action.error) {
is WalletConnectError.ApprovalErrorAddNetwork -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.AddNetwork(action.error.networks),
),
)
}
is WalletConnectError.ApprovalErrorUnsupportedNetwork -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.UnsupportedNetwork(action.error.unsupportedNetworks),
),
)
}
is WalletConnectError.UnsupportedDApp -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.UnsupportedDapp,
),
)
}
is WalletConnectError.UnknownError -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
AppDialog.SimpleOkDialogRes(
headerId = R.string.wallet_connect_title,
messageId = R.string.wallet_connect_error_with_framework_message,
args = listOf(action.error.message),
),
),
)
}
is WalletConnectError.ExternalApprovalError -> {
Timber.e(action.error, "ExternalApprovalError ${action.error.message}")
// do not show dialog on this event
// val message = action.error.message
// if (!message.isNullOrEmpty()) {
// store.dispatchOnMain(
// GlobalAction.ShowDialog(
// AppDialog.SimpleOkWarningDialog(
// message = message,
// ),
// ),
// )
// }
}
else -> Unit
}
}
is WalletConnectAction.ShowSessionRequest -> {
val dialog: WalletConnectDialog = when (val request = action.sessionRequest) {
is WcPreparedRequest.BnbTransaction -> WalletConnectDialog.BnbTransactionDialog(request)
is WcPreparedRequest.EthTransaction -> WalletConnectDialog.RequestTransaction(request)
is WcPreparedRequest.EthSign -> WalletConnectDialog.PersonalSign(request)
is WcPreparedRequest.SolanaSignTransaction ->
WalletConnectDialog.SignTransactionDialog(request)
is WcPreparedRequest.SolanaSignMultipleTransactions ->
WalletConnectDialog.SignTransactionsDialog(request)
}
store.dispatch(GlobalAction.ShowDialog(dialog))
}
is WalletConnectAction.PerformRequestedAction -> {
scope.launch { walletConnectInteractor.continueWithRequest(action.sessionRequest) }
}
is WalletConnectAction.RejectUnsupportedRequest -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork()))
}
is WalletConnectAction.PairConnectErrorAction -> {
store.dispatch(GlobalAction.ShowDialog(WalletConnectDialog.PairConnectErrorDialog(action.throwable)))
}
}
}
private suspend fun getWalletManagers(): List<WalletManager> {
val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade)
val userWallet = getSelectedWallet() ?: return emptyList()
return walletManagerFacade.getStoredWalletManagers(userWallet.walletId)
}
private suspend fun getSelectedWallet(): UserWallet? {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles)
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.selectedUserWalletSync()
} else {
userWalletsListManager.selectedUserWalletSync
}
}
}

View file

@ -1,26 +0,0 @@
package com.tangem.tap.features.details.redux.walletconnect
import org.rekotlin.Action
object WalletConnectReducer {
fun reduce(action: Action, state: WalletConnectState): WalletConnectState {
if (action !is WalletConnectAction) return state
return when (action) {
is WalletConnectAction.OpenSession -> {
state.copy(loading = true)
}
is WalletConnectAction.ApproveProposal -> state.copy(loading = true)
is WalletConnectAction.RejectProposal,
is WalletConnectAction.SessionEstablished,
is WalletConnectAction.SessionRejected,
is WalletConnectAction.PairConnectErrorAction,
is WalletConnectAction.UnsupportedDappRequest,
-> state.copy(loading = false)
is WalletConnectAction.SessionListUpdated -> state.copy(
wc2Sessions = action.sessions,
)
else -> state
}
}
}

View file

@ -1,173 +0,0 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.squareup.moshi.JsonClass
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData
data class WalletConnectState(
val loading: Boolean = false,
val wc2Sessions: List<WcSessionForScreen> = listOf(),
val newSessionData: NewWcSessionData? = null,
)
data class NewWcSessionData(
val session: WalletConnectSession,
val scanResponse: ScanResponse,
val blockchain: Blockchain?,
)
data class WalletConnectSession(
val peerId: String,
val remotePeerId: String?,
val wallet: WalletForSession,
) {
fun getAddress(): String? {
val key = wallet.derivedPublicKey ?: wallet.walletPublicKey ?: return null
return wallet.blockchain?.makeAddresses(key)?.first()?.value
}
}
@JsonClass(generateAdapter = true)
data class WalletForSession(
val walletPublicKey: ByteArray? = null,
val derivedPublicKey: ByteArray? = null,
val derivationPath: DerivationPath? = null,
val derivationStyle: DerivationStyle? = null,
val isTestNet: Boolean = false,
val blockchain: Blockchain? = if (isTestNet) Blockchain.EthereumTestnet else Blockchain.Ethereum,
) {
fun getBlockchainForSession(): Blockchain {
return blockchain ?: if (isTestNet) Blockchain.EthereumTestnet else Blockchain.Ethereum
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as WalletForSession
if (walletPublicKey != null) {
if (other.walletPublicKey == null) return false
if (!walletPublicKey.contentEquals(other.walletPublicKey)) return false
} else if (other.walletPublicKey != null) return false
if (derivedPublicKey != null) {
if (other.derivedPublicKey == null) return false
if (!derivedPublicKey.contentEquals(other.derivedPublicKey)) return false
} else if (other.derivedPublicKey != null) return false
if (derivationPath != other.derivationPath) return false
if (isTestNet != other.isTestNet) return false
return blockchain == other.blockchain
}
override fun hashCode(): Int {
var result = walletPublicKey?.contentHashCode() ?: 0
result = 31 * result + (derivedPublicKey?.contentHashCode() ?: 0)
result = 31 * result + (derivationPath?.hashCode() ?: 0)
result = 31 * result + isTestNet.hashCode()
result = 31 * result + (blockchain?.hashCode() ?: 0)
return result
}
}
sealed class WalletConnectDialog : StateDialog {
data object UnsupportedWcVersion : WalletConnectDialog()
data object UnsupportedCard : WalletConnectDialog()
data class UnsupportedNetwork(val networks: List<String>? = null) : WalletConnectDialog()
data object UnsupportedDapp : WalletConnectDialog()
data class AddNetwork(val networks: List<String>) : WalletConnectDialog()
data object OpeningSessionRejected : WalletConnectDialog()
data object SessionTimeout : WalletConnectDialog()
data class ApproveWcSession(
val session: WalletConnectSession,
val networks: List<Blockchain>,
) : WalletConnectDialog()
data class SessionProposalDialog(
val sessionProposal: WalletConnectEvents.SessionProposal,
val networks: String,
val onApprove: () -> Unit,
val onReject: () -> Unit,
) : WalletConnectDialog()
data class ChooseNetwork(
val session: WalletConnectSession,
val networks: List<Blockchain>,
) : WalletConnectDialog()
data class RequestTransaction(val data: WcPreparedRequest.EthTransaction) :
WalletConnectDialog()
data class PersonalSign(val data: WcPreparedRequest.EthSign) : WalletConnectDialog()
data class BnbTransactionDialog(
val data: WcPreparedRequest.BnbTransaction,
) : WalletConnectDialog()
data class SignTransactionDialog(
val data: WcPreparedRequest.SolanaSignTransaction,
) : WalletConnectDialog()
data class SignTransactionsDialog(
val data: WcPreparedRequest.SolanaSignMultipleTransactions,
) : WalletConnectDialog()
data class PairConnectErrorDialog(val error: Throwable) : WalletConnectDialog()
}
data class WcTransactionData(
val type: WcEthTransactionType,
val transaction: TransactionData,
val topic: String,
val id: Long,
val walletManager: WalletManager,
val dialogData: TransactionRequestDialogData,
)
enum class WcEthTransactionType {
EthSignTransaction,
EthSendTransaction,
}
data class WcPersonalSignData(
val hash: ByteArray,
val topic: String,
val id: Long,
val dialogData: PersonalSignDialogData,
val type: WcSignMessage.WCSignType,
)
sealed class BinanceMessageData(
val address: String,
val data: ByteArray,
) {
class Trade(
val tradeData: List<TradeData>,
address: String,
data: ByteArray,
) : BinanceMessageData(address, data)
class Transfer(
val outputAddress: String,
val amount: String,
address: String,
data: ByteArray,
) : BinanceMessageData(address, data)
}
data class TradeData(
val price: String,
val quantity: String,
val amount: String,
val symbol: String,
)

View file

@ -1,73 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import com.arkivanov.essenty.lifecycle.subscribe
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent
import com.tangem.tap.store
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import org.rekotlin.StoreSubscriber
@Suppress("UnusedPrivateMember")
internal class DefaultWalletConnectComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: WalletConnectComponent.Params,
) : WalletConnectComponent, AppComponentContext by appComponentContext, StoreSubscriber<WalletConnectState> {
private val model: WalletConnectModel = getOrCreateModel(params)
private var screenState: MutableState<WalletConnectScreenState> =
mutableStateOf(model.updateState(store.state.walletConnectState))
init {
lifecycle.subscribe(
onCreate = {
Analytics.send(WalletConnect.ScreenOpened())
},
onStart = {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.walletConnectState == newState.walletConnectState
}.select { it.walletConnectState }
}
},
onStop = {
store.unsubscribe(this)
},
)
}
override fun newState(state: WalletConnectState) {
screenState.value = model.updateState(state)
}
@Composable
override fun Content(modifier: Modifier) {
WalletConnectScreen(
modifier = modifier,
state = screenState.value,
onBackClick = {
store.dispatchNavigationAction(AppRouter::pop)
},
)
}
@AssistedFactory
interface Factory : WalletConnectComponent.Factory {
override fun create(
context: AppComponentContext,
params: WalletConnectComponent.Params,
): DefaultWalletConnectComponent
}
}

View file

@ -1,72 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.qrscanning.models.QrResultSource
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Stable
@ModelScoped
internal class WalletConnectModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
paramsContainer: ParamsContainer,
) : Model() {
private val params = paramsContainer.require<WalletConnectComponent.Params>()
init {
modelScope.launch {
listenToQrScanningUseCase.listen(SourceType.WALLET_CONNECT)
.getOrElse { emptyFlow() }
.map { result ->
val source = when (result.resultSource) {
QrResultSource.CLIPBOARD -> WalletConnectAction.OpenSession.SourceType.CLIPBOARD
QrResultSource.CAMERA,
QrResultSource.GALLERY,
-> WalletConnectAction.OpenSession.SourceType.QR
}
WalletConnectAction.OpenSession(
wcUri = result.qrCode,
source = source,
userWalletId = params.userWalletId,
)
}
.collect { store.dispatch(it) }
}
}
fun updateState(state: WalletConnectState): WalletConnectScreenState {
Timber.d("WC2 Sessions: ${state.wc2Sessions}")
val sessions = state.wc2Sessions
return WalletConnectScreenState(
sessions.toImmutableList(),
isLoading = state.loading,
onRemoveSession = { sessionUri -> onRemoveSession(sessionUri, sessions) },
onAddSession = {
store.dispatch(WalletConnectAction.StartWalletConnect)
},
)
}
private fun onRemoveSession(sessionUri: String, wc2sessions: List<WcSessionForScreen>) {
wc2sessions.firstOrNull { it.sessionId == sessionUri }?.let {
store.dispatch(WalletConnectAction.DisconnectSession(sessionUri))
}
}
}

View file

@ -1,169 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun WalletConnectScreen(
state: WalletConnectScreenState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
SettingsScreensScaffold(
modifier = modifier,
content = {
if (state.sessions.isEmpty()) {
EmptyScreen(state)
} else {
WalletConnectSessions(state)
}
},
fab = {
if (!state.isLoading) {
AddSessionFab(
onAddSession = {
Analytics.send(Settings.ButtonStartWalletConnectSession())
state.onAddSession()
},
)
}
},
titleRes = R.string.wallet_connect_title,
onBackClick = onBackClick,
)
}
@Composable
private fun AddSessionFab(onAddSession: () -> Unit, modifier: Modifier = Modifier) {
FloatingActionButton(
onClick = onAddSession,
containerColor = TangemTheme.colors.button.primary,
contentColor = TangemTheme.colors.icon.primary2,
shape = RoundedCornerShape(16.dp),
modifier = modifier,
) {
Icon(
painter = painterResource(id = R.drawable.ic_plus_24),
contentDescription = "",
)
}
}
@Composable
private fun EmptyScreen(state: WalletConnectScreenState) {
if (state.isLoading) {
TangemLinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
color = TangemTheme.colors.icon.accent,
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(bottom = 64.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Image(
painter = painterResource(id = R.drawable.ic_wallet_connect_24),
contentDescription = "",
colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive),
contentScale = ContentScale.FillWidth,
modifier = Modifier.width(width = 100.dp),
)
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResourceSafe(id = R.string.wallet_connect_subtitle),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
}
@Composable
private fun WalletConnectSessions(state: WalletConnectScreenState) {
if (state.isLoading) {
TangemLinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.height(2.dp),
color = TangemTheme.colors.icon.accent,
)
} else {
Spacer(modifier = Modifier.height(2.dp))
}
LazyColumn(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
items(state.sessions) { session ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = session.description,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.weight(1f),
)
IconButton(
onClick = {
Analytics.send(Settings.ButtonStopWalletConnectSession())
state.onRemoveSession(session.sessionId)
},
) {
Icon(
painter = painterResource(id = R.drawable.ic_cross_rounded_24),
contentDescription = "",
tint = TangemTheme.colors.icon.warning,
)
}
}
}
}
}
@Composable
@Preview
private fun WalletConnectScreenPreview() {
WalletConnectScreen(
state = WalletConnectScreenState(
sessions = persistentListOf(
WcSessionForScreen(
description = "session from some dApp",
sessionId = "",
),
),
isLoading = true,
),
{},
)
}

View file

@ -1,15 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect
import kotlinx.collections.immutable.ImmutableList
internal data class WalletConnectScreenState(
val sessions: ImmutableList<WcSessionForScreen>,
val isLoading: Boolean = false,
val onRemoveSession: (String) -> Unit = {},
val onAddSession: () -> Unit = {},
)
data class WcSessionForScreen(
val description: String,
val sessionId: String,
)

View file

@ -1,10 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
interface WalletConnectComponent : ComposableContentComponent {
data class Params(val userWalletId: UserWalletId)
interface Factory : ComponentFactory<Params, WalletConnectComponent>
}

View file

@ -1,25 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect.di
import com.tangem.core.decompose.model.Model
import com.tangem.tap.features.details.ui.walletconnect.DefaultWalletConnectComponent
import com.tangem.tap.features.details.ui.walletconnect.WalletConnectModel
import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(SingletonComponent::class)
internal interface WalletConnectFeatureModule {
@Binds
fun bindFactory(impl: DefaultWalletConnectComponent.Factory): WalletConnectComponent.Factory
@Binds
@IntoMap
@ClassKey(WalletConnectModel::class)
fun bindModel(model: WalletConnectModel): Model
}

View file

@ -1,55 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.store
import com.tangem.wallet.R
object BnbTransactionDialog {
fun create(preparedData: WcPreparedRequest.BnbTransaction, context: Context): AlertDialog {
val data = preparedData.preparedRequestData.data
val message = when (data) {
is BinanceMessageData.Trade -> data.tradeData.map {
context.getString(
R.string.wallet_connect_bnb_trade_order_message,
it.symbol,
it.price,
it.quantity,
it.amount,
)
}.joinToString(separator = "\n\n")
is BinanceMessageData.Transfer -> context.getString(
R.string.wallet_connect_bnb_transaction_message,
data.address,
data.outputAddress,
data.amount,
)
}
val fullMessage = context.getString(
R.string.wallet_connect_bnb_sign_message,
preparedData.preparedRequestData.dAppName,
message,
)
val positiveButtonTitle = context.getText(R.string.common_sign)
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(context.getString(R.string.wallet_connect_title))
setMessage(fullMessage)
setPositiveButton(positiveButtonTitle) { _, _ ->
store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData))
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
store.dispatch(WalletConnectAction.RejectRequest(preparedData.topic, preparedData.requestId))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -1,37 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.store
import com.tangem.wallet.R
object PersonalSignDialog {
fun create(preparedData: WcPreparedRequest.EthSign, context: Context): AlertDialog {
val data = preparedData.preparedRequestData.dialogData
val message = context.getString(R.string.wallet_connect_alert_sign_message, data.message)
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(context.getString(R.string.wallet_connect_title))
setMessage(message)
setPositiveButton(context.getText(R.string.common_sign)) { _, _ ->
store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData))
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}
data class PersonalSignDialogData(
val dAppName: String,
val message: String,
val topic: String,
val id: Long,
)

View file

@ -1,42 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
import com.tangem.tap.store
import com.tangem.wallet.R
object SessionProposalDialog {
fun create(
sessionProposal: WalletConnectEvents.SessionProposal,
networks: String,
context: Context,
onApprove: () -> Unit,
onReject: () -> Unit,
): AlertDialog {
val message = context.getString(
R.string.wallet_connect_request_session_start,
sessionProposal.name,
networks,
sessionProposal.url,
)
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(context.getString(R.string.wallet_connect_title))
setMessage(message)
setPositiveButton(context.getText(R.string.common_start)) { _, _ ->
store.dispatch(GlobalAction.HideDialog)
onApprove()
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
store.dispatch(GlobalAction.HideDialog)
onReject()
}
setOnCancelListener {
store.dispatch(GlobalAction.HideDialog)
onReject()
}
}.create()
}
}

View file

@ -1,30 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.store
import com.tangem.wallet.R
internal object SignTransactionDialog {
fun create(preparedData: WcPreparedRequest.SolanaSignTransaction, context: Context): AlertDialog {
val signMessage = context.getString(R.string.wallet_connect_alert_sign_message, "")
val message = "${preparedData.preparedRequestData.dAppName}\n$signMessage"
return AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.wallet_connect_title))
setMessage(message)
setPositiveButton(context.getText(R.string.common_sign)) { _, _ ->
store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData))
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
store.dispatch(WalletConnectAction.RejectRequest(preparedData.topic, preparedData.requestId))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -1,30 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.store
import com.tangem.wallet.R
internal object SignTransactionsDialog {
fun create(preparedData: WcPreparedRequest.SolanaSignMultipleTransactions, context: Context): AlertDialog {
val signMessage = context.getString(R.string.wallet_connect_alert_sign_message, "")
val message = "${preparedData.preparedRequestData.dAppName}\n$signMessage"
return AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.wallet_connect_title))
setMessage(message)
setPositiveButton(context.getText(R.string.common_sign)) { _, _ ->
store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData))
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
store.dispatch(WalletConnectAction.RejectRequest(preparedData.topic, preparedData.requestId))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -1,61 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType
import com.tangem.tap.store
import com.tangem.wallet.R
object TransactionDialog {
fun create(preparedData: WcPreparedRequest.EthTransaction, context: Context): AlertDialog {
val data = preparedData.preparedRequestData.dialogData
val message = context.getString(
R.string.wallet_connect_create_tx_message,
data.dAppName,
data.dAppUrl,
data.amount,
data.feeAmount,
data.totalAmount,
data.balance,
)
val positiveButtonTitle = when (data.type) {
WcEthTransactionType.EthSignTransaction -> context.getText(R.string.common_sign)
WcEthTransactionType.EthSendTransaction -> context.getText(R.string.common_sign_and_send)
}
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(context.getString(R.string.wallet_connect_title))
setMessage(message)
setPositiveButton(positiveButtonTitle) { _, _ ->
if (data.isEnoughFundsToSend) {
store.dispatch(WalletConnectAction.PerformRequestedAction(preparedData))
} else {
store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id))
}
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
store.dispatch(WalletConnectAction.RejectRequest(data.topic, data.id))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}
data class TransactionRequestDialogData(
val dAppName: String,
val dAppUrl: String,
val amount: String,
val feeAmount: String,
val totalAmount: String,
val balance: String,
val isEnoughFundsToSend: Boolean,
val topic: String,
val id: Long,
val type: WcEthTransactionType,
)

View file

@ -1,48 +0,0 @@
package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.removePrefixOrNull
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.intentHandler.AffectsNavigation
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.store
import timber.log.Timber
import java.net.URLDecoder
/**
[REDACTED_AUTHOR]
*/
class WalletConnectLinkIntentHandler : IntentHandler, AffectsNavigation {
override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean {
val intentData = intent?.data ?: return false
val scheme = intent.scheme ?: return false
val wcUri = when (scheme) {
WC_SCHEME -> intentData.toString()
TANGEM_SCHEME -> intentData.toString().removePrefixOrNull(TANGEM_WC_PREFIX)
else -> null
}
return if (wcUri.isNullOrBlank()) {
false
} else {
val decodedWcUri = try {
URLDecoder.decode(wcUri, DEFAULT_CHARSET_NAME)
} catch (e: Exception) {
Timber.e(e)
return false
}
store.dispatchOnMain(WalletConnectAction.HandleDeepLink(decodedWcUri))
true
}
}
private companion object {
const val TANGEM_SCHEME = "tangem"
const val TANGEM_WC_PREFIX = "tangem://wc?uri="
const val DEFAULT_CHARSET_NAME = "UTF-8"
const val WC_SCHEME = "wc"
}
}

View file

@ -2,12 +2,15 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.Analytics
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.Onboarding.*
import com.tangem.tap.common.extensions.*
import com.tangem.tap.backupService
import com.tangem.tap.common.analytics.events.Onboarding.Finished
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@ -24,7 +27,7 @@ class BackupMiddleware {
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) {
if (DemoHelper.tryHandle(appState, action)) return
if (DemoHelper.tryHandle(appState)) return
when (action) {
is BackupAction.DiscardBackup -> {

View file

@ -30,7 +30,7 @@ object TradeCryptoMiddleware {
}
private fun handle(state: () -> AppState?, action: TradeCryptoAction) {
if (DemoHelper.tryHandle(state, action)) return
if (DemoHelper.tryHandle(state)) return
when (action) {
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)

View file

@ -20,7 +20,6 @@ import com.tangem.domain.wallets.legacy.unlockIfLockable
import com.tangem.tap.*
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.proxy.redux.DaggerGraphState
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@ -32,21 +31,19 @@ internal class WelcomeMiddleware {
{ action ->
val appState = appStateProvider()
if (action is WelcomeAction && appState != null) {
handleAction(action, appState.welcomeState)
handleAction(action)
}
next(action)
}
}
}
private fun handleAction(action: WelcomeAction, state: WelcomeState) {
private fun handleAction(action: WelcomeAction) {
mainScope.launch {
when (action) {
is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent)
is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics(
afterUnlockIntent = action.afterUnlockIntent ?: state.intent,
)
is WelcomeAction.ProceedWithCard -> proceedWithCard(afterScanIntent = state.intent)
is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics()
is WelcomeAction.ProceedWithCard -> proceedWithCard()
is WelcomeAction.ClearUserWallets -> disableUserWalletsSaving()
else -> Unit
}
@ -70,13 +67,8 @@ internal class WelcomeMiddleware {
}
}
private suspend fun proceedWithBiometrics(afterUnlockIntent: Intent?) {
Timber.d(
"""
Proceeding with biometry
|- Intent: $afterUnlockIntent
""".trimIndent(),
)
private suspend fun proceedWithBiometrics() {
Timber.d("Proceeding with biometry")
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.unlockIfLockable(type = UnlockType.ANY)
@ -93,20 +85,11 @@ internal class WelcomeMiddleware {
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Success)
store.onUserWalletSelected(userWallet = selectedUserWallet)
afterUnlockIntent?.let {
WalletConnectLinkIntentHandler().handleIntent(it, false)
}
}
}
private suspend fun proceedWithCard(afterScanIntent: Intent?) {
Timber.d(
"""
Proceeding with card
|- Intent: $afterScanIntent
""".trimIndent(),
)
private suspend fun proceedWithCard() {
Timber.d("Proceeding with card")
scanCardInternal { scanResponse ->
val userWalletBuilder = store.inject(DaggerGraphState::coldUserWalletBuilderFactory).create(scanResponse)
@ -125,10 +108,6 @@ internal class WelcomeMiddleware {
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success)
store.onUserWalletSelected(userWallet = userWallet)
afterScanIntent?.let {
WalletConnectLinkIntentHandler().handleIntent(it, false)
}
}
}
}