Updated on 2026-08-14

This commit is contained in:
Tangem 2021-06-07 21:27:28 +03:00
parent 73339902b8
commit 0a40ed4957
46 changed files with 1856 additions and 69 deletions

View file

@ -65,6 +65,8 @@ repositories {
}
dependencies {
implementation fileTree(include: ['*.aar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
@ -75,9 +77,10 @@ dependencies {
implementation 'com.google.android.play:core-ktx:1.8.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'com.tangem:blockchain:develop-11'
implementation 'com.tangem:core:develop-15'
implementation 'com.tangem:sdk:develop-15'
// implementation 'com.tangem:blockchain:develop-11' //TODO: set correct one
implementation 'blockchain-sdk-kotlin:blockchain:tangem-20210607.120025-19'
implementation 'com.tangem:core:develop-36'
implementation 'com.tangem:sdk:develop-36'
// WebView
implementation "androidx.browser:browser:1.3.0"
@ -106,6 +109,9 @@ dependencies {
implementation 'com.google.firebase:firebase-config-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'
// WalletConnect
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
testImplementation 'junit:junit:4.13.2'
testImplementation "com.google.truth:truth:1.0.1"
androidTestImplementation 'androidx.test.ext:junit:1.1.2'

BIN
app/libs/walletconnect.aar Normal file

Binary file not shown.

View file

@ -78,6 +78,14 @@
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="wc"/>
</intent-filter>
</activity>
<activity android:name="com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity" />

View file

@ -882,6 +882,12 @@
"name" : "WAX",
"contractAddress" : "0x39Bb259F66E1C59d5ABEF88375979b4D20D98022"
},
{
"decimalCount" : 18,
"symbol" : "WETH",
"name" : "Wrapped Ether",
"contractAddress" : "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
},
{
"decimalCount" : 18,
"symbol" : "WPR",

View file

@ -2,8 +2,6 @@ package com.tangem.tap
import android.content.Intent
import android.content.pm.ActivityInfo
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Bundle
import android.util.Log
import android.view.View
@ -14,10 +12,14 @@ import com.tangem.Config
import com.tangem.TangemSdk
import com.tangem.commands.common.card.CardType
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.IntentHandler
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.activity_main.*
@ -53,9 +55,11 @@ private fun initCoroutineExceptionHandler(): CoroutineExceptionHandler {
}
}
class MainActivity : AppCompatActivity() {
class MainActivity : AppCompatActivity(), SnackbarHandler {
private var snackbar: Snackbar? = null
private val dialogManager = DialogManager()
private val intentHandler = IntentHandler()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -65,43 +69,37 @@ class MainActivity : AppCompatActivity() {
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
tangemSdk = TangemSdk.init(
this, Config(cardFilter = CardFilter(EnumSet.allOf(CardType::class.java)))
this, Config(cardFilter = CardFilter(EnumSet.allOf(CardType::class.java)))
)
tangemSdkManager = TangemSdkManager(this)
store.dispatch(WalletConnectAction.RestoreSessions)
}
override fun onResume() {
super.onResume()
notificationsHandler = NotificationsHandler(fragment_container)
if (supportFragmentManager.backStackEntryCount == 0 ||
store.state.globalState.scanNoteResponse == null) {
store.state.globalState.scanNoteResponse == null
) {
store.dispatch(HomeAction.CheckIfFirstLaunch)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
}
handleBackgroundScan(intent)
intentHandler.handleIntent(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
handleBackgroundScan(intent)
intentHandler.handleIntent(intent)
}
private fun handleBackgroundScan(intent: Intent?) {
if (intent != null && (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action ||
NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action
)) {
val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
if (tag != null) {
intent.action = null
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatch(HomeAction.ReadCard)
}
}
override fun onStart() {
super.onStart()
dialogManager.onStart(this)
}
override fun onStop() {
notificationsHandler = null
dialogManager.onStop()
super.onStop()
}
@ -110,11 +108,11 @@ class MainActivity : AppCompatActivity() {
super.onDestroy()
}
fun showSnackbar(text: Int, buttonTitle: Int? = null, action: View.OnClickListener? = null) {
override fun showSnackbar(text: Int, buttonTitle: Int?, action: View.OnClickListener?) {
if (snackbar != null) return
snackbar = Snackbar.make(
fragment_container, getString(text), Snackbar.LENGTH_INDEFINITE
fragment_container, getString(text), Snackbar.LENGTH_INDEFINITE
)
if (buttonTitle != null && action != null) {
snackbar?.setAction(getString(buttonTitle), action)
@ -122,7 +120,7 @@ class MainActivity : AppCompatActivity() {
snackbar?.show()
}
fun dismissSnackbar() {
override fun dismissSnackbar() {
snackbar?.dismiss()
snackbar = null
}

View file

@ -15,6 +15,7 @@ import com.tangem.tap.domain.configurable.config.FeaturesRemoteLoader
import com.tangem.tap.domain.configurable.warningMessage.RemoteWarningLoader
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.features.feedback.AdditionalEmailInfo
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.feedback.TangemLogCollector
@ -32,6 +33,7 @@ val store = Store(
)
lateinit var preferencesStorage: PreferencesStorage
lateinit var currenciesRepository: CurrenciesRepository
lateinit var walletConnectRepository: WalletConnectRepository
class TapApplication : Application() {
override fun onCreate() {
@ -52,6 +54,7 @@ class TapApplication : Application() {
preferencesStorage = PreferencesStorage(this)
PicassoHelper.initPicassoWithCaching(this)
currenciesRepository = CurrenciesRepository(this)
walletConnectRepository = WalletConnectRepository(this)
initFeedbackManager()
loadConfigs()

View file

@ -12,7 +12,7 @@ class CurrencyConverter(
private val rateValue: BigDecimal,
private val decimals: Int
) {
private val roundingMode = RoundingMode.DOWN
private val roundingMode = RoundingMode.HALF_UP
fun toFiat(crypto: BigDecimal, fiatDecimals: Int = 2): BigDecimal {
return toFiatUnscaled(crypto).setScale(fiatDecimals, roundingMode)

View file

@ -0,0 +1,55 @@
package com.tangem.tap.common
import android.app.Dialog
import android.content.Context
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.ApproveWcSessionDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.ClipboardOrScanQrDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionDialog
import com.tangem.tap.store
import org.rekotlin.StoreSubscriber
class DialogManager : StoreSubscriber<GlobalState> {
var context: Context? = null
private var dialog: Dialog? = null
fun onStart(context: Context) {
this.context = context
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.globalState == newState.globalState
}.select { it.globalState }
}
}
fun onStop() {
this.context = null
store.unsubscribe(this)
}
override fun newState(state: GlobalState) {
if (state.dialog == null) {
dialog?.dismiss()
dialog = null
return
}
val context = context ?: return
if (dialog != null) return
when (state.dialog) {
is WalletConnectDialog.ApproveWcSession ->
dialog = ApproveWcSessionDialog.create(state.dialog.session, context)
is WalletConnectDialog.ClipboardOrScanQr ->
dialog = ClipboardOrScanQrDialog.create(state.dialog.clipboardUri, context)
is WalletConnectDialog.RequestTransaction ->
dialog = TransactionDialog.create(state.dialog.dialogData, context)
is WalletConnectDialog.PersonalSign ->
dialog = PersonalSignDialog.create(state.dialog.data, context)
}
dialog?.show()
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.tap.common
import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.store
class IntentHandler {
fun handleIntent(intent: Intent?) {
handleBackgroundScan(intent)
handleWalletConnectLink(intent)
}
private fun handleBackgroundScan(intent: Intent?) {
if (intent != null && (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action ||
NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action
)
) {
val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
if (tag != null) {
intent.action = null
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatch(HomeAction.ReadCard)
}
}
}
private fun handleWalletConnectLink(intent: Intent?) {
if (intent?.scheme == WalletConnectManager.WC_SCHEME) {
store.dispatch(WalletConnectAction.HandleDeepLink(intent.data?.toString()))
}
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.tap.common
import android.view.View
interface SnackbarHandler {
fun showSnackbar(text: Int, buttonTitle: Int? = null, action: View.OnClickListener? = null)
fun dismissSnackbar()
}

View file

@ -9,6 +9,8 @@ import com.tangem.tap.features.details.ui.DetailsFragment
import com.tangem.tap.features.details.ui.DetailsSecurityFragment
import com.tangem.tap.features.details.ui.twins.CreateTwinWalletFragment
import com.tangem.tap.features.details.ui.twins.TwinWalletWarningFragment
import com.tangem.tap.features.details.ui.walletconnect.QrScanFragment
import com.tangem.tap.features.details.ui.walletconnect.WalletConnectSessionsFragment
import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment
import com.tangem.tap.features.home.HomeFragment
import com.tangem.tap.features.send.ui.SendFragment
@ -50,5 +52,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.TwinsOnboarding -> TwinsOnboardingFragment()
AppScreen.AddTokens -> AddTokensFragment()
AppScreen.WalletDetails -> WalletDetailsFragment()
AppScreen.WalletConnectSessions -> WalletConnectSessionsFragment()
AppScreen.QrScan -> QrScanFragment()
}
}

View file

@ -22,13 +22,16 @@ fun BigDecimal.toFormattedString(
return df.format(this)
}
fun BigDecimal.toFormattedCurrencyString(decimals: Int, currency: String): String {
return "${this.toFormattedString(decimals)} $currency"
fun BigDecimal.toFormattedCurrencyString(
decimals: Int, currency: String, roundingMode: RoundingMode = RoundingMode.DOWN
): String {
val formattedAmount = this.toFormattedString(decimals = decimals, roundingMode = roundingMode)
return "$formattedAmount $currency"
}
fun BigDecimal.toFiatString(rateValue: BigDecimal, fiatCurrencyName: FiatCurrencyName): String {
var fiatValue = rateValue.multiply(this)
fiatValue = fiatValue.setScale(2, RoundingMode.DOWN)
fiatValue = fiatValue.setScale(2, RoundingMode.HALF_UP)
return "≈ ${fiatCurrencyName} $fiatValue"
}

View file

@ -0,0 +1,15 @@
package com.tangem.tap.common.extensions
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Store
fun Store<AppState>.dispatchOnMain(action: Action) {
scope.launch(Dispatchers.Main) {
store.dispatch(action)
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.globalReducer
import com.tangem.tap.common.redux.navigation.NavigationReducer
import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer
import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer
import com.tangem.tap.features.home.redux.HomeReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
@ -23,6 +24,7 @@ fun appReducer(action: Action, state: AppState?): AppState {
detailsState = DetailsReducer.reduce(action, state),
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState)
)
}

View file

@ -6,6 +6,8 @@ import com.tangem.tap.common.redux.navigation.NavigationState
import com.tangem.tap.common.redux.navigation.navigationMiddleware
import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.features.disclaimer.redux.DisclaimerMiddleware
import com.tangem.tap.features.disclaimer.redux.DisclaimerState
import com.tangem.tap.features.home.redux.HomeMiddleware
@ -20,14 +22,15 @@ import org.rekotlin.Middleware
import org.rekotlin.StateType
data class AppState(
val navigationState: NavigationState = NavigationState(),
val globalState: GlobalState = GlobalState(),
val homeState: HomeState = HomeState(),
val walletState: WalletState = WalletState(),
val sendState: SendState = SendState(),
val detailsState: DetailsState = DetailsState(),
val disclaimerState: DisclaimerState = DisclaimerState(),
val tokensState: TokensState = TokensState(),
val navigationState: NavigationState = NavigationState(),
val globalState: GlobalState = GlobalState(),
val homeState: HomeState = HomeState(),
val walletState: WalletState = WalletState(),
val sendState: SendState = SendState(),
val detailsState: DetailsState = DetailsState(),
val disclaimerState: DisclaimerState = DisclaimerState(),
val tokensState: TokensState = TokensState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
) : StateType {
companion object {
@ -40,6 +43,7 @@ data class AppState(
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
TokensMiddleware().tokensMiddleware,
WalletConnectMiddleware().walletConnectMiddleware
)
}
}

View file

@ -38,4 +38,7 @@ sealed class GlobalAction : Action {
data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction()
data class SendFeedback(val emailData: EmailData) : GlobalAction()
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
object HideDialog : GlobalAction()
}

View file

@ -67,6 +67,12 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.SetFeedbackManager -> {
globalState.copy(feedbackManager = action.feedbackManager)
}
is GlobalAction.ShowDialog -> {
globalState.copy(dialog = action.stateDialog)
}
is GlobalAction.HideDialog -> {
globalState.copy(dialog = null)
}
else -> globalState
}
}

View file

@ -22,6 +22,7 @@ data class GlobalState(
val feedbackManager: FeedbackManager? = null,
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null
) : StateType
typealias CryptoCurrencyName = String

View file

@ -11,5 +11,6 @@ data class NavigationState(
enum class AppScreen {
Home, Wallet, WalletDetails, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer,
CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding, AddTokens
CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding, AddTokens, WalletConnectSessions,
QrScan
}

View file

@ -33,10 +33,14 @@ class TangemSdkManager(val activity: ComponentActivity) {
activity, Config(cardFilter = CardFilter(EnumSet.allOf(CardType::class.java)))
)
suspend fun scanNote(analyticsHandler: AnalyticsHandler): CompletionResult<ScanNoteResponse> {
suspend fun scanNote(
analyticsHandler: AnalyticsHandler, messageRes: Int? = null
): CompletionResult<ScanNoteResponse> {
analyticsHandler.triggerEvent(AnalyticsEvent.READY_TO_SCAN, null)
return runTaskAsyncReturnOnMain(ScanNoteTask(),
initialMessage = Message(activity.getString(R.string.initial_message_scan_header)))
initialMessage = Message(
activity.getString(messageRes ?: R.string.initial_message_scan_header)
))
}
suspend fun createWallet(cardId: String?): CompletionResult<Card> {

View file

@ -3,6 +3,7 @@ package com.tangem.tap.domain
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.EllipticCurve
import com.tangem.commands.common.card.masks.Product
import com.tangem.tap.domain.extensions.getSingleWallet
import java.util.*
object TapWorkarounds {
@ -43,6 +44,6 @@ val Card.isMultiwalletAllowed: Boolean
get() {
return cardData?.productMask?.contains(Product.TwinCard) != true
&& !TapWorkarounds.isStart2Coin
&& (this.firmwareVersion.major >= 4 ||
this.getWallets().getOrNull(0)?.curve == EllipticCurve.Secp256k1)
&& (firmwareVersion.major >= 4 ||
getSingleWallet()?.curve == EllipticCurve.Secp256k1)
}

View file

@ -28,7 +28,7 @@ fun Card.getSingleWallet(): CardWallet? {
fun Card.getStatus(): CardStatus {
if (firmwareVersion < FirmwareConstraints.AvailabilityVersions.walletData) return status!!
return if (getWallets().any { it.status == WalletStatus.Loaded }) {
return if (wallets.any { it.status == WalletStatus.Loaded }) {
CardStatus.Loaded
} else {
CardStatus.Empty
@ -36,11 +36,11 @@ fun Card.getStatus(): CardStatus {
}
fun Card.hasSignedHashes(): Boolean {
return getWallets().any { it.status == WalletStatus.Loaded && it.signedHashes ?: 0 > 0 }
return wallets.any { it.status == WalletStatus.Loaded && it.signedHashes ?: 0 > 0 }
}
fun Card.signedHashesCount(): Int {
return getWallets().map { it.signedHashes ?: 0 }.sum()
return wallets.map { it.signedHashes ?: 0 }.sum()
}
val Card.remainingSignatures: Int?

View file

@ -15,7 +15,7 @@ fun WalletManagerFactory.makeWalletManagerForApp(
blockchain: Blockchain,
): WalletManager? {
val supportedCurves = blockchain.getSupportedCurves() ?: return null
val wallets = card.getWallets().filter { wallet -> supportedCurves.contains(wallet.curve) }
val wallets = card.wallets.filter { wallet -> supportedCurves.contains(wallet.curve) }
val wallet = selectWallet(wallets)
val publicKey = wallet?.publicKey ?: return null
val curveToUse = wallet.curve ?: return null
@ -42,7 +42,7 @@ fun WalletManagerFactory.makePrimaryWalletManager(
val card = data.card
val blockchain = card.getBlockchain()
val supportedCurves = blockchain?.getSupportedCurves() ?: return null
val wallets = card.getWallets().filter { wallet -> supportedCurves.contains(wallet.curve) }
val wallets = card.wallets.filter { wallet -> supportedCurves.contains(wallet.curve) }
val wallet = selectWallet(wallets)
val publicKey = wallet?.publicKey ?: return null
val curveToUse = wallet.curve ?: return null

View file

@ -43,8 +43,8 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
is CompletionResult.Success -> {
val card = this.card?.copy(
isPin1Default = result.data.isPin1Default,
isPin2Default = result.data.isPin2Default
)?.also { it.setWallets(card.getWallets()) } ?: result.data
isPin2Default = result.data.isPin2Default,
) ?: result.data
val error = getErrorIfExcludedCard(card)
if (error != null) {
@ -88,7 +88,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
return
}
val curvesPresent = card.getWallets().map { it.curve }
val curvesPresent = card.wallets.map { it.curve }
val curvesToCreate = EllipticCurve.values().subtract(curvesPresent)
if (curvesToCreate.isEmpty()) {

View file

@ -86,13 +86,11 @@ fun Card.getTwinCardIdForUser(): String {
}
fun Card.changeStatusToLoaded(): Card {
val wallets = getWallets().map { it.copy(status = WalletStatus.Loaded) }
return copy(status = CardStatus.Loaded)
.also { it.setWallets(wallets) }
val wallets = wallets.map { it.copy(status = WalletStatus.Loaded) }
return copy(status = CardStatus.Loaded, wallets = wallets)
}
fun Card.changeStatusToEmpty(): Card {
val wallets = getWallets().map { it.copy(status = WalletStatus.Empty) }
return copy(status = CardStatus.Empty)
.also { it.setWallets(wallets) }
val wallets = wallets.map { it.copy(status = WalletStatus.Empty) }
return copy(status = CardStatus.Empty, wallets = wallets)
}

View file

@ -0,0 +1,324 @@
package com.tangem.tap.domain.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.hexToBytes
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.walletconnect.*
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.walletConnectRepository
import com.trustwallet.walletconnect.WCClient
import com.trustwallet.walletconnect.models.WCPeerMeta
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
import com.trustwallet.walletconnect.models.session.WCSession
import com.trustwallet.walletconnect.models.session.WCSessionUpdate
import kotlinx.coroutines.launch
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
import java.util.*
import java.util.concurrent.TimeUnit
class WalletConnectManager {
private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.addInterceptor(interceptor)
.addInterceptor(RetryInterceptor())
.build()
}
private val interceptor by lazy {
HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }
}
private var sessions: MutableMap<WCSession, WalletConnectActiveData> = mutableMapOf()
fun connect(wcUri: String, wallet: WalletForSession) {
val session = WCSession.from(wcUri) ?: return
val client = WCClient(httpClient = okHttpClient)
setListeners(client)
val peerId = UUID.randomUUID().toString()
client.connect(session, tangemPeerMeta, peerId)
sessions[session] = WalletConnectActiveData(
peerId = peerId,
remotePeerId = null,
session = session,
client = client,
wallet = wallet
)
}
fun restoreSessions() {
val sessions = walletConnectRepository.loadSavedSessions()
this.sessions = sessions
.map { session ->
WalletConnectActiveData(
peerId = session.peerId,
remotePeerId = session.remotePeerId,
client = WCClient(httpClient = okHttpClient),
session = session.session,
peerMeta = session.peerMeta,
wallet = session.wallet
)
.also {
setListeners(it.client)
it.client.connect(it.session, tangemPeerMeta, it.peerId, it.remotePeerId)
}
}
.map { it.session to it }.toMap().toMutableMap()
store.dispatchOnMain(WalletConnectAction.SetSessionsRestored(sessions))
}
fun approve(session: WCSession) {
val activeData = sessions[session] ?: return
removeSimilarSessions(activeData)
val approved = activeData.client.approveSession(
accounts = listOf(Blockchain.Ethereum.makeAddresses(
activeData.wallet.walletPublicKey.hexToBytes()).first().value
),
chainId = activeData.wallet.chainId
)
if (approved) {
val walletConnectSession = WalletConnectSession(
peerId = activeData.peerId,
remotePeerId = activeData.remotePeerId,
wallet = activeData.wallet,
session = session,
peerMeta = activeData.peerMeta!!
)
walletConnectRepository.saveSession(walletConnectSession)
store.dispatchOnMain(WalletConnectAction.ApproveSession.Success(
walletConnectSession))
}
}
fun removeSimilarSessions(activeData: WalletConnectActiveData) {
val sessionsToRemove = sessions.filter {
it.value.wallet.walletPublicKey == activeData.wallet.walletPublicKey
&& it.value.peerMeta?.url == activeData.peerMeta?.url
&& it.value.session != activeData.session
}
Timber.d("RemoveSimilarSessions: ${sessionsToRemove.values.map { it.client.session }}")
sessionsToRemove.forEach { disconnect(it.value.session) }
}
fun rejectRequest(session: WCSession, id: Long) {
val activeData = sessions[session] ?: return
activeData.client.rejectRequest(id)
}
fun acceptRequest(session: WCSession, id: Long, data: String) {
val activeData = sessions[session] ?: return
activeData.client.approveRequest(id, data)
}
fun disconnect(session: WCSession) {
val activeData = sessions[session] ?: return
val disconnected = activeData.client.killSession()
if (disconnected) {
onSessionClosed(session)
}
}
private fun onSessionClosed(session: WCSession) {
sessions.remove(session)
walletConnectRepository.removeSession(session)
store.dispatchOnMain(WalletConnectAction.RemoveSession(session))
}
fun handleTransactionRequest(
transaction: WCEthereumTransaction,
session: WalletConnectSession,
id: Long,
type: WcTransactionType,
) {
val activeData = sessions[session.session] ?: return
scope.launch {
val data = WalletConnectSdkHelper().prepareTransactionData(
transaction = transaction,
session = session,
id = id,
type = type
).guard {
sessions[session.session] = activeData.copy(transactionData = null)
store.dispatchOnMain(WalletConnectAction.RejectRequest(
session.session,
id
))
return@launch
}
sessions[session.session] = activeData.copy(transactionData = data)
store.dispatchOnMain(WalletConnectAction.SetDataToSend(data))
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.RequestTransaction(
data.dialogData)))
}
}
fun completeTransaction(session: WCSession) {
val activeData = sessions[session]
val data = activeData?.transactionData ?: return
scope.launch {
val hash = WalletConnectSdkHelper().completeTransaction(data).guard {
sessions[data.session.session] = activeData.copy(transactionData = null)
store.dispatchOnMain(WalletConnectAction.RejectRequest(data.session.session,
data.id
))
return@launch
}
acceptRequest(data.session.session, data.id, hash)
sessions[data.session.session] = activeData.copy(transactionData = null)
}
}
fun handlePersonalSignRequest(
message: WCEthereumSignMessage,
session: WalletConnectSession,
id: Long,
) {
val activeData = sessions[session.session] ?: return
scope.launch {
val data = WalletConnectSdkHelper().prepareDataForPersonalSign(
message = message, session = session, id = id
)
sessions[session.session] = activeData.copy(personalSignData = data)
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.PersonalSign(
data.dialogData
)))
}
}
fun sendSignedMessage(session: WCSession) {
val activeData = sessions[session]
val data = activeData?.personalSignData ?: return
scope.launch {
val hash = WalletConnectSdkHelper().signPersonalMessage(data.hash, activeData.wallet)
.guard {
sessions[data.session.session] = activeData.copy(transactionData = null)
store.dispatchOnMain(WalletConnectAction.RejectRequest(data.session.session,
data.id
))
return@launch
}
sessions[session] = activeData.copy(personalSignData = data)
acceptRequest(data.session.session, data.id, hash)
sessions[data.session.session] = activeData.copy(transactionData = null)
}
}
fun setListeners(client: WCClient) {
client.onSessionRequest = { id: Long, peer: WCPeerMeta ->
Timber.d("OnSessionRequest: $peer")
val session = client.session
val data = sessions[session]?.copy(peerMeta = peer, remotePeerId = client.remotePeerId)
if (data != null && session != null) {
sessions[session] = data
val sessionData = data.toWalletConnectSession()
sessionData?.let {
store.dispatchOnMain(WalletConnectAction.AcceptOpeningSession(
sessionData))
}
}
}
client.onSessionUpdate = { id: Long, update: WCSessionUpdate ->
Timber.d("onSessionUpdate: $update")
val session = client.session
if (session != null && !update.approved) onSessionClosed(session)
}
client.onEthSendTransaction = { id: Long, transaction: WCEthereumTransaction ->
Timber.d("onEthSendTransaction: $transaction")
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(WalletConnectAction.HandleTransactionRequest(
transaction = transaction,
session = sessionData,
id = id,
type = WcTransactionType.EthSendTransaction
))
}
}
client.onEthSignTransaction = { id: Long, transaction: WCEthereumTransaction ->
Timber.d("onEthSignTransaction: $transaction")
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(WalletConnectAction.HandleTransactionRequest(
transaction = transaction,
session = sessionData,
id = id,
type = WcTransactionType.EthSignTransaction
))
}
}
client.onEthSign = { id: Long, message: WCEthereumSignMessage ->
Timber.d("onEthSign: $message")
sessions[client.session]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(WalletConnectAction.HandlePersonalSignRequest(
message,
sessionData,
id))
}
}
client.onDisconnect = { code: Int, reason: String ->
val session = client.session
if (session != null) {
onSessionClosed(session)
}
}
}
companion object {
private val tangemPeerMeta =
WCPeerMeta(name = "Tangem Wallet", url = "https://tangem.com")
fun isCorrectWcUri(string: String): Boolean = WCSession.from(string) != null
const val WC_SCHEME = "wc"
}
}
data class WalletConnectActiveData(
val peerId: String,
val remotePeerId: String?,
val client: WCClient,
val session: WCSession,
val peerMeta: WCPeerMeta? = null,
val wallet: WalletForSession,
val transactionData: WcTransactionData? = null,
val personalSignData: WcPersonalSignData? = null,
) {
fun toWalletConnectSession(): WalletConnectSession? {
if (peerMeta == null) return null
return WalletConnectSession(
peerId = peerId,
remotePeerId = remotePeerId,
wallet = wallet,
session = session,
peerMeta = peerMeta
)
}
}
class RetryInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
val response = chain.proceed(request)
when (response.code) {
502 -> {
return chain.proceed(request)
}
}
return response
}
}

View file

@ -0,0 +1,94 @@
package com.tangem.tap.domain.walletconnect
import android.app.Application
import android.content.Context
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Types
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
import com.tangem.tap.network.createMoshi
import com.trustwallet.walletconnect.models.WCPeerMeta
import com.trustwallet.walletconnect.models.session.WCSession
class WalletConnectRepository(val context: Application) {
private val moshi = createMoshi()
private val walletConnectAdapter: JsonAdapter<List<SessionDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, SessionDao::class.java)
)
fun saveSession(session: WalletConnectSession) {
val sessions = loadSavedSessions() + session
saveSessions(sessions)
}
fun removeSession(session: WalletConnectSession) {
val sessions = loadSavedSessions().filterNot { it == session }
saveSessions(sessions)
}
fun removeSession(session: WCSession) {
val sessions = loadSavedSessions().filterNot { it.session == session }
saveSessions(sessions)
}
fun loadSavedSessions(): List<WalletConnectSession> {
return try {
val json = context.readFileText(FILE_NAME_PREFIX_SESSIONS)
walletConnectAdapter.fromJson(json)!!.map { it.toSession() }
} catch (exception: Exception) {
emptyList()
}
}
private fun saveSessions(sessions: List<WalletConnectSession>) {
val json = walletConnectAdapter.toJson(sessions.map { SessionDao.fromSession(it) })
context.rewriteFile(json, FILE_NAME_PREFIX_SESSIONS)
}
private fun Context.readFileText(fileName: String): String =
this.openFileInput(fileName).bufferedReader().readText()
private fun Context.rewriteFile(content: String, fileName: String) {
this.openFileOutput(fileName, Context.MODE_PRIVATE).use {
it.write(content.toByteArray(), 0, content.length)
}
}
companion object {
private const val FILE_NAME_PREFIX_SESSIONS = "wc_sessions"
}
}
@JsonClass(generateAdapter = true)
data class SessionDao(
val peerId: String,
val remotePeerId: String?,
val wallet: WalletForSession,
val session: WCSession,
val peerMeta: WCPeerMeta,
) {
fun toSession(): WalletConnectSession {
return WalletConnectSession(
peerId = peerId,
remotePeerId = remotePeerId,
wallet = wallet,
session = session,
peerMeta = peerMeta
)
}
companion object {
fun fromSession(session: WalletConnectSession): SessionDao {
return SessionDao(
peerId = session.peerId,
remotePeerId = session.remotePeerId,
wallet = session.wallet,
session = session.session,
peerMeta = session.peerMeta
)
}
}
}

View file

@ -0,0 +1,240 @@
package com.tangem.tap.domain.walletconnect
import com.google.common.base.CharMatcher
import com.tangem.Message
import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader
import com.tangem.blockchain.blockchains.ethereum.EthereumHelper
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.Signer
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.blockchain.extensions.hexToBigDecimal
import com.tangem.commands.SignCommand
import com.tangem.commands.wallet.WalletIndex
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.features.details.redux.walletconnect.*
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import com.tangem.tap.tangemSdkManager
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
import org.kethereum.crypto.api.ec.ECDSASignature
import org.kethereum.crypto.determineRecId
import org.kethereum.crypto.impl.ec.canonicalise
import org.kethereum.keccakshortcut.keccak
import org.kethereum.model.PublicKey
import timber.log.Timber
import java.math.BigDecimal
import java.math.BigInteger
class WalletConnectSdkHelper {
suspend fun prepareTransactionData(
transaction: WCEthereumTransaction,
session: WalletConnectSession,
id: Long,
type: WcTransactionType,
): WcTransactionData? {
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
val walletManager = factory.makeEthereumWalletManager(
session.wallet.cardId,
session.wallet.walletPublicKey.hexToBytes(),
emptyList(),
isTestNet = session.wallet.isTestNet
) ?: return null
try {
walletManager.update()
} catch (exception: Exception) {
Timber.e(exception)
return null
}
val blockchain = walletManager.wallet.blockchain
val balance =
walletManager.wallet.amounts[AmountType.Coin]?.value ?: return null
val gas = transaction.gas?.hexToBigDecimal()
?: transaction.gasLimit?.hexToBigDecimal() ?: return null
val decimals = blockchain.decimals()
val value = transaction.value?.hexToBigDecimal()
?.movePointLeft(decimals) ?: return null
val gasPrice = transaction.gasPrice?.hexToBigDecimal()
?: when (val result =
(walletManager as? EthereumGasLoader)?.getGasPrice()) {
is Result.Success -> result.data.toBigDecimal()
is Result.Failure -> {
Timber.e(result.error)
return null
}
null -> return null
}
val fee = (gas * gasPrice).movePointLeft(decimals)
val total = value + fee
val transactionData = TransactionData(
amount = Amount(value, blockchain),
fee = Amount(fee, blockchain),
sourceAddress = transaction.from,
destinationAddress = transaction.to!!,
extras = EthereumTransactionExtras(
data = transaction.data.removePrefix(HEX_PREFIX).hexToBytes(),
gasLimit = gas.toBigInteger(),
nonce = transaction.nonce?.hexToBigDecimal()?.toBigInteger()
)
)
val dialogData = TransactionRequestDialogData(
cardId = session.wallet.cardId,
dAppName = session.peerMeta.name,
dAppUrl = session.peerMeta.url,
amount = value.toFormattedString(decimals),
gasAmount = fee.toFormattedString(decimals),
totalAmount = total.toFormattedString(decimals),
balance = balance.toFormattedString(decimals),
isEnoughFundsToSend = (balance - total) >= BigDecimal.ZERO,
session = session.session,
id = id,
type = type
)
return WcTransactionData(
type = type,
transaction = transactionData,
session = session,
id = id,
walletManager = walletManager,
dialogData = dialogData
)
}
suspend fun completeTransaction(data: WcTransactionData): String? {
return when (data.type) {
WcTransactionType.EthSendTransaction -> sendTransaction(data)
WcTransactionType.EthSignTransaction -> signTransaction(data)
}
}
private suspend fun sendTransaction(data: WcTransactionData): String? {
val result = (data.walletManager as TransactionSender).send(
transactionData = data.transaction,
signer = Signer(tangemSdk)
)
return when (result) {
SimpleResult.Success -> {
HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash
}
is SimpleResult.Failure -> {
Timber.e(result.error)
null
}
}
}
private suspend fun signTransaction(data: WcTransactionData): String? {
val dataToSign = EthereumHelper.buildTransactionToSign(
transactionData = data.transaction,
nonce = null,
blockchain = data.walletManager.wallet.blockchain,
gasLimit = null
) ?: return null
val command = SignCommand(
hashes = arrayOf(dataToSign.hash),
walletIndex = WalletIndex.PublicKey(data.walletManager.wallet.publicKey)
)
val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message())
return when (result) {
is CompletionResult.Success -> {
HEX_PREFIX + result.data
}
is CompletionResult.Failure -> {
Timber.e(result.error.customMessage)
null
}
}
}
fun prepareDataForPersonalSign(
message: WCEthereumSignMessage,
session: WalletConnectSession,
id: Long,
): WcPersonalSignData {
val messageData = message.data.removePrefix(HEX_PREFIX).hexToBytes()
val messageString = message.data.hexToAscii() ?: message.data
val prefixData = (ETH_MESSAGE_PREFIX + messageData.size.toString()).toByteArray()
val hashToSign = (prefixData + messageData).keccak()
val dialogData = PersonalSignDialogData(
cardId = session.wallet.cardId,
dAppName = session.peerMeta.name,
message = messageString,
session = session.session,
id = id
)
return WcPersonalSignData(
hash = hashToSign,
session = session,
id = id,
dialogData = dialogData
)
}
private fun String.hexToAscii(): String? {
return removePrefix(HEX_PREFIX).hexToBytes()
.map {
val char = it.toInt().toChar()
if (char.isAscii()) char else return null
}
.joinToString("")
}
private fun Char.isAscii(): Boolean = CharMatcher.ascii().matches(this)
suspend fun signPersonalMessage(hashToSign: ByteArray, wallet: WalletForSession): String? {
val publicKey = wallet.walletPublicKey.hexToBytes()
val command = SignCommand(
arrayOf(hashToSign),
WalletIndex.PublicKey(publicKey))
return when (val result = tangemSdkManager.runTaskAsync(command, wallet.cardId)) {
is CompletionResult.Success -> {
val hash = result.data.signatures.first()
val r = BigInteger(1, hash.copyOfRange(0, 32))
val s = BigInteger(1, hash.copyOfRange(32, 64))
val ecdsaSignature = ECDSASignature(r, s).canonicalise()
val recId = ecdsaSignature.determineRecId(hashToSign,
PublicKey(publicKey.sliceArray(1..64)))
val v = (recId + 27).toBigInteger()
return HEX_PREFIX + ecdsaSignature.r.toString(16) + ecdsaSignature.s.toString(16) +
v.toString(16)
}
is CompletionResult.Failure -> {
Timber.e(result.error.customMessage)
null
}
}
}
companion object {
private const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"
private const val HEX_PREFIX = "0x"
}
}

View file

@ -0,0 +1,79 @@
package com.tangem.tap.features.details.redux.walletconnect
import android.app.Activity
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.wallet.R
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
import com.trustwallet.walletconnect.models.session.WCSession
import org.rekotlin.Action
sealed class WalletConnectAction : Action {
data class HandleDeepLink(val wcUri: String?) : WalletConnectAction()
object RestoreSessions : WalletConnectAction()
data class StartWalletConnect(
val activity: Activity,
) : WalletConnectAction()
data class ShowClipboardOrScanQrDialog(val wcUri: String) : WalletConnectAction()
data class ScanCard(val wcUri: String) : WalletConnectAction()
object UnsupportedCard : WalletConnectAction()
data class OpenSession(
val wcUri: String,
val wallet: WalletForSession,
) : WalletConnectAction()
data class AcceptOpeningSession(val session: WalletConnectSession) : WalletConnectAction()
data class ApproveSession(
val session: WCSession,
) : WalletConnectAction() {
data class Success(val session: WalletConnectSession) : WalletConnectAction()
}
object FailureEstablishingSession : WalletConnectAction()
data class SetSessionsRestored(val sessions: List<WalletConnectSession>) :
WalletConnectAction()
data class DisconnectSession(val session: WCSession) : WalletConnectAction()
data class RemoveSession(val session: WCSession) : WalletConnectAction()
data class HandleTransactionRequest(
val transaction: WCEthereumTransaction,
val session: WalletConnectSession,
val id: Long,
val type: WcTransactionType,
) :
WalletConnectAction()
data class SetDataToSend(val transactionData: WcTransactionData) : WalletConnectAction()
data class HandlePersonalSignRequest(
val message: WCEthereumSignMessage,
val session: WalletConnectSession,
val id: Long,
) : WalletConnectAction()
data class SendTransaction(val session: WCSession) : WalletConnectAction()
data class SignMessage(val session: WCSession) : WalletConnectAction()
data class RejectRequest(val session: WCSession, val id: Long) : WalletConnectAction()
object NotEnoughFunds : WalletConnectAction(), NotificationAction {
override val messageResource = R.string.wallet_connect_create_tx_not_enough_funds
}
object NotifyCameraPermissionIsRequired : WalletConnectAction(), NotificationAction {
override val messageResource = R.string.common_camera_denied_alert_message
}
}

View file

@ -0,0 +1,156 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.*
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.toHexString
import com.tangem.tap.*
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.isMultiwalletAllowed
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Middleware
class WalletConnectMiddleware {
private val walletConnectManager = WalletConnectManager()
val walletConnectMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is WalletConnectAction.RestoreSessions -> {
walletConnectManager.restoreSessions()
}
is WalletConnectAction.HandleDeepLink -> {
if (!action.wcUri.isNullOrBlank()) {
if (WalletConnectManager.isCorrectWcUri(action.wcUri)) {
store.dispatchOnMain(WalletConnectAction.ScanCard(action.wcUri))
}
}
}
is WalletConnectAction.StartWalletConnect -> {
val uri = action.activity.getFromClipboard()?.toString()
if (uri != null && WalletConnectManager.isCorrectWcUri(uri)) {
store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri))
} else {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.QrScan))
}
}
is WalletConnectAction.ShowClipboardOrScanQrDialog -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.ClipboardOrScanQr(
action.wcUri)))
}
is WalletConnectAction.ScanCard -> {
scope.launch {
val result = tangemSdkManager.scanNote(
FirebaseAnalyticsHandler,
R.string.wallet_connect_scan_card_message
)
withContext(Dispatchers.Main) {
when (result) {
is CompletionResult.Success -> {
val card = result.data.card
val factory =
store.state.globalState.tapWalletManager.walletManagerFactory
val walletManager = if (card.isMultiwalletAllowed) {
if (currenciesRepository.loadCardCurrencies(card.cardId)?.blockchains?.contains(
Blockchain.Ethereum) == true
) {
factory.makeWalletManagerForApp(result.data.card,
Blockchain.Ethereum)
} else {
factory.makeWalletManagerForApp(result.data.card,
Blockchain.Ethereum)?.also {
currenciesRepository.saveAddedBlockchain(card.cardId,
Blockchain.Ethereum)
}
}
} else {
null
}
if (walletManager == null) {
store.dispatchOnMain(WalletConnectAction.UnsupportedCard)
return@withContext
};
val key = walletManager.wallet.publicKey
store.dispatchOnMain(WalletConnectAction.OpenSession(
wcUri = action.wcUri,
wallet = WalletForSession(
card.cardId, key.toHexString(),
isTestNet = false
),
))
}
is CompletionResult.Failure ->
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession)
}
}
}
}
is WalletConnectAction.OpenSession -> {
walletConnectManager.connect(
wcUri = action.wcUri,
wallet = action.wallet
)
}
is WalletConnectAction.AcceptOpeningSession -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.ApproveWcSession(
action.session)))
}
is WalletConnectAction.ApproveSession -> {
walletConnectManager.approve(action.session)
}
is WalletConnectAction.DisconnectSession -> {
walletConnectManager.disconnect(action.session)
}
is WalletConnectAction.HandleTransactionRequest -> {
walletConnectManager.handleTransactionRequest(
transaction = action.transaction,
session = action.session,
id = action.id,
type = action.type
)
}
is WalletConnectAction.HandlePersonalSignRequest -> {
walletConnectManager.handlePersonalSignRequest(
message = action.message,
session = action.session,
id = action.id
)
}
is WalletConnectAction.RejectRequest -> {
walletConnectManager.rejectRequest(action.session, action.id)
}
is WalletConnectAction.SendTransaction -> {
walletConnectManager.completeTransaction(action.session)
}
is WalletConnectAction.SignMessage -> {
walletConnectManager.sendSignedMessage(action.session)
}
}
next(action)
}
}
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.tap.features.details.redux.walletconnect
import org.rekotlin.Action
class WalletConnectReducer {
companion object {
fun reduce(
action: Action, state: WalletConnectState,
): WalletConnectState {
if (action !is WalletConnectAction) return state
return when (action) {
is WalletConnectAction.ApproveSession.Success -> {
state.copy(
loading = false,
sessions = state.sessions + action.session
)
}
is WalletConnectAction.SetSessionsRestored ->
WalletConnectState(sessions = action.sessions)
is WalletConnectAction.RemoveSession -> {
val sessions =
state.sessions.filterNot { it.session.toUri() == action.session.toUri() }
state.copy(sessions = sessions)
}
is WalletConnectAction.ScanCard -> state.copy(loading = true)
is WalletConnectAction.FailureEstablishingSession -> state.copy(loading = false)
else -> state
}
}
}
}

View file

@ -0,0 +1,70 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.hexToBytes
import com.tangem.tap.common.redux.global.StateDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData
import com.trustwallet.walletconnect.models.WCPeerMeta
import com.trustwallet.walletconnect.models.session.WCSession
data class WalletConnectState(
val loading: Boolean = false,
val sessions: List<WalletConnectSession> = listOf(),
)
data class WalletConnectSession(
val peerId: String,
val remotePeerId: String?,
val wallet: WalletForSession,
val session: WCSession,
val peerMeta: WCPeerMeta,
)
data class WalletForSession(
val cardId: String,
val walletPublicKey: String,
val isTestNet: Boolean = false,
) {
val chainId
get() = if (isTestNet) 4 else 1
fun getAddress(): String {
val blockchain = if (isTestNet) Blockchain.EthereumTestnet else Blockchain.Ethereum
return blockchain.makeAddresses(walletPublicKey.hexToBytes()).first().value
}
}
sealed class WalletConnectDialog : StateDialog {
data class ClipboardOrScanQr(val clipboardUri: String) : WalletConnectDialog()
data class ApproveWcSession(val session: WalletConnectSession) : WalletConnectDialog()
data class RequestTransaction(val dialogData: TransactionRequestDialogData) :
WalletConnectDialog()
data class PersonalSign(val data: PersonalSignDialogData) : WalletConnectDialog()
}
data class WcTransactionData(
val type: WcTransactionType,
val transaction: TransactionData,
val session: WalletConnectSession,
val id: Long,
val walletManager: WalletManager,
val dialogData: TransactionRequestDialogData,
)
enum class WcTransactionType {
EthSignTransaction,
EthSendTransaction,
}
data class WcPersonalSignData(
val hash: ByteArray,
val session: WalletConnectSession,
val id: Long,
val dialogData: PersonalSignDialogData,
)

View file

@ -9,6 +9,7 @@ import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.twins.getTwinCardIdForUser
import com.tangem.tap.domain.twins.isTwinCard
@ -120,6 +121,10 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
store.dispatch(GlobalAction.SendFeedback(FeedbackEmail()))
}
tv_wallet_connect.setOnClickListener {
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
}
tv_security_title.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)
}

View file

@ -0,0 +1,90 @@
package com.tangem.tap.features.details.ui.walletconnect
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.google.zxing.Result
import com.otaliastudios.cameraview.CameraView
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.store
import me.dm7.barcodescanner.zxing.ZXingScannerView
class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
private var scannerView: ZXingScannerView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
if (!permissionIsGranted()) requestPermission()
scannerView = ZXingScannerView(activity)
return scannerView
}
override fun onPause() {
super.onPause()
scannerView?.stopCamera()
}
override fun onResume() {
super.onResume()
scannerView?.setResultHandler(this)
scannerView?.startCamera()
}
override fun handleResult(result: Result) {
store.dispatch(NavigationAction.PopBackTo())
if (!result.text.isNullOrBlank()) {
store.dispatch(WalletConnectAction.ScanCard(result.text))
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
) {
if (requestCode != CameraView.PERMISSION_REQUEST_CODE) return
if (grantResults.isEmpty() || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
store.dispatch(WalletConnectAction.NotifyCameraPermissionIsRequired)
store.dispatch(NavigationAction.PopBackTo())
}
}
private fun permissionIsGranted(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val cameraPermission =
ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA)
cameraPermission == PackageManager.PERMISSION_GRANTED
} else {
true
}
}
private fun requestPermission() {
requestPermissions(arrayOf(Manifest.permission.CAMERA), CameraView.PERMISSION_REQUEST_CODE)
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.tap.features.details.ui.walletconnect
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.item_wallet_connect_session.view.*
class WalletConnectSessionsAdapter
: ListAdapter<WalletConnectSession, WalletConnectSessionsAdapter.SessionsViewHolder>(
DiffUtilCallback
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SessionsViewHolder {
val layout = LayoutInflater.from(parent.context)
.inflate(R.layout.item_wallet_connect_session, parent, false)
return SessionsViewHolder(layout)
}
override fun onBindViewHolder(holder: SessionsViewHolder, position: Int) {
holder.bind(currentList[position])
}
object DiffUtilCallback : DiffUtil.ItemCallback<WalletConnectSession>() {
override fun areContentsTheSame(
oldItem: WalletConnectSession, newItem: WalletConnectSession,
) = oldItem == newItem
override fun areItemsTheSame(
oldItem: WalletConnectSession, newItem: WalletConnectSession,
) = oldItem == newItem
}
class SessionsViewHolder(val view: View) :
RecyclerView.ViewHolder(view) {
fun bind(session: WalletConnectSession) {
view.tv_card_id.text = view.context.getString(
R.string.wallet_connect_card_number, session.wallet.cardId
)
view.tv_d_app_name.text = session.peerMeta.name
view.btn_disconnect.setOnClickListener {
store.dispatch(WalletConnectAction.DisconnectSession(session.session))
}
}
}
}

View file

@ -0,0 +1,87 @@
package com.tangem.tap.features.details.ui.walletconnect
import android.app.Dialog
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_wallet_connect_sessions.*
import org.rekotlin.StoreSubscriber
class WalletConnectSessionsFragment : Fragment(R.layout.fragment_wallet_connect_sessions),
StoreSubscriber<WalletConnectState> {
private lateinit var walletConnectSessionsAdapter: WalletConnectSessionsAdapter
private var dialog: Dialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(
this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.walletConnectState == newState.walletConnectState
}.select { it.walletConnectState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
setOnClickListeners()
setupTransactionsRecyclerView()
}
private fun setOnClickListeners() {
fab_open_session.setOnClickListener {
store.dispatch(WalletConnectAction.StartWalletConnect(activity = requireActivity()))
}
}
private fun setupTransactionsRecyclerView() {
walletConnectSessionsAdapter = WalletConnectSessionsAdapter()
rv_wallet_connect_sessions.layoutManager = LinearLayoutManager(requireContext())
rv_wallet_connect_sessions.adapter = walletConnectSessionsAdapter
walletConnectSessionsAdapter.submitList(store.state.walletConnectState.sessions)
}
override fun newState(state: WalletConnectState) {
if (activity == null) return
fab_open_session.show(!state.loading)
pb_wallet_connect.show(state.loading)
walletConnectSessionsAdapter.submitList(state.sessions)
rv_wallet_connect_sessions.show(state.sessions.isNotEmpty())
tv_no_sessions.show(state.sessions.isEmpty())
tv_no_sessions_title.show(state.sessions.isEmpty())
}
}

View file

@ -0,0 +1,35 @@
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.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.store
import com.tangem.wallet.R
class ApproveWcSessionDialog {
companion object {
fun create(session: WalletConnectSession, context: Context): AlertDialog {
val message = context.getString(
R.string.wallet_connect_request_session_start,
session.wallet.cardId,
session.peerMeta.name,
session.peerMeta.url
)
return AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.wallet_connect))
setMessage(message)
setPositiveButton(context.getText(R.string.common_start)) { _, _ ->
store.dispatch(WalletConnectAction.ApproveSession(
session.session
))
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ -> }
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}
}

View file

@ -0,0 +1,30 @@
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.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.store
import com.tangem.wallet.R
class ClipboardOrScanQrDialog {
companion object {
fun create(wcUri: String, context: Context): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.wallet_connect))
setMessage(context.getText(R.string.wallet_connect_clipboard_alert))
setPositiveButton(context.getText(R.string.wallet_connect_paste_from_clipboard)) { _, _ ->
store.dispatch(WalletConnectAction.ScanCard(wcUri))
}
setNegativeButton(context.getText(R.string.wallet_connect_scan_new_code)) { _, _ ->
store.dispatch(NavigationAction.NavigateTo(AppScreen.QrScan))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}
}

View file

@ -0,0 +1,45 @@
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.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.store
import com.tangem.wallet.R
import com.trustwallet.walletconnect.models.session.WCSession
class PersonalSignDialog {
companion object {
fun create(
data: PersonalSignDialogData,
context: Context,
): AlertDialog {
val message =
context.getString(R.string.wallet_connect_alert_sign_message, data.cardId) +
context.getString(R.string.wallet_connect_personal_sign_message,
data.dAppName,
data.message)
return AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.wallet_connect))
setMessage(message)
setPositiveButton(context.getText(R.string.common_sign)) { _, _ ->
store.dispatch(WalletConnectAction.SignMessage(data.session))
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
store.dispatch(WalletConnectAction.RejectRequest(data.session, data.id))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}
}
data class PersonalSignDialogData(
val cardId: String,
val dAppName: String,
val message: String,
val session: WCSession,
val id: Long,
)

View file

@ -0,0 +1,67 @@
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.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WcTransactionType
import com.tangem.tap.store
import com.tangem.wallet.R
import com.trustwallet.walletconnect.models.session.WCSession
class TransactionDialog {
companion object {
fun create(
data: TransactionRequestDialogData,
context: Context,
): AlertDialog {
val message = context.getString(
R.string.wallet_connect_create_tx_message,
data.cardId,
data.dAppName,
data.dAppUrl,
data.amount,
data.gasAmount,
data.totalAmount,
data.balance
)
val positiveButtonTitle = when (data.type) {
WcTransactionType.EthSignTransaction -> context.getText(R.string.common_sign)
WcTransactionType.EthSendTransaction -> context.getText(R.string.common_sign_and_send)
}
return AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.wallet_connect))
setMessage(message)
setPositiveButton(positiveButtonTitle) { _, _ ->
if (data.isEnoughFundsToSend) {
store.dispatch(WalletConnectAction.SendTransaction(data.session))
} else {
store.dispatch(WalletConnectAction.RejectRequest(data.session, data.id))
store.dispatch(WalletConnectAction.NotEnoughFunds)
}
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
store.dispatch(WalletConnectAction.RejectRequest(data.session, data.id))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}
}
data class TransactionRequestDialogData(
val cardId: String,
val dAppName: String,
val dAppUrl: String,
val amount: String,
val gasAmount: String,
val totalAmount: String,
val balance: String,
val isEnoughFundsToSend: Boolean,
val session: WCSession,
val id: Long,
val type: WcTransactionType,
)

View file

@ -15,6 +15,7 @@ import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import org.rekotlin.Action
import java.math.BigDecimal
import java.math.RoundingMode
class WalletReducer {
companion object {
@ -291,7 +292,7 @@ private fun setNewFiatRate(
appCurrency: FiatCurrencyName, state: WalletState
): WalletState {
val rate = fiatRate.second ?: return state
val rateFormatted = rate.toFormattedCurrencyString(2, appCurrency)
val rateFormatted = rate.toFormattedCurrencyString(2, appCurrency, RoundingMode.HALF_UP)
val currency = fiatRate.first
return if (!state.isMultiwalletAllowed) {

View file

@ -10,8 +10,7 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import com.squareup.picasso.Picasso
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.MainActivity
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.global.StateDialog
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -25,8 +24,6 @@ import kotlinx.android.synthetic.main.fragment_details_twin_cards.*
import kotlinx.android.synthetic.main.fragment_wallet_details.*
import kotlinx.android.synthetic.main.fragment_wallet_details.toolbar
import kotlinx.android.synthetic.main.item_currency_wallet.view.*
import kotlinx.android.synthetic.main.item_currency_wallet.view.iv_currency
import kotlinx.android.synthetic.main.item_currency_wallet.view.tv_token_letter
import kotlinx.android.synthetic.main.item_popular_token.view.*
import kotlinx.android.synthetic.main.layout_balance_error.*
import kotlinx.android.synthetic.main.layout_balance_wallet_details.*
@ -184,13 +181,13 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
if (state.state == ProgressState.Error) {
if (state.error == ErrorType.NoInternetConnection) {
srl_wallet_details?.isRefreshing = false
(activity as? MainActivity)?.showSnackbar(
(activity as? SnackbarHandler)?.showSnackbar(
text = R.string.wallet_notification_no_internet,
buttonTitle = R.string.common_retry
) { store.dispatch(WalletAction.LoadData) }
}
} else {
(activity as? MainActivity)?.dismissSnackbar()
(activity as? SnackbarHandler)?.dismissSnackbar()
}
}

View file

@ -106,10 +106,10 @@
android:id="@+id/tv_signed_hashes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/darkGray1"
android:textSize="16sp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:textColor="@color/darkGray1"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_issuer"
tools:text="48 hashes" />
@ -132,16 +132,16 @@
android:id="@+id/tv_card_tou"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="15dp"
android:paddingTop="7dp"
android:paddingBottom="10dp"
android:drawablePadding="15dp"
android:text="@string/details_row_title_card_tou"
android:textColor="@color/darkGray6"
android:textSize="16sp"
android:visibility="gone"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_disclaimer"
android:text="@string/details_row_title_card_tou" />
app:layout_constraintTop_toBottomOf="@id/tv_disclaimer" />
<TextView
android:id="@+id/tv_settings_title"
@ -200,6 +200,26 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_app_currency_title" />
<TextView
android:id="@+id/tv_wallet_connect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="@string/wallet_connect"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_send_feedback" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="tv_wallet_connect,tv_send_feedback" />
<TextView
android:id="@+id/tv_card_title"
android:layout_width="wrap_content"
@ -211,7 +231,7 @@
android:textColor="@color/colorSecondary"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_send_feedback" />
app:layout_constraintTop_toBottomOf="@id/barrier" />
<View
android:layout_width="match_parent"

View file

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroundLightGray"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
style="@style/Widget.MaterialComponents.Toolbar.Surface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
android:fitsSystemWindows="true"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:title="@string/wallet_connect_sessions_title" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="16dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/pb_wallet_connect"
android:indeterminate="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/tv_no_sessions_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/wallet_connect_no_sessions_title"
android:textColor="@color/darkGray6"
android:textSize="32sp"
app:layout_constraintBottom_toTopOf="@id/tv_no_sessions"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/tv_no_sessions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/wallet_connect_no_sessions_message"
android:textAlignment="center"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_no_sessions_title" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_wallet_connect_sessions"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_open_session"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="33dp"
android:contentDescription="@string/wallet_connect_open_session"
android:src="@drawable/ic_add"
app:backgroundTint="@color/accent"
app:borderWidth="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:tint="@android:color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="1dp"
android:background="@android:color/white">
<TextView
android:id="@+id/tv_d_app_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:textColor="@color/darkGray6"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/tv_card_id"
app:layout_constraintEnd_toStartOf="@id/btn_disconnect"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="WalletConnect Example fsdljfsdlkjfsdlkjflsdkjflsdjf slfjdskfjskldjflskd" />
<TextView
android:id="@+id/tv_card_id"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:textColor="@color/darkGray6"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/btn_disconnect"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_d_app_name"
tools:text="Card: CB241024012410" />
<com.google.android.material.chip.Chip
android:id="@+id/btn_disconnect"
style="@style/Widget.MaterialComponents.Chip.Action"
android:layout_width="102dp"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:src="@drawable/ic_inactive"
android:text="@string/common_disconnect"
android:textAlignment="center"
android:textColor="@android:color/white"
app:chipBackgroundColor="@color/accent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -113,4 +113,44 @@ this wallet.
<string name="alert_button_i_understand" translatable="false">I understand</string>
<string name="warning_low_signatures_format" translatable="false">There are only %s signatures available on this card. You must withdraw all of your funds.</string>
//Wallet Connect
<string name="wallet_connect" translatable="false">Wallet Connect</string>
<string name="wallet_connect_sessions_title" translatable="false">Wallet Connect Sessions</string>
<string name="wallet_connect_session_opened" translatable="false">WalletConnect session opened with %s</string>
<string name="wallet_connect_no_sessions_title" translatable="false">Ooops. No Sessions.</string>
<string name="wallet_connect_no_sessions_message" translatable="false">No opened WalletConnect sessions</string>
<string name="wallet_connect_open_session" translatable="false">Open session</string>
<string name="wallet_connect_card_number" translatable="false">Card: %s</string>
<string name="wallet_connect_scan_card_message" translatable="false">Tap card to bind to wallet connect</string>
<string name="wallet_connect_create_tx_message" translatable="false">Card: %1s
Request to create transaction for %2s
\n%3s
\n\nAmount: %4s
\nFee: %5s
\nTotal: %6s
\nBalance: %7s</string>
<string name="wallet_connect_create_tx_not_enough_funds" translatable="false">Can\'t send transaction. Not enough funds.</string>
<string name="wallet_connect_request_session_start" translatable="false">Request to start a session for card with ID %1s\n
for %2s\n\n
URL: %3s</string>
<string name="wallet_connect_alert_sign_message" translatable="false">Requesting to sign a message\nwith card %s\n\n</string>
<string name="wallet_connect_personal_sign_message" translatable="false">Message for %s:\n%s</string>
<string name="common_disconnect" translatable="false">Disconnect</string>
<string name="common_sign" translatable="false">Sign</string>
<string name="common_sign_and_send" translatable="false">Sign and send</string>
<string name="common_reject" translatable="false">Reject</string>
<string name="wallet_connect_clipboard_alert" translatable="false">Clipboard contain WalletConnect code. Use copied value or scan QR-code</string>
<string name="wallet_connect_paste_from_clipboard" translatable="false">Paste from clipboard</string>
<string name="wallet_connect_scan_new_code" translatable="false">Scan new code</string>
</resources>