Updated on 2026-08-14
This commit is contained in:
commit
448fcc09b7
138 changed files with 3030 additions and 3981 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
# Built application files
|
||||
/build
|
||||
/buildSrc
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
|
|
|||
5
.idea/codeStyles/Project.xml
generated
5
.idea/codeStyles/Project.xml
generated
|
|
@ -13,9 +13,6 @@
|
|||
<package name="io.ktor" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="5" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="3" />
|
||||
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
||||
<option name="BLANK_LINES_BEFORE_DECLARATION_WITH_COMMENT_OR_ANNOTATION_ON_SEPARATE_LINE" value="0" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
|
|
@ -205,4 +202,4 @@
|
|||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
</component>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
{
|
||||
"isWalletPayIdEnabled": true,
|
||||
"isSendingToPayIdEnabled": true,
|
||||
"isTopUpEnabled": true,
|
||||
"isCreatingTwinCardsAllowed": true
|
||||
}
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
{
|
||||
"isWalletPayIdEnabled": false,
|
||||
"isSendingToPayIdEnabled": true,
|
||||
"isTopUpEnabled": true,
|
||||
"isCreatingTwinCardsAllowed": true
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -118,7 +118,9 @@ internal class BiometricUserWalletsListManager(
|
|||
}
|
||||
.flatMap { updatedUserWallet ->
|
||||
saveInternal(updatedUserWallet, changeSelectedUserWallet = false)
|
||||
.map { updatedUserWallet }
|
||||
}
|
||||
.flatMap {
|
||||
get(userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)) }
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
5
app/src/main/res/values-v24/bool.xml
Normal file
5
app/src/main/res/values-v24/bool.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<resources>
|
||||
|
||||
<bool name="largeHeap">false</bool>
|
||||
|
||||
</resources>
|
||||
5
app/src/main/res/values/bool.xml
Normal file
5
app/src/main/res/values/bool.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<resources>
|
||||
|
||||
<bool name="largeHeap">true</bool>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
package com.tangem;
|
||||
|
||||
public class Test2 {
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="tangem_app_name" translatable="false">Tangem</string>
|
||||
|
||||
</resources>
|
||||
|
|
@ -51,4 +51,7 @@ interface TangemTechApi {
|
|||
@Header("card_id") cardId: String,
|
||||
@Body startReferralBody: StartReferralBody,
|
||||
): ReferralResponse
|
||||
|
||||
@GET("shops")
|
||||
suspend fun getShopInfo(@Query(value = "name") name: String): ShopResponse
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
|
||||
/**
|
||||
* Shop response
|
||||
*
|
||||
* @property isOrderingAvailable ordering availability
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class ShopResponse(
|
||||
@Json(name = "canOrder") val isOrderingAvailable: Boolean,
|
||||
)
|
||||
|
|
@ -17,7 +17,6 @@ interface ConfigManager {
|
|||
fun resetToDefault(name: String)
|
||||
|
||||
companion object {
|
||||
const val IS_SENDING_TO_PAY_ID_ENABLED = "isSendingToPayIdEnabled"
|
||||
const val IS_CREATING_TWIN_CARDS_ALLOWED = "isCreatingTwinCardsAllowed"
|
||||
const val IS_TOP_UP_ENABLED = "isTopUpEnabled"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
package com.tangem.datasource.config
|
||||
|
||||
import com.tangem.blockchain.common.BlockchainSdkConfig
|
||||
import com.tangem.blockchain.common.BlockchairCredentials
|
||||
import com.tangem.blockchain.common.GetBlockCredentials
|
||||
import com.tangem.blockchain.common.NowNodeCredentials
|
||||
import com.tangem.blockchain.common.QuickNodeCredentials
|
||||
import com.tangem.blockchain.common.TonCenterCredentials
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.datasource.config.ConfigManager.Companion.IS_CREATING_TWIN_CARDS_ALLOWED
|
||||
import com.tangem.datasource.config.ConfigManager.Companion.IS_SENDING_TO_PAY_ID_ENABLED
|
||||
import com.tangem.datasource.config.ConfigManager.Companion.IS_TOP_UP_ENABLED
|
||||
import com.tangem.datasource.config.models.Config
|
||||
import com.tangem.datasource.config.models.ConfigModel
|
||||
|
|
@ -34,7 +28,6 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
|
|||
|
||||
override fun turnOff(name: String) {
|
||||
when (name) {
|
||||
IS_SENDING_TO_PAY_ID_ENABLED -> config = config.copy(isSendingToPayIdEnabled = false)
|
||||
IS_TOP_UP_ENABLED -> config = config.copy(isTopUpEnabled = false)
|
||||
IS_CREATING_TWIN_CARDS_ALLOWED -> config = config.copy(isCreatingTwinCardsAllowed = false)
|
||||
}
|
||||
|
|
@ -42,13 +35,13 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
|
|||
|
||||
override fun resetToDefault(name: String) {
|
||||
when (name) {
|
||||
IS_SENDING_TO_PAY_ID_ENABLED ->
|
||||
config =
|
||||
config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled)
|
||||
IS_TOP_UP_ENABLED -> config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled)
|
||||
IS_CREATING_TWIN_CARDS_ALLOWED ->
|
||||
config =
|
||||
config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
|
||||
IS_TOP_UP_ENABLED -> {
|
||||
config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled)
|
||||
}
|
||||
IS_CREATING_TWIN_CARDS_ALLOWED -> {
|
||||
config = config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -57,12 +50,11 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
|
|||
|
||||
config = config.copy(
|
||||
isTopUpEnabled = model.isTopUpEnabled,
|
||||
isSendingToPayIdEnabled = model.isSendingToPayIdEnabled,
|
||||
isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed,
|
||||
)
|
||||
|
||||
defaultConfig = defaultConfig.copy(
|
||||
isTopUpEnabled = model.isTopUpEnabled,
|
||||
isSendingToPayIdEnabled = model.isSendingToPayIdEnabled,
|
||||
isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ data class Config(
|
|||
val appsFlyerDevKey: String = "",
|
||||
val amplitudeApiKey: String = "",
|
||||
val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(),
|
||||
val isSendingToPayIdEnabled: Boolean = true,
|
||||
val isTopUpEnabled: Boolean = false,
|
||||
@Deprecated("Not relevant since version 3.23")
|
||||
val isCreatingTwinCardsAllowed: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import com.squareup.moshi.Json
|
|||
|
||||
class FeatureModel(
|
||||
val isTopUpEnabled: Boolean,
|
||||
val isSendingToPayIdEnabled: Boolean,
|
||||
val isCreatingTwinCardsAllowed: Boolean,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
},
|
||||
{
|
||||
"name": "NEW_CARD_SCANNING_ENABLED",
|
||||
"version": "4.7.0"
|
||||
"version": "4.8.0"
|
||||
},
|
||||
{
|
||||
"name": "REDESIGNED_WALLET_SCREEN_ENABLED",
|
||||
|
|
|
|||
|
|
@ -34,11 +34,7 @@
|
|||
<string name="main_processing_full_amount">Der Betrag enthält nicht einige Ihrer Mittel</string>
|
||||
<string name="send_amount_label">Betrag</string>
|
||||
<string name="send_destination_hint_address">Adresse</string>
|
||||
<string name="send_destination_hint_address_payid">Adresse oder PayString</string>
|
||||
<string name="send_error_address_same_as_wallet">Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein</string>
|
||||
<string name="send_error_payid_not_registered">PayString ist nicht registriert</string>
|
||||
<string name="send_error_payid_request_failed">PayString-Anfrage ist fehlgeschlagen</string>
|
||||
<string name="send_error_payid_unsupported_by_blockchain">PayString wird von der Blockchain nicht unterstützt</string>
|
||||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">inkl. Gebühr</string>
|
||||
|
|
@ -57,7 +53,6 @@
|
|||
<string name="send_validation_invalid_address">Ungültige Adresse</string>
|
||||
<string name="shop_one_wallet">Tangem Wallet</string>
|
||||
<string name="twins_recreate_toolbar">Tangem Twin</string>
|
||||
<string name="wallet_address_button_create_payid">PayString erstellen</string>
|
||||
<string name="wallet_balance_loading">Die Bilanz wird aufgeladen…</string>
|
||||
<string name="wallet_balance_tx_in_progress">Die Transaktion läuft…</string>
|
||||
<string name="wallet_balance_verified">Verifizierte Bilanz</string>
|
||||
|
|
|
|||
|
|
@ -34,11 +34,7 @@
|
|||
<string name="main_processing_full_amount">Le montant n\'inclut pas certains de vos fonds</string>
|
||||
<string name="send_amount_label">Somme</string>
|
||||
<string name="send_destination_hint_address">Adresse</string>
|
||||
<string name="send_destination_hint_address_payid">Adresse ou PayString</string>
|
||||
<string name="send_error_address_same_as_wallet">L\'adresse est la même que celle de votre portefeuille</string>
|
||||
<string name="send_error_payid_not_registered">PayString non enregistré</string>
|
||||
<string name="send_error_payid_request_failed">La demande de PayString a échoué</string>
|
||||
<string name="send_error_payid_unsupported_by_blockchain">PayString non pris en charge par la blockchain</string>
|
||||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">Inclure les commissions</string>
|
||||
|
|
@ -57,7 +53,6 @@
|
|||
<string name="send_validation_invalid_address">Adresse incorrecte</string>
|
||||
<string name="shop_one_wallet">Tangem Wallet</string>
|
||||
<string name="twins_recreate_toolbar">Tangem Twin</string>
|
||||
<string name="wallet_address_button_create_payid">Créer PayString</string>
|
||||
<string name="wallet_balance_loading">Solde est en cours de téléchargement…</string>
|
||||
<string name="wallet_balance_tx_in_progress">Transaction en cours…</string>
|
||||
<string name="wallet_balance_verified">Solde confirmé</string>
|
||||
|
|
|
|||
|
|
@ -34,11 +34,7 @@
|
|||
<string name="main_processing_full_amount">L\'importo non include alcuni dei tuoi fondi</string>
|
||||
<string name="send_amount_label">Importo</string>
|
||||
<string name="send_destination_hint_address">Indirizzo</string>
|
||||
<string name="send_destination_hint_address_payid">Indirizzo o PayString</string>
|
||||
<string name="send_error_address_same_as_wallet">L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio</string>
|
||||
<string name="send_error_payid_not_registered">PayString non registrato</string>
|
||||
<string name="send_error_payid_request_failed">Richiesta PayString fallita</string>
|
||||
<string name="send_error_payid_unsupported_by_blockchain">PayString non supportato dalla blockchain</string>
|
||||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">Includi commissione</string>
|
||||
|
|
@ -57,7 +53,6 @@
|
|||
<string name="send_validation_invalid_address">Indirizzo non valido</string>
|
||||
<string name="shop_one_wallet">Tangem Wallet</string>
|
||||
<string name="twins_recreate_toolbar">Tangem Twin</string>
|
||||
<string name="wallet_address_button_create_payid">Crea PayString</string>
|
||||
<string name="wallet_balance_loading">Il saldo sta per essere caricato…</string>
|
||||
<string name="wallet_balance_tx_in_progress">Transazione in corso…</string>
|
||||
<string name="wallet_balance_verified">Saldo verificato</string>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@
|
|||
<string name="chat_button_title">Чат</string>
|
||||
<string name="common_accept">Принять</string>
|
||||
<string name="common_add">Добавить</string>
|
||||
<string name="common_apply">Применить</string>
|
||||
<string name="common_approval">Одобрение</string>
|
||||
<string name="common_attention">Внимание</string>
|
||||
<string name="common_balance">Баланс: %s</string>
|
||||
<string name="common_biometric_authentication">биометрическую аутентификацию</string>
|
||||
|
|
@ -60,6 +62,7 @@
|
|||
<string name="common_done">Готово</string>
|
||||
<string name="common_enable">Включить</string>
|
||||
<string name="common_enabled">Включено</string>
|
||||
<string name="common_explorer">Обозреватель</string>
|
||||
<string name="common_no">Нет</string>
|
||||
<string name="common_ok">Ок</string>
|
||||
<string name="common_origin_card">Основная карта</string>
|
||||
|
|
@ -73,7 +76,10 @@
|
|||
<string name="common_start">Начать</string>
|
||||
<string name="common_submit">Отправить</string>
|
||||
<string name="common_success">Успешно</string>
|
||||
<string name="common_swap">Обмен</string>
|
||||
<string name="common_terms_and_conditions">условия участия</string>
|
||||
<string name="common_transactions">Транзакции</string>
|
||||
<string name="common_transfer">Перевод</string>
|
||||
<string name="common_understand">Я понял</string>
|
||||
<string name="common_yes">Да</string>
|
||||
<string name="contract_address_copied_message">Адрес контракта скопирован!</string>
|
||||
|
|
@ -255,6 +261,9 @@
|
|||
<string name="onboarding_wallet_info_title_fourth">Восстановление кода доступа</string>
|
||||
<string name="onboarding_wallet_info_title_second">Идентичные карты</string>
|
||||
<string name="onboarding_wallet_info_title_third">Код доступа</string>
|
||||
<string name="organize_tokens_group">Группировка</string>
|
||||
<string name="organize_tokens_sort_by_balance">По балансу</string>
|
||||
<string name="organize_tokens_title">Сортировка токенов</string>
|
||||
<string name="referral_button_participate">Участвовать</string>
|
||||
<string name="referral_error_failed_to_load_info">Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже.</string>
|
||||
<string name="referral_error_failed_to_load_info_with_reason">Не удалось загрузить информацию по реферальной программе. Код ошибки: %s. Пожалуйста, попробуйте позже.</string>
|
||||
|
|
@ -308,11 +317,7 @@
|
|||
<string name="search_tokens_title">Поиск валют</string>
|
||||
<string name="send_amount_label">Сумма</string>
|
||||
<string name="send_destination_hint_address">Адрес</string>
|
||||
<string name="send_destination_hint_address_payid">Адрес или PayString</string>
|
||||
<string name="send_error_address_same_as_wallet">Адрес совпадает с адресом кошелька</string>
|
||||
<string name="send_error_payid_not_registered">PayString не зарегистрирован</string>
|
||||
<string name="send_error_payid_request_failed">Не удалось выполнить запрос PayString</string>
|
||||
<string name="send_error_payid_unsupported_by_blockchain">PayString не поддерживается блокчейном</string>
|
||||
<string name="send_extras_error_invalid_destination_tag">Недопустимый Tag. Он не будет добавлен в транзакцию.</string>
|
||||
<string name="send_extras_error_invalid_memo">Недопустимый Memo. Он не будет добавлен в транзакцию.</string>
|
||||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
|
|
@ -336,7 +341,7 @@
|
|||
<string name="shop_i_have_a_promo_code">У меня есть промо-код…</string>
|
||||
<string name="shop_one_wallet">Tangem Wallet</string>
|
||||
<string name="shop_other_payment_methods">Другие способы оплаты</string>
|
||||
<string name="shop_sold_out_description_prefix">Из-за большого количества заказов, которые мы получаем, cроки доставки могут быть увеличены.</string>
|
||||
<string name="shop_sold_out_description">Из-за высокого количества заказов, которые мы получаем, доставка может быть задержана на срок до 5 недель в зависимости от вашего местоположения</string>
|
||||
<string name="shop_total">Итого</string>
|
||||
<string name="solana_rent_warning">Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату.</string>
|
||||
<string name="story_awe_description">Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте.</string>
|
||||
|
|
@ -382,7 +387,6 @@
|
|||
<string name="swapping_permission_unlimited">Безлимитно</string>
|
||||
<string name="swapping_success_view_explorer_button_title">Открыть в обозревателе</string>
|
||||
<string name="swapping_success_view_title">В процессе</string>
|
||||
<string name="swapping_swap">Обмен</string>
|
||||
<string name="swapping_swap_action">Обменять</string>
|
||||
<string name="swapping_swap_of_to">Обмен %s на</string>
|
||||
<string name="swapping_tangem_fee_disclaimer">Котировки включают дополнительную комиссию Tangem в размере %s. Это помогает нам предоставлять первоклассный продукт.</string>
|
||||
|
|
@ -411,9 +415,11 @@
|
|||
<item quantity="many">%d токенов</item>
|
||||
<item quantity="other">%d токенов</item>
|
||||
</plurals>
|
||||
<string name="transaction_history_contract_address">контракт: %s</string>
|
||||
<string name="transaction_history_empty_transactions">У вас еще нет транзакций</string>
|
||||
<string name="transaction_history_error_failed_to_load">Не удалось загрузить транзакции</string>
|
||||
<string name="transaction_history_title">Транзакции</string>
|
||||
<string name="transaction_history_error_failed_to_load">Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию.</string>
|
||||
<string name="transaction_history_not_supported_description">История транзакций в настоящее время не поддерживается для этого блокчейна. Но не волнуйтесь, мы работаем над этим! А пока вы можете проверить ее в обозревателе.</string>
|
||||
<string name="transaction_history_transaction_from_address">от: %s</string>
|
||||
<string name="transaction_history_tx_in_progress">В процессе…</string>
|
||||
<string name="twin_error_same_card">Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d</string>
|
||||
<string name="twins_onboarding_description_format">Это карта, которую вы держите в руках. У парной карты номер %s.\n\nОбе карты можно использовать для вывода средств из этого кошелька.</string>
|
||||
|
|
@ -435,7 +441,6 @@
|
|||
<string name="user_wallet_list_single_header">Одновалютные</string>
|
||||
<string name="user_wallet_list_title">Мои кошельки</string>
|
||||
<string name="user_wallet_list_unlock_all">Разблокировать все с %s</string>
|
||||
<string name="wallet_address_button_create_payid">Создать PayString</string>
|
||||
<string name="wallet_address_button_explore">История транзакций</string>
|
||||
<string name="wallet_balance_blockchain_unreachable">Сеть недоступна</string>
|
||||
<string name="wallet_balance_blockchain_unreachable_try_later">Блокчейн недоступен. Попробуй позже.</string>
|
||||
|
|
@ -457,6 +462,7 @@
|
|||
<string name="wallet_connect_create_tx_not_enough_funds">Невозможно отправить транзакцию. Недостаточно средств.</string>
|
||||
<string name="wallet_connect_error_failed_to_connect">Не удалось установить сессию WalletConnect. Пожалуйста, повторите попытку позже.</string>
|
||||
<string name="wallet_connect_error_timeout">Не удалось установить сессию WalletConnect за отведённое время. Пожалуйста, повторите попытку позже.</string>
|
||||
<string name="wallet_connect_error_unsupported_blockchains">Запрос на подключение через WalletConnect содержит неподдерживаемые блокчеины. Неподдерживаемые блокчеины:\n</string>
|
||||
<string name="wallet_connect_error_unsupported_dapp">Cоединение с этим Dapp сервисом не может быть установлено из-за его технической реализации.</string>
|
||||
<string name="wallet_connect_network_not_found_format">Сеть %s не найдена. Пожалуйста, добавьте её и попробуйте заново.</string>
|
||||
<string name="wallet_connect_paste_from_clipboard">Вставить из буфера обмена</string>
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
<string name="common_done">完成</string>
|
||||
<string name="common_enable">允許</string>
|
||||
<string name="common_enabled">啟用</string>
|
||||
<string name="common_exchange">交易</string>
|
||||
<string name="common_no">否</string>
|
||||
<string name="common_ok">OK</string>
|
||||
<string name="common_origin_card">主卡片</string>
|
||||
|
|
@ -69,7 +70,9 @@
|
|||
<string name="common_start">開始</string>
|
||||
<string name="common_submit">提交</string>
|
||||
<string name="common_success">成功</string>
|
||||
<string name="common_swap">交換</string>
|
||||
<string name="common_terms_and_conditions">條款和條件</string>
|
||||
<string name="common_transactions">交易</string>
|
||||
<string name="common_understand">我了解</string>
|
||||
<string name="common_yes">是</string>
|
||||
<string name="contract_address_copied_message">已複製代幣地址</string>
|
||||
|
|
@ -298,11 +301,7 @@
|
|||
<string name="search_tokens_title">搜尋代幣</string>
|
||||
<string name="send_amount_label">數量</string>
|
||||
<string name="send_destination_hint_address">地址</string>
|
||||
<string name="send_destination_hint_address_payid">地址或 PayString</string>
|
||||
<string name="send_error_address_same_as_wallet">地址與錢包地址相同</string>
|
||||
<string name="send_error_payid_not_registered">PayString 未註冊</string>
|
||||
<string name="send_error_payid_request_failed">PayString 請求失敗</string>
|
||||
<string name="send_error_payid_unsupported_by_blockchain">PayString 不被區塊鏈支持</string>
|
||||
<string name="send_extras_error_invalid_destination_tag">標籤無效。它不會被添加到交易中</string>
|
||||
<string name="send_extras_error_invalid_memo">Memo無效。 它不會被添加到交易中</string>
|
||||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
|
|
@ -367,7 +366,6 @@
|
|||
<string name="swapping_permission_subheader">要繼續,您需要允許 1inch 智能合約使用您的 %s</string>
|
||||
<string name="swapping_success_view_explorer_button_title">在瀏覽器中查看</string>
|
||||
<string name="swapping_success_view_title">進行中</string>
|
||||
<string name="swapping_swap">交換</string>
|
||||
<string name="swapping_swap_action">交易</string>
|
||||
<string name="swapping_swap_of_to">交易 %s 至</string>
|
||||
<string name="swapping_tangem_fee_disclaimer">此外,報價包括%s的 Tangem 費用。這有助於我們提供一流的產品</string>
|
||||
|
|
@ -392,7 +390,6 @@
|
|||
</plurals>
|
||||
<string name="transaction_history_empty_transactions">您還沒有任何交易</string>
|
||||
<string name="transaction_history_error_failed_to_load">無法加載交易</string>
|
||||
<string name="transaction_history_title">交易</string>
|
||||
<string name="transaction_history_tx_in_progress">進行中…</string>
|
||||
<string name="twin_error_same_card">您掃描了同一張卡片。要創建雙錢包,您需要掃描編號為 %d 的卡</string>
|
||||
<string name="twins_onboarding_description_format">這一個是你手裡拿著的,另一個是編號為 %s 的,這兩張卡都可以用來從這個錢包中提取資金</string>
|
||||
|
|
@ -414,7 +411,6 @@
|
|||
<string name="user_wallet_list_single_header">單一幣種</string>
|
||||
<string name="user_wallet_list_title">我的錢包</string>
|
||||
<string name="user_wallet_list_unlock_all">用 %s 解鎖全部</string>
|
||||
<string name="wallet_address_button_create_payid">創建支付字符串</string>
|
||||
<string name="wallet_address_button_explore">交易記錄</string>
|
||||
<string name="wallet_balance_blockchain_unreachable">網路無法使用</string>
|
||||
<string name="wallet_balance_blockchain_unreachable_try_later">區塊鍊無法使用。稍後再試</string>
|
||||
|
|
@ -436,6 +432,7 @@
|
|||
<string name="wallet_connect_create_tx_not_enough_funds">無法交易,無足夠資金</string>
|
||||
<string name="wallet_connect_error_failed_to_connect">未能建立 WalletConnect 連接。請稍後再試</string>
|
||||
<string name="wallet_connect_error_timeout">無法建立 WalletConnect 連接:超時錯誤。請稍後再試</string>
|
||||
<string name="wallet_connect_error_unsupported_blockchains">會話請求包含不支持 WalletConnect 連接的區塊鏈。不支持的區塊鏈:</string>
|
||||
<string name="wallet_connect_error_unsupported_dapp">由於技術問題,無法與此 Dapp 建立連接</string>
|
||||
<string name="wallet_connect_network_not_found_format">沒有 %s 網路,請先加入後再試一次</string>
|
||||
<string name="wallet_connect_paste_from_clipboard">從剪貼板貼上</string>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@
|
|||
<string name="chat_button_title">Support</string>
|
||||
<string name="common_accept">Accept</string>
|
||||
<string name="common_add">Add</string>
|
||||
<string name="common_apply">Apply</string>
|
||||
<string name="common_approval">Approval</string>
|
||||
<string name="common_attention">Attention</string>
|
||||
<string name="common_balance">Balance: %s</string>
|
||||
<string name="common_biometric_authentication">biometric authentication</string>
|
||||
|
|
@ -58,6 +60,7 @@
|
|||
<string name="common_done">Done</string>
|
||||
<string name="common_enable">Enable</string>
|
||||
<string name="common_enabled">Enabled</string>
|
||||
<string name="common_explorer">Explorer</string>
|
||||
<string name="common_no">No</string>
|
||||
<string name="common_ok">OK</string>
|
||||
<string name="common_origin_card">Primary Card</string>
|
||||
|
|
@ -71,7 +74,10 @@
|
|||
<string name="common_start">Start</string>
|
||||
<string name="common_submit">Submit</string>
|
||||
<string name="common_success">Success</string>
|
||||
<string name="common_swap">Swap</string>
|
||||
<string name="common_terms_and_conditions">terms and conditions</string>
|
||||
<string name="common_transactions">Transactions</string>
|
||||
<string name="common_transfer">Transfer</string>
|
||||
<string name="common_understand">I understand</string>
|
||||
<string name="common_yes">Yes</string>
|
||||
<string name="contract_address_copied_message">Contract address copied!</string>
|
||||
|
|
@ -253,6 +259,9 @@
|
|||
<string name="onboarding_wallet_info_title_fourth">Access code restore</string>
|
||||
<string name="onboarding_wallet_info_title_second">Identical cards</string>
|
||||
<string name="onboarding_wallet_info_title_third">Access code</string>
|
||||
<string name="organize_tokens_group">Group</string>
|
||||
<string name="organize_tokens_sort_by_balance">By balance</string>
|
||||
<string name="organize_tokens_title">Organize tokens</string>
|
||||
<string name="referral_button_participate">Participate</string>
|
||||
<string name="referral_error_failed_to_load_info">Failed to load the information about the referral program. Please try again later.</string>
|
||||
<string name="referral_error_failed_to_load_info_with_reason">Failed to load the information about the referral program. Error code: %s. Please try again later.</string>
|
||||
|
|
@ -304,11 +313,7 @@
|
|||
<string name="search_tokens_title">Search tokens</string>
|
||||
<string name="send_amount_label">Amount</string>
|
||||
<string name="send_destination_hint_address">Address</string>
|
||||
<string name="send_destination_hint_address_payid">Address or PayString</string>
|
||||
<string name="send_error_address_same_as_wallet">Address is the same as wallet address</string>
|
||||
<string name="send_error_payid_not_registered">PayString not registered</string>
|
||||
<string name="send_error_payid_request_failed">PayString request failed</string>
|
||||
<string name="send_error_payid_unsupported_by_blockchain">PayString unsupported by blockchain</string>
|
||||
<string name="send_extras_error_invalid_destination_tag">Invalid Tag. It won\'t be added to the transaction.</string>
|
||||
<string name="send_extras_error_invalid_memo">Invalid Memo. It won\'t be added to the transaction.</string>
|
||||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
|
|
@ -332,7 +337,7 @@
|
|||
<string name="shop_i_have_a_promo_code">I have a promo code…</string>
|
||||
<string name="shop_one_wallet">Tangem Wallet</string>
|
||||
<string name="shop_other_payment_methods">Other payment methods</string>
|
||||
<string name="shop_sold_out_description_prefix">Due to the high volume of orders we are receiving, Shipping and Local Delivery orders may be delayed.</string>
|
||||
<string name="shop_sold_out_description">Due to the high volume of orders we are receiving shipping may be delayed up to 5 weeks depending on your location</string>
|
||||
<string name="shop_total">Total</string>
|
||||
<string name="solana_rent_warning">Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free.</string>
|
||||
<string name="story_awe_description">Store your crypto assets secure while keeping private keys contained in your card</string>
|
||||
|
|
@ -378,7 +383,6 @@
|
|||
<string name="swapping_permission_unlimited">Unlimited</string>
|
||||
<string name="swapping_success_view_explorer_button_title">View in Explorer</string>
|
||||
<string name="swapping_success_view_title">In progress</string>
|
||||
<string name="swapping_swap">Swap</string>
|
||||
<string name="swapping_swap_action">Swap</string>
|
||||
<string name="swapping_swap_of_to">Swap of %s to</string>
|
||||
<string name="swapping_tangem_fee_disclaimer">Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product.</string>
|
||||
|
|
@ -403,9 +407,12 @@
|
|||
<item quantity="one">%d token</item>
|
||||
<item quantity="other">%d tokens</item>
|
||||
</plurals>
|
||||
<string name="transaction_history_contract_address">contract: %s</string>
|
||||
<string name="transaction_history_empty_transactions">You don\'t have any transactions yet</string>
|
||||
<string name="transaction_history_error_failed_to_load">Failed to load transactions</string>
|
||||
<string name="transaction_history_title">Transactions</string>
|
||||
<string name="transaction_history_error_failed_to_load">Failed to load transaction history.\nClick on reload button to update the information.</string>
|
||||
<string name="transaction_history_not_supported_description">Transaction history is currently not supported for this blockchain. But don\'t worry, we\'re working on it! In the meantime you can check it in the explorer.</string>
|
||||
<string name="transaction_history_transaction_from_address">from: %s</string>
|
||||
<string name="transaction_history_transaction_to_address">to: %s</string>
|
||||
<string name="transaction_history_tx_in_progress">In progress…</string>
|
||||
<string name="twin_error_same_card">You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d</string>
|
||||
<string name="twins_onboarding_description_format">This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet.</string>
|
||||
|
|
@ -427,7 +434,6 @@
|
|||
<string name="user_wallet_list_single_header">Single-currency</string>
|
||||
<string name="user_wallet_list_title">My Wallets</string>
|
||||
<string name="user_wallet_list_unlock_all">Unlock all with %s</string>
|
||||
<string name="wallet_address_button_create_payid">Create PayString</string>
|
||||
<string name="wallet_address_button_explore">Transaction history</string>
|
||||
<string name="wallet_balance_blockchain_unreachable">Network is unreachable</string>
|
||||
<string name="wallet_balance_blockchain_unreachable_try_later">Blockchain is unreachable. Try later</string>
|
||||
|
|
@ -449,6 +455,7 @@
|
|||
<string name="wallet_connect_create_tx_not_enough_funds">Can\'t send transaction. Not enough funds.</string>
|
||||
<string name="wallet_connect_error_failed_to_connect">Failed to establish WalletConnect session. Please, try again later.</string>
|
||||
<string name="wallet_connect_error_timeout">Failed to establish WalletConnect session: timeout error. Please, try again later.</string>
|
||||
<string name="wallet_connect_error_unsupported_blockchains">Session request contains unsupported blockchains for WalletConnect connection. Unsupported blockchains:\n</string>
|
||||
<string name="wallet_connect_error_unsupported_dapp">Connection with this Dapp cannot be established due to its technical implementation.</string>
|
||||
<string name="wallet_connect_network_not_found_format">%s network not found. Please, add it first and try again.</string>
|
||||
<string name="wallet_connect_paste_from_clipboard">Paste from clipboard</string>
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ dependencies {
|
|||
implementation(deps.androidx.fragment.ktx)
|
||||
|
||||
/** Compose */
|
||||
implementation(deps.compose.constraintLayout)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.material)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
|
||||
/** Other libraries */
|
||||
|
|
|
|||
|
|
@ -312,14 +312,8 @@ private fun TangemButton(
|
|||
textStyle: TextStyle = TangemTheme.typography.button,
|
||||
) {
|
||||
Button(
|
||||
modifier = modifier
|
||||
.width(IntrinsicSize.Min)
|
||||
.heightIn(min = size.toHeightDp()),
|
||||
onClick = {
|
||||
if (!showProgress) {
|
||||
onClick()
|
||||
}
|
||||
},
|
||||
modifier = modifier.heightIn(min = size.toHeightDp()),
|
||||
onClick = { if (!showProgress) onClick() },
|
||||
enabled = enabled,
|
||||
elevation = elevation,
|
||||
shape = size.toShape(),
|
||||
|
|
@ -588,59 +582,44 @@ private open class TangemButtonColors(
|
|||
|
||||
// region Preview
|
||||
@Composable
|
||||
private fun PrimaryButtonSample(modifier: Modifier = Modifier) {
|
||||
private fun PrimaryButtonSample() {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary),
|
||||
modifier = Modifier.background(TangemTheme.colors.background.primary),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Manage tokens",
|
||||
onClick = { /* no-op */ },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
showProgress = true,
|
||||
text = "Manage tokens",
|
||||
onClick = { /* no-op */ },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
PrimaryButton(modifier = Modifier.fillMaxWidth(), text = "Manage tokens", onClick = { })
|
||||
PrimaryButton(modifier = Modifier.fillMaxWidth(), showProgress = true, text = "Manage tokens", onClick = { })
|
||||
PrimaryButtonIconEnd(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Manage tokens",
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
onClick = { /* no-op */ },
|
||||
onClick = { },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
PrimaryButtonIconStart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Manage tokens",
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
onClick = { /* no-op */ },
|
||||
onClick = { },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Manage tokens",
|
||||
enabled = false,
|
||||
onClick = { /* no-op */ },
|
||||
onClick = { },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
PrimaryButtonIconEnd(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Manage tokens",
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
enabled = false,
|
||||
onClick = { /* no-op */ },
|
||||
onClick = { },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
PrimaryButtonIconStart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Manage tokens",
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
enabled = false,
|
||||
onClick = { /* no-op */ },
|
||||
onClick = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -662,59 +641,44 @@ private fun PrimaryButtonPreview_Dark() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun SecondaryButtonSample(modifier: Modifier = Modifier) {
|
||||
private fun SecondaryButtonSample() {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary),
|
||||
modifier = Modifier.background(TangemTheme.colors.background.primary),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
SecondaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Manage tokens",
|
||||
onClick = { /* no-op */ },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
SecondaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
showProgress = true,
|
||||
text = "Manage tokens",
|
||||
onClick = { /* no-op */ },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
SecondaryButton(modifier = Modifier.fillMaxWidth(), text = "Manage tokens", onClick = { })
|
||||
SecondaryButton(modifier = Modifier.fillMaxWidth(), showProgress = true, text = "Manage tokens", onClick = { })
|
||||
SecondaryButtonIconEnd(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Manage tokens",
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
onClick = { /* no-op */ },
|
||||
onClick = { },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
SecondaryButtonIconStart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Manage tokens",
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
onClick = { /* no-op */ },
|
||||
onClick = { },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
SecondaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Manage tokens",
|
||||
enabled = false,
|
||||
onClick = { /* no-op */ },
|
||||
onClick = { },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
SecondaryButtonIconEnd(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Manage tokens",
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
enabled = false,
|
||||
onClick = { /* no-op */ },
|
||||
onClick = { },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
SecondaryButtonIconStart(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Manage tokens",
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
enabled = false,
|
||||
onClick = { /* no-op */ },
|
||||
onClick = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -736,50 +700,23 @@ private fun SecondaryButtonPreview_Dark() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun TextButtonSample(modifier: Modifier = Modifier) {
|
||||
private fun TextButtonSample() {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary),
|
||||
modifier = Modifier.background(TangemTheme.colors.background.primary),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
TextButton(
|
||||
text = "Enabled",
|
||||
onClick = { /* no-op */ },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
TextButtonIconStart(
|
||||
text = "Enabled",
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
onClick = { /* no-op */ },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
TextButton(
|
||||
text = "Enabled",
|
||||
enabled = false,
|
||||
onClick = { /* no-op */ },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
TextButtonIconStart(
|
||||
text = "Enabled",
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
enabled = false,
|
||||
onClick = { /* no-op */ },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
WarningTextButton(
|
||||
text = "Delete",
|
||||
onClick = { /* no-op */ },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
SelectorButton(
|
||||
text = "USD",
|
||||
onClick = { /* no-op */ },
|
||||
)
|
||||
TextButton(text = "Enabled", onClick = { })
|
||||
TextButtonIconStart(text = "Enabled", iconResId = R.drawable.ic_plus_24, onClick = { })
|
||||
TextButton(text = "Enabled", enabled = false, onClick = { })
|
||||
TextButtonIconStart(text = "Enabled", iconResId = R.drawable.ic_plus_24, enabled = false, onClick = { })
|
||||
WarningTextButton(text = "Delete", onClick = { })
|
||||
SelectorButton(text = "USD", onClick = { })
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun TextButtonPreview_Light() {
|
||||
private fun TextButtonPreview_LightTheme() {
|
||||
TangemTheme {
|
||||
TextButtonSample()
|
||||
}
|
||||
|
|
@ -787,62 +724,30 @@ private fun TextButtonPreview_Light() {
|
|||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun TextButtonPreview_Dark() {
|
||||
private fun TextButtonPreview_DarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
TextButtonSample()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionButtonSample(modifier: Modifier = Modifier) {
|
||||
private fun ActionButtonSample() {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary),
|
||||
modifier = Modifier.background(TangemTheme.colors.background.primary),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
RoundedActionButton(
|
||||
text = "Send",
|
||||
iconResId = R.drawable.ic_arrow_up_24,
|
||||
onClick = { /* [REDACTED_TODO_COMMENT]*/ },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
ActionButton(
|
||||
text = "Send",
|
||||
iconResId = R.drawable.ic_arrow_up_24,
|
||||
onClick = { /* [REDACTED_TODO_COMMENT]*/ },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
BackgroundActionButton(
|
||||
text = "Send",
|
||||
iconResId = R.drawable.ic_arrow_up_24,
|
||||
onClick = { /* [REDACTED_TODO_COMMENT]*/ },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
RoundedActionButton(
|
||||
text = "Send",
|
||||
iconResId = R.drawable.ic_arrow_up_24,
|
||||
enabled = false,
|
||||
onClick = { /* [REDACTED_TODO_COMMENT]*/ },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
ActionButton(
|
||||
text = "Send",
|
||||
iconResId = R.drawable.ic_arrow_up_24,
|
||||
enabled = false,
|
||||
onClick = { /* [REDACTED_TODO_COMMENT]*/ },
|
||||
)
|
||||
Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8))
|
||||
BackgroundActionButton(
|
||||
text = "Send",
|
||||
iconResId = R.drawable.ic_arrow_up_24,
|
||||
enabled = false,
|
||||
onClick = { /* [REDACTED_TODO_COMMENT]*/ },
|
||||
)
|
||||
RoundedActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, onClick = { })
|
||||
ActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, onClick = { })
|
||||
BackgroundActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, onClick = { })
|
||||
RoundedActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, enabled = false, onClick = { })
|
||||
ActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, enabled = false, onClick = { })
|
||||
BackgroundActionButton(text = "Send", iconResId = R.drawable.ic_arrow_up_24, enabled = false, onClick = { })
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun ActionButtonPreview_Light() {
|
||||
private fun ActionButtonPreview_LightTheme() {
|
||||
TangemTheme {
|
||||
ActionButtonSample()
|
||||
}
|
||||
|
|
@ -850,7 +755,7 @@ private fun ActionButtonPreview_Light() {
|
|||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun ActionButtonPreview_Dark() {
|
||||
private fun ActionButtonPreview_DarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
ActionButtonSample()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,202 +0,0 @@
|
|||
package com.tangem.core.ui.components
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Closable notification with custom icon
|
||||
* Child of parent component
|
||||
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1045-807&t=6CVvYDJe0sB7wBKE-0">Figma component</a>
|
||||
*
|
||||
* Use to show banner with custom icon and possibility to close
|
||||
* i.e. Feedback notification
|
||||
*
|
||||
* @param title notification title
|
||||
* @param icon drawable res on icon
|
||||
* @param iconColor icon color
|
||||
* @param onClick callback on click
|
||||
* @param onCloseClick callback on close icon click
|
||||
*/
|
||||
@Composable
|
||||
fun ClosableNotification(
|
||||
title: String,
|
||||
@DrawableRes icon: Int,
|
||||
iconColor: Color,
|
||||
onClick: (() -> Unit),
|
||||
onCloseClick: (() -> Unit),
|
||||
) {
|
||||
NotificationCardTemplate(onClick) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size20)
|
||||
.align(Alignment.CenterStart),
|
||||
painter = painterResource(id = icon),
|
||||
tint = iconColor,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing28)
|
||||
.align(Alignment.CenterStart),
|
||||
text = title,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size20)
|
||||
.align(Alignment.CenterEnd)
|
||||
.clickable(onClick = onCloseClick),
|
||||
painter = painterResource(id = R.drawable.ic_close_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification component from Design system
|
||||
* There are few states for this component, but only one parent, see link below
|
||||
*
|
||||
* Use this for Notification with title, subtitle, clickable or not
|
||||
*
|
||||
* @param title notification title
|
||||
* @param subtitle notification subtitle
|
||||
* @param onClick click on notification, if its null then no chevron icon
|
||||
*
|
||||
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1045-807&t=6CVvYDJe0sB7wBKE-0">Figma component</a>
|
||||
*/
|
||||
@Composable
|
||||
fun WarningNotification(title: String, subtitle: String?, onClick: (() -> Unit)?) {
|
||||
NotificationCardTemplate(onClick) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size20)
|
||||
.align(Alignment.CenterStart),
|
||||
painter = painterResource(id = R.drawable.img_attention_20),
|
||||
contentDescription = null,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing28)
|
||||
.align(Alignment.CenterStart),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
if (!subtitle.isNullOrEmpty()) {
|
||||
SpacerH2()
|
||||
Text(
|
||||
text = subtitle,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (onClick != null) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size20)
|
||||
.align(Alignment.CenterEnd),
|
||||
painter = painterResource(id = R.drawable.ic_chevron_right_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun NotificationCardTemplate(onClick: (() -> Unit)? = null, content: @Composable BoxScope.() -> Unit) {
|
||||
Surface(
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius18),
|
||||
onClick = onClick ?: {},
|
||||
enabled = onClick != null,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing12,
|
||||
vertical = TangemTheme.dimens.spacing8,
|
||||
)
|
||||
.wrapContentSize(),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
|
||||
@Composable
|
||||
private fun WarningNotificationPreview() {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
WarningNotification(
|
||||
title = "Your wallet hasn’t been backed up",
|
||||
subtitle = "Lorem ipsum dolor sit amet, consectetur " +
|
||||
"adipiscing elit, sed do eiusmod tempor incididunt ut labore et...",
|
||||
onClick = {},
|
||||
)
|
||||
SpacerH32()
|
||||
WarningNotification(
|
||||
title = "Your wallet hasn’t been backed up",
|
||||
subtitle = null,
|
||||
onClick = {},
|
||||
)
|
||||
SpacerH32()
|
||||
WarningNotification(
|
||||
title = "Your wallet hasn’t been backed up",
|
||||
subtitle = "Lorem ipsum dolor sit amet, consectetur " +
|
||||
"adipiscing elit, sed do eiusmod tempor incididunt ut labore et...",
|
||||
onClick = null,
|
||||
)
|
||||
SpacerH32()
|
||||
WarningNotification(
|
||||
title = "Your wallet hasn’t been backed up",
|
||||
subtitle = null,
|
||||
onClick = null,
|
||||
)
|
||||
SpacerH32()
|
||||
ClosableNotification(
|
||||
title = "Like tangem app?",
|
||||
icon = R.drawable.ic_star_24,
|
||||
iconColor = TangemTheme.colors.icon.attention,
|
||||
onClick = {},
|
||||
onCloseClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_WarningNotification_InLightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
WarningNotificationPreview()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_WarningNotification_InDarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
WarningNotificationPreview()
|
||||
}
|
||||
}
|
||||
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
package com.tangem.core.ui.components.notifications
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH2
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Notification component from Design system.
|
||||
* Use this for Notification with title, subtitle, clickable or not.
|
||||
*
|
||||
* @param state component state
|
||||
* @param modifier modifier
|
||||
*
|
||||
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1045-807&t=6CVvYDJe0sB7wBKE-0"
|
||||
* >Figma component</a>
|
||||
*/
|
||||
@Composable
|
||||
fun Notification(state: NotificationState, modifier: Modifier = Modifier) {
|
||||
Surface(
|
||||
onClick = if (state is NotificationState.Action) {
|
||||
state.onClick
|
||||
} else {
|
||||
{}
|
||||
},
|
||||
modifier = modifier,
|
||||
enabled = when (state) {
|
||||
is NotificationState.Simple -> false
|
||||
is NotificationState.Action -> true
|
||||
},
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius18),
|
||||
color = TangemTheme.colors.button.secondary,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12, vertical = TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
NotificationIcon(
|
||||
iconResId = state.iconResId,
|
||||
iconTint = state.tint,
|
||||
modifier = Modifier
|
||||
.size(size = TangemTheme.dimens.size20)
|
||||
.align(alignment = Alignment.CenterStart),
|
||||
)
|
||||
|
||||
NotificationInfoBlock(
|
||||
title = state.title,
|
||||
subtitle = state.subtitle,
|
||||
modifier = Modifier.align(alignment = Alignment.CenterStart),
|
||||
)
|
||||
|
||||
if (state is NotificationState.Action) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(size = TangemTheme.dimens.size20)
|
||||
.align(alignment = Alignment.CenterEnd),
|
||||
painter = painterResource(id = R.drawable.ic_chevron_right_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationIcon(@DrawableRes iconResId: Int, iconTint: Color?, modifier: Modifier = Modifier) {
|
||||
if (iconTint != null) {
|
||||
Icon(
|
||||
painter = painterResource(id = iconResId),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
tint = iconTint,
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
painter = painterResource(id = iconResId),
|
||||
contentDescription = null,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationInfoBlock(title: String, subtitle: String?, modifier: Modifier = Modifier) {
|
||||
Column(modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing30)) {
|
||||
Text(
|
||||
text = title,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
|
||||
if (!subtitle.isNullOrEmpty()) {
|
||||
SpacerH2()
|
||||
Text(
|
||||
text = subtitle,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_WarningNotification_Light(
|
||||
@PreviewParameter(NotificationStateProvider::class)
|
||||
state: NotificationState,
|
||||
) {
|
||||
TangemTheme(isDark = false) {
|
||||
Notification(state)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_WarningNotification_Dark(
|
||||
@PreviewParameter(NotificationStateProvider::class)
|
||||
state: NotificationState,
|
||||
) {
|
||||
TangemTheme(isDark = true) {
|
||||
Notification(state)
|
||||
}
|
||||
}
|
||||
|
||||
private class NotificationStateProvider : CollectionPreviewParameterProvider<NotificationState>(
|
||||
collection = listOf(
|
||||
NotificationState.Simple(
|
||||
title = "Your wallet hasn’t been backed up",
|
||||
subtitle = "Lorem ipsum dolor sit amet, consectetur " +
|
||||
"adipiscing elit, sed do eiusmod tempor incididunt ut labore et...",
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
),
|
||||
NotificationState.Simple(
|
||||
title = "Your wallet hasn’t been backed up",
|
||||
subtitle = null,
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
tint = TangemColorPalette.Amaranth,
|
||||
),
|
||||
NotificationState.Action(
|
||||
title = "Your wallet hasn’t been backed up",
|
||||
subtitle = "Lorem ipsum dolor sit amet, consectetur " +
|
||||
"adipiscing elit, sed do eiusmod tempor incididunt ut labore et...",
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
onClick = {},
|
||||
),
|
||||
NotificationState.Action(
|
||||
title = "Your wallet hasn’t been backed up",
|
||||
subtitle = null,
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
tint = TangemColorPalette.Amaranth,
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.core.ui.components.notifications
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Notification component state
|
||||
*
|
||||
* @property title title
|
||||
* @property subtitle subtitle
|
||||
* @property iconResId icon resource id
|
||||
* @property tint icon tint
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class NotificationState(
|
||||
open val title: String,
|
||||
open val subtitle: String? = null,
|
||||
@DrawableRes open val iconResId: Int,
|
||||
open val tint: Color? = null,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Simple notification state. Non clickable.
|
||||
*
|
||||
* @property title title
|
||||
* @property subtitle subtitle
|
||||
* @property iconResId icon resource id
|
||||
* @property tint icon tint
|
||||
*/
|
||||
data class Simple(
|
||||
override val title: String,
|
||||
override val subtitle: String? = null,
|
||||
@DrawableRes override val iconResId: Int,
|
||||
override val tint: Color? = null,
|
||||
) : NotificationState(title, subtitle, iconResId, tint)
|
||||
|
||||
/**
|
||||
* Clickable notification state
|
||||
*
|
||||
* @property title title
|
||||
* @property subtitle subtitle
|
||||
* @property iconResId icon resource id
|
||||
* @property tint icon tint
|
||||
* @param onClick lambda be invoked when notification component is clicked
|
||||
*/
|
||||
data class Action(
|
||||
override val title: String,
|
||||
override val subtitle: String? = null,
|
||||
@DrawableRes override val iconResId: Int,
|
||||
override val tint: Color? = null,
|
||||
val onClick: () -> Unit,
|
||||
) : NotificationState(title, subtitle, iconResId, tint)
|
||||
}
|
||||
|
|
@ -0,0 +1,355 @@
|
|||
package com.tangem.core.ui.components.transactions
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Transaction component
|
||||
*
|
||||
* @param state state
|
||||
* @param modifier modifier
|
||||
*
|
||||
* @see <a href = "https://www.figma.com/file/RU7AIgwHtGdMfy83T5UOoR/iOS?type=design&node-id=446-438&t=71jPDxMMk4e0
|
||||
* a025-4">Figma Component</a>
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun Transaction(state: TransactionState, modifier: Modifier = Modifier) {
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.defaultMinSize(minHeight = TangemTheme.dimens.size56)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12, vertical = TangemTheme.dimens.spacing10),
|
||||
color = TangemTheme.colors.background.primary,
|
||||
) {
|
||||
@Suppress("DestructuringDeclarationWithTooManyEntries")
|
||||
ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
|
||||
val (iconItem, titleItem, subtitleItem, amountItem, timestampItem) = createRefs()
|
||||
|
||||
Icon(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size40)
|
||||
.constrainAs(iconItem) {
|
||||
start.linkTo(parent.start)
|
||||
centerVerticallyTo(parent)
|
||||
},
|
||||
)
|
||||
|
||||
Title(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12)
|
||||
.constrainAs(titleItem) {
|
||||
start.linkTo(iconItem.end)
|
||||
top.linkTo(iconItem.top)
|
||||
},
|
||||
)
|
||||
|
||||
Subtitle(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing6)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12)
|
||||
.constrainAs(subtitleItem) {
|
||||
start.linkTo(iconItem.end)
|
||||
top.linkTo(amountItem.bottom)
|
||||
end.linkTo(timestampItem.start)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
)
|
||||
|
||||
Amount(
|
||||
state = state,
|
||||
modifier = Modifier.constrainAs(amountItem) {
|
||||
start.linkTo(titleItem.end)
|
||||
top.linkTo(titleItem.top)
|
||||
end.linkTo(parent.end)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
)
|
||||
|
||||
Timestamp(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing6)
|
||||
.constrainAs(timestampItem) {
|
||||
top.linkTo(amountItem.bottom)
|
||||
end.linkTo(parent.end)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Icon(state: TransactionState, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is TransactionState.Content -> {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens.size40)
|
||||
.background(
|
||||
color = when (state) {
|
||||
is TransactionState.ProcessedTransactionContent -> {
|
||||
TangemTheme.colors.icon.attention.copy(alpha = 0.1f)
|
||||
}
|
||||
is TransactionState.CompletedTransactionContent -> {
|
||||
TangemTheme.colors.background.secondary
|
||||
}
|
||||
},
|
||||
shape = CircleShape,
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
id = when (state) {
|
||||
is TransactionState.Sending,
|
||||
is TransactionState.Send,
|
||||
-> R.drawable.ic_arrow_up_24
|
||||
|
||||
is TransactionState.Receiving,
|
||||
is TransactionState.Receive,
|
||||
-> R.drawable.ic_arrow_down_24
|
||||
|
||||
is TransactionState.Approving,
|
||||
is TransactionState.Approved,
|
||||
-> R.drawable.ic_doc_24
|
||||
|
||||
is TransactionState.Swapping,
|
||||
is TransactionState.Swapped,
|
||||
-> R.drawable.ic_exchange_vertical_24
|
||||
},
|
||||
),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size20)
|
||||
.align(Alignment.Center),
|
||||
tint = when (state) {
|
||||
is TransactionState.ProcessedTransactionContent -> TangemTheme.colors.icon.attention
|
||||
is TransactionState.CompletedTransactionContent -> TangemTheme.colors.icon.informative
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
is TransactionState.Loading -> {
|
||||
CircleShimmer(modifier = modifier.size(TangemTheme.dimens.size40))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(state: TransactionState, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is TransactionState.ProcessedTransactionContent -> {
|
||||
Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6)) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = when (state) {
|
||||
is TransactionState.Sending -> R.string.common_transfer
|
||||
is TransactionState.Receiving -> R.string.common_transfer
|
||||
is TransactionState.Approving -> R.string.common_approval
|
||||
is TransactionState.Swapping -> R.string.common_swap
|
||||
},
|
||||
),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
|
||||
Image(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
painter = painterResource(id = R.drawable.img_loader_15),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
is TransactionState.CompletedTransactionContent -> {
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = when (state) {
|
||||
is TransactionState.Send -> R.string.common_transfer
|
||||
is TransactionState.Receive -> R.string.common_transfer
|
||||
is TransactionState.Approved -> R.string.common_approval
|
||||
is TransactionState.Swapped -> R.string.common_swap
|
||||
},
|
||||
),
|
||||
modifier = modifier,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
}
|
||||
is TransactionState.Loading -> {
|
||||
RectangleShimmer(
|
||||
modifier = modifier.size(width = TangemTheme.dimens.size70, height = TangemTheme.dimens.size12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Subtitle(state: TransactionState, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is TransactionState.Content -> {
|
||||
Text(
|
||||
text = when (state) {
|
||||
is TransactionState.Sending,
|
||||
is TransactionState.Send,
|
||||
-> stringResource(
|
||||
id = R.string.transaction_history_transaction_to_address,
|
||||
state.address,
|
||||
)
|
||||
is TransactionState.Receiving,
|
||||
is TransactionState.Receive,
|
||||
is TransactionState.Approving,
|
||||
is TransactionState.Approved,
|
||||
-> stringResource(
|
||||
id = R.string.transaction_history_transaction_from_address,
|
||||
state.address,
|
||||
)
|
||||
is TransactionState.Swapping,
|
||||
is TransactionState.Swapped,
|
||||
-> stringResource(
|
||||
id = R.string.transaction_history_contract_address,
|
||||
state.address,
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
textAlign = TextAlign.Start,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption,
|
||||
)
|
||||
}
|
||||
is TransactionState.Loading -> {
|
||||
RectangleShimmer(
|
||||
modifier = modifier.size(width = TangemTheme.dimens.size52, height = TangemTheme.dimens.size12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Amount(state: TransactionState, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is TransactionState.Content -> {
|
||||
Text(
|
||||
text = state.amount,
|
||||
modifier = modifier,
|
||||
textAlign = TextAlign.End,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
is TransactionState.Loading -> {
|
||||
RectangleShimmer(
|
||||
modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Timestamp(state: TransactionState, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is TransactionState.Content -> {
|
||||
Text(
|
||||
text = state.timestamp,
|
||||
modifier = modifier,
|
||||
textAlign = TextAlign.End,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption,
|
||||
)
|
||||
}
|
||||
is TransactionState.Loading -> {
|
||||
RectangleShimmer(
|
||||
modifier = modifier.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_TransactionItem_LightTheme(
|
||||
@PreviewParameter(TransactionItemStateProvider::class) state: TransactionState,
|
||||
) {
|
||||
TangemTheme(isDark = false) {
|
||||
Transaction(state)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_TransactionItem_DarkTheme(
|
||||
@PreviewParameter(TransactionItemStateProvider::class) state: TransactionState,
|
||||
) {
|
||||
TangemTheme(isDark = true) {
|
||||
Transaction(state)
|
||||
}
|
||||
}
|
||||
|
||||
private class TransactionItemStateProvider : CollectionPreviewParameterProvider<TransactionState>(
|
||||
collection = listOf(
|
||||
TransactionState.Sending(
|
||||
address = "33BddS...ga2B",
|
||||
amount = "-0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
),
|
||||
TransactionState.Receiving(
|
||||
address = "33BddS...ga2B",
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
),
|
||||
TransactionState.Approving(
|
||||
address = "33BddS...ga2B",
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
),
|
||||
TransactionState.Swapping(
|
||||
address = "33BddS...ga2B",
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
),
|
||||
TransactionState.Send(
|
||||
address = "33BddS...ga2B",
|
||||
amount = "-0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
),
|
||||
TransactionState.Receive(
|
||||
address = "33BddS...ga2B",
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
),
|
||||
TransactionState.Approved(
|
||||
address = "33BddS...ga2B",
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
),
|
||||
TransactionState.Swapped(
|
||||
address = "33BddS...ga2B",
|
||||
amount = "+0.500913 BTC",
|
||||
timestamp = "8:41",
|
||||
),
|
||||
TransactionState.Loading,
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package com.tangem.core.ui.components.transactions
|
||||
|
||||
/**
|
||||
* Transaction component state
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed interface TransactionState {
|
||||
|
||||
/**
|
||||
* Content state
|
||||
*
|
||||
* @property address address
|
||||
* @property amount amount
|
||||
* @property timestamp timestamp
|
||||
*/
|
||||
sealed class Content(
|
||||
open val address: String,
|
||||
open val amount: String,
|
||||
open val timestamp: String,
|
||||
) : TransactionState
|
||||
|
||||
/**
|
||||
* Content state for processed transaction
|
||||
*
|
||||
* @property address address
|
||||
* @property amount amount
|
||||
* @property timestamp timestamp
|
||||
*/
|
||||
sealed class ProcessedTransactionContent(
|
||||
override val address: String,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : Content(address, amount, timestamp)
|
||||
|
||||
/**
|
||||
* Content state for completed transaction
|
||||
*
|
||||
* @property address address
|
||||
* @property amount amount
|
||||
* @property timestamp timestamp
|
||||
*/
|
||||
sealed class CompletedTransactionContent(
|
||||
override val address: String,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : Content(address, amount, timestamp)
|
||||
|
||||
/**
|
||||
* Processed sending transaction state
|
||||
*
|
||||
* @property address address
|
||||
* @property amount amount
|
||||
* @property timestamp timestamp
|
||||
*/
|
||||
data class Sending(
|
||||
override val address: String,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : ProcessedTransactionContent(address, amount, timestamp)
|
||||
|
||||
/**
|
||||
* Processed receiving transaction state
|
||||
*
|
||||
* @property address address
|
||||
* @property amount amount
|
||||
* @property timestamp timestamp
|
||||
*/
|
||||
data class Receiving(
|
||||
override val address: String,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : ProcessedTransactionContent(address, amount, timestamp)
|
||||
|
||||
/**
|
||||
* Processed approving transaction state
|
||||
*
|
||||
* @property address address
|
||||
* @property amount amount
|
||||
* @property timestamp timestamp
|
||||
*/
|
||||
data class Approving(
|
||||
override val address: String,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : ProcessedTransactionContent(address, amount, timestamp)
|
||||
|
||||
/**
|
||||
* Processed swapping transaction state
|
||||
*
|
||||
* @property address address
|
||||
* @property amount amount
|
||||
* @property timestamp timestamp
|
||||
*/
|
||||
data class Swapping(
|
||||
override val address: String,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : ProcessedTransactionContent(address, amount, timestamp)
|
||||
|
||||
/**
|
||||
* Completed sending transaction state
|
||||
*
|
||||
* @property address address
|
||||
* @property amount amount
|
||||
* @property timestamp timestamp
|
||||
*/
|
||||
data class Send(
|
||||
override val address: String,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : CompletedTransactionContent(address, amount, timestamp)
|
||||
|
||||
/**
|
||||
* Completed receiving transaction state
|
||||
*
|
||||
* @property address address
|
||||
* @property amount amount
|
||||
* @property timestamp timestamp
|
||||
*/
|
||||
data class Receive(
|
||||
override val address: String,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : CompletedTransactionContent(address, amount, timestamp)
|
||||
|
||||
/**
|
||||
* Completed approving transaction state
|
||||
*
|
||||
* @property address address
|
||||
* @property amount amount
|
||||
* @property timestamp timestamp
|
||||
*/
|
||||
data class Approved(
|
||||
override val address: String,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : CompletedTransactionContent(address, amount, timestamp)
|
||||
|
||||
/**
|
||||
* Completed swapping transaction state
|
||||
*
|
||||
* @property address address
|
||||
* @property amount amount
|
||||
* @property timestamp timestamp
|
||||
*/
|
||||
data class Swapped(
|
||||
override val address: String,
|
||||
override val amount: String,
|
||||
override val timestamp: String,
|
||||
) : CompletedTransactionContent(address, amount, timestamp)
|
||||
|
||||
/** Loading state */
|
||||
object Loading : TransactionState
|
||||
}
|
||||
|
|
@ -2,22 +2,42 @@ package com.tangem.core.ui.extensions
|
|||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
||||
/**
|
||||
* Utility class for creating text as [String] or [StringRes].
|
||||
* It necessary to use [Immutable] annotation because all sealed interface has runtime stability.
|
||||
* All subclasses are stable.
|
||||
*/
|
||||
@Immutable
|
||||
sealed interface TextReference {
|
||||
class Res(@StringRes val id: Int, val formatArgs: List<Any> = emptyList()) : TextReference {
|
||||
constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList())
|
||||
}
|
||||
|
||||
class Str(val value: String) : TextReference
|
||||
/**
|
||||
* Text resource id
|
||||
*
|
||||
* @property id resource id
|
||||
* @property formatArgs arguments
|
||||
*
|
||||
* Impossible to use [kotlinx.collections.immutable.ImmutableList] because [Any] is unstable.
|
||||
*/
|
||||
data class Res(@StringRes val id: Int, val formatArgs: WrappedList<Any> = WrappedList(emptyList())) : TextReference
|
||||
|
||||
/**
|
||||
* Text string
|
||||
*
|
||||
* @property value value
|
||||
*/
|
||||
data class Str(val value: String) : TextReference
|
||||
}
|
||||
|
||||
/** Get text */
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun TextReference.resolveReference(): String {
|
||||
return when (this) {
|
||||
is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray())
|
||||
is TextReference.Str -> this.value
|
||||
is TextReference.Res -> stringResource(id, *formatArgs.toTypedArray())
|
||||
is TextReference.Str -> value
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.core.ui.extensions
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@JvmInline
|
||||
@Immutable
|
||||
value class WrappedList<T>(val data: List<T>) : List<T> by data
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue