Updated on 2026-08-14

This commit is contained in:
Tangem 2023-06-27 20:24:22 +03:00
commit 448fcc09b7
138 changed files with 3030 additions and 3981 deletions

View file

@ -27,16 +27,16 @@
</queries>
<application
android:theme="@style/SplashTheme"
android:label="@string/tangem_app_name"
android:name="com.tangem.tap.TapApplication"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:allowBackup="false"
android:fullBackupContent="false"
android:networkSecurityConfig="@xml/network_security_config"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/tangem_app_name"
android:largeHeap="@bool/largeHeap"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:allowBackup, android:fullBackupContent">
@ -45,10 +45,11 @@
android:value="true" />
<activity
android:exported="true"
android:name="com.tangem.tap.MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/SplashTheme"
android:windowSoftInputMode="adjustResize">
<intent-filter>
@ -125,13 +126,17 @@
</activity>
<activity android:name="com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity" />
<activity
android:name="com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity"
android:theme="@style/AppTheme" />
<activity
android:name="zendesk.messaging.MessagingActivity"
android:theme="@style/ZendeskTheme" />
<activity android:name="com.tangem.tap.features.sprinklr.ui.SprinklrActivity" />
<activity
android:name="com.tangem.tap.features.sprinklr.ui.SprinklrActivity"
android:theme="@style/AppTheme" />
<provider
android:name="androidx.core.content.FileProvider"

View file

@ -1,6 +1,4 @@
{
"isWalletPayIdEnabled": true,
"isSendingToPayIdEnabled": true,
"isTopUpEnabled": true,
"isCreatingTwinCardsAllowed": true
}

View file

@ -1,6 +1,4 @@
{
"isWalletPayIdEnabled": false,
"isSendingToPayIdEnabled": true,
"isTopUpEnabled": true,
"isCreatingTwinCardsAllowed": true
}

View file

@ -100,12 +100,19 @@ class DialogManager : StoreSubscriber<GlobalState> {
preparedData = state.dialog.data,
context = context,
)
is WalletConnectDialog.UnsupportedNetwork ->
is WalletConnectDialog.UnsupportedNetwork -> {
val warning = if (state.dialog.networks.isNullOrEmpty()) {
context.getString(R.string.wallet_connect_scanner_error_unsupported_network)
} else {
context.getString(R.string.wallet_connect_error_unsupported_blockchains) +
state.dialog.networks
}
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_scanner_error_unsupported_network,
message = warning,
context = context,
)
}
is WalletConnectDialog.SessionProposalDialog -> {
SessionProposalDialog.create(
sessionProposal = state.dialog.sessionProposal,

View file

@ -10,7 +10,6 @@ import com.tangem.wallet.R
fun Blockchain.getGreyedOutIconRes(): Int {
return when (this) {
Blockchain.Arbitrum, Blockchain.ArbitrumTestnet -> R.drawable.ic_arbitrum_no_color
// Blockchain.Ducatus -> R.drawable.ic_ducatus
Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> R.drawable.ic_bitcoin_no_color
Blockchain.BitcoinCash -> R.drawable.ic_bitcoin_cash_no_color
Blockchain.Litecoin -> R.drawable.ic_litecoin_no_color

View file

@ -6,7 +6,6 @@ import com.tangem.tap.common.analytics.topup.TopUpController
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
@ -15,11 +14,11 @@ import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import org.rekotlin.StateType
data class GlobalState(
@Deprecated("Use scan response from selected user wallet")
val scanResponse: ScanResponse? = null,
val onboardingState: OnboardingState = OnboardingState(),
val cardVerifiedOnline: Boolean = false,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val payIdManager: PayIdManager = PayIdManager(),
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val feedbackManager: FeedbackManager? = null,

View file

@ -1,57 +0,0 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.tap.network.payid.PayIdVerifyService
import com.tangem.tap.network.payid.VerifyPayIdResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.*
class PayIdManager {
@Suppress("MagicNumber")
suspend fun verifyPayId(payId: String, blockchain: Blockchain): Result<VerifyPayIdResponse> =
withContext(Dispatchers.IO) {
val splitPayId = payId.split("\$")
val user = splitPayId[0]
val baseUrl = "https://${splitPayId[1]}/"
return@withContext PayIdVerifyService(baseUrl).verifyAddress(user, blockchain.getPayIdNetwork())
}
private fun Blockchain.getPayIdNetwork(): String {
return when (this) {
Blockchain.XRP -> "XRPL"
Blockchain.RSK -> "RSK"
else -> this.currency
}.lowercase(Locale.getDefault())
}
companion object {
private val payIdRegExp = (
"^[a-z0-9!#@%&*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#@%&*+/=?^_`{|}~-]+)*\\\$(?:(?:[a-z0-9]" +
"(?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z-]*[a-z0-9])?|(?:[0-9]{1,3}\\.){3}[0-9]{1,3})\$"
).toRegex()
val payIdSupported: EnumSet<Blockchain> = EnumSet.of(
Blockchain.XRP,
Blockchain.Ethereum,
Blockchain.Bitcoin,
Blockchain.Litecoin,
Blockchain.Stellar,
Blockchain.Cardano,
Blockchain.CardanoShelley,
Blockchain.Ducatus,
Blockchain.BitcoinCash,
Blockchain.Binance,
Blockchain.RSK,
Blockchain.Tezos,
)
fun isPayId(value: String?): Boolean = value?.contains(payIdRegExp) ?: false
}
}
fun Blockchain.isPayIdSupported(): Boolean {
return PayIdManager.payIdSupported.contains(this)
}

View file

@ -1,6 +1,9 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.Analytics
@ -83,9 +86,9 @@ class TapWalletManager(
}
private fun setupWalletConnectV2(userWallet: UserWallet) {
val cardId = if (userWallet.cardsInWallet.size == 1) {
val cardId = if (userWallet.scanResponse.card.backupStatus?.isActive != true) {
userWallet.cardId
} else {
} else { // if wallet has backup, any card from wallet can be used to sign
null
}
scope.launch {
@ -134,22 +137,13 @@ class TapWalletManager(
fun updateConfigManager(data: ScanResponse) {
val configManager = store.state.globalState.configManager
val blockchain = data.cardTypesResolver.getBlockchain()
if (data.cardTypesResolver.isStart2Coin()) {
configManager?.turnOff(ConfigManager.IS_SENDING_TO_PAY_ID_ENABLED)
configManager?.turnOff(ConfigManager.IS_TOP_UP_ENABLED)
} else if (blockchain == Blockchain.Bitcoin ||
data.walletData?.blockchain == Blockchain.Bitcoin.id
) {
configManager?.resetToDefault(ConfigManager.IS_SENDING_TO_PAY_ID_ENABLED)
configManager?.resetToDefault(ConfigManager.IS_TOP_UP_ENABLED)
} else {
configManager?.resetToDefault(ConfigManager.IS_SENDING_TO_PAY_ID_ENABLED)
configManager?.resetToDefault(ConfigManager.IS_TOP_UP_ENABLED)
}
}
}
fun Wallet.getFirstToken(): Token? {
return getTokens().toList().getOrNull(0)
}
fun Wallet.getFirstToken(): Token? = getTokens().toList().getOrNull(index = 0)

View file

@ -118,7 +118,9 @@ internal class BiometricUserWalletsListManager(
}
.flatMap { updatedUserWallet ->
saveInternal(updatedUserWallet, changeSelectedUserWallet = false)
.map { updatedUserWallet }
}
.flatMap {
get(userWalletId)
}
}

View file

@ -1,13 +1,10 @@
package com.tangem.tap.domain.walletCurrencies.implementation
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.CompletionResult
import com.tangem.common.flatMap
import com.tangem.common.fold
import com.tangem.common.map
import com.tangem.domain.models.scan.CardDTO
import com.tangem.common.*
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.util.UserWalletId
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.builders.WalletStoreBuilder
@ -103,7 +100,7 @@ internal class DefaultWalletCurrenciesManager(
.flatMap {
updateWalletStores(userWallet, remainingCurrencies.toBlockchainNetworks())
}
.map {
.doOnResult {
saveUserCurrencies(card, remainingCurrencies)
}
}

View file

@ -7,16 +7,11 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.Result.Failure
import com.tangem.blockchain.extensions.Result.Success
import com.tangem.common.CompletionResult
import com.tangem.common.catching
import com.tangem.common.*
import com.tangem.common.core.TangemError
import com.tangem.common.flatMap
import com.tangem.common.flatMapOnFailure
import com.tangem.common.fold
import com.tangem.common.map
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.common.util.UserWalletId
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.entities.FiatCurrency
@ -25,15 +20,7 @@ import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.walletStores.WalletStoresError
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
import com.tangem.tap.domain.walletStores.repository.implementation.utils.replaceWalletStore
import com.tangem.tap.domain.walletStores.repository.implementation.utils.replaceWalletStores
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithAmounts
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithDemoAmounts
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithError
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithFiatRates
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithMissedDerivation
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithRent
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithUnreachable
import com.tangem.tap.domain.walletStores.repository.implementation.utils.*
import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage
import com.tangem.tap.domain.walletStores.storage.WalletStoresStorage
import com.tangem.tap.features.demo.DemoHelper
@ -45,13 +32,8 @@ import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.math.BigDecimal
import kotlin.time.Duration
@ -200,37 +182,40 @@ internal class DefaultWalletAmountsRepository(
walletStore: WalletStoreModel,
walletManager: WalletManager?,
): CompletionResult<Unit> {
val hasMissedDerivations = with(walletStore) {
val isDerivationMissed = with(walletStore) {
derivationPath != null && !scanResponse.hasDerivation(blockchain, derivationPath.rawPath)
}
return when {
hasMissedDerivations -> {
isDerivationMissed -> {
updateWalletStoreWithMissedDerivation(walletStore)
}
walletManager == null -> {
updateWalletStoreWithUnreachable(walletStore)
}
else -> {
updateWalletManager(scanResponse, walletManager).map {
updateWalletManagerInStorage(
userWalletId,
walletManager,
)
}.flatMap {
updateWalletStoreWithAmounts(
walletStore = walletStore,
updatedWallet = walletManager.wallet,
// FIXME: move DemoHelper to Demo core module maybe
isDemo = DemoHelper.isDemoCardId(scanResponse.card.cardId),
)
}.flatMap { fetchWalletStoreRentIfNeeded(walletStore, walletManager) }.flatMapOnFailure { error ->
updateWalletStoreWithError(
walletStore = walletStore,
wallet = walletManager.wallet,
error = error,
)
}
updateWalletManager(scanResponse, walletManager)
.map {
updateWalletManagerInStorage(userWalletId, walletManager)
}
.flatMap {
updateWalletStoreWithAmounts(
walletStore = walletStore,
updatedWallet = walletManager.wallet,
// FIXME: move DemoHelper to Demo core module maybe
isDemo = DemoHelper.isDemoCardId(scanResponse.card.cardId),
)
}
.flatMap {
fetchWalletStoreRentIfNeeded(walletStore, walletManager)
}
.flatMapOnFailure { error ->
updateWalletStoreWithError(
walletStore = walletStore,
wallet = walletManager.wallet,
error = error,
)
}
}
}
}

View file

@ -34,7 +34,6 @@ import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder
import com.tangem.tap.features.details.redux.walletconnect.*
import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType
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
@ -194,7 +193,12 @@ class WalletConnectSdkHelper {
val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)
return when (result) {
is CompletionResult.Success -> {
HEX_PREFIX + result.data
HEX_PREFIX + EthereumUtils.prepareTransactionToSend(
signature = result.data.signature,
transactionToSign = dataToSign,
walletPublicKey = data.walletManager.wallet.publicKey,
blockchain = data.walletManager.wallet.blockchain,
).toHexString()
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.SignError(it)) }

View file

@ -1,15 +1,22 @@
package com.tangem.tap.domain.walletconnect2.data
import android.app.*
import com.tangem.tap.domain.walletconnect2.domain.*
import android.app.Application
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.walletconnect.android.*
import com.walletconnect.android.relay.*
import com.walletconnect.web3.wallet.client.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.*
import javax.inject.*
import com.walletconnect.android.Core
import com.walletconnect.android.CoreClient
import com.walletconnect.android.relay.ConnectionType
import com.walletconnect.web3.wallet.client.Wallet
import com.walletconnect.web3.wallet.client.Web3Wallet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class WalletConnectRepositoryImpl @Inject constructor(
private val application: Application,
@ -181,7 +188,7 @@ class WalletConnectRepositoryImpl @Inject constructor(
userNamespaces = userNamespaces,
)
if (missingNetworks.isNotEmpty()) {
Timber.e("Unsupported blockchains: $missingNetworks")
Timber.e("Not added blockchains: $missingNetworks")
scope.launch {
_events.emit(
WalletConnectEvents.SessionApprovalError(

View file

@ -1,13 +1,16 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.tap.common.extensions.*
import com.tangem.tap.domain.walletconnect.*
import com.tangem.tap.common.extensions.filterNotNull
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.tangem.tap.features.details.ui.walletconnect.*
import com.tangem.utils.coroutines.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.*
import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
class WalletConnectInteractor(
private val handler: WalletConnectEventsHandler,
@ -34,11 +37,11 @@ class WalletConnectInteractor(
suspend fun startListening(userWalletId: String, cardId: String?) {
this.userWalletId = userWalletId
this.cardId = cardId
walletConnectRepository.updateSessions()
coroutineScope {
launch { subscribeToEvents() }
launch { subscribeToSessions() }
}
walletConnectRepository.updateSessions()
}
private suspend fun subscribeToEvents() {
@ -47,6 +50,14 @@ class WalletConnectInteractor(
when (wcEvent) {
is WalletConnectEvents.SessionProposal -> {
Timber.d("WC session proposal event received")
val unsupportedNetworks = wcEvent.chainIds
.filter { blockchainHelper.chainIdToNetworkIdOrNull(it) == null }
if (unsupportedNetworks.isNotEmpty()) {
val error = WalletConnectError.ApprovalErrorUnsupportedNetwork(unsupportedNetworks)
handler.onSessionRejected(error)
return@onEach
}
val networksFormatted = wcEvent.chainIds
.mapNotNull { blockchainHelper.chainIdToFullNameOrNull(it) }
.toString()
@ -57,12 +68,7 @@ class WalletConnectInteractor(
is WalletConnectError.ApprovalErrorMissingNetworks -> {
val missingNetworks = wcEvent.error.missingChains
.map { blockchainHelper.chainIdToNetworkIdOrNull(it) }
val containsUnsupportedNetworks = missingNetworks.any { it == null }
if (containsUnsupportedNetworks) {
WalletConnectError.ApprovalErrorUnsupportedNetwork
} else {
WalletConnectError.ApprovalErrorAddNetwork(missingNetworks.filterNotNull())
}
WalletConnectError.ApprovalErrorAddNetwork(missingNetworks.filterNotNull())
}
else -> wcEvent.error
}

View file

@ -130,7 +130,8 @@ class WcSessionRequestConverter(
): String? {
return sessionsRepository.loadSessions(userWalletId)
.firstOrNull { it.topic == sessionRequest.topic }
?.accounts?.firstOrNull { it.chainId == sessionRequest.chainId && it.walletAddress == walletAddress }
?.derivationPath
?.accounts?.firstOrNull {
it.chainId == sessionRequest.chainId && it.walletAddress.lowercase() == walletAddress?.lowercase()
}?.derivationPath
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.tap.domain.walletconnect2.domain.models
sealed class WalletConnectError : Exception() {
data class ApprovalErrorMissingNetworks(val missingChains: List<String>) : WalletConnectError()
data class ApprovalErrorAddNetwork(val networks: List<String>) : WalletConnectError()
object ApprovalErrorUnsupportedNetwork : WalletConnectError()
data class ApprovalErrorUnsupportedNetwork(val unsupportedNetworks: List<String>) : WalletConnectError()
data class ExternalApprovalError(override val message: String?) : WalletConnectError()
object WrongUserWallet : WalletConnectError()
object UnsupportedMethod : WalletConnectError()

View file

@ -4,9 +4,10 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.transition.TransitionInflater
@ -25,8 +26,6 @@ import dagger.hilt.android.AndroidEntryPoint
internal class AddCustomTokenFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
with(TransitionInflater.from(requireContext())) {
enterTransition = inflateTransition(R.transition.fade)
exitTransition = inflateTransition(R.transition.fade)
@ -41,7 +40,10 @@ internal class AddCustomTokenFragment : Fragment() {
}
TangemTheme {
AddCustomTokenScreen(stateHolder = viewModel.uiState)
AddCustomTokenScreen(
modifier = Modifier.systemBarsPadding(),
stateHolder = viewModel.uiState,
)
}
}
}

View file

@ -8,11 +8,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.FabPosition
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
@ -33,11 +29,12 @@ import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCu
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenContent(state: AddCustomTokenStateHolder.Content) {
internal fun AddCustomTokenContent(state: AddCustomTokenStateHolder.Content, modifier: Modifier = Modifier) {
BackHandler(onBack = state.onBackButtonClick)
var floatingButtonHeight by remember { mutableStateOf(0.dp) }
Scaffold(
modifier = modifier,
topBar = {
AddCustomTokenToolbar(
title = state.toolbar.title,

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
@ -15,10 +16,10 @@ import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTok
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenScreen(stateHolder: AddCustomTokenStateHolder) {
internal fun AddCustomTokenScreen(stateHolder: AddCustomTokenStateHolder, modifier: Modifier = Modifier) {
when (stateHolder) {
is AddCustomTokenStateHolder.Content -> AddCustomTokenContent(state = stateHolder)
is AddCustomTokenStateHolder.TestContent -> AddCustomTokenTestContent(state = stateHolder)
is AddCustomTokenStateHolder.Content -> AddCustomTokenContent(stateHolder, modifier)
is AddCustomTokenStateHolder.TestContent -> AddCustomTokenTestContent(stateHolder, modifier)
}
}

View file

@ -1,31 +1,11 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.BottomSheetScaffold
import androidx.compose.material.BottomSheetScaffoldState
import androidx.compose.material.BottomSheetState
import androidx.compose.material.BottomSheetValue
import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FabPosition
import androidx.compose.material.Text
import androidx.compose.material.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@ -59,7 +39,7 @@ import kotlinx.coroutines.launch
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestContent) {
internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestContent, modifier: Modifier = Modifier) {
val coroutineScope = rememberCoroutineScope()
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed),
@ -77,6 +57,7 @@ internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestCont
var floatingButtonHeight by remember { mutableStateOf(0.dp) }
BottomSheetScaffold(
modifier = modifier,
sheetContent = {
SheetContent(
coroutineScope = coroutineScope,

View file

@ -8,7 +8,6 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -161,7 +160,6 @@ private fun SelectorField(model: AddCustomTokenSelectorField) {
expanded = isExpanded && isEnabled,
onDismissRequest = { isExpanded = false },
) {
FocusRequester
model.items.forEachIndexed { index, item ->
DropdownMenuItem(
onClick = {

View file

@ -41,6 +41,7 @@ import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -207,7 +208,8 @@ internal class AddCustomTokenViewModel @Inject constructor(
val defaultNetwork = createNetworkSelectorItem(blockchain = Blockchain.Unknown)
return listOf(defaultNetwork) + Blockchain.values()
.filter { blockchain ->
reduxStateHolder.scanResponse?.card?.supportedBlockchains()?.contains(blockchain) == true
reduxStateHolder.scanResponse?.card?.supportedBlockchains()?.contains(blockchain) == true &&
blockchain != Blockchain.Cardano
}
.sortedBy(Blockchain::fullName)
.map(::createNetworkSelectorItem)
@ -270,7 +272,9 @@ internal class AddCustomTokenViewModel @Inject constructor(
type = DerivationPathSelectorType.CUSTOM,
),
) + Blockchain.values()
.filter { blockchain -> blockchain.isSupportedInApp() && !blockchain.isTestnet() }
.filter { blockchain ->
blockchain.isSupportedInApp() && !blockchain.isTestnet() && blockchain != Blockchain.Cardano
}
.sortedBy(Blockchain::fullName)
.map(::createDerivationPathSelectorAdditionalItem)
}
@ -510,7 +514,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
val sameAddress = contractAddress == wrappedCurrency.token.contractAddress
val sameBlockchain =
Blockchain.fromNetworkId(networkSelectorValue.toNetworkId()) == wrappedCurrency.blockchain
val isSameDerivationPath = getDerivationPath()?.rawPath == wrappedCurrency.derivationPath
val isSameDerivationPath = getDerivationPath().isSameDerivationPath(wrappedCurrency.derivationPath)
sameId && sameAddress && sameBlockchain && isSameDerivationPath
}
}
@ -522,10 +526,15 @@ internal class AddCustomTokenViewModel @Inject constructor(
.filterIsInstance<Currency.Blockchain>()
.any {
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
networkSelectorValue == it.blockchain && getDerivationPath()?.rawPath == it.derivationPath
networkSelectorValue == it.blockchain &&
getDerivationPath().isSameDerivationPath(it.derivationPath)
}
}
private fun DerivationPath?.isSameDerivationPath(rawDerivationPath: String?): Boolean {
return this == rawDerivationPath?.let { DerivationPath(it) }
}
private fun handleContractAddressErrorValidation(type: AddCustomTokenError) {
when {
isNetworkSelected() && type == AddCustomTokenError.InvalidContractAddress -> {
@ -605,7 +614,11 @@ internal class AddCustomTokenViewModel @Inject constructor(
private inner class ActionsHandler(private val featureRouter: CustomTokenRouter) {
fun onBackButtonClick() {
featureRouter.popBackStack()
viewModelScope.launch(dispatchers.main) {
// need delay before close, cause crashed in compose PopUpMenu as
delay(timeMillis = 100)
featureRouter.popBackStack()
}
}
fun onContactAddressValueChange(enteredValue: String) {

View file

@ -66,7 +66,7 @@ class WalletConnectMiddleware {
walletConnectManager.restoreSessions(action.scanResponse)
}
is WalletConnectAction.HandleDeepLink -> {
if (!action.wcUri.isNullOrBlank() && WalletConnectManager.isCorrectWcUri(action.wcUri)) {
if (!action.wcUri.isNullOrBlank()) {
store.dispatchOnMain(WalletConnectAction.OpenSession(action.wcUri))
}
}
@ -238,7 +238,7 @@ class WalletConnectMiddleware {
return
}
val blockchain = action.blockchain.guard {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork()))
return
}
val walletManager = getWalletManager(
@ -298,19 +298,25 @@ class WalletConnectMiddleware {
),
)
}
WalletConnectError.ApprovalErrorUnsupportedNetwork -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
}
is WalletConnectError.ExternalApprovalError -> {
is WalletConnectError.ApprovalErrorUnsupportedNetwork -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
AppDialog.SimpleOkWarningDialog(
message = action.error.message ?: "",
),
WalletConnectDialog.UnsupportedNetwork(action.error.unsupportedNetworks),
),
)
}
is WalletConnectError.ExternalApprovalError -> {
val message = action.error.message
if (!message.isNullOrEmpty()) {
store.dispatchOnMain(
GlobalAction.ShowDialog(
AppDialog.SimpleOkWarningDialog(
message = message,
),
),
)
}
}
else -> Unit
}
}
@ -327,7 +333,7 @@ class WalletConnectMiddleware {
scope.launch { walletConnectInteractor.continueWithRequest(action.sessionRequest) }
}
is WalletConnectAction.RejectUnsupportedRequest -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork()))
}
}
}
@ -337,7 +343,7 @@ class WalletConnectMiddleware {
chainId = chainId,
peer = session.peerMeta,
).guard {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork()))
return
}

View file

@ -91,7 +91,7 @@ data class WalletForSession(
sealed class WalletConnectDialog : StateDialog {
data class ClipboardOrScanQr(val clipboardUri: String) : WalletConnectDialog()
object UnsupportedCard : WalletConnectDialog()
object UnsupportedNetwork : WalletConnectDialog()
data class UnsupportedNetwork(val networks: List<String>? = null) : WalletConnectDialog()
data class AddNetwork(val network: String) : WalletConnectDialog()
object OpeningSessionRejected : WalletConnectDialog()
object SessionTimeout : WalletConnectDialog()

View file

@ -24,7 +24,7 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
setFitSystemWindows(fit = true)
activity?.onBackPressedDispatcher?.addCallback(
this,
object : OnBackPressedCallback(true) {
@ -47,6 +47,11 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
scannerView?.stopCamera()
}
override fun onDestroy() {
super.onDestroy()
setFitSystemWindows(fit = false)
}
override fun onResume() {
super.onResume()
scannerView?.setResultHandler(this)
@ -55,7 +60,7 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
override fun handleResult(result: Result) {
store.dispatch(NavigationAction.PopBackTo())
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, false) }
setFitSystemWindows(fit = false)
if (!result.text.isNullOrBlank()) {
store.dispatch(WalletConnectAction.OpenSession(result.text))
}
@ -83,4 +88,10 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
private fun requestPermission() {
requestPermissions(arrayOf(Manifest.permission.CAMERA), CameraView.PERMISSION_REQUEST_CODE)
}
private fun setFitSystemWindows(fit: Boolean) {
activity?.window?.let {
WindowCompat.setDecorFitsSystemWindows(it, fit)
}
}
}

View file

@ -7,7 +7,6 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.core.TangemSdkError
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.domain.TapError
@ -35,15 +34,14 @@ data class PrepareSendScreen(
val tokenRate: BigDecimal? = null,
) : SendScreenAction
// Address or PayId
sealed class AddressPayIdActionUi : SendScreenActionUi {
data class HandleUserInput(val data: String) : AddressPayIdActionUi()
data class PasteAddressPayId(val data: String, val sourceType: AddressEntered.SourceType) : AddressPayIdActionUi()
data class CheckClipboard(val data: String?) : AddressPayIdActionUi()
data class CheckAddressPayId(val sourceType: AddressEntered.SourceType?) : AddressPayIdActionUi()
data class SetTruncateHandler(val handler: (String) -> String) : AddressPayIdActionUi()
data class TruncateOrRestore(val truncate: Boolean) : AddressPayIdActionUi()
data class ChangePayIdState(val sendingToPayIdEnabled: Boolean) : AddressPayIdActionUi()
// Address
sealed class AddressActionUi : SendScreenActionUi {
data class HandleUserInput(val data: String) : AddressActionUi()
data class PasteAddress(val data: String, val sourceType: AddressEntered.SourceType) : AddressActionUi()
data class CheckClipboard(val data: String?) : AddressActionUi()
data class CheckAddress(val sourceType: AddressEntered.SourceType?) : AddressActionUi()
data class SetTruncateHandler(val handler: (String) -> String) : AddressActionUi()
data class TruncateOrRestore(val truncate: Boolean) : AddressActionUi()
}
sealed class TransactionExtrasAction : SendScreenActionUi {
@ -77,27 +75,15 @@ sealed class TransactionExtrasAction : SendScreenActionUi {
}
}
sealed class AddressPayIdVerifyAction : SendScreenAction {
sealed class AddressVerifyAction : SendScreenAction {
enum class Error {
PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN,
PAY_ID_NOT_REGISTERED,
PAY_ID_REQUEST_FAILED,
ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN,
ADDRESS_SAME_AS_WALLET,
}
data class ChangePasteBtnEnableState(val isEnabled: Boolean) : AddressPayIdVerifyAction()
data class ChangePasteBtnEnableState(val isEnabled: Boolean) : AddressVerifyAction()
sealed class PayIdVerification : AddressPayIdVerifyAction() {
data class SetPayIdError(val error: Error?) : PayIdVerification()
data class SetPayIdWalletAddress(
val payId: String,
val payIdWalletAddress: String,
val isUserInput: Boolean,
) : PayIdVerification()
}
sealed class AddressVerification : AddressPayIdVerifyAction() {
sealed class AddressVerification : AddressVerifyAction() {
data class SetAddressError(val error: Error?) : AddressVerification()
data class SetWalletAddress(val address: String, val isUserInput: Boolean) : AddressVerification()
}
@ -156,8 +142,6 @@ sealed class SendAction : SendScreenAction {
override val messageResource: Int = R.string.send_transaction_success
}
data class SendError(override val error: TapError) : SendAction(), ErrorAction
sealed class Dialog : SendAction(), StateDialog {
data class TezosWarningDialog(
val reduceCallback: () -> Unit,

View file

@ -2,49 +2,39 @@ package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.isPayIdSupported
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification.SetAddressError
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification.SetWalletAddress
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification.SetPayIdError
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification.SetPayIdWalletAddress
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetAddressError
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetWalletAddress
import com.tangem.tap.features.send.redux.AddressVerifyAction.Error
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
/**
[REDACTED_AUTHOR]
*/
internal class AddressPayIdMiddleware {
internal class AddressMiddleware {
fun handle(action: AddressPayIdActionUi, appState: AppState?, dispatch: (Action) -> Unit) {
fun handle(action: AddressActionUi, appState: AppState?, dispatch: (Action) -> Unit) {
when (action) {
is AddressPayIdActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch)
is AddressPayIdActionUi.PasteAddressPayId -> pasteAddressPayId(action.data, action.sourceType, dispatch)
is AddressPayIdActionUi.CheckClipboard -> verifyClipboard(action.data, appState, dispatch)
is AddressPayIdActionUi.CheckAddressPayId -> verifyAddressPayId(action.sourceType, appState, dispatch)
is AddressActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch)
is AddressActionUi.PasteAddress -> pasteAddress(action.data, action.sourceType, dispatch)
is AddressActionUi.CheckClipboard -> verifyClipboard(action.data, appState, dispatch)
is AddressActionUi.CheckAddress -> verifyAddress(action.sourceType, appState, dispatch)
else -> return
}
}
private fun handleUserInput(input: String, appState: AppState?, dispatch: DispatchFunction) {
val sendState = appState?.sendState ?: return
if (input == sendState.addressPayIdState.viewFieldValue.value) return
if (input == sendState.addressState.viewFieldValue.value) return
setAddressAndCheck(data = input, sourceType = null, isUserInput = true, dispatch = dispatch)
}
private fun pasteAddressPayId(data: String, sourceType: AddressEntered.SourceType, dispatch: (Action) -> Unit) {
private fun pasteAddress(data: String, sourceType: AddressEntered.SourceType, dispatch: (Action) -> Unit) {
setAddressAndCheck(data = data, sourceType = sourceType, isUserInput = false, dispatch = dispatch)
}
@ -54,74 +44,27 @@ internal class AddressPayIdMiddleware {
isUserInput: Boolean,
dispatch: (Action) -> Unit,
) {
val potentialPayId = data.lowercase()
if (isPayIdEnabled() && PayIdManager.isPayId(potentialPayId)) {
dispatch(SetPayIdWalletAddress(potentialPayId, "", isUserInput))
} else {
dispatch(SetWalletAddress(data, isUserInput))
}
dispatch(AddressPayIdActionUi.CheckAddressPayId(sourceType))
dispatch(SetWalletAddress(data, isUserInput))
dispatch(AddressActionUi.CheckAddress(sourceType))
}
private fun verifyAddressPayId(
private fun verifyAddress(
sourceType: AddressEntered.SourceType?,
appState: AppState?,
dispatch: (Action) -> Unit,
) {
val sendState = appState?.sendState ?: return
val wallet = sendState.walletManager?.wallet ?: return
val addressPayId = sendState.addressPayIdState.normalFieldValue ?: return
val isUserInput = sendState.addressPayIdState.viewFieldValue.isFromUserInput
val address = sendState.addressState.normalFieldValue ?: return
val isUserInput = sendState.addressState.viewFieldValue.isFromUserInput
if (isPayIdEnabled() && PayIdManager.isPayId(addressPayId)) {
verifyPayId(addressPayId, wallet, isUserInput, dispatch)
} else {
verifyAddress(
address = addressPayId,
wallet = wallet,
isUserInput = isUserInput,
dispatch = dispatch,
sourceType = sourceType,
)
}
}
private fun verifyPayId(payId: String, wallet: Wallet, isUserInput: Boolean, dispatch: DispatchFunction) {
val blockchain = wallet.blockchain
if (!blockchain.isPayIdSupported()) {
dispatch(SetPayIdError(Error.PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN))
return
}
scope.launch {
val result = PayIdManager().verifyPayId(payId, blockchain)
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> {
val addressDetails = result.data.getAddressDetails()
if (addressDetails == null) {
dispatch(SetPayIdError(Error.PAY_ID_NOT_REGISTERED))
return@withContext
}
val address = addressDetails.address
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(wallet, address)
if (failReason == null) {
dispatch(SetPayIdWalletAddress(payId, address, isUserInput))
dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, addressDetails.tag))
dispatch(FeeAction.RequestFee)
} else {
dispatch(SetAddressError(failReason))
dispatch(TransactionExtrasAction.Release)
}
}
is Result.Failure -> {
dispatch(SetPayIdError(Error.PAY_ID_REQUEST_FAILED))
dispatch(TransactionExtrasAction.Release)
}
}
}
}
verifyAddress(
address = address,
wallet = wallet,
isUserInput = isUserInput,
dispatch = dispatch,
sourceType = sourceType,
)
}
private fun verifyAddress(
@ -219,41 +162,33 @@ internal class AddressPayIdMiddleware {
}
private fun verifyClipboard(input: String?, appState: AppState?, dispatch: DispatchFunction) {
val addressPayId = input ?: return
val address = input ?: return
val wallet = appState?.sendState?.walletManager?.wallet ?: return
val internalDispatcher: (Action) -> Unit = {
when (it) {
is SetWalletAddress, is SetPayIdWalletAddress -> {
dispatch(AddressPayIdVerifyAction.ChangePasteBtnEnableState(true))
is SetWalletAddress -> {
dispatch(AddressVerifyAction.ChangePasteBtnEnableState(true))
}
is SetAddressError, is SetPayIdError -> {
dispatch(AddressPayIdVerifyAction.ChangePasteBtnEnableState(false))
is SetAddressError -> {
dispatch(AddressVerifyAction.ChangePasteBtnEnableState(false))
}
}
}
if (PayIdManager.isPayId(addressPayId) && isPayIdEnabled()) {
verifyPayId(addressPayId, wallet, false, internalDispatcher)
} else {
verifyAddress(
address = addressPayId,
wallet = wallet,
sourceType = null,
isUserInput = false,
dispatch = internalDispatcher,
)
}
}
private fun isPayIdEnabled(): Boolean {
return store.state.globalState.configManager?.config?.isSendingToPayIdEnabled ?: false
verifyAddress(
address = address,
wallet = wallet,
sourceType = null,
isUserInput = false,
dispatch = internalDispatcher,
)
}
}
fun String.splitToMap(firstDelimiter: String, secondDelimiter: String): Map<String, String> {
return this.split(firstDelimiter)
return this
.split(firstDelimiter)
.map { it.split(secondDelimiter) }
.map { it.first() to it.last().toString() }
.toMap()
.associate { it.first() to it.last() }
}

View file

@ -39,7 +39,7 @@ class RequestFeeMiddleware {
}
val typedAmount = sendState.amountState.amountToExtract ?: return
val destinationAddress = sendState.addressPayIdState.destinationWalletAddress!!
val destinationAddress = sendState.addressState.destinationWalletAddress!!
val destinationAmount = Amount(typedAmount, sendState.amountState.amountToSendCrypto)
val txSender = if (scanResponse.isDemoCard()) {
DemoTransactionSender(walletManager)

View file

@ -6,7 +6,7 @@ import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
import com.tangem.blockchain.blockchains.ton.TonTransactionExtras
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder.XrpTransactionExtras
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.SimpleResult
@ -56,18 +56,17 @@ class SendMiddleware {
{ nextDispatch ->
{ action ->
when (action) {
is AddressPayIdActionUi -> AddressPayIdMiddleware().handle(action, appState(), dispatch)
is AddressActionUi -> AddressMiddleware().handle(action, appState(), dispatch)
is AmountActionUi -> AmountMiddleware().handle(action, appState(), dispatch)
is RequestFee -> RequestFeeMiddleware().handle(appState(), dispatch)
is SendActionUi.SendAmountToRecipient ->
verifyAndSendTransaction(action, appState(), dispatch)
is PrepareSendScreen -> setIfSendingToPayIdEnabled(appState(), dispatch)
is SendAction.Warnings.Update -> updateWarnings(dispatch)
is SendActionUi.CheckIfTransactionDataWasProvided -> {
val transactionData = appState()?.sendState?.externalTransactionData
if (transactionData != null) {
store.dispatchOnMain(
AddressPayIdVerifyAction.AddressVerification.SetWalletAddress(
AddressVerifyAction.AddressVerification.SetWalletAddress(
address = transactionData.destinationAddress,
isUserInput = false,
),
@ -97,7 +96,7 @@ private fun verifyAndSendTransaction(
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
val card = appState.globalState.scanResponse?.card ?: return
val destinationAddress = sendState.addressPayIdState.destinationWalletAddress ?: return
val destinationAddress = sendState.addressState.destinationWalletAddress ?: return
val typedAmount = sendState.amountState.amountToExtract ?: return
val fee = sendState.feeState.currentFee ?: return
@ -169,9 +168,7 @@ private fun sendTransaction(
transactionExtras.xlmMemo?.memo?.let { txData = txData.copy(extras = StellarTransactionExtras(it)) }
transactionExtras.binanceMemo?.memo?.let { txData = txData.copy(extras = BinanceTransactionExtras(it.toString())) }
transactionExtras.xrpDestinationTag?.tag?.let {
txData = txData.copy(extras = XrpTransactionBuilder.XrpTransactionExtras(it))
}
transactionExtras.xrpDestinationTag?.tag?.let { txData = txData.copy(extras = XrpTransactionExtras(it)) }
transactionExtras.cosmosMemoState?.memo?.let { txData = txData.copy(extras = CosmosTransactionExtras(it)) }
transactionExtras.tonMemoState?.memo?.let { txData = txData.copy(extras = TonTransactionExtras(it)) }
@ -251,17 +248,10 @@ private fun sendTransaction(
Analytics.send(
Basic.TransactionSent(
sentFrom = AnalyticsParam.TxSentFrom.Sell,
memoType = if (txData.extras != null) MemoType.Full else MemoType.Empty,
),
)
Analytics.send(
Token.Send.SelectedCurrency(
currency = when (mainCurrencyType) {
MainCurrencyType.FIAT -> CurrencyType.AppCurrency
MainCurrencyType.CRYPTO -> CurrencyType.AppCurrency
},
memoType = getMemoType(transactionExtras),
),
)
Analytics.sendSelectedCurrencyEvent(mainCurrencyType)
dispatch(WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
} else {
Analytics.send(
@ -271,9 +261,10 @@ private fun sendTransaction(
token = amountToSend.currencySymbol,
feeType = feeType.convertToAnalyticsFeeType(),
),
memoType = if (txData.extras != null) MemoType.Full else MemoType.Empty,
memoType = getMemoType(transactionExtras),
),
)
Analytics.sendSelectedCurrencyEvent(mainCurrencyType)
dispatch(NavigationAction.PopBackTo())
}
scope.launch(Dispatchers.IO) {
@ -337,6 +328,25 @@ private fun sendTransaction(
}
}
private fun getMemoType(transactionExtras: TransactionExtrasState): MemoType {
return when {
transactionExtras.isEmpty() -> MemoType.Empty
transactionExtras.isNull() -> MemoType.Null
else -> MemoType.Full
}
}
private fun Analytics.sendSelectedCurrencyEvent(mainCurrencyType: MainCurrencyType) {
send(
Token.Send.SelectedCurrency(
currency = when (mainCurrencyType) {
MainCurrencyType.FIAT -> CurrencyType.AppCurrency
MainCurrencyType.CRYPTO -> CurrencyType.Token
},
),
)
}
private fun updateFeedbackManagerInfo(
walletManager: WalletManager,
amountToSend: Amount,
@ -381,12 +391,6 @@ fun createValidateTransactionError(
return TapError.ValidateTransactionErrors(tapErrors) { it.joinToString("\r\n") }
}
private fun setIfSendingToPayIdEnabled(appState: AppState?, dispatch: (Action) -> Unit) {
val isSendingToPayIdEnabled =
appState?.globalState?.configManager?.config?.isSendingToPayIdEnabled ?: false
dispatch(AddressPayIdActionUi.ChangePayIdState(isSendingToPayIdEnabled))
}
private fun updateWarnings(dispatch: (Action) -> Unit) {
val warningsManager = store.state.globalState.warningManager ?: return
val blockchain = store.state.sendState.walletManager?.wallet?.blockchain ?: return

View file

@ -1,72 +0,0 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.AddressVerification
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.PayIdVerification
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.AddressPayIdState
import com.tangem.tap.features.send.redux.states.InputViewValue
import com.tangem.tap.features.send.redux.states.SendState
/**
[REDACTED_AUTHOR]
*/
class AddressPayIdReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) {
is AddressPayIdActionUi -> handleUiAction(action, sendState, sendState.addressPayIdState)
is AddressPayIdVerifyAction -> handleAction(action, sendState, sendState.addressPayIdState)
else -> sendState
}
private fun handleUiAction(
action: AddressPayIdActionUi,
sendState: SendState,
state: AddressPayIdState,
): SendState {
val result = when (action) {
is AddressPayIdActionUi.HandleUserInput -> state
is AddressPayIdActionUi.SetTruncateHandler -> state.copy(truncateHandler = action.handler)
is AddressPayIdActionUi.TruncateOrRestore -> {
val value = if (action.truncate) state.truncatedFieldValue ?: "" else state.normalFieldValue ?: ""
state.copy(viewFieldValue = state.viewFieldValue.copy(value = value))
}
is AddressPayIdActionUi.PasteAddressPayId -> return sendState
is AddressPayIdActionUi.CheckClipboard -> return sendState
is AddressPayIdActionUi.CheckAddressPayId -> return sendState
is AddressPayIdActionUi.ChangePayIdState -> state.copy(sendingToPayIdEnabled = action.sendingToPayIdEnabled)
}
return updateLastState(sendState.copy(addressPayIdState = result), result)
}
private fun handleAction(
action: AddressPayIdVerifyAction,
sendState: SendState,
state: AddressPayIdState,
): SendState {
val result = when (action) {
is PayIdVerification.SetPayIdWalletAddress -> {
state.copy(
viewFieldValue = InputViewValue(action.payId, action.isUserInput),
normalFieldValue = action.payId,
truncatedFieldValue = state.truncate(action.payId),
destinationWalletAddress = action.payIdWalletAddress,
error = null,
)
}
is AddressVerification.SetWalletAddress -> {
state.copy(
viewFieldValue = InputViewValue(action.address, action.isUserInput),
normalFieldValue = action.address,
truncatedFieldValue = state.truncate(action.address),
destinationWalletAddress = action.address,
error = null,
)
}
is AddressPayIdVerifyAction.ChangePasteBtnEnableState -> state.copy(pasteIsEnabled = action.isEnabled)
is AddressVerification.SetAddressError -> state.copy(error = action.error, destinationWalletAddress = null)
is PayIdVerification.SetPayIdError -> state.copy(error = action.error, destinationWalletAddress = null)
}
return updateLastState(sendState.copy(addressPayIdState = result), result)
}
}

View file

@ -0,0 +1,52 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.tap.features.send.redux.AddressActionUi
import com.tangem.tap.features.send.redux.AddressVerifyAction
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.AddressState
import com.tangem.tap.features.send.redux.states.InputViewValue
import com.tangem.tap.features.send.redux.states.SendState
/**
[REDACTED_AUTHOR]
*/
class AddressReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) {
is AddressActionUi -> handleUiAction(action, sendState, sendState.addressState)
is AddressVerifyAction -> handleAction(action, sendState, sendState.addressState)
else -> sendState
}
private fun handleUiAction(action: AddressActionUi, sendState: SendState, state: AddressState): SendState {
val result = when (action) {
is AddressActionUi.HandleUserInput -> state
is AddressActionUi.SetTruncateHandler -> state.copy(truncateHandler = action.handler)
is AddressActionUi.TruncateOrRestore -> {
val value = if (action.truncate) state.truncatedFieldValue ?: "" else state.normalFieldValue ?: ""
state.copy(viewFieldValue = state.viewFieldValue.copy(value = value))
}
is AddressActionUi.PasteAddress -> return sendState
is AddressActionUi.CheckClipboard -> return sendState
is AddressActionUi.CheckAddress -> return sendState
}
return updateLastState(sendState.copy(addressState = result), result)
}
private fun handleAction(action: AddressVerifyAction, sendState: SendState, state: AddressState): SendState {
val result = when (action) {
is AddressVerification.SetWalletAddress -> {
state.copy(
viewFieldValue = InputViewValue(action.address, action.isUserInput),
normalFieldValue = action.address,
truncatedFieldValue = state.truncate(action.address),
destinationWalletAddress = action.address,
error = null,
)
}
is AddressVerifyAction.ChangePasteBtnEnableState -> state.copy(pasteIsEnabled = action.isEnabled)
is AddressVerification.SetAddressError -> state.copy(error = action.error, destinationWalletAddress = null)
}
return updateLastState(sendState.copy(addressState = result), result)
}
}

View file

@ -25,7 +25,7 @@ object SendScreenReducer {
val reducer: SendInternalReducer = when (action) {
is PrepareSendScreen -> PrepareSendScreenStatesReducer()
is AddressPayIdActionUi, is AddressPayIdVerifyAction -> AddressPayIdReducer()
is AddressActionUi, is AddressVerifyAction -> AddressReducer()
is TransactionExtrasAction -> TransactionExtrasReducer()
is AmountActionUi, is AmountAction -> AmountReducer()
is FeeActionUi, is FeeAction -> FeeReducer()
@ -71,7 +71,7 @@ private class SendReducer : SendInternalReducer {
amountState = state.amountState.copy(
inputIsEnabled = false,
),
addressPayIdState = state.addressPayIdState.copy(
addressState = state.addressState.copy(
inputIsEnabled = false,
),
)

View file

@ -2,17 +2,16 @@ package com.tangem.tap.features.send.redux.states
import androidx.core.text.isDigitsOnly
import com.tangem.blockchain.blockchains.stellar.StellarMemo
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
import com.tangem.tap.features.send.redux.AddressVerifyAction
import java.math.BigInteger
data class AddressPayIdState(
data class AddressState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val normalFieldValue: String? = null,
val truncatedFieldValue: String? = null,
val destinationWalletAddress: String? = null,
val error: AddressPayIdVerifyAction.Error? = null,
val error: AddressVerifyAction.Error? = null,
val truncateHandler: ((String) -> String)? = null,
val sendingToPayIdEnabled: Boolean = false,
val pasteIsEnabled: Boolean = false,
val inputIsEnabled: Boolean = true,
) : SendScreenState {
@ -22,8 +21,6 @@ data class AddressPayIdState(
fun truncate(value: String): String = truncateHandler?.invoke(value) ?: value
fun isReady(): Boolean = error == null && destinationWalletAddress?.isNotEmpty() ?: false
fun isPayIdState(): Boolean = destinationWalletAddress != null && destinationWalletAddress != normalFieldValue
}
data class TransactionExtrasState(
@ -34,6 +31,21 @@ data class TransactionExtrasState(
val cosmosMemoState: CosmosMemoState? = null,
) : IdStateHolder {
override val stateId: StateId = StateId.TRANSACTION_EXTRAS
fun isNull(): Boolean {
return xlmMemo == null && binanceMemo == null && xrpDestinationTag == null && tonMemoState == null &&
cosmosMemoState == null
}
fun isEmpty(): Boolean {
val isXlmEmpty = xlmMemo?.viewFieldValue?.value?.isEmpty() ?: false
val isBinanceEmpty = binanceMemo?.viewFieldValue?.value?.isEmpty() ?: false
val isXrpEmpty = xrpDestinationTag?.viewFieldValue?.value?.isEmpty() ?: false
val isTonEmpty = tonMemoState?.viewFieldValue?.value?.isEmpty() ?: false
val isCosmosEmpty = cosmosMemoState?.viewFieldValue?.value?.isEmpty() ?: false
return isXlmEmpty || isBinanceEmpty || isXrpEmpty || isTonEmpty || isCosmosEmpty
}
}
enum class XlmMemoType {
@ -83,11 +95,7 @@ data class BinanceMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val memo: BigInteger? = null,
val error: TransactionExtraError? = null,
) {
companion object {
val MAX_NUMBER: BigInteger = BigInteger("FFFFFFFFFFFFFFFF", 16)
}
}
)
// tag must contains only digits
data class XrpDestinationTagState(
@ -105,6 +113,7 @@ data class TonMemoState(
val memo: String? = null,
val error: TransactionExtraError? = null,
)
data class CosmosMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val memo: String? = null,

View file

@ -34,7 +34,7 @@ data class SendState(
val coinConverter: CurrencyConverter? = null,
val tokenConverter: CurrencyConverter? = null,
val lastChangedStates: LinkedHashSet<StateId> = linkedSetOf(),
val addressPayIdState: AddressPayIdState = AddressPayIdState(),
val addressState: AddressState = AddressState(),
val transactionExtrasState: TransactionExtrasState = TransactionExtrasState(),
val amountState: AmountState = AmountState(),
val feeState: FeeState = FeeState(),
@ -52,7 +52,7 @@ data class SendState(
MainCurrencyType.CRYPTO -> amountState.amountToExtract?.decimals ?: 0
}
fun convertFiatToCoin(value: BigDecimal): BigDecimal {
private fun convertFiatToCoin(value: BigDecimal): BigDecimal {
return if (!this.coinIsConvertible()) value else coinConverter!!.toCrypto(value)
}
@ -60,14 +60,14 @@ data class SendState(
return if (!this.tokenIsConvertible()) value else tokenConverter!!.toCrypto(value)
}
fun convertCoinToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
private fun convertCoinToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
if (!this.coinIsConvertible()) return value
val converter = coinConverter!!
return if (!scaleWithPrecision) converter.toFiat(value) else converter.toFiatWithPrecision(value)
}
fun convertTokenToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
private fun convertTokenToFiat(value: BigDecimal, scaleWithPrecision: Boolean = false): BigDecimal {
if (!this.tokenIsConvertible()) return value
val converter = tokenConverter!!
@ -107,13 +107,13 @@ data class SendState(
}
companion object {
fun addressPayIdIsReady(): Boolean = store.state.sendState.addressPayIdState.isReady()
private fun addressIsReady(): Boolean = store.state.sendState.addressState.isReady()
fun amountIsReady(): Boolean = store.state.sendState.amountState.isReady()
private fun amountIsReady(): Boolean = store.state.sendState.amountState.isReady()
fun isReadyToRequestFee(): Boolean = addressPayIdIsReady() && amountIsReady()
fun isReadyToRequestFee(): Boolean = addressIsReady() && amountIsReady()
fun isReadyToSend(): Boolean = addressPayIdIsReady() && amountIsReady() &&
fun isReadyToSend(): Boolean = addressIsReady() && amountIsReady() &&
store.state.sendState.feeState.isReady()
}
}

View file

@ -5,14 +5,16 @@ import android.content.Context
import android.util.AttributeSet
import com.google.android.material.textfield.TextInputEditText
class EditTextCustomPaste @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : TextInputEditText(context, attrs, defStyleAttr) {
class EditTextCustomPaste : TextInputEditText {
private var onSystemPasteButtonClickListener: (() -> Unit)? = null
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
fun setOnSystemPasteButtonClickListener(callback: () -> Unit) {
onSystemPasteButtonClickListener = callback
}

View file

@ -33,7 +33,7 @@ import com.tangem.tap.common.toggleWidget.ViewStateWidget
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.*
import com.tangem.tap.features.send.redux.AddressActionUi.*
import com.tangem.tap.features.send.redux.AmountActionUi.*
import com.tangem.tap.features.send.redux.FeeActionUi.*
import com.tangem.tap.features.send.redux.states.FeeType
@ -80,7 +80,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
etAmountToSend = view.findViewById(R.id.etAmountToSend)
initSendButtonStates()
setupAddressOrPayIdLayout()
setupAddressLayout()
setupTransactionExtrasLayout()
setupAmountLayout()
setupFeeLayout()
@ -99,14 +99,14 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
sendBtn = IndeterminateProgressButtonWidget(btnSend, progress)
}
private fun setupAddressOrPayIdLayout() = with(binding.lSendAddressPayid) {
store.dispatch(SetTruncateHandler { etAddressOrPayId.truncateMiddleWith(it, "...") })
private fun setupAddressLayout() = with(binding.lSendAddress) {
store.dispatch(SetTruncateHandler { etAddress.truncateMiddleWith(it, "...") })
store.dispatch(CheckClipboard(requireContext().getFromClipboard()?.toString()))
etAddressOrPayId.apply {
etAddress.apply {
setOnSystemPasteButtonClickListener {
store.dispatch(
PasteAddressPayId(
PasteAddress(
data = requireContext().getFromClipboard()?.toString() ?: "",
sourceType = Token.Send.AddressEntered.SourceType.PastePopup,
),
@ -119,9 +119,9 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter { store.state.sendState.addressPayIdState.viewFieldValue.value != it }
.filter { store.state.sendState.addressState.viewFieldValue.value != it }
.onEach {
store.dispatch(AddressPayIdActionUi.HandleUserInput(it))
store.dispatch(AddressActionUi.HandleUserInput(it))
}
.launchIn(mainScope)
}
@ -129,12 +129,12 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
imvPaste.setOnClickListener {
Analytics.send(Token.Send.ButtonPaste())
store.dispatch(
PasteAddressPayId(
PasteAddress(
data = requireContext().getFromClipboard()?.toString() ?: "",
sourceType = Token.Send.AddressEntered.SourceType.PasteButton,
),
)
store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused))
store.dispatch(TruncateOrRestore(!etAddress.isFocused))
}
imvQrCode.setOnClickListener {
Analytics.send(Token.Send.ButtonQRCode())
@ -145,7 +145,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
}
private fun setupTransactionExtrasLayout() = with(binding.lSendAddressPayid) {
private fun setupTransactionExtrasLayout() = with(binding.lSendAddress) {
// TODO: [REDACTED_TASK_KEY]
etXlmMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
@ -202,15 +202,15 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
// Delayed launch is needed in order for the UI to be drawn and to process the sent events.
// If do not use the delay, then etAmount error field is not displayed when
// inserting an incorrect amount by shareUri
binding.lSendAddressPayid.imvQrCode.postDelayed(
binding.lSendAddress.imvQrCode.postDelayed(
{
store.dispatch(
PasteAddressPayId(
PasteAddress(
data = scannedCode,
sourceType = Token.Send.AddressEntered.SourceType.QRCode,
),
)
store.dispatch(TruncateOrRestore(!binding.lSendAddressPayid.etAddressOrPayId.isFocused))
store.dispatch(TruncateOrRestore(!binding.lSendAddress.etAddress.isFocused))
},
200,
)

View file

@ -7,30 +7,15 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.text.bold
import com.tangem.common.extensions.remove
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.enableError
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.update
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.getMessageString
import com.tangem.tap.common.text.DecimalDigitsInputFilter
import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.assembleErrors
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
import com.tangem.tap.features.send.redux.AddressVerifyAction.Error
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.states.AddressPayIdState
import com.tangem.tap.features.send.redux.states.AmountState
import com.tangem.tap.features.send.redux.states.FeeState
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.redux.states.ReceiptLayoutType
import com.tangem.tap.features.send.redux.states.ReceiptState
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.send.redux.states.StateId
import com.tangem.tap.features.send.redux.states.TransactionExtraError
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.features.send.ui.FeeUiHelper
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.send.ui.dialogs.KaspaWarningDialog
@ -61,7 +46,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
lastChangedStates.forEach {
when (it) {
StateId.SEND_SCREEN -> handleSendScreen(fg, state)
StateId.ADDRESS_PAY_ID -> handleAddressPayIdState(fg, state.addressPayIdState)
StateId.ADDRESS_PAY_ID -> handleAddressState(fg, state.addressState)
StateId.TRANSACTION_EXTRAS -> handleTransactionExtrasState(fg, state.transactionExtrasState)
StateId.AMOUNT -> handleAmountState(fg, state.amountState)
StateId.FEE -> handleFeeState(fg, state.feeState)
@ -72,7 +57,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
@Suppress("ComplexMethod")
private fun handleTransactionExtrasState(fg: SendFragment, infoState: TransactionExtrasState) =
with(fg.binding.lSendAddressPayid) {
with(fg.binding.lSendAddress) {
fun showView(view: View, info: Any?) {
view.show(info != null)
}
@ -192,13 +177,10 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
)
}
private fun handleAddressPayIdState(fg: SendFragment, state: AddressPayIdState) =
with(fg.binding.lSendAddressPayid) {
private fun handleAddressState(fg: SendFragment, state: AddressState) {
with(fg.binding.lSendAddress) {
fun parseError(context: Context, error: Error?): String? {
val resId = when (error) {
Error.PAY_ID_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.send_error_payid_unsupported_by_blockchain
Error.PAY_ID_NOT_REGISTERED -> R.string.send_error_payid_not_registered
Error.PAY_ID_REQUEST_FAILED -> R.string.send_error_payid_request_failed
Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN -> R.string.send_validation_invalid_address
Error.ADDRESS_SAME_AS_WALLET -> R.string.send_error_address_same_as_wallet
else -> null
@ -208,8 +190,8 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
imvPaste.isEnabled = state.pasteIsEnabled
val et = etAddressOrPayId
val til = tilAddressOrPayId
val et = etAddress
val til = tilAddress
val parsedError = parseError(til.context, state.error)
til.isEnabled = state.inputIsEnabled
@ -218,19 +200,15 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
flPaste.show(state.inputIsEnabled)
flQrCode.show(state.inputIsEnabled)
val hintResId = if (state.sendingToPayIdEnabled) {
R.string.send_destination_hint_address_payid
} else {
R.string.send_destination_hint_address
}
til.hint = til.getString(hintResId)
til.hint = til.getString(R.string.send_destination_hint_address)
til.error = parsedError
til.isErrorEnabled = parsedError != null
til.helperText = state.destinationWalletAddress
til.isHelperTextEnabled = state.isPayIdState() && parsedError == null
til.isHelperTextEnabled = parsedError == null
if (!state.viewFieldValue.isFromUserInput) et.update(state.viewFieldValue.value)
}
}
private fun handleAmountState(fg: SendFragment, state: AmountState) = with(fg.binding.lSendAmount) {
if (state.error != null) {

View file

@ -0,0 +1,37 @@
package com.tangem.tap.features.shop.data
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.ShopResponse
import com.tangem.tap.features.shop.domain.ShopRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import timber.log.Timber
/**
* Default implementation of shop feature repository
*
* @property tangemTechApi TangemTech API
* @property dispatchers coroutine dispatchers provider
*
[REDACTED_AUTHOR]
*/
internal class DefaultShopRepository(
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
) : ShopRepository {
override suspend fun isShopifyOrderingAvailable(): Boolean {
return runCatching(dispatchers.io) { tangemTechApi.getShopInfo(name = SHOPIFY_NAME) }
.fold(
onSuccess = ShopResponse::isOrderingAvailable,
onFailure = {
Timber.e("Server error. isShopifyOrderingAvailable returns default value (true)")
true
},
)
}
private companion object {
const val SHOPIFY_NAME = "shopify"
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.tap.features.shop.di
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.tap.features.shop.data.DefaultShopRepository
import com.tangem.tap.features.shop.domain.DefaultShopifyOrderingAvailabilityUseCase
import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
internal object ShopUseCaseModule {
@Provides
@ViewModelScoped
fun provideShopifyOrderingAvailabilityUseCase(
tangemTechApi: TangemTechApi,
dispatchers: CoroutineDispatcherProvider,
): ShopifyOrderingAvailabilityUseCase {
return DefaultShopifyOrderingAvailabilityUseCase(
shopRepository = DefaultShopRepository(tangemTechApi, dispatchers),
)
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.tap.features.shop.domain
/**
* Default implementation of use case to define shopify ordering availability
*
* @property shopRepository shop feature repository
*
[REDACTED_AUTHOR]
*/
internal class DefaultShopifyOrderingAvailabilityUseCase(
private val shopRepository: ShopRepository,
) : ShopifyOrderingAvailabilityUseCase {
override suspend fun invoke() = shopRepository.isShopifyOrderingAvailable()
}

View file

@ -0,0 +1,12 @@
package com.tangem.tap.features.shop.domain
/**
* Shop feature repository
*
[REDACTED_AUTHOR]
*/
internal interface ShopRepository {
/** Get shopify ordering availability */
suspend fun isShopifyOrderingAvailable(): Boolean
}

View file

@ -0,0 +1,12 @@
package com.tangem.tap.features.shop.domain
/**
* Use case to define shopify ordering availability
*
[REDACTED_AUTHOR]
*/
internal interface ShopifyOrderingAvailabilityUseCase {
/** Get availability */
suspend operator fun invoke(): Boolean
}

View file

@ -0,0 +1,39 @@
package com.tangem.tap.features.shop.presentation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* Shop screen view model
*
* @property shopifyOrderingAvailabilityUseCase use case to define shopify ordering availability
* @property dispatchers coroutine dispatchers provider
* @property appStateHolder redux state holder
*
[REDACTED_AUTHOR]
*/
@HiltViewModel
internal class ShopViewModel @Inject constructor(
private val shopifyOrderingAvailabilityUseCase: ShopifyOrderingAvailabilityUseCase,
private val dispatchers: CoroutineDispatcherProvider,
private val appStateHolder: AppStateHolder,
) : ViewModel() {
/** Check ordering delay block visibility */
fun checkOrderingDelayBlockVisibility() {
viewModelScope.launch(dispatchers.main) {
val visibility = runCatching(dispatchers.io) { shopifyOrderingAvailabilityUseCase() }
.fold(onSuccess = { !it }, onFailure = { false })
appStateHolder.mainStore?.dispatch(action = ShopAction.SetOrderingDelayBlockVisibility(visibility))
}
}
}

View file

@ -8,38 +8,40 @@ import com.tangem.tap.common.shop.googlepay.GooglePayService
import com.tangem.wallet.R
import org.rekotlin.Action
sealed class ShopAction : Action {
sealed interface ShopAction : Action {
object LoadProducts : ShopAction() {
data class Success(val products: List<TangemProduct>) : ShopAction()
object Failure : ShopAction(), NotificationAction {
object LoadProducts : ShopAction {
data class Success(val products: List<TangemProduct>) : ShopAction
object Failure : ShopAction, NotificationAction {
override val messageResource = R.string.common_server_unavailable
}
}
data class ApplyPromoCode(val promoCode: String) : ShopAction() {
data class Success(val promoCode: String?, val products: List<TangemProduct>) : ShopAction()
object InvalidPromoCode : ShopAction()
data class ApplyPromoCode(val promoCode: String) : ShopAction {
data class Success(val promoCode: String?, val products: List<TangemProduct>) : ShopAction
object InvalidPromoCode : ShopAction
}
object BuyWithGooglePay : ShopAction() {
object UserCancelled : ShopAction()
data class HandleGooglePayResponse(val resultCode: Int, val data: Intent?) : ShopAction()
object BuyWithGooglePay : ShopAction {
object UserCancelled : ShopAction
data class HandleGooglePayResponse(val resultCode: Int, val data: Intent?) : ShopAction
data class Failure(val exception: Throwable) : ShopAction()
object Success : ShopAction()
data class Failure(val exception: Throwable) : ShopAction
object Success : ShopAction
}
object StartWebCheckout : ShopAction()
object StartWebCheckout : ShopAction
data class CheckIfGooglePayAvailable(val googlePayService: GooglePayService) : ShopAction() {
object Success : ShopAction()
object Failure : ShopAction()
data class CheckIfGooglePayAvailable(val googlePayService: GooglePayService) : ShopAction {
object Success : ShopAction
object Failure : ShopAction
}
data class SelectProduct(val productType: ProductType) : ShopAction()
data class SelectProduct(val productType: ProductType) : ShopAction
object FinishSuccessfulOrder : ShopAction()
object FinishSuccessfulOrder : ShopAction
object ResetState : ShopAction()
object ResetState : ShopAction
data class SetOrderingDelayBlockVisibility(val visibility: Boolean) : ShopAction
}

View file

@ -6,63 +6,38 @@ object ShopReducer {
fun reduce(action: Action, state: ShopState): ShopState = internalReduce(action, state)
}
@Suppress("ComplexMethod")
private fun internalReduce(action: Action, state: ShopState): ShopState {
if (action !is ShopAction) return state
return when (action) {
is ShopAction.ApplyPromoCode -> state.copy(
promoCode = action.promoCode,
promoCodeLoading = true,
)
ShopAction.BuyWithGooglePay -> state
ShopAction.LoadProducts -> state
is ShopAction.LoadProducts.Success -> {
state.copy(
availableProducts = action.products,
)
}
ShopAction.StartWebCheckout -> state
ShopAction.ApplyPromoCode.InvalidPromoCode -> state.copy(
promoCode = null,
promoCodeLoading = false,
)
is ShopAction.ApplyPromoCode -> state.copy(promoCode = action.promoCode, promoCodeLoading = true)
is ShopAction.LoadProducts.Success -> state.copy(availableProducts = action.products)
is ShopAction.ApplyPromoCode.InvalidPromoCode -> state.copy(promoCode = null, promoCodeLoading = false)
is ShopAction.ApplyPromoCode.Success -> {
state.copy(
promoCode = action.promoCode,
availableProducts = action.products,
promoCodeLoading = false,
)
}
is ShopAction.SelectProduct -> state.copy(selectedProduct = action.productType)
)
}
is ShopAction.SelectProduct -> {
state.copy(
selectedProduct = action.productType,
)
}
is ShopAction.CheckIfGooglePayAvailable -> {
state
}
ShopAction.CheckIfGooglePayAvailable.Failure -> {
state.copy(isGooglePayAvailable = false)
}
ShopAction.CheckIfGooglePayAvailable.Success -> {
state.copy(isGooglePayAvailable = false) // TODO: change when we add support for GPay
}
is ShopAction.BuyWithGooglePay.Failure -> {
state
}
is ShopAction.BuyWithGooglePay.HandleGooglePayResponse -> {
state
}
ShopAction.BuyWithGooglePay.Success -> {
state
}
ShopAction.BuyWithGooglePay.UserCancelled -> {
state
}
ShopAction.FinishSuccessfulOrder -> state
ShopAction.ResetState -> ShopState()
ShopAction.LoadProducts.Failure -> state
// TODO: change when we add support for GPay
is ShopAction.CheckIfGooglePayAvailable.Failure -> state.copy(isGooglePayAvailable = false)
is ShopAction.CheckIfGooglePayAvailable.Success -> state.copy(isGooglePayAvailable = false)
is ShopAction.ResetState -> ShopState()
is ShopAction.SetOrderingDelayBlockVisibility -> state.copy(isOrderingDelayBlockVisible = action.visibility)
is ShopAction.BuyWithGooglePay,
is ShopAction.LoadProducts,
is ShopAction.StartWebCheckout,
is ShopAction.CheckIfGooglePayAvailable,
is ShopAction.BuyWithGooglePay.Failure,
is ShopAction.BuyWithGooglePay.HandleGooglePayResponse,
is ShopAction.BuyWithGooglePay.Success,
is ShopAction.BuyWithGooglePay.UserCancelled,
is ShopAction.FinishSuccessfulOrder,
is ShopAction.LoadProducts.Failure,
-> state
}
}

View file

@ -10,6 +10,7 @@ data class ShopState(
val promoCode: String? = null,
val promoCodeLoading: Boolean = false,
val isGooglePayAvailable: Boolean = false, // TODO: change when we add support for GPay
val isOrderingDelayBlockVisible: Boolean = false,
) : StateType {
val total: String?
get() = availableProducts.firstOrNull { it.type == selectedProduct }?.totalSum?.finalValue

View file

@ -9,28 +9,35 @@ import android.view.View.OnFocusChangeListener
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.viewModels
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tap.common.GlobalLayoutStateHandler
import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.extensions.getQuantityString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.shop.data.ProductType
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.shop.presentation.ShopViewModel
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.features.shop.redux.ShopState
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentShopBinding
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<ShopState> {
@AndroidEntryPoint
internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<ShopState> {
private val binding: FragmentShopBinding by viewBinding(FragmentShopBinding::bind)
private var cardTranslationY = 70f
private lateinit var keyboardObserver: KeyboardObserver
private val viewModel by viewModels<ShopViewModel>()
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
@ -43,6 +50,8 @@ class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.checkOrderingDelayBlockVisibility()
activity?.onBackPressedDispatcher?.addCallback(
this,
object : OnBackPressedCallback(true) {
@ -133,6 +142,7 @@ class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<
animateProductSelection(state.selectedProduct)
handlePriceState(state)
handlePromoCodeState(state)
handleOrderingDelayBlock(isVisible = state.isOrderingDelayBlockVisible)
handleButtonsState(state)
}
@ -173,6 +183,10 @@ class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<
pbPromoCode.show(state.promoCodeLoading)
}
private fun handleOrderingDelayBlock(isVisible: Boolean) {
if (isVisible) binding.tvSoldOutDesc.show() else binding.tvSoldOutDesc.hide()
}
private fun handleButtonsState(state: ShopState) = with(binding) {
btnPayGooglePay.root.show(state.isGooglePayAvailable)
btnAlternativePayment.show(state.isGooglePayAvailable)

View file

@ -4,9 +4,10 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.transition.TransitionInflater
@ -25,8 +26,6 @@ import dagger.hilt.android.AndroidEntryPoint
internal class TokensListFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
with(TransitionInflater.from(requireContext())) {
enterTransition = inflateTransition(R.transition.fade)
exitTransition = inflateTransition(R.transition.fade)
@ -41,7 +40,10 @@ internal class TokensListFragment : Fragment() {
}
TangemTheme {
TokensListScreen(stateHolder = viewModel.uiState)
TokensListScreen(
modifier = Modifier.systemBarsPadding(),
stateHolder = viewModel.uiState,
)
}
}
}

View file

@ -1,12 +1,16 @@
package com.tangem.tap.features.tokens.impl.presentation.states
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
/**
* Network item state
* Token item state.
* All subclasses is stable, but @Immutable annotation is required to use this sealed class like as
* field of TokensListStateHolder.
*
[REDACTED_AUTHOR]
*/
@Immutable
sealed interface TokenItemState {
/** Token id */

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.tokens.impl.presentation.states
import androidx.compose.runtime.Immutable
import androidx.paging.LoadState
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow
@ -9,6 +10,7 @@ import kotlinx.coroutines.flow.Flow
*
[REDACTED_AUTHOR]
*/
@Immutable
internal sealed interface TokensListStateHolder {
/** Toolbar state */

View file

@ -1,6 +1,13 @@
package com.tangem.tap.features.tokens.impl.presentation.states
/** Toolbar state */
import androidx.compose.runtime.Immutable
/**
* Toolbar state.
* All subclasses is stable, but @Immutable annotation is required to use this sealed class like as
* field of TokensListStateHolder.
*/
@Immutable
sealed interface TokensListToolbarState {
/** Callback to be invoked when BackButton is being clicked */

View file

@ -56,12 +56,13 @@ import kotlinx.coroutines.flow.flowOf
[REDACTED_AUTHOR]
*/
@Composable
internal fun TokensListScreen(stateHolder: TokensListStateHolder) {
internal fun TokensListScreen(stateHolder: TokensListStateHolder, modifier: Modifier = Modifier) {
BackHandler(onBack = stateHolder.toolbarState.onBackButtonClick)
var floatingButtonHeight by remember { mutableStateOf(value = 0.dp) }
Scaffold(
modifier = modifier,
topBar = { TokensListToolbar(state = stateHolder.toolbarState) },
floatingActionButton = {
if (stateHolder is TokensListStateHolder.ManageContent) {

View file

@ -9,7 +9,7 @@ import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -114,7 +114,8 @@ class MultiWalletMiddleware {
)
}
.doOnSuccess { updatedUserWallet ->
store.dispatchOnMain(WalletAction.MultiWallet.AddMissingDerivations(emptyList()))
store.dispatchWithMain(WalletAction.MultiWallet.AddMissingDerivations(emptyList()))
store.dispatchWithMain(GlobalAction.SaveScanResponse(updatedUserWallet.scanResponse))
store.state.globalState.tapWalletManager.loadData(updatedUserWallet, refresh = true)
}
}

View file

@ -17,6 +17,7 @@ import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.*
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.TapError
@ -97,12 +98,12 @@ class WalletMiddleware {
Timber.e("Unable to create wallet, no user wallet selected")
return@launch
}
val updatedScanResponse = selectedUserWallet.scanResponse.copy(
card = result.data,
)
store.dispatchWithMain(GlobalAction.SaveScanResponse(updatedScanResponse))
userWalletsListManager.update(selectedUserWallet.walletId) { userWallet ->
userWallet.copy(
scanResponse = userWallet.scanResponse.copy(
card = result.data,
),
)
userWallet.copy(scanResponse = updatedScanResponse)
}
}
is CompletionResult.Failure -> Unit

View file

@ -1,24 +1,48 @@
package com.tangem.tap.features.wallet.ui
import android.view.View
import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressType
import com.tangem.blockchain.blockchains.cardano.CardanoAddressType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.address.AddressType
import com.tangem.wallet.R
object MultipleAddressUiHelper {
fun typeToId(type: AddressType): Int {
return when (type) {
AddressType.Legacy -> R.id.chip_legacy
AddressType.Default -> R.id.chip_default
is BitcoinAddressType.Legacy -> R.id.chip_legacy
is BitcoinAddressType.Segwit -> R.id.chip_default
is CardanoAddressType.Byron -> R.id.chip_legacy
is CardanoAddressType.Shelley -> R.id.chip_default
else -> View.NO_ID
}
}
fun idToType(id: Int, blockchain: Blockchain?): AddressType? {
return when (id) {
R.id.chip_default -> AddressType.Default
R.id.chip_legacy -> AddressType.Legacy
R.id.chip_default -> {
when (blockchain) {
Blockchain.Bitcoin,
Blockchain.BitcoinTestnet,
Blockchain.Litecoin,
Blockchain.BitcoinCash,
-> BitcoinAddressType.Segwit
Blockchain.CardanoShelley -> CardanoAddressType.Shelley
else -> null
}
}
R.id.chip_legacy -> {
when (blockchain) {
Blockchain.Bitcoin,
Blockchain.BitcoinTestnet,
Blockchain.Litecoin,
Blockchain.BitcoinCash,
-> BitcoinAddressType.Legacy
Blockchain.CardanoShelley -> CardanoAddressType.Byron
else -> null
}
}
else -> null
}
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.extensions.toFormattedFiatValue
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.WalletWarning
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.utils.UNKNOWN_AMOUNT_SIGN
@ -19,9 +20,24 @@ import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import java.math.BigDecimal
internal fun WalletDataModel.mainButton(blockchainAmount: BigDecimal): WalletMainButton = WalletMainButton.SendButton(
enabled = !isEmptyAmount && status.pendingTransactions.isEmpty() && !blockchainAmount.isZero(),
enabled = !isEmptyAmount &&
hasPendingTransactions() &&
!blockchainAmount.isZero(),
)
internal fun WalletDataModel.hasPendingTransactions(): Boolean {
// for now check pending ongoing only just for BTC, later test and add other utxo networks
val isBitcoinBlockchain =
currency.blockchain == Blockchain.Bitcoin || currency.blockchain == Blockchain.BitcoinTestnet
if (currency.isBlockchain() && isBitcoinBlockchain) {
val outgoingTransactions = status.pendingTransactions.filter {
it.type == PendingTransactionType.Outgoing
}
return outgoingTransactions.isEmpty()
}
return status.pendingTransactions.isEmpty()
}
internal fun WalletDataModel.getFormattedAmount(): String {
return status.amount.toFormattedCurrencyString(
decimals = currency.decimals,
@ -82,7 +98,7 @@ internal fun WalletDataModel.getAvailableActions(
internal fun WalletDataModel.shouldShowMultipleAddress(): Boolean {
val listOfAddresses = walletAddresses?.list.orEmpty()
return listOfAddresses.size > 1
return listOfAddresses.size > 1 && currency.blockchain != Blockchain.BitcoinCash
}
internal fun WalletDataModel.assembleWarnings(

View file

@ -4,14 +4,7 @@ import android.content.Context
import android.util.AttributeSet
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.*
import androidx.compose.material.Divider
import androidx.compose.material.Surface
import androidx.compose.material.Text
@ -27,11 +20,7 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SelectorButton
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerW16
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.domain.model.TotalFiatBalance
@ -42,7 +31,8 @@ import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.*
import java.util.Currency
import java.util.Locale
internal class TotalBalanceCard @JvmOverloads constructor(
context: Context,
@ -125,7 +115,7 @@ private fun TotalBalanceCardContent(state: TotalBalanceCardState, modifier: Modi
-> LoadedAmount(
amount = buildAmountString(
amount = state.amount,
fiatCurrencySymbol = state.fiatCurrency.symbol,
fiatCurrency = state.fiatCurrency,
),
)
}
@ -228,30 +218,45 @@ private fun LoadedAmount(amount: AnnotatedString, modifier: Modifier = Modifier)
}
@Composable
private fun buildAmountString(amount: BigDecimal?, fiatCurrencySymbol: String): AnnotatedString {
private fun buildAmountString(amount: BigDecimal?, fiatCurrency: FiatCurrency): AnnotatedString {
if (amount == null) return AnnotatedString(text = UNKNOWN_AMOUNT_SIGN)
val formatter = NumberFormat.getInstance(Locale.getDefault()) as? DecimalFormat
?: return AnnotatedString("${amount.toPlainString()} $fiatCurrencySymbol")
val decimalFormat = formatter.apply {
maximumFractionDigits = 2
minimumFractionDigits = 2
isGroupingUsed = true
this.roundingMode = RoundingMode.HALF_UP
val locale = Locale.getDefault()
val fractionDigits = 2
val formatter = NumberFormat.getCurrencyInstance(locale) as? DecimalFormat
?: return AnnotatedString("${amount.toPlainString()} ${fiatCurrency.symbol}")
val currencyToShow = "${fiatCurrency.symbol}"
val scaledAmount = Currency.getInstance(fiatCurrency.code)?.let { currency ->
formatter.currency = currency
formatter.maximumFractionDigits = fractionDigits
formatter.minimumFractionDigits = fractionDigits
formatter.isGroupingUsed = true
formatter.roundingMode = RoundingMode.HALF_UP
formatter.format(amount).replace(currency.symbol, currencyToShow)
} ?: formatter.format(amount)
val integer = scaledAmount.substringBefore(formatter.decimalFormatSymbols.decimalSeparator)
var reminder = scaledAmount.substringAfter(formatter.decimalFormatSymbols.decimalSeparator)
// if locale formatted currency at the end, remember it and place out of AnnotatedString
val currency = if (reminder.endsWith(currencyToShow)) {
reminder = reminder.dropLast(currencyToShow.length)
currencyToShow
} else {
""
}
val scaledAmount = decimalFormat.format(amount)
val integer = scaledAmount.substringBefore(decimalFormat.decimalFormatSymbols.decimalSeparator)
val reminder = scaledAmount.substringAfter(decimalFormat.decimalFormatSymbols.decimalSeparator)
return buildAnnotatedString {
append(integer)
append(decimalFormat.decimalFormatSymbols.decimalSeparator)
append(formatter.decimalFormatSymbols.decimalSeparator)
append(
AnnotatedString(
text = "$reminder $fiatCurrencySymbol",
text = reminder,
spanStyle = TangemTheme.typography.h3.toSpanStyle(),
),
)
append(currency) // it is not empty if was placed at the end after locale formatting
}
}

View file

@ -1,40 +0,0 @@
package com.tangem.tap.network.payid
import com.squareup.moshi.JsonClass
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Path
/**
[REDACTED_AUTHOR]
*/
interface PayIdVerifyApi {
@GET("{user}")
suspend fun verifyAddress(
@Path("user") user: String,
@Header("Accept") acceptNetworkHeader: String,
@Header("PayID-Version") payIdVersion: String = "1.0",
): VerifyPayIdResponse
}
@JsonClass(generateAdapter = true)
data class VerifyPayIdResponse(
val addresses: List<PayIdAddress> = mutableListOf(),
val payId: String? = null,
) {
fun getAddressDetails(): PayIdAddressDetails? = if (addresses.isNotEmpty()) addresses[0].addressDetails else null
}
@JsonClass(generateAdapter = true)
data class PayIdAddress(
var paymentNetwork: String,
var environment: String,
var addressDetailsType: String,
var addressDetails: PayIdAddressDetails,
)
@JsonClass(generateAdapter = true)
data class PayIdAddressDetails(
var address: String,
var tag: String? = null,
)

View file

@ -1,24 +0,0 @@
package com.tangem.tap.network.payid
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.common.createRetrofitInstance
/**
[REDACTED_AUTHOR]
*/
class PayIdVerifyService(
private val baseUrl: String,
) {
private val api = createRetrofitInstance(
baseUrl = baseUrl,
logEnabled = false,
).create(PayIdVerifyApi::class.java)
suspend fun verifyAddress(user: String, network: String): Result<VerifyPayIdResponse> {
return performRequest { api.verifyAddress(user, createNetworkHeader(network)) }
}
private fun createNetworkHeader(network: String): String = "application/$network-mainnet+json"
}

View file

@ -19,6 +19,7 @@ import javax.inject.Inject
*/
class AppStateHolder @Inject constructor() {
@Deprecated("Use scan response from selected user wallet")
var scanResponse: ScanResponse? = null
var walletState: WalletState? = null
var userTokensRepository: UserTokensRepository? = null

View file

@ -39,8 +39,8 @@
android:orientation="vertical">
<include
android:id="@+id/l_send_address_payid"
layout="@layout/layout_send_address_payid"
android:id="@+id/l_send_address"
layout="@layout/layout_send_address"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp" />

View file

@ -271,7 +271,7 @@
android:layout_marginTop="14dp"
android:background="@drawable/shape_rectangle_rounded_4"
android:padding="16dp"
android:text="@string/shop_sold_out_description_prefix"
android:text="@string/shop_sold_out_description"
android:textColor="@color/text_tertiary"
android:textSize="16sp" />
</LinearLayout>

View file

@ -150,63 +150,6 @@
app:layout_constraintTop_toBottomOf="@id/tv_explore"
tools:text="Send only Ethereum (ETH) from Ethereum network to this address. Using other tokens and networks may result in loss of funds." />
<View
android:id="@+id/v_payid_divider"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="@color/lightGray5"
android:layout_marginTop="25dp"
app:layout_constraintTop_toBottomOf="@id/btn_copy" />
<ImageView
android:id="@+id/iv_payid_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:src="@drawable/ic_payid"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/v_payid_divider"
android:importantForAccessibility="no" />
<TextView
android:id="@+id/tv_create_payid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="9dp"
android:padding="16dp"
android:text="@string/wallet_address_button_create_payid"
android:textColor="@color/darkGray6"
android:textSize="14sp"
android:textStyle="bold"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/v_payid_divider" />
<TextView
android:id="@+id/tv_payid_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="16dp"
android:textAlignment="textEnd"
android:textColor="@color/darkGray1"
android:textSize="13sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintStart_toEndOf="@id/iv_payid_icon"
app:layout_constraintTop_toBottomOf="@id/v_payid_divider"
tools:text="romafdffdfdfn$payid.tangem.com" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_payid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="v_payid_divider, iv_payid_icon, tv_payid_address, tv_create_payid" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -18,10 +18,9 @@
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAddressOrPayId"
android:id="@+id/tilAddress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/send_destination_hint_address_payid"
app:boxBackgroundColor="@color/backgroundLightGray"
app:errorIconDrawable="@null"
app:layout_constraintEnd_toEndOf="parent"
@ -29,7 +28,7 @@
app:layout_constraintTop_toTopOf="parent">
<com.tangem.tap.features.send.ui.EditTextCustomPaste
android:id="@+id/etAddressOrPayId"
android:id="@+id/etAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
@ -71,7 +70,7 @@
android:layout_marginTop="10dp"
android:background="@drawable/shape_ellipse"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/tilAddressOrPayId">
app:layout_constraintTop_toTopOf="@+id/tilAddress">
<ImageView
android:id="@+id/imvQrCode"
@ -104,41 +103,6 @@
android:visibility="gone"
tools:visibility="visible">
<!-- <com.google.android.material.chip.ChipGroup-->
<!-- android:id="@+id/groupMemo"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginTop="8dp"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toStartOf="parent"-->
<!-- app:layout_constraintTop_toTopOf="parent"-->
<!-- app:selectionRequired="true"-->
<!-- app:singleLine="true"-->
<!-- app:singleSelection="true">-->
<!-- <com.google.android.material.chip.Chip-->
<!-- android:id="@+id/chipMemoText"-->
<!-- style="@style/TapChip"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="Text" />-->
<!-- <com.google.android.material.chip.Chip-->
<!-- android:id="@+id/chipMemoId"-->
<!-- style="@style/TapChip"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="ID" />-->
<!-- <com.google.android.material.chip.Chip-->
<!-- android:id="@+id/chipMemoHash"-->
<!-- style="@style/TapChip"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="Hash" />-->
<!-- </com.google.android.material.chip.ChipGroup>-->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilXlmMemo"
android:layout_width="match_parent"

View file

@ -5,7 +5,7 @@
android:id="@+id/amountContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/tilAddressOrPayId">
app:layout_constraintTop_toBottomOf="@+id/tilAddress">
<FrameLayout
android:id="@+id/flAmountToSend"

View file

@ -0,0 +1,5 @@
<resources>
<bool name="largeHeap">false</bool>
</resources>

View file

@ -0,0 +1,5 @@
<resources>
<bool name="largeHeap">true</bool>
</resources>

View file

@ -1,4 +0,0 @@
package com.tangem;
public class Test2 {
}

View file

@ -1,305 +0,0 @@
package com.tangem.ui
import android.app.Activity
import android.content.SharedPreferences
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Build
import android.os.Bundle
import android.preference.PreferenceManager
import android.text.Editable
import android.text.Html
import android.text.TextWatcher
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.core.os.bundleOf
import com.tangem.Constant
import com.tangem.data.Blockchain
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD_UID
import com.tangem.tangem_sdk.data.loadFromBundle
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.fragment.pin.PinRequestFragment
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.UtilHelper
import com.tangem.wallet.CoinEngine
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import kotlinx.android.synthetic.tangemAccess.fragment_confirm_transaction.*
import java.io.IOException
import java.util.*
class ConfirmTransactionFragment : BaseFragment(), NavigationResultListener, NfcAdapter.ReaderCallback {
override val layoutId = R.layout.fragment_confirm_transaction
private lateinit var sp: SharedPreferences
private lateinit var ctx: TangemContext
private lateinit var amount: CoinEngine.Amount
private var isIncludeFee: Boolean = true
private var requestPIN2Count = 0
private var nodeCheck = true
private var dtVerified: Date? = null
private var blockchainCallbacks: CoinEngine.BlockchainRequestsCallbacks? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sp = PreferenceManager.getDefaultSharedPreferences(context)
ctx = TangemContext.loadFromBundle(requireContext(), arguments)
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navigateUp()
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val engine = CoinEngineFactory.create(ctx)
@Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
Html.fromHtml(engine!!.balanceHTML, Html.FROM_HTML_MODE_LEGACY)
else
Html.fromHtml(engine!!.balanceHTML)
tvBalance.text = html
isIncludeFee = arguments?.getBoolean(Constant.EXTRA_FEE_INCLUDED, true) ?: true
if (isIncludeFee)
tvIncFee.setText(R.string.confirm_transaction_including_fee)
else
tvIncFee.setText(R.string.confirm_transaction_not_including_fee)
amount = CoinEngine.Amount(arguments?.getString(Constant.EXTRA_AMOUNT) ?: "0",
arguments?.getString(Constant.EXTRA_AMOUNT_CURRENCY) ?: "")
if (engine.allowSelectFeeInclusion())
tvIncFee.visibility = View.VISIBLE
else
tvIncFee.visibility = View.INVISIBLE
if (ctx.card.blockchainID == Blockchain.Token.id) {
// for Blockchain.Token limit decimals
etAmount.setText(amount.toValueString(ctx.card.tokensDecimal))
} else {
// for others
etAmount.setText(amount.toValueString())
}
tvCurrency.text = engine.balanceCurrency
tvCurrency2.text = engine.feeCurrency
tvCardID.text = ctx.card.cidDescription
etWallet.setText(arguments?.getString(Constant.EXTRA_TARGET_ADDRESS))
btnSend.visibility = View.INVISIBLE
if (!engine.allowSelectFeeLevel()) {
rgFee.visibility = View.INVISIBLE
}
etFee.isEnabled = sp.getBoolean(getString(R.string.pref_manual_editing_fee), false)
// set listeners
rgFee.setOnCheckedChangeListener { _, checkedId -> doSetFee(checkedId) }
etFee.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
try {
val eqFee = engine.evaluateFeeEquivalent(etFee!!.text.toString())
tvFeeEquivalent.text = eqFee
if (!ctx.coinData!!.amountEquivalentDescriptionAvailable) {
tvFeeEquivalent.error = getString(R.string.confirm_transaction_error_service_unavailable)
tvCurrency2.visibility = View.GONE
tvFeeEquivalent.visibility = View.GONE
} else
tvFeeEquivalent.error = null
if (sp.getBoolean(getString(R.string.pref_manual_editing_fee), false))
(activity as MainActivity).toastHelper
.showSingleToast(context, getString(R.string.confirm_transaction_warning_risk_delaying))
} catch (e: Exception) {
e.printStackTrace()
tvFeeEquivalent.text = ""
}
}
override fun afterTextChanged(s: Editable) {
}
})
btnSend.setOnClickListener {
if (UtilHelper.isOnline(requireContext())) {
val calendar = Calendar.getInstance()
calendar.add(Calendar.MINUTE, -1)
if (dtVerified == null || dtVerified!!.before(calendar.time)) {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.confirm_transaction_error_data_is_outdated))
return@setOnClickListener
}
val engineCoin = CoinEngineFactory.create(ctx)
if (engineCoin!!.isNeedCheckNode && !nodeCheck) {
Toast.makeText(context, getString(R.string.confirm_transaction_error_cannot_reach_node), Toast.LENGTH_LONG).show()
return@setOnClickListener
}
val txFee = engineCoin.convertToAmount(etFee.text.toString(), tvCurrency2.text.toString())
val txAmount = engineCoin.convertToAmount(etAmount.text.toString(), tvCurrency.text.toString())
if (!engineCoin.hasBalanceInfo()) {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.confirm_transaction_error_cannot_check_balance))
return@setOnClickListener
} else if (!engineCoin.isBalanceNotZero) {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.general_wallet_empty))
return@setOnClickListener
} else if (!engineCoin.isExtractPossible) {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.confirm_transaction_error_incoming_transaction_unconfirmed))
return@setOnClickListener
}
if (!engineCoin.checkNewTransactionAmountAndFee(txAmount, txFee, isIncludeFee)) {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.prepare_transaction_error_not_enough_funds))
return@setOnClickListener
}
requestPIN2Count = 0
val data = Bundle()
data.putString(Constant.EXTRA_MODE, PinRequestFragment.Mode.RequestPIN2.toString())
ctx.saveToBundle(data)
data.putBoolean(Constant.EXTRA_FEE_INCLUDED, isIncludeFee)
navigateForResult(Constant.REQUEST_CODE_REQUEST_PIN2_, R.id.action_confirmTransactionFragment_to_pinRequestFragment, data)
} else
Toast.makeText(context, getString(R.string.general_error_no_connection), Toast.LENGTH_SHORT).show()
}
progressBar.visibility = View.VISIBLE
if (!navigatedBack) requestFee()
}
private fun requestFee() {
val coinEngine = CoinEngineFactory.create(ctx)
coinEngine!!.requestFee(
object : CoinEngine.BlockchainRequestsCallbacks {
override fun onComplete(success: Boolean) {
if (success) {
progressBar?.visibility = View.INVISIBLE
dtVerified = Date()
doSetFee(rgFee?.checkedRadioButtonId ?: R.id.rbNormalFee)
} else {
finishWithError(Activity.RESULT_CANCELED, ctx.error)
}
}
override fun onProgress() {
}
override fun allowAdvance(): Boolean {
return UtilHelper.isOnline(requireContext())
}
},
etWallet.text.toString(),
amount)
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
Log.d("LIFECYCLE", "NavigationResult assessed ${this::class.java.simpleName}")
if (requestCode == Constant.REQUEST_CODE_SIGN_TRANSACTION) {
if (data != null) {
if (data.containsKey(EXTRA_TANGEM_CARD_UID) && data.containsKey(EXTRA_TANGEM_CARD)) {
val updatedCard = TangemCard(data.getString(EXTRA_TANGEM_CARD_UID))
updatedCard.loadFromBundle(data.getBundle(EXTRA_TANGEM_CARD))
ctx.card = updatedCard
}
}
if (resultCode == Constant.RESULT_INVALID_PIN_ && requestPIN2Count < 2) {
requestPIN2Count++
val bundle = Bundle()
bundle.putString(Constant.EXTRA_MODE, PinRequestFragment.Mode.RequestPIN2.toString())
ctx.saveToBundle(bundle)
bundle.putBoolean(Constant.EXTRA_FEE_INCLUDED, isIncludeFee)
navigateForResult(Constant.REQUEST_CODE_REQUEST_PIN2_,
R.id.action_confirmTransactionFragment_to_pinRequestFragment,
bundle)
return
}
navigateBackWithResult(resultCode, data)
} else if (requestCode == Constant.REQUEST_CODE_REQUEST_PIN2_) {
if (resultCode == Activity.RESULT_OK) {
val bundle = Bundle()
ctx.saveToBundle(bundle)
bundle.putString(Constant.EXTRA_TARGET_ADDRESS, etWallet!!.text.toString())
bundle.putString(Constant.EXTRA_AMOUNT, etAmount.text.toString())
bundle.putString(Constant.EXTRA_AMOUNT_CURRENCY, tvCurrency.text.toString())
bundle.putString(Constant.EXTRA_FEE, etFee.text.toString())
bundle.putString(Constant.EXTRA_FEE_CURRENCY, tvCurrency2.text.toString())
bundle.putBoolean(Constant.EXTRA_FEE_INCLUDED, isIncludeFee)
navigateForResult(Constant.REQUEST_CODE_SIGN_TRANSACTION,
R.id.action_confirmTransactionFragment_to_signTransactionFragment,
bundle)
} else
Toast.makeText(context, R.string.confirm_transaction_error_pin_2_is_required, Toast.LENGTH_LONG).show()
}
}
override fun onTagDiscovered(tag: Tag) {
try {
(activity as MainActivity).nfcManager.ignoreTag(tag)
} catch (e: IOException) {
e.printStackTrace()
}
}
private fun doSetFee(checkedRadioButtonId: Int) {
var txtFee = ""
when (checkedRadioButtonId) {
R.id.rbMinimalFee ->
if (ctx.coinData.minFee != null) {
txtFee = ctx.coinData.minFee!!.toValueString()
btnSend?.visibility = View.VISIBLE
} else
btnSend?.visibility = View.INVISIBLE
R.id.rbNormalFee ->
if (ctx.coinData.normalFee != null) {
txtFee = ctx.coinData.normalFee!!.toValueString()
btnSend?.visibility = View.VISIBLE
} else
btnSend?.visibility = View.INVISIBLE
R.id.rbMaximumFee ->
if (ctx.coinData.maxFee != null) {
txtFee = ctx.coinData.maxFee!!.toValueString()
btnSend?.visibility = View.VISIBLE
} else
btnSend?.visibility = View.INVISIBLE
}
etFee?.setText(txtFee.replace(',', '.'))
}
private fun finishWithError(errorCode: Int, message: String) {
navigateBackWithResult(
errorCode,
bundleOf(Constant.EXTRA_MESSAGE to message),
R.id.loadedWalletFragment)
}
}

View file

@ -1,182 +0,0 @@
package com.tangem.ui
import android.app.Activity
import android.content.Context
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Build
import android.os.Bundle
import android.text.Html
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import com.tangem.Constant
import com.tangem.data.isPayIdSupported
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.fragment.qr.CameraPermissionManager
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.UtilHelper
import com.tangem.util.extensions.isStart2CoinCard
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import kotlinx.android.synthetic.tangemAccess.fragment_prepare_transaction.*
import java.io.IOException
class PrepareTransactionFragment : BaseFragment(), NavigationResultListener, NfcAdapter.ReaderCallback {
companion object {
val TAG: String = PrepareTransactionFragment::class.java.simpleName
}
override val layoutId = R.layout.fragment_prepare_transaction
private val ctx: TangemContext by lazy { TangemContext.loadFromBundle(context, arguments) }
private val cameraPermissionManager: CameraPermissionManager by lazy { CameraPermissionManager(this) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
tvCardID.text = ctx.card?.cidDescription
val engine = CoinEngineFactory.create(ctx)
@Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
Html.fromHtml(engine!!.balanceHTML, Html.FROM_HTML_MODE_LEGACY)
else
Html.fromHtml(engine!!.balanceHTML)
tvBalance.text = html
if (ctx.blockchain.isPayIdSupported() && !ctx.card.isStart2CoinCard()) {
etWallet.hint = getString(R.string.prepare_transaction_hint_address_or_pay_id)
}
if (!engine.allowSelectFeeInclusion()) {
rgIncFee.visibility = View.INVISIBLE
} else {
rgIncFee.visibility = View.VISIBLE
}
if (ctx.card!!.remainingSignatures < 2) {
etAmount.isEnabled = false
}
if (ctx.card.remainingSignatures == 1) {
androidx.appcompat.app.AlertDialog.Builder(requireContext())
.setTitle(R.string.prepare_transaction_warning_last_signature)
.setMessage(R.string.prepare_transaction_warning_send_full_amount)
.setPositiveButton(R.string.general_ok) { _, _ -> }
.create()
.show()
}
tvCurrency.text = engine.balance.currency
etAmount.setText(engine.balance.toValueString())
// limit number of symbols after comma
etAmount.filters = engine.amountInputFilters
// set listeners
etAmount.setOnEditorActionListener { lv, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
val imm = lv.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(lv.windowToken, 0)
lv.clearFocus()
true
} else {
false
}
}
btnVerify.setOnClickListener {
if (!UtilHelper.isOnline(requireContext())) {
Toast.makeText(context, R.string.general_error_no_connection, Toast.LENGTH_LONG).show()
return@setOnClickListener
}
val engine1 = CoinEngineFactory.create(ctx)
val strAmount: String = etAmount.text.toString().replace(",", ".")
val amount = engine1!!.convertToAmount(etAmount.text.toString(), tvCurrency.text.toString())
try {
if (!engine.checkNewTransactionAmount(amount))
etAmount.error = getString(R.string.prepare_transaction_error_not_enough_funds)
else
etAmount.error = null
} catch (e: Exception) {
etAmount.error = getString(R.string.prepare_transaction_error_unknown_amount_format)
}
// check wallet address
if (!engine1.validateAddress(etWallet.text.toString())) {
etWallet.error = getString(R.string.prepare_transaction_error_incorrect_destination)
return@setOnClickListener
} else
etWallet.error = null
if (etWallet.text.toString() == ctx.coinData!!.wallet) {
etWallet.error = getString(R.string.prepare_transaction_error_same_address)
return@setOnClickListener
}
if (!etAmount.error.isNullOrEmpty() || !etWallet.error.isNullOrEmpty()) {
return@setOnClickListener
}
val data = Bundle()
ctx.saveToBundle(data)
data.putString(Constant.EXTRA_TARGET_ADDRESS, etWallet!!.text.toString())
data.putBoolean(Constant.EXTRA_FEE_INCLUDED, (rgIncFee!!.checkedRadioButtonId == R.id.rbFeeIn))
data.putString(Constant.EXTRA_AMOUNT, strAmount)
data.putString(Constant.EXTRA_AMOUNT_CURRENCY, tvCurrency.text.toString())
navigateForResult(
Constant.REQUEST_CODE_SEND_TRANSACTION__,
R.id.action_prepareTransactionFragment_to_confirmTransactionFragment,
data)
}
ivCamera.setOnClickListener {
if (cameraPermissionManager.isPermissionGranted()) {
navigateForResult(Constant.REQUEST_CODE_SCAN_QR, R.id.action_prepareTransactionFragment_to_qrScanFragment)
} else {
cameraPermissionManager.requirePermission()
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
cameraPermissionManager.handleRequestPermissionResult(requestCode, grantResults) {
navigateForResult(Constant.REQUEST_CODE_SCAN_QR, R.id.action_prepareTransactionFragment_to_qrScanFragment)
}
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
if (requestCode == Constant.REQUEST_CODE_SCAN_QR && resultCode == Activity.RESULT_OK && data != null && data.containsKey("QRCode")) {
val code = data.getString("QRCode")
val schemeSplit = code!!.split(":")
when (schemeSplit.size) {
2 -> {
if (schemeSplit[0] == ctx.blockchain.uriScheme) {
etWallet?.setText(schemeSplit[1])
} else {
etWallet?.setText(code)
}
}
else -> {
etWallet?.setText(code)
}
}
} else if (requestCode == Constant.REQUEST_CODE_SEND_TRANSACTION__) {
navigateBackWithResult(resultCode, data)
}
}
override fun onTagDiscovered(tag: Tag) {
try {
(activity as MainActivity).nfcManager.ignoreTag(tag)
} catch (e: IOException) {
e.printStackTrace()
}
}
}

View file

@ -1,312 +0,0 @@
package com.tangem.ui
import android.app.Activity
import android.content.res.ColorStateList
import android.graphics.Color
import android.media.MediaPlayer
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.App
import com.tangem.Constant
import com.tangem.tangem_card.reader.CardProtocol
import com.tangem.tangem_card.tasks.SignTask
import com.tangem.tangem_card.util.Util
import com.tangem.tangem_sdk.android.nfc.NfcDeviceAntennaLocation
import com.tangem.tangem_sdk.android.reader.NfcReader
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD_UID
import com.tangem.tangem_sdk.data.asBundle
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.dialog.NoExtendedLengthSupportDialog
import com.tangem.ui.dialog.WaitSecurityDelayDialog
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.Analytics
import com.tangem.util.AnalyticsEvent
import com.tangem.util.LOG
import com.tangem.wallet.CoinEngine
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import kotlinx.android.synthetic.main.layout_progress_horizontal.*
import kotlinx.android.synthetic.main.layout_touch_card.*
import kotlinx.android.synthetic.tangemAccess.fragment_sign_transaction.*
class SignTransactionFragment : BaseFragment(), NavigationResultListener,
NfcAdapter.ReaderCallback, CardProtocol.Notifications {
companion object {
val TAG: String = SignTransactionFragment::class.java.simpleName
}
override val layoutId = R.layout.fragment_sign_transaction
private lateinit var ctx: TangemContext
private lateinit var mpFinishSignSound: MediaPlayer
private lateinit var nfcDeviceAntenna: NfcDeviceAntennaLocation
private var signTransactionTask: SignTask? = null
private lateinit var amount: CoinEngine.Amount
private lateinit var fee: CoinEngine.Amount
private var isIncludeFee = true
private var outAddressStr: String? = null
private var lastReadSuccess = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ctx = TangemContext.loadFromBundle(context, arguments)
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navigateBackWithResult(Activity.RESULT_CANCELED)
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mpFinishSignSound = MediaPlayer.create(context, R.raw.scan_card_sound)
// init NFC Antenna
nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna.init()
amount = CoinEngine.Amount(arguments?.getString(Constant.EXTRA_AMOUNT), arguments?.getString(Constant.EXTRA_AMOUNT_CURRENCY))
fee = CoinEngine.Amount(arguments?.getString(Constant.EXTRA_FEE), arguments?.getString(Constant.EXTRA_FEE_CURRENCY))
isIncludeFee = arguments?.getBoolean(Constant.EXTRA_FEE_INCLUDED, true) ?: true
outAddressStr = arguments?.getString(Constant.EXTRA_TARGET_ADDRESS)
tvCardID.text = ctx.card!!.cidDescription
progressBar.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar.visibility = View.INVISIBLE
FirebaseAnalytics.getInstance(requireActivity())
.logEvent(AnalyticsEvent.READY_TO_SIGN.event, Analytics.setCardData(ctx))
}
override fun onPause() {
signTransactionTask?.cancel(true)
super.onPause()
}
override fun onStop() {
signTransactionTask?.cancel(true)
super.onStop()
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
if (requestCode == Constant.REQUEST_CODE_SEND_TRANSACTION_) {
navigateBackWithResult(resultCode, data)
}
}
override fun onTagDiscovered(tag: Tag) {
try {
// get IsoDep handle and run cardReader thread
val isoDep = IsoDep.get(tag)
val uid = tag.id
val sUID = Util.byteArrayToHexString(uid)
if (sUID == ctx.card.uid) {
if (lastReadSuccess)
isoDep.timeout = ctx.card.pauseBeforePIN2 + 5000
else
isoDep.timeout = ctx.card.pauseBeforePIN2 + 65000
val coinEngine = CoinEngineFactory.create(ctx)
coinEngine?.setOnNeedSendTransaction { tx ->
if (tx != null) {
val data = Bundle()
ctx.saveToBundle(data)
data.putByteArray(Constant.EXTRA_TX, tx)
navigateForResult(
Constant.REQUEST_CODE_SEND_TRANSACTION_,
R.id.action_signTransactionFragment_to_sendTransactionFragment,
data)
}
}
val transactionToSign = coinEngine?.constructTransaction(amount, fee, isIncludeFee, outAddressStr)
signTransactionTask = SignTask(ctx.card, NfcReader((activity as MainActivity).nfcManager, isoDep),
App.localStorage, App.pinStorage, this, transactionToSign)
signTransactionTask?.start()
} else
(activity as MainActivity).nfcManager.ignoreTag(isoDep.tag)
} catch (e: CardProtocol.TangemException_WrongAmount) {
try {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_wrong_amount))
data.putString(EXTRA_TANGEM_CARD_UID, ctx.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, ctx.card.asBundle)
navigateBackWithResult(Activity.RESULT_CANCELED, data)
} catch (e: Exception) {
e.printStackTrace()
}
} catch(e: IllegalArgumentException) {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, e.message)
navigateBackWithResult(Activity.RESULT_CANCELED, data, R.id.loadedWalletFragment)
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onReadStart(cardProtocol: CardProtocol) {
rlProgressBar?.post { rlProgressBar.visibility = View.VISIBLE }
progressBar?.post {
progressBar?.visibility = View.VISIBLE
progressBar?.progress = 5
}
}
override fun onReadProgress(protocol: CardProtocol, progress: Int) {
progressBar?.post { progressBar?.progress = progress }
}
override fun onReadFinish(cardProtocol: CardProtocol?) {
signTransactionTask = null
if (cardProtocol != null) {
if (cardProtocol.error == null) {
FirebaseAnalytics.getInstance(requireActivity())
.logEvent(AnalyticsEvent.SIGNED.event, Analytics.setCardData(ctx))
rlProgressBar?.post { rlProgressBar?.visibility = View.GONE }
progressBar?.post {
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.GREEN)
}
mpFinishSignSound.start()
} else {
lastReadSuccess = false
FirebaseCrashlytics.getInstance().recordException(cardProtocol.error)
if (cardProtocol.error.javaClass == CardProtocol.TangemException_InvalidPIN::class.java) {
progressBar?.post {
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.RED)
}
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_cannot_sign))
data.putString(EXTRA_TANGEM_CARD_UID, cardProtocol.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, cardProtocol.card.asBundle)
navigateBackWithResult(Constant.RESULT_INVALID_PIN_, data)
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
} else {
if (cardProtocol.error is CardProtocol.TangemException_WrongAmount) {
try {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_wrong_amount))
data.putString(EXTRA_TANGEM_CARD_UID, cardProtocol.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, cardProtocol.card.asBundle)
navigateBackWithResult(Activity.RESULT_CANCELED, data)
} catch (e: Exception) {
e.printStackTrace()
}
}
progressBar?.post {
if (cardProtocol.error is CardProtocol.TangemException_ExtendedLengthNotSupported) {
if (!NoExtendedLengthSupportDialog.allReadyShowed) {
NoExtendedLengthSupportDialog.message = getText(R.string.dialog_the_nfc_adapter_length_apdu).toString() + "\n" + getText(R.string.dialog_the_nfc_adapter_length_apdu_advice).toString()
NoExtendedLengthSupportDialog().show(requireFragmentManager(), NoExtendedLengthSupportDialog.TAG)
}
} else {
(activity as? MainActivity)?.toastHelper?.showSingleToast(
context, getString(R.string.general_notification_scan_again)
)
}
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.RED)
}
}
}
}
rlProgressBar?.postDelayed({
try {
rlProgressBar?.visibility = View.GONE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
override fun onReadCancel() {
signTransactionTask = null
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
// private val waitSecurityDelayDialogNew = WaitSecurityDelayDialogNew()
override fun onReadBeforeRequest(timeout: Int) {
LOG.i(TAG, "onReadBeforeRequest timeout $timeout")
activity?.let { WaitSecurityDelayDialog.onReadBeforeRequest(it, timeout) }
// if (!waitSecurityDelayDialogNew.isAdded)
// waitSecurityDelayDialogNew.show(supportFragmentManager, WaitSecurityDelayDialogNew.TAG)
// val readBeforeRequest = ReadBeforeRequest()
// readBeforeRequest.timeout = timeout
// EventBus.getDefault().post(readBeforeRequest)
}
override fun onReadAfterRequest() {
LOG.i(TAG, "onReadAfterRequest")
activity?.let { WaitSecurityDelayDialog.onReadAfterRequest(it) }
// val readAfterRequest = ReadAfterRequest()
// EventBus.getDefault().post(readAfterRequest)
}
override fun onReadWait(msec: Int) {
LOG.i(TAG, "onReadWait msec $msec")
activity?.let { WaitSecurityDelayDialog.onReadWait(it, msec) }
// val readWait = ReadWait()
// readWait.msec = msec
// EventBus.getDefault().post(readWait)
}
}

View file

@ -1,343 +0,0 @@
<?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:id="@+id/cl"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.tangem.ui.ConfirmTransactionFragment"
tools:ignore="Autofill">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:paddingTop="8dp"
android:paddingBottom="4dp"
android:text="@string/general_send_transaction"
android:textAlignment="center"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_large"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/llTransaction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/btn_light"
android:orientation="vertical"
android:paddingStart="5dp"
android:paddingTop="8dp"
android:paddingEnd="5dp"
android:paddingBottom="8dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="@string/general_from_card"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium" />
<TextView
android:id="@+id/tvCardID"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:fontFamily="@font/maax"
android:textColor="@color/black"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
tools:text="BB00 0000 1210 0233" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:fontFamily="@font/maax"
android:text="@string/general_balance"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium" />
<TextView
android:id="@+id/tvBalance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:fontFamily="@font/maax"
android:textColor="@color/black"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
tools:text="4.51735 BTC" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/maax"
android:text="@string/general_send_to_wallet"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<EditText
android:id="@+id/etWallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:fontFamily="@font/maax"
android:hint="@string/confirm_transaction_hint_target_address"
android:inputType="textMultiLine"
android:maxLines="2"
android:padding="12dp"
android:singleLine="false"
android:textColor="@color/black"
android:textSize="@dimen/text_size_1_small"
android:textStyle="bold" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="@font/maax"
android:text="@string/general_amount"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvIncFee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginBottom="1dp"
android:fontFamily="@font/maax"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_small"
app:layout_constraintBottom_toBottomOf="@+id/textView3"
app:layout_constraintLeft_toRightOf="@+id/textView3"
tools:text="(including fee)" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/etAmount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:digits="0123456789.,"
android:fontFamily="@font/maax"
android:hint="@string/prepare_transaction_hint_enter_amount"
android:imeOptions="actionDone"
android:inputType="numberDecimal"
android:padding="12dp"
android:textSize="@dimen/text_size_1_large"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tvCurrency"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="150dp"
android:enabled="false"
android:textColor="@color/black"
tools:text="1.34343434343443434343434"/>
<TextView
android:id="@+id/tvCurrency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:fontFamily="@font/maax"
android:text="@string/general_btc"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_1_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/etAmount"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/llFee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/llTransaction">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="4dp"
android:fontFamily="@font/maax"
android:text="@string/confirm_transaction_fee"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<info.hoang8f.android.segmented.SegmentedGroup
android:id="@+id/rgFee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:checkedButton="@+id/rbNormalFee"
android:gravity="center"
android:orientation="horizontal"
app:sc_border_width="2dp"
app:sc_corner_radius="10dp"
app:sc_tint_color="@color/colorPrimary">
<RadioButton
android:id="@+id/rbMinimalFee"
style="@style/RadioButton"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_fee_minimal"
android:textSize="@dimen/text_size_medium" />
<RadioButton
android:id="@+id/rbNormalFee"
style="@style/RadioButton"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_fee_normal"
android:textSize="@dimen/text_size_medium" />
<RadioButton
android:id="@+id/rbMaximumFee"
style="@style/RadioButton"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_fee_priority"
android:textSize="@dimen/text_size_medium" />
</info.hoang8f.android.segmented.SegmentedGroup>
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/etFee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:fontFamily="@font/maax"
android:hint="@string/confirm_transaction_hint_fee_amount"
android:inputType="numberDecimal"
android:padding="12dp"
android:textColor="@color/black"
android:textColorLink="@android:color/holo_blue_dark"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="100dp" />
<TextView
android:id="@+id/tvCurrency2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:fontFamily="@font/maax"
android:text="@string/general_btc"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/etFee"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvFeeEquivalent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:textColor="@android:color/darker_gray"
android:textSize="@dimen/text_size_1_small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/tvCurrency2"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<Button
android:id="@+id/btnSend"
style="@style/AppTheme.RoundedCornerMaterialButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="16dp"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_send"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<ProgressBar
android:id="@+id/progressBar"
style="@android:style/Widget.Holo.Light.ProgressBar"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_marginTop="4dp"
android:elevation="1dp"
android:indeterminate="true"
android:indeterminateTint="@color/colorPrimary"
android:visibility="invisible"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@+id/llFee" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,266 +0,0 @@
<?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="match_parent"
tools:context="com.tangem.ui.PrepareTransactionFragment"
tools:ignore="contentDescription">
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="@string/general_send_transaction"
android:textAlignment="center"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_large"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/llFrom"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/btn_light"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingTop="24dp"
android:paddingEnd="8dp"
android:paddingBottom="24dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:layout_marginTop="8dp"
android:fontFamily="@font/maax"
android:text="@string/general_from_card"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium" />
<TextView
android:id="@+id/tvCardID"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:fontFamily="@font/maax"
android:textColor="@color/black"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
tools:text="BB00 0000 1210 0233" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:layout_marginTop="12dp"
android:fontFamily="@font/maax"
android:text="@string/general_balance"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tvBalance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="2dp"
android:fontFamily="@font/maax"
android:textColor="@color/black"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
tools:text="4.51735 Btc" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/llTo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/card_state_loaded_with_zero"
android:orientation="vertical"
android:paddingTop="32dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/llFrom">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:fontFamily="@font/maax"
android:text="@string/general_send_to_wallet"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/ivCamera"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="12dp"
android:src="@drawable/qr_scan"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
<EditText
android:id="@+id/etWallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:fontFamily="@font/maax"
android:hint="@string/prepare_transaction_hint_enter_address"
android:imeOptions="actionNext"
android:inputType="text|textMultiLine|textNoSuggestions"
android:padding="12dp"
android:singleLine="false"
android:textColor="@color/colorPrimaryDark"
android:textColorLink="@android:color/holo_blue_dark"
android:textSize="@dimen/text_size_medium"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/ivCamera"
tools:layout_editor_absoluteY="2dp" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="8dp"
android:fontFamily="@font/maax"
android:text="@string/general_amount"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp">
<EditText
android:id="@+id/etAmount"
tools:text="1.34343434343443434343434"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:digits="0123456789.,"
android:fontFamily="@font/maax"
android:hint="@string/prepare_transaction_hint_enter_amount"
android:imeOptions="actionDone"
android:inputType="numberDecimal"
android:padding="12dp"
android:textColor="@color/colorPrimaryDark"
android:textSize="@dimen/text_size_1_large"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tvCurrency"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="150dp" />
<TextView
android:id="@+id/tvCurrency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:fontFamily="@font/maax"
android:text="@string/general_btc"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_1_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/etAmount"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<info.hoang8f.android.segmented.SegmentedGroup
android:id="@+id/rgIncFee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:checkedButton="@+id/rbFeeIn"
android:gravity="center"
android:orientation="horizontal"
app:sc_border_width="2dp"
app:sc_corner_radius="10dp"
app:sc_tint_color="@color/colorPrimary"
tools:layout_editor_absoluteX="10dp">
<RadioButton
android:id="@+id/rbFeeIn"
style="@style/RadioButton"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_including_fee"
android:textSize="@dimen/text_size_medium" />
<RadioButton
android:id="@+id/rbFeeOut"
style="@style/RadioButton"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_not_including_fee"
android:textSize="@dimen/text_size_medium" />
</info.hoang8f.android.segmented.SegmentedGroup>
</LinearLayout>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnVerify"
style="@style/AppTheme.RoundedCornerMaterialButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="16dp"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/prepare_transaction_btn_verify"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,61 +0,0 @@
<?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="match_parent">
<include
layout="@layout/layout_touch_card"
android:layout_width="400dp"
android:layout_height="250dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.75">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="@string/now_touch_the_card_with_id"
android:textAlignment="center"
android:textSize="@dimen/text_size_medium" />
<TextView
android:id="@+id/tvCardID"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:fontFamily="@font/maax"
android:textAlignment="center"
android:textColor="@color/black"
android:textSize="@dimen/text_size_large"
tools:text="CB02 0000 0002 5000" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="@string/to_sign_the_transaction"
android:textAlignment="center"
android:textSize="@dimen/text_size_medium" />
</LinearLayout>
<include layout="@layout/layout_progress_horizontal" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tangem_app_name" translatable="false">Tangem</string>
</resources>