Updated on 2026-08-14
This commit is contained in:
parent
73339902b8
commit
0a40ed4957
46 changed files with 1856 additions and 69 deletions
|
|
@ -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
BIN
app/libs/walletconnect.aar
Normal file
Binary file not shown.
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -882,6 +882,12 @@
|
|||
"name" : "WAX",
|
||||
"contractAddress" : "0x39Bb259F66E1C59d5ABEF88375979b4D20D98022"
|
||||
},
|
||||
{
|
||||
"decimalCount" : 18,
|
||||
"symbol" : "WETH",
|
||||
"name" : "Wrapped Ether",
|
||||
"contractAddress" : "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
||||
},
|
||||
{
|
||||
"decimalCount" : 18,
|
||||
"symbol" : "WPR",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
55
app/src/main/java/com/tangem/tap/common/DialogManager.kt
Normal file
55
app/src/main/java/com/tangem/tap/common/DialogManager.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
41
app/src/main/java/com/tangem/tap/common/IntentHandler.kt
Normal file
41
app/src/main/java/com/tangem/tap/common/IntentHandler.kt
Normal 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()))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
15
app/src/main/java/com/tangem/tap/common/extensions/Store.kt
Normal file
15
app/src/main/java/com/tangem/tap/common/extensions/Store.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
92
app/src/main/res/layout/fragment_wallet_connect_sessions.xml
Normal file
92
app/src/main/res/layout/fragment_wallet_connect_sessions.xml
Normal 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>
|
||||
57
app/src/main/res/layout/item_wallet_connect_session.xml
Normal file
57
app/src/main/res/layout/item_wallet_connect_session.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue