Updated on 2026-08-14

This commit is contained in:
Tangem 2023-06-29 11:42:56 +03:00
commit 9abedff8b5
80 changed files with 1567 additions and 3658 deletions

View file

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

View file

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

View file

@ -31,12 +31,14 @@ sealed class Basic(
currency: AnalyticsParam.CardCurrency,
batch: String,
signInType: SignInType,
walletsCount: String,
) : Basic(
event = "Signed in",
params = mapOf(
AnalyticsParam.CURRENCY to currency.value,
AnalyticsParam.BATCH to batch,
"Sign in type" to signInType.name,
"Wallets Count" to walletsCount,
),
) {
enum class SignInType {

View file

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

View file

@ -6,7 +6,6 @@ import com.tangem.tap.common.analytics.topup.TopUpController
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
@ -20,7 +19,6 @@ data class GlobalState(
val onboardingState: OnboardingState = OnboardingState(),
val cardVerifiedOnline: Boolean = false,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val payIdManager: PayIdManager = PayIdManager(),
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val feedbackManager: FeedbackManager? = null,

View file

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

View file

@ -1,6 +1,9 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.Analytics
@ -134,22 +137,13 @@ class TapWalletManager(
fun updateConfigManager(data: ScanResponse) {
val configManager = store.state.globalState.configManager
val blockchain = data.cardTypesResolver.getBlockchain()
if (data.cardTypesResolver.isStart2Coin()) {
configManager?.turnOff(ConfigManager.IS_SENDING_TO_PAY_ID_ENABLED)
configManager?.turnOff(ConfigManager.IS_TOP_UP_ENABLED)
} else if (blockchain == Blockchain.Bitcoin ||
data.walletData?.blockchain == Blockchain.Bitcoin.id
) {
configManager?.resetToDefault(ConfigManager.IS_SENDING_TO_PAY_ID_ENABLED)
configManager?.resetToDefault(ConfigManager.IS_TOP_UP_ENABLED)
} else {
configManager?.resetToDefault(ConfigManager.IS_SENDING_TO_PAY_ID_ENABLED)
configManager?.resetToDefault(ConfigManager.IS_TOP_UP_ENABLED)
}
}
}
fun Wallet.getFirstToken(): Token? {
return getTokens().toList().getOrNull(0)
}
fun Wallet.getFirstToken(): Token? = getTokens().toList().getOrNull(index = 0)

View file

@ -26,6 +26,11 @@ interface UserWalletsListManager {
* */
val hasUserWallets: Boolean
/**
* Count of saved user wallets
*/
val walletsCount: Int
/**
* Set [UserWallet] with provided [UserWalletId] as selected
*

View file

@ -53,6 +53,9 @@ internal class BiometricUserWalletsListManager(
override val hasUserWallets: Boolean
get() = keysRepository.hasSavedEncryptionKeys()
override val walletsCount: Int
get() = state.value.userWallets.size
override suspend fun unlock(): CompletionResult<UserWallet> {
return unlockWithBiometryInternal()
.mapFailure { error ->

View file

@ -7,13 +7,7 @@ import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.userWalletList.UserWalletsListError
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.flow.*
@OptIn(ExperimentalCoroutinesApi::class)
internal class RuntimeUserWalletsListManager : UserWalletsListManager {
@ -36,6 +30,12 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager {
override val hasUserWallets: Boolean
get() = state.value.userWallet != null
/**
* only 1 wallet stored in runtime implementation
*/
override val walletsCount: Int
get() = 1
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
state.value.userWallet
?.takeIf { it.walletId == userWalletId }

View file

@ -6,7 +6,6 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
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
@ -34,15 +33,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 {
@ -76,27 +74,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()
}
@ -155,8 +141,6 @@ sealed class SendAction : SendScreenAction {
override val messageResource: Int = R.string.send_transaction_success
}
data class SendError(override val error: TapError) : SendAction(), ErrorAction
sealed class Dialog : SendAction(), StateDialog {
data class TezosWarningDialog(
val reduceCallback: () -> Unit,

View file

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

View file

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

View file

@ -55,18 +55,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,
),
@ -96,7 +95,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 feeAmount = sendState.feeState.currentFee ?: return
@ -391,12 +390,6 @@ fun createValidateTransactionError(
return TapError.ValidateTransactionErrors(tapErrors) { it.joinToString("\r\n") }
}
private fun setIfSendingToPayIdEnabled(appState: AppState?, dispatch: (Action) -> Unit) {
val isSendingToPayIdEnabled =
appState?.globalState?.configManager?.config?.isSendingToPayIdEnabled ?: false
dispatch(AddressPayIdActionUi.ChangePayIdState(isSendingToPayIdEnabled))
}
private fun updateWarnings(dispatch: (Action) -> Unit) {
val warningsManager = store.state.globalState.warningManager ?: return
val blockchain = store.state.sendState.walletManager?.wallet?.blockchain ?: return

View file

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

View file

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

View file

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

View file

@ -2,17 +2,16 @@ package com.tangem.tap.features.send.redux.states
import androidx.core.text.isDigitsOnly
import com.tangem.blockchain.blockchains.stellar.StellarMemo
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
import com.tangem.tap.features.send.redux.AddressVerifyAction
import java.math.BigInteger
data class AddressPayIdState(
data class AddressState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val normalFieldValue: String? = null,
val truncatedFieldValue: String? = null,
val destinationWalletAddress: String? = null,
val error: AddressPayIdVerifyAction.Error? = null,
val error: AddressVerifyAction.Error? = null,
val truncateHandler: ((String) -> String)? = null,
val sendingToPayIdEnabled: Boolean = false,
val pasteIsEnabled: Boolean = false,
val inputIsEnabled: Boolean = true,
) : SendScreenState {
@ -22,8 +21,6 @@ data class AddressPayIdState(
fun truncate(value: String): String = truncateHandler?.invoke(value) ?: value
fun isReady(): Boolean = error == null && destinationWalletAddress?.isNotEmpty() ?: false
fun isPayIdState(): Boolean = destinationWalletAddress != null && destinationWalletAddress != normalFieldValue
}
data class TransactionExtrasState(
@ -98,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(

View file

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

View file

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

View file

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

View file

@ -17,16 +17,8 @@ import com.tangem.tap.features.wallet.ui.analytics.WalletAnalyticsEventsMapper
import com.tangem.tap.store
import com.tangem.tap.walletStoresManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@ -71,6 +63,7 @@ internal class WalletViewModel @Inject constructor(
currency = currency,
batch = scanResponse.card.batchId,
signInType = signInType,
walletsCount = store.state.globalState.userWalletsListManager?.walletsCount.toString(),
),
)
}

View file

@ -98,7 +98,7 @@ internal fun WalletDataModel.getAvailableActions(
internal fun WalletDataModel.shouldShowMultipleAddress(): Boolean {
val listOfAddresses = walletAddresses?.list.orEmpty()
return listOfAddresses.size > 1 && currency.blockchain != Blockchain.BitcoinCash
return listOfAddresses.size > 1
}
internal fun WalletDataModel.assembleWarnings(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"
}

View file

@ -2,7 +2,6 @@ package com.tangem.datasource.config
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
@ -29,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)
}
@ -37,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
}
}
@ -52,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,
)
}

View file

@ -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,

View file

@ -8,7 +8,6 @@ import com.squareup.moshi.Json
class FeatureModel(
val isTopUpEnabled: Boolean,
val isSendingToPayIdEnabled: Boolean,
val isCreatingTwinCardsAllowed: Boolean,
)

View file

@ -35,11 +35,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>

View file

@ -35,11 +35,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>

View file

@ -35,11 +35,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>

View file

@ -318,11 +318,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>
@ -451,7 +447,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>

View file

@ -281,11 +281,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>
@ -395,7 +391,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>

View file

@ -314,11 +314,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>
@ -443,7 +439,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>

View file

@ -2,19 +2,17 @@ package com.tangem.core.ui.components
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.res.TangemTheme
// region TextButton
@ -26,7 +24,7 @@ fun TextButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier,
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.None,
icon = TangemButtonIconPosition.None,
onClick = onClick,
enabled = enabled,
showProgress = false,
@ -49,7 +47,7 @@ fun TextButtonIconStart(
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.Start(iconResId),
icon = TangemButtonIconPosition.Start(iconResId),
onClick = onClick,
enabled = enabled,
showProgress = false,
@ -63,7 +61,7 @@ fun WarningTextButton(text: String, onClick: () -> Unit, modifier: Modifier = Mo
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.None,
icon = TangemButtonIconPosition.None,
onClick = onClick,
enabled = enabled,
showProgress = false,
@ -85,7 +83,7 @@ fun PrimaryButton(
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.None,
icon = TangemButtonIconPosition.None,
onClick = onClick,
colors = TangemButtonsDefaults.primaryButtonColors,
enabled = enabled,
@ -108,7 +106,7 @@ fun PrimaryButtonIconEnd(
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.End(iconResId),
icon = TangemButtonIconPosition.End(iconResId),
onClick = onClick,
colors = TangemButtonsDefaults.primaryButtonColors,
enabled = enabled,
@ -131,7 +129,7 @@ fun PrimaryButtonIconStart(
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.Start(iconResId),
icon = TangemButtonIconPosition.Start(iconResId),
onClick = onClick,
colors = TangemButtonsDefaults.primaryButtonColors,
enabled = enabled,
@ -152,7 +150,7 @@ fun SecondaryButton(
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.None,
icon = TangemButtonIconPosition.None,
onClick = onClick,
colors = TangemButtonsDefaults.secondaryButtonColors,
enabled = enabled,
@ -175,7 +173,7 @@ fun SecondaryButtonIconEnd(
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.End(iconResId),
icon = TangemButtonIconPosition.End(iconResId),
onClick = onClick,
colors = TangemButtonsDefaults.secondaryButtonColors,
enabled = enabled,
@ -198,7 +196,7 @@ fun SecondaryButtonIconStart(
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.Start(iconResId),
icon = TangemButtonIconPosition.Start(iconResId),
onClick = onClick,
colors = TangemButtonsDefaults.secondaryButtonColors,
enabled = enabled,
@ -214,7 +212,7 @@ fun SelectorButton(text: String, onClick: () -> Unit, modifier: Modifier = Modif
modifier = modifier,
text = text,
textStyle = TangemTheme.typography.subtitle2,
icon = TangemButtonIcon.End(iconResId = R.drawable.ic_chevron_24),
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
onClick = onClick,
colors = TangemButtonsDefaults.selectorButtonColors,
showProgress = false,
@ -224,362 +222,6 @@ fun SelectorButton(text: String, onClick: () -> Unit, modifier: Modifier = Modif
}
// endregion Other
// region Action
/**
* [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=290-305&t=3z98eFnTeyIx5TH5-4)
* */
@Composable
fun RoundedActionButton(
text: String,
@DrawableRes iconResId: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.Start(iconResId),
onClick = onClick,
enabled = enabled,
showProgress = false,
colors = TangemButtonsDefaults.secondaryButtonColors,
size = TangemButtonSize.RoundedAction,
)
}
/**
* [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=1208-1395&t=3z98eFnTeyIx5TH5-4)
* */
@Composable
fun ActionButton(
text: String,
@DrawableRes iconResId: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.Start(iconResId),
onClick = onClick,
enabled = enabled,
showProgress = false,
colors = TangemButtonsDefaults.secondaryButtonColors,
size = TangemButtonSize.Action,
)
}
/**
* Same as [RoundedActionButton] but colored in primary background color
* */
@Composable
fun BackgroundActionButton(
text: String,
@DrawableRes iconResId: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
TangemButton(
modifier = modifier,
text = text,
icon = TangemButtonIcon.Start(iconResId),
onClick = onClick,
enabled = enabled,
showProgress = false,
colors = TangemButtonsDefaults.backgroundButtonColors,
size = TangemButtonSize.RoundedAction,
)
}
// endregion Action
// region Defaults
@Suppress("LongParameterList")
@Composable
private fun TangemButton(
text: String,
icon: TangemButtonIcon,
onClick: () -> Unit,
colors: ButtonColors,
showProgress: Boolean,
enabled: Boolean,
modifier: Modifier = Modifier,
size: TangemButtonSize = TangemButtonSize.Default,
elevation: ButtonElevation = TangemButtonsDefaults.elevation,
textStyle: TextStyle = TangemTheme.typography.button,
) {
Button(
modifier = modifier.heightIn(min = size.toHeightDp()),
onClick = { if (!showProgress) onClick() },
enabled = enabled,
elevation = elevation,
shape = size.toShape(),
colors = colors,
contentPadding = size.toContentPadding(icon = icon),
) {
ButtonContent(
text = text,
textStyle = textStyle,
buttonIcon = icon,
colors = colors,
showProgress = showProgress,
enabled = enabled,
size = size,
)
}
}
@Suppress("LongParameterList")
@Composable
private fun ButtonContent(
text: String,
textStyle: TextStyle,
buttonIcon: TangemButtonIcon,
colors: ButtonColors,
size: TangemButtonSize,
enabled: Boolean,
showProgress: Boolean,
) {
val icon = @Composable { iconResId: Int ->
Icon(
modifier = Modifier.size(TangemTheme.dimens.size20),
painter = painterResource(id = iconResId),
tint = colors.contentColor(enabled = enabled).value,
contentDescription = null,
)
}
if (showProgress) {
Box(modifier = Modifier.wrapContentSize()) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.Center)
.size(TangemTheme.dimens.size24),
color = colors.contentColor(enabled = enabled).value,
strokeWidth = TangemTheme.dimens.size4,
)
}
} else {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(size.toIconPadding()),
) {
if (buttonIcon is TangemButtonIcon.Start) {
icon(buttonIcon.iconResId)
}
Text(
text = text,
style = textStyle,
color = colors.contentColor(enabled = enabled).value,
maxLines = 1,
)
if (buttonIcon is TangemButtonIcon.End) {
icon(buttonIcon.iconResId)
}
}
}
}
@Immutable
private sealed interface TangemButtonIcon {
val iconResId: Int?
data class Start(override val iconResId: Int) : TangemButtonIcon
data class End(override val iconResId: Int) : TangemButtonIcon
object None : TangemButtonIcon {
override val iconResId: Int? = null
}
}
private enum class TangemButtonSize {
Default,
Text,
Selector,
Action,
RoundedAction,
}
@Composable
@ReadOnlyComposable
private fun TangemButtonSize.toHeightDp(): Dp = when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.size48
TangemButtonSize.Text -> TangemTheme.dimens.size40
TangemButtonSize.Selector -> TangemTheme.dimens.size24
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> TangemTheme.dimens.size36
}
@Composable
@ReadOnlyComposable
private fun TangemButtonSize.toShape(): Shape = when (this) {
TangemButtonSize.Default -> TangemTheme.shapes.roundedCornersMedium
TangemButtonSize.Text -> TangemTheme.shapes.roundedCornersSmall
TangemButtonSize.Selector -> TangemTheme.shapes.roundedCornersSmall
TangemButtonSize.Action -> TangemTheme.shapes.roundedCornersMedium
TangemButtonSize.RoundedAction -> TangemTheme.shapes.roundedCornersLarge
}
@Composable
@ReadOnlyComposable
private fun TangemButtonSize.toIconPadding(): Dp = when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.spacing8
TangemButtonSize.Text -> TangemTheme.dimens.spacing8
TangemButtonSize.Selector -> 0.dp
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> TangemTheme.dimens.spacing8
}
@Composable
@ReadOnlyComposable
private fun TangemButtonSize.toContentPadding(icon: TangemButtonIcon): PaddingValues {
val horizontalPadding = this.toHorizontalContentPadding(icon = icon)
return when (this) {
TangemButtonSize.Default -> PaddingValues(
top = TangemTheme.dimens.spacing14,
bottom = TangemTheme.dimens.spacing14,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
TangemButtonSize.Text -> PaddingValues(
top = TangemTheme.dimens.spacing10,
bottom = TangemTheme.dimens.spacing10,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
TangemButtonSize.Selector -> PaddingValues(
top = TangemTheme.dimens.spacing0_5,
bottom = TangemTheme.dimens.spacing0_5,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> PaddingValues(
top = TangemTheme.dimens.spacing8,
bottom = TangemTheme.dimens.spacing8,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
}
}
@Composable
@ReadOnlyComposable
private fun TangemButtonSize.toHorizontalContentPadding(icon: TangemButtonIcon): Pair<Dp, Dp> {
return when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.spacing32 to TangemTheme.dimens.spacing32
TangemButtonSize.Text -> when (icon) {
is TangemButtonIcon.None -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing16
is TangemButtonIcon.Start -> TangemTheme.dimens.spacing14 to TangemTheme.dimens.spacing16
is TangemButtonIcon.End -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing14
}
TangemButtonSize.Selector -> TangemTheme.dimens.spacing0_5 to TangemTheme.dimens.spacing0_5
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> when (icon) {
is TangemButtonIcon.None -> TangemTheme.dimens.spacing24 to TangemTheme.dimens.spacing24
is TangemButtonIcon.Start -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing24
is TangemButtonIcon.End -> TangemTheme.dimens.spacing24 to TangemTheme.dimens.spacing16
}
}
}
private object TangemButtonsDefaults {
val elevation: ButtonElevation
@Composable get() = ButtonDefaults
.elevation(
defaultElevation = TangemTheme.dimens.elevation0,
pressedElevation = TangemTheme.dimens.elevation0,
)
val primaryButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = TangemTheme.colors.button.primary,
contentColor = TangemTheme.colors.text.primary2,
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val secondaryButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = TangemTheme.colors.button.secondary,
contentColor = TangemTheme.colors.text.primary1,
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val defaultTextButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = Color.Transparent,
contentColor = TangemTheme.colors.text.secondary,
disabledBackgroundColor = Color.Transparent,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val warningTextButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = Color.Transparent,
contentColor = TangemTheme.colors.text.warning,
disabledBackgroundColor = Color.Transparent,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val selectorButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = Color.Transparent,
contentColor = TangemTheme.colors.text.tertiary,
disabledBackgroundColor = Color.Transparent,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val backgroundButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = TangemTheme.colors.background.primary,
contentColor = TangemTheme.colors.text.primary1,
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
)
}
@Immutable
private open class TangemButtonColors(
private val backgroundColor: Color,
private val contentColor: Color,
private val disabledBackgroundColor: Color,
private val disabledContentColor: Color,
) : ButtonColors {
@Composable
override fun backgroundColor(enabled: Boolean): State<Color> {
return rememberUpdatedState(newValue = if (enabled) backgroundColor else disabledBackgroundColor)
}
@Composable
override fun contentColor(enabled: Boolean): State<Color> {
return rememberUpdatedState(newValue = if (enabled) contentColor else disabledContentColor)
}
}
// endregion Defaults
// region Preview
@Composable
private fun PrimaryButtonSample() {
@ -730,34 +372,4 @@ private fun TextButtonPreview_DarkTheme() {
}
}
@Composable
private fun ActionButtonSample() {
Column(
modifier = Modifier.background(TangemTheme.colors.background.primary),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
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_LightTheme() {
TangemTheme {
ActionButtonSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun ActionButtonPreview_DarkTheme() {
TangemTheme(isDark = true) {
ActionButtonSample()
}
}
// endregion Preview

View file

@ -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 hasnt 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 hasnt been backed up",
subtitle = null,
onClick = {},
)
SpacerH32()
WarningNotification(
title = "Your wallet hasnt 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 hasnt 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

View file

@ -0,0 +1,20 @@
package com.tangem.core.ui.components.buttons.actions
import androidx.annotation.DrawableRes
/**
* Action button config
*
* @property text text
* @property iconResId icon resource id
* @property onClick lambda be invoked when action component is clicked
* @property enabled enabled
*
[REDACTED_AUTHOR]
*/
data class ActionConfig(
val text: String,
@DrawableRes val iconResId: Int,
val onClick: () -> Unit,
val enabled: Boolean = true,
)

View file

@ -0,0 +1,96 @@
package com.tangem.core.ui.components.buttons.actions
import androidx.compose.foundation.layout.Column
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
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.res.TangemTheme
/**
* [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=290-305&t=3z98eFnTeyIx5TH5-4)
*/
@Composable
fun RoundedActionButton(config: ActionConfig, modifier: Modifier = Modifier) {
TangemButton(
modifier = modifier,
text = config.text,
icon = TangemButtonIconPosition.Start(config.iconResId),
onClick = config.onClick,
enabled = config.enabled,
showProgress = false,
colors = TangemButtonsDefaults.secondaryButtonColors,
size = TangemButtonSize.RoundedAction,
)
}
/**
* [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=1208-1395&t=3z98eFnTeyIx5TH5-4)
*/
@Composable
fun ActionButton(config: ActionConfig, modifier: Modifier = Modifier) {
TangemButton(
modifier = modifier,
text = config.text,
icon = TangemButtonIconPosition.Start(config.iconResId),
onClick = config.onClick,
enabled = config.enabled,
showProgress = false,
colors = TangemButtonsDefaults.secondaryButtonColors,
size = TangemButtonSize.Action,
)
}
/**
* Same as [RoundedActionButton] but colored in primary background color
*/
@Composable
fun BackgroundActionButton(config: ActionConfig, modifier: Modifier = Modifier) {
TangemButton(
modifier = modifier,
text = config.text,
icon = TangemButtonIconPosition.Start(config.iconResId),
onClick = config.onClick,
enabled = config.enabled,
showProgress = false,
colors = TangemButtonsDefaults.backgroundButtonColors,
size = TangemButtonSize.RoundedAction,
)
}
@Preview(showBackground = true)
@Composable
private fun Preview_ActionButton_Light(@PreviewParameter(ActionStateProvider::class) state: ActionConfig) {
TangemTheme(isDark = false) {
Column {
RoundedActionButton(state)
ActionButton(state)
BackgroundActionButton(state)
}
}
}
@Preview(showBackground = true)
@Composable
private fun Preview_ActionButton_Dark(@PreviewParameter(ActionStateProvider::class) state: ActionConfig) {
TangemTheme {
Column {
RoundedActionButton(state)
ActionButton(state)
BackgroundActionButton(state)
}
}
}
private class ActionStateProvider : CollectionPreviewParameterProvider<ActionConfig>(
collection = listOf(
ActionConfig(text = "Send", iconResId = R.drawable.ic_arrow_up_24, onClick = {}),
ActionConfig(text = "Receive", iconResId = R.drawable.ic_arrow_down_24, enabled = false, onClick = {}),
),
)

View file

@ -0,0 +1,96 @@
package com.tangem.core.ui.components.buttons.common
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
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.text.TextStyle
import com.tangem.core.ui.res.TangemTheme
@Suppress("LongParameterList")
@Composable
internal fun TangemButton(
text: String,
icon: TangemButtonIconPosition,
onClick: () -> Unit,
colors: ButtonColors,
showProgress: Boolean,
enabled: Boolean,
modifier: Modifier = Modifier,
size: TangemButtonSize = TangemButtonSize.Default,
elevation: ButtonElevation = TangemButtonsDefaults.elevation,
textStyle: TextStyle = TangemTheme.typography.button,
) {
Button(
modifier = modifier.heightIn(min = size.toHeightDp()),
onClick = { if (!showProgress) onClick() },
enabled = enabled,
elevation = elevation,
shape = size.toShape(),
colors = colors,
contentPadding = size.toContentPadding(icon = icon),
) {
ButtonContent(
text = text,
textStyle = textStyle,
buttonIcon = icon,
colors = colors,
showProgress = showProgress,
enabled = enabled,
size = size,
)
}
}
@Suppress("LongParameterList")
@Composable
private fun ButtonContent(
text: String,
textStyle: TextStyle,
buttonIcon: TangemButtonIconPosition,
colors: ButtonColors,
size: TangemButtonSize,
enabled: Boolean,
showProgress: Boolean,
) {
val icon = @Composable { iconResId: Int ->
Icon(
modifier = Modifier.size(TangemTheme.dimens.size20),
painter = painterResource(id = iconResId),
tint = colors.contentColor(enabled = enabled).value,
contentDescription = null,
)
}
if (showProgress) {
Box(modifier = Modifier.wrapContentSize()) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.Center)
.size(TangemTheme.dimens.size24),
color = colors.contentColor(enabled = enabled).value,
strokeWidth = TangemTheme.dimens.size4,
)
}
} else {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(size.toIconPadding()),
) {
if (buttonIcon is TangemButtonIconPosition.Start) {
icon(buttonIcon.iconResId)
}
Text(
text = text,
style = textStyle,
color = colors.contentColor(enabled = enabled).value,
maxLines = 1,
)
if (buttonIcon is TangemButtonIconPosition.End) {
icon(buttonIcon.iconResId)
}
}
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.core.ui.components.buttons.common
import androidx.compose.material.ButtonColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.graphics.Color
internal class TangemButtonColors(
private val backgroundColor: Color,
private val contentColor: Color,
private val disabledBackgroundColor: Color,
private val disabledContentColor: Color,
) : ButtonColors {
@Composable
override fun backgroundColor(enabled: Boolean): State<Color> {
return rememberUpdatedState(newValue = if (enabled) backgroundColor else disabledBackgroundColor)
}
@Composable
override fun contentColor(enabled: Boolean): State<Color> {
return rememberUpdatedState(newValue = if (enabled) contentColor else disabledContentColor)
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.core.ui.components.buttons.common
import androidx.annotation.DrawableRes
internal sealed interface TangemButtonIconPosition {
val iconResId: Int?
data class Start(@DrawableRes override val iconResId: Int) : TangemButtonIconPosition
data class End(@DrawableRes override val iconResId: Int) : TangemButtonIconPosition
object None : TangemButtonIconPosition {
@DrawableRes
override val iconResId: Int? = null
}
}

View file

@ -0,0 +1,105 @@
package com.tangem.core.ui.components.buttons.common
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
internal enum class TangemButtonSize {
Default,
Text,
Selector,
Action,
RoundedAction,
}
@Composable
@ReadOnlyComposable
internal fun TangemButtonSize.toHeightDp(): Dp = when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.size48
TangemButtonSize.Text -> TangemTheme.dimens.size40
TangemButtonSize.Selector -> TangemTheme.dimens.size24
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> TangemTheme.dimens.size36
}
@Composable
@ReadOnlyComposable
internal fun TangemButtonSize.toShape(): Shape = when (this) {
TangemButtonSize.Default -> TangemTheme.shapes.roundedCornersMedium
TangemButtonSize.Text -> TangemTheme.shapes.roundedCornersSmall
TangemButtonSize.Selector -> TangemTheme.shapes.roundedCornersSmall
TangemButtonSize.Action -> TangemTheme.shapes.roundedCornersMedium
TangemButtonSize.RoundedAction -> TangemTheme.shapes.roundedCornersLarge
}
@Composable
@ReadOnlyComposable
internal fun TangemButtonSize.toIconPadding(): Dp = when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.spacing8
TangemButtonSize.Text -> TangemTheme.dimens.spacing8
TangemButtonSize.Selector -> 0.dp
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> TangemTheme.dimens.spacing8
}
@Composable
@ReadOnlyComposable
internal fun TangemButtonSize.toContentPadding(icon: TangemButtonIconPosition): PaddingValues {
val horizontalPadding = this.toHorizontalContentPadding(icon = icon)
return when (this) {
TangemButtonSize.Default -> PaddingValues(
top = TangemTheme.dimens.spacing14,
bottom = TangemTheme.dimens.spacing14,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
TangemButtonSize.Text -> PaddingValues(
top = TangemTheme.dimens.spacing10,
bottom = TangemTheme.dimens.spacing10,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
TangemButtonSize.Selector -> PaddingValues(
top = TangemTheme.dimens.spacing0_5,
bottom = TangemTheme.dimens.spacing0_5,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> PaddingValues(
top = TangemTheme.dimens.spacing8,
bottom = TangemTheme.dimens.spacing8,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
}
}
@Composable
@ReadOnlyComposable
internal fun TangemButtonSize.toHorizontalContentPadding(icon: TangemButtonIconPosition): Pair<Dp, Dp> {
return when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.spacing32 to TangemTheme.dimens.spacing32
TangemButtonSize.Text -> when (icon) {
is TangemButtonIconPosition.None -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing16
is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing14 to TangemTheme.dimens.spacing16
is TangemButtonIconPosition.End -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing14
}
TangemButtonSize.Selector -> TangemTheme.dimens.spacing0_5 to TangemTheme.dimens.spacing0_5
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> when (icon) {
is TangemButtonIconPosition.None -> TangemTheme.dimens.spacing24 to TangemTheme.dimens.spacing24
is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing24
is TangemButtonIconPosition.End -> TangemTheme.dimens.spacing24 to TangemTheme.dimens.spacing16
}
}
}

View file

@ -0,0 +1,78 @@
package com.tangem.core.ui.components.buttons.common
import androidx.compose.material.ButtonColors
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.ButtonElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.res.TangemTheme
internal object TangemButtonsDefaults {
val elevation: ButtonElevation
@Composable get() = ButtonDefaults.elevation(
defaultElevation = TangemTheme.dimens.elevation0,
pressedElevation = TangemTheme.dimens.elevation0,
)
val primaryButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = TangemTheme.colors.button.primary,
contentColor = TangemTheme.colors.text.primary2,
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val secondaryButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = TangemTheme.colors.button.secondary,
contentColor = TangemTheme.colors.text.primary1,
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val defaultTextButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = Color.Transparent,
contentColor = TangemTheme.colors.text.secondary,
disabledBackgroundColor = Color.Transparent,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val warningTextButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = Color.Transparent,
contentColor = TangemTheme.colors.text.warning,
disabledBackgroundColor = Color.Transparent,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val selectorButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = Color.Transparent,
contentColor = TangemTheme.colors.text.tertiary,
disabledBackgroundColor = Color.Transparent,
disabledContentColor = TangemTheme.colors.text.disabled,
)
val backgroundButtonColors: ButtonColors
@Composable
@ReadOnlyComposable
get() = TangemButtonColors(
backgroundColor = TangemTheme.colors.background.primary,
contentColor = TangemTheme.colors.text.primary1,
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
)
}

View file

@ -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 hasnt 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 hasnt been backed up",
subtitle = null,
iconResId = R.drawable.ic_alert_circle_24,
tint = TangemColorPalette.Amaranth,
),
NotificationState.Action(
title = "Your wallet hasnt 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 hasnt been backed up",
subtitle = null,
iconResId = R.drawable.ic_alert_circle_24,
tint = TangemColorPalette.Amaranth,
onClick = {},
),
),
)

View file

@ -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)
}

View file

@ -76,6 +76,7 @@ data class TangemDimens internal constructor(
val size200: Dp = 200.dp,
// endregion Size
// region Spacing
val spacing0: Dp = 0.dp,
val spacing0_5: Dp = 0.5.dp,
val spacing2: Dp = 2.dp,
val spacing4: Dp = 4.dp,
@ -91,6 +92,7 @@ data class TangemDimens internal constructor(
val spacing24: Dp = 24.dp,
val spacing26: Dp = 26.dp,
val spacing28: Dp = 28.dp,
val spacing30: Dp = 30.dp,
val spacing32: Dp = 32.dp,
val spacing34: Dp = 34.dp,
val spacing36: Dp = 34.dp,

View file

@ -29,7 +29,6 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
"bitcoin-cash/test" -> Blockchain.BitcoinCashTestnet
"cardano" -> Blockchain.CardanoShelley
"dogecoin" -> Blockchain.Dogecoin
"ducatus" -> Blockchain.Ducatus
"litecoin" -> Blockchain.Litecoin
"rootstock" -> Blockchain.RSK
"stellar" -> Blockchain.Stellar
@ -83,7 +82,6 @@ fun Blockchain.toNetworkId(): String {
Blockchain.Cardano -> "cardano"
Blockchain.CardanoShelley -> "cardano"
Blockchain.Dogecoin -> "dogecoin"
Blockchain.Ducatus -> "ducatus"
Blockchain.Ethereum -> "ethereum"
Blockchain.EthereumTestnet -> "ethereum/test"
Blockchain.EthereumClassic -> "ethereum-classic"
@ -144,7 +142,6 @@ fun Blockchain.toCoinId(): String {
Blockchain.Fantom, Blockchain.FantomTestnet -> "fantom"
Blockchain.Tron, Blockchain.TronTestnet -> "tron"
Blockchain.Polkadot, Blockchain.PolkadotTestnet -> "polkadot"
Blockchain.Ducatus -> "ducatus"
Blockchain.Litecoin -> "litecoin"
Blockchain.RSK -> "rootstock"
Blockchain.Tezos -> "tezos"
@ -172,7 +169,4 @@ fun Blockchain.isSupportedInApp(): Boolean {
return !excludedBlockchains.contains(this)
}
private val excludedBlockchains = listOf(
Blockchain.Ducatus,
Blockchain.Unknown,
)
private val excludedBlockchains = listOf(Blockchain.Unknown)

View file

@ -22,6 +22,7 @@ dependencies {
implementation(deps.compose.ui.tooling)
implementation(deps.compose.shimmer)
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.reorderable)
/** Other libraries */
implementation(deps.kotlin.immutable.collections)

View file

@ -7,10 +7,7 @@ import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenO
import com.tangem.feature.wallet.presentation.organizetokens.DraggableItem
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder
import com.tangem.feature.wallet.presentation.wallet.state.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder
import com.tangem.feature.wallet.presentation.wallet.state.WalletTopBarConfig
import com.tangem.feature.wallet.presentation.wallet.state.*
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import java.util.UUID
@ -108,21 +105,22 @@ internal object WalletPreviewData {
private const val tokensSize = 3
val draggableItems = List(networksSize) { it }
.flatMap { index ->
val n = index + 1
val lastNetworkIndex = networksSize - 1
val networkNumber = index + 1
val group = DraggableItem.GroupHeader(
id = "group_$n",
networkName = "$n",
id = "group_$networkNumber",
networkName = "$networkNumber",
)
val tokens: MutableList<DraggableItem.Token> = mutableListOf()
repeat(times = tokensSize) { i ->
val nt = i + 1
val tokenNumber = i + 1
tokens.add(
DraggableItem.Token(
tokenItemState = tokenItemDragState.copy(
id = "${group.id}_token_$nt",
name = "Token $nt",
id = "${group.id}_token_$tokenNumber",
name = "Token $tokenNumber",
networkIconResId = R.drawable.img_eth_22.takeIf { i != 0 },
),
groupId = group.id,
@ -130,9 +128,14 @@ internal object WalletPreviewData {
)
}
val divider = DraggableItem.GroupPlaceholder(id = "divider_$networkNumber")
buildList {
add(group)
addAll(tokens)
if (index != lastNetworkIndex) {
add(divider)
}
}
}
.toPersistentList()
@ -149,6 +152,12 @@ internal object WalletPreviewData {
onSortByBalanceClick = {},
onGroupByNetworkClick = {},
),
dragConfig = OrganizeTokensStateHolder.DragConfig(
onItemDragged = { _, _ -> },
onDragStart = {},
canDragItemOver = { _, _ -> false },
onItemDragEnd = {},
),
actions = OrganizeTokensStateHolder.ActionsConfig(
onApplyClick = {},
onCancelClick = {},
@ -161,6 +170,14 @@ internal object WalletPreviewData {
),
)
val manageButtons = persistentListOf(
WalletManageButton.Buy(onClick = {}),
WalletManageButton.Send(onClick = {}),
WalletManageButton.Receive(onClick = {}),
WalletManageButton.Exchange(onClick = {}),
WalletManageButton.CopyAddress(onClick = {}),
)
val multicurrencyWalletScreenState = WalletStateHolder.MultiCurrencyContent(
onBackClick = {},
topBarConfig = walletTopBarConfig,
@ -220,6 +237,12 @@ internal object WalletPreviewData {
),
),
),
notifications = persistentListOf(
WalletNotification.UnreachableNetworks,
WalletNotification.LikeTangemApp(onClick = {}),
WalletNotification.NeedToBackup(onClick = {}),
WalletNotification.ScanCard(onClick = {}),
),
onOrganizeTokensClick = {},
)
@ -252,5 +275,12 @@ internal object WalletPreviewData {
),
),
),
notifications = persistentListOf(
WalletNotification.UnreachableNetworks,
WalletNotification.LikeTangemApp(onClick = {}),
WalletNotification.NeedToBackup(onClick = {}),
WalletNotification.ScanCard(onClick = {}),
),
buttons = manageButtons,
)
}

View file

@ -14,6 +14,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
@Composable
internal fun NetworkGroupItem(networkName: String, modifier: Modifier = Modifier) {
@ -24,16 +26,33 @@ internal fun NetworkGroupItem(networkName: String, modifier: Modifier = Modifier
}
@Composable
internal fun DraggableNetworkGroupItem(networkName: String, modifier: Modifier = Modifier) {
internal fun DraggableNetworkGroupItem(
networkName: String,
modifier: Modifier = Modifier,
reorderableTokenListState: ReorderableLazyListState? = null,
) {
InternalNetworkGroupItem(
modifier = modifier,
networkName = networkName,
endIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_group_drop_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
Box(
modifier = Modifier
.size(TangemTheme.dimens.size32)
.let {
if (reorderableTokenListState != null) {
it.detectReorder(reorderableTokenListState)
} else {
it
}
},
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_group_drop_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
},
)
}

View file

@ -34,6 +34,8 @@ import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
private const val DOTS = "•••"
val TOKEN_ITEM_HEIGHT: Dp
@ -46,7 +48,7 @@ internal fun TokenItem(state: TokenItemState, modifier: Modifier = Modifier) {
when (state) {
is TokenItemState.Content -> ContentTokenItem(state, modifier)
is TokenItemState.Loading -> LoadingTokenItem(modifier)
is TokenItemState.Draggable -> DraggableTokenItem(state, modifier)
is TokenItemState.Draggable -> DraggableTokenItem(state, modifier, reorderableTokenListState = null)
is TokenItemState.Unreachable -> UnreachableTokenItem(state, modifier)
}
}
@ -71,7 +73,11 @@ private fun ContentTokenItem(content: TokenItemState.Content, modifier: Modifier
}
@Composable
internal fun DraggableTokenItem(state: TokenItemState.Draggable, modifier: Modifier = Modifier) {
internal fun DraggableTokenItem(
state: TokenItemState.Draggable,
modifier: Modifier = Modifier,
reorderableTokenListState: ReorderableLazyListState? = null,
) {
InternalTokenItem(
modifier = modifier,
name = state.name,
@ -81,13 +87,25 @@ internal fun DraggableTokenItem(state: TokenItemState.Draggable, modifier: Modif
amount = state.fiatAmount,
hasPending = false,
options = { ref ->
Icon(
Box(
modifier = Modifier
.constrainAsOptionsItem(scope = this, ref),
painter = painterResource(id = R.drawable.ic_drag_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
.size(TangemTheme.dimens.size32)
.constrainAsOptionsItem(scope = this, ref)
.let {
if (reorderableTokenListState != null) {
it.detectReorder(reorderableTokenListState)
} else {
it
}
},
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_drag_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
},
)
}

View file

@ -9,10 +9,7 @@ import androidx.compose.material.AppBarDefaults
import androidx.compose.material3.FabPosition
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
@ -24,14 +21,19 @@ import androidx.compose.ui.res.stringResource
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.components.BackgroundActionButton
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.buttons.actions.ActionConfig
import com.tangem.core.ui.components.buttons.actions.BackgroundActionButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkGroupItem
import com.tangem.feature.wallet.presentation.common.component.DraggableTokenItem
import org.burnoutcrew.reorderable.ReorderableItem
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.rememberReorderableLazyListState
import org.burnoutcrew.reorderable.reorderable
@Composable
internal fun OrganizeTokensScreen(state: OrganizeTokensStateHolder, modifier: Modifier = Modifier) {
@ -47,6 +49,7 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensStateHolder, modifier: Mo
modifier = Modifier.padding(paddingValues),
listState = tokensListState,
state = state.itemsState,
dragConfig = state.dragConfig,
)
},
floatingActionButtonPosition = FabPosition.Center,
@ -57,28 +60,56 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensStateHolder, modifier: Mo
)
}
// TODO: Fix list animations
@Composable
private fun TokenList(listState: LazyListState, state: OrganizeTokensListState, modifier: Modifier = Modifier) {
private fun TokenList(
listState: LazyListState,
state: OrganizeTokensListState,
dragConfig: OrganizeTokensStateHolder.DragConfig,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
val reorderableListState = rememberReorderableLazyListState(
onMove = dragConfig.onItemDragged,
listState = listState,
canDragOver = dragConfig.canDragItemOver,
onDragEnd = { _, _ -> dragConfig.onItemDragEnd() },
)
val items = state.items
val lastItemIndex = items.lastIndex
LazyColumn(
modifier = Modifier
.reorderable(reorderableListState)
.align(Alignment.TopCenter)
.padding(horizontal = TangemTheme.dimens.spacing16),
state = listState,
state = reorderableListState.listState,
contentPadding = PaddingValues(
top = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing92,
),
) {
itemsIndexed(
items = state.items,
items = items,
key = { _, item -> item.id },
) { index, item ->
val onDragStart = remember(item) {
{ dragConfig.onDragStart(item) }
}
DraggableItem(
item = item,
index = index,
lastItemIndex = state.items.lastIndex,
lastItemIndex = lastItemIndex,
reorderableState = reorderableListState,
onDragStart = onDragStart,
)
if (item is DraggableItem.GroupPlaceholder) {
// This item should be displayed in the list but remain invisible
Box(modifier = Modifier.fillMaxWidth())
}
}
}
@ -87,19 +118,39 @@ private fun TokenList(listState: LazyListState, state: OrganizeTokensListState,
}
@Composable
private fun DraggableItem(index: Int, item: DraggableItem, lastItemIndex: Int) {
val itemModifier = Modifier
.clipFirstAndLastItems(index, lastItemIndex)
private fun LazyItemScope.DraggableItem(
index: Int,
item: DraggableItem,
lastItemIndex: Int,
reorderableState: ReorderableLazyListState,
onDragStart: () -> Unit,
) {
ReorderableItem(
reorderableState = reorderableState,
index = index,
key = item.id,
) { isDragging ->
when (item) {
is DraggableItem.GroupHeader -> DraggableNetworkGroupItem(
modifier = itemModifier,
networkName = item.networkName,
)
is DraggableItem.Token -> DraggableTokenItem(
modifier = itemModifier,
state = item.tokenItemState,
)
if (isDragging) {
onDragStart()
}
val itemModifier = Modifier
.clipFirstLastAndDraggingItems(index, lastItemIndex, isDragging)
when (item) {
is DraggableItem.GroupHeader -> DraggableNetworkGroupItem(
modifier = itemModifier,
networkName = item.networkName,
reorderableTokenListState = reorderableState,
)
is DraggableItem.Token -> DraggableTokenItem(
modifier = itemModifier,
state = item.tokenItemState,
reorderableTokenListState = reorderableState,
)
is DraggableItem.GroupPlaceholder -> Unit
}
}
}
@ -164,16 +215,20 @@ private fun TopBar(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
BackgroundActionButton(
config = ActionConfig(
text = stringResource(id = R.string.organize_tokens_sort_by_balance),
iconResId = R.drawable.ic_sort_24,
onClick = config.onSortByBalanceClick,
),
modifier = Modifier.weight(1f),
text = stringResource(id = R.string.organize_tokens_sort_by_balance),
iconResId = R.drawable.ic_sort_24,
onClick = config.onSortByBalanceClick,
)
BackgroundActionButton(
config = ActionConfig(
text = stringResource(id = R.string.organize_tokens_group),
iconResId = R.drawable.ic_group_24,
onClick = config.onGroupByNetworkClick,
),
modifier = Modifier.weight(1f),
text = stringResource(id = R.string.organize_tokens_group),
iconResId = R.drawable.ic_group_24,
onClick = config.onGroupByNetworkClick,
)
}
}
@ -201,29 +256,36 @@ private fun Actions(config: OrganizeTokensStateHolder.ActionsConfig, modifier: M
}
}
private fun Modifier.clipFirstAndLastItems(index: Int, lastItemIndex: Int): Modifier = composed {
when (index) {
0 -> {
this
.clip(
private fun Modifier.clipFirstLastAndDraggingItems(index: Int, lastItemIndex: Int, isDragging: Boolean): Modifier =
composed {
when {
isDragging -> {
val elevation by animateDpAsState(
targetValue = TangemTheme.dimens.elevation12,
label = "dragging_item_shadow_elevation",
)
this.shadow(elevation, shape = TangemTheme.shapes.roundedCornersXMedium)
}
index == 0 -> {
this.clip(
RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
),
)
}
lastItemIndex -> {
this
.clip(
}
index == lastItemIndex -> {
this.clip(
RoundedCornerShape(
bottomStart = TangemTheme.dimens.radius16,
bottomEnd = TangemTheme.dimens.radius16,
),
)
}
else -> this
}
else -> this
}
}
// region Preview

View file

@ -2,12 +2,14 @@ package com.tangem.feature.wallet.presentation.organizetokens
import androidx.compose.runtime.Immutable
import com.tangem.feature.wallet.presentation.common.state.TokenItemState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
import org.burnoutcrew.reorderable.ItemPosition
internal data class OrganizeTokensStateHolder(
val header: HeaderConfig,
val itemsState: OrganizeTokensListState,
val dragConfig: DragConfig,
val actions: ActionsConfig,
) {
@ -20,44 +22,79 @@ internal data class OrganizeTokensStateHolder(
val onApplyClick: () -> Unit,
val onCancelClick: () -> Unit,
)
data class DragConfig(
val onItemDragged: (from: ItemPosition, to: ItemPosition) -> Unit,
val canDragItemOver: (dragOver: ItemPosition, dragging: ItemPosition) -> Boolean,
val onItemDragEnd: () -> Unit,
val onDragStart: (item: DraggableItem) -> Unit,
)
}
@Immutable
internal sealed class OrganizeTokensListState {
abstract val items: ImmutableList<DraggableItem>
internal sealed interface OrganizeTokensListState {
val items: PersistentList<DraggableItem>
data class GroupedByNetwork(
override val items: ImmutableList<DraggableItem>,
) : OrganizeTokensListState()
override val items: PersistentList<DraggableItem>,
) : OrganizeTokensListState
data class Ungrouped(
override val items: ImmutableList<DraggableItem.Token>,
) : OrganizeTokensListState()
override val items: PersistentList<DraggableItem.Token>,
) : OrganizeTokensListState
@Suppress("UNCHECKED_CAST")
inline fun updateItems(update: (List<DraggableItem>) -> List<DraggableItem>): OrganizeTokensListState {
fun updateItems(update: (PersistentList<DraggableItem>) -> List<DraggableItem>): OrganizeTokensListState {
val updatedItems = update(this.items).toPersistentList()
return when (this) {
is GroupedByNetwork -> this.copy(items = updatedItems)
is Ungrouped -> this.copy(items = updatedItems as ImmutableList<DraggableItem.Token>)
is Ungrouped -> this.copy(items = updatedItems as PersistentList<DraggableItem.Token>)
}
}
}
/**
* Helper class for the DND list items
*
* @property id ID of the item
* */
@Immutable
internal sealed interface DraggableItem {
val id: String
/**
* Item for network group header.
*
* @property id ID of the network group
* @property networkName network group name
* */
data class GroupHeader(
override val id: String,
val networkName: String,
) : DraggableItem
/**
* Item for token.
*
* @property tokenItemState state of the token item
* @property groupId ID of the network group which contains this token
* @property id ID of the token
* */
data class Token(
val tokenItemState: TokenItemState.Draggable,
val groupId: String,
) : DraggableItem {
override val id: String = tokenItemState.id
}
/**
* Helper item used to detect possible positions where a network group can be placed.
* Used only on [OrganizeTokensListState.GroupedByNetwork] and placed between network groups.
*
* @property id ID of the placeholder
* */
data class GroupPlaceholder(
override val id: String,
) : DraggableItem
}

View file

@ -5,10 +5,16 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder.DragConfig
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder.HeaderConfig
import com.tangem.feature.wallet.presentation.organizetokens.utils.checkCanMoveHeaderOver
import com.tangem.feature.wallet.presentation.organizetokens.utils.checkCanMoveTokenOver
import com.tangem.feature.wallet.presentation.organizetokens.utils.findItemsToMove
import com.tangem.feature.wallet.presentation.organizetokens.utils.moveItem
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.toPersistentList
import org.burnoutcrew.reorderable.ItemPosition
import javax.inject.Inject
import kotlin.properties.Delegates
@ -16,6 +22,10 @@ import kotlin.properties.Delegates
@HiltViewModel
internal class OrganizeTokensViewModel @Inject constructor() : ViewModel() {
// TODO: Move to domain
@Volatile
private var groupIdToTokens: Map<String, List<DraggableItem.Token>>? = null
var router: InnerWalletRouter by Delegates.notNull()
var uiState: OrganizeTokensStateHolder by mutableStateOf(getInitialState())
@ -25,6 +35,12 @@ internal class OrganizeTokensViewModel @Inject constructor() : ViewModel() {
itemsState = OrganizeTokensListState.Ungrouped(
items = WalletPreviewData.draggableTokens,
),
dragConfig = DragConfig(
onItemDragged = this::moveItem,
canDragItemOver = this::checkCanMoveItemOver,
onItemDragEnd = this::expandGroups,
onDragStart = this::collapseGroup,
),
header = HeaderConfig(
onSortByBalanceClick = { /* no-op */ },
onGroupByNetworkClick = this::toggleTokensByNetworkGrouping,
@ -43,4 +59,69 @@ internal class OrganizeTokensViewModel @Inject constructor() : ViewModel() {
uiState = uiState.copy(itemsState = newListState)
}
private fun checkCanMoveItemOver(moveOverItemPosition: ItemPosition, movedItemPosition: ItemPosition): Boolean {
val items = (uiState.itemsState as? OrganizeTokensListState.GroupedByNetwork)
?.items
?: return true // If ungrouped then item can be moved anywhere
val (moveOverItem, movedItem) = items.findItemsToMove(moveOverItemPosition.key, movedItemPosition.key)
if (moveOverItem == null || movedItem == null) {
return false
}
return when (movedItem) {
is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(moveOverItemPosition, moveOverItem, items.lastIndex)
is DraggableItem.Token -> checkCanMoveTokenOver(movedItem, moveOverItem)
is DraggableItem.GroupPlaceholder -> false
}
}
private fun collapseGroup(item: DraggableItem) {
if (!groupIdToTokens.isNullOrEmpty() || item is DraggableItem.Token) return
val itemsState = uiState.itemsState as? OrganizeTokensListState.GroupedByNetwork ?: return
groupIdToTokens = itemsState.items
.asSequence()
.filterIsInstance<DraggableItem.Token>()
.groupBy(DraggableItem.Token::groupId)
uiState = uiState.copy(
itemsState = itemsState.updateItems { items ->
items.filterNot { it is DraggableItem.Token && it.groupId == item.id }
},
)
}
private fun expandGroups() {
if (groupIdToTokens.isNullOrEmpty()) return
val itemsState = uiState.itemsState as? OrganizeTokensListState.GroupedByNetwork ?: return
val currentGroups = itemsState.items.filterIsInstance<DraggableItem.GroupHeader>()
val newItems = currentGroups
.flatMapIndexed { index, group ->
mutableListOf<DraggableItem>(group)
.also { it.addAll(groupIdToTokens?.get(group.id).orEmpty()) }
.also {
if (index != currentGroups.lastIndex) {
it.add(DraggableItem.GroupPlaceholder(id = "group_divider_$index"))
}
}
}
uiState = uiState.copy(
itemsState = itemsState.updateItems { newItems },
)
groupIdToTokens = null
}
private fun moveItem(from: ItemPosition, to: ItemPosition) {
uiState = uiState.copy(
itemsState = uiState.itemsState.updateItems {
it.moveItem(from.index, to.index)
},
)
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.feature.wallet.presentation.organizetokens.utils
import com.tangem.feature.wallet.presentation.organizetokens.DraggableItem
import kotlinx.collections.immutable.PersistentList
import org.burnoutcrew.reorderable.ItemPosition
internal fun List<DraggableItem>.findItemsToMove(
moveOverItemKey: Any?,
movedItemKey: Any?,
): Pair<DraggableItem?, DraggableItem?> {
var moveOverItem: DraggableItem? = null
var movedItem: DraggableItem? = null
for (item in this) {
if (item.id == moveOverItemKey) {
moveOverItem = item
}
if (item.id == movedItemKey) {
movedItem = item
}
if (moveOverItem != null && movedItem != null) {
break
}
}
return Pair(moveOverItem, movedItem)
}
internal fun checkCanMoveHeaderOver(
moveOverItemPosition: ItemPosition,
moveOverItem: DraggableItem,
lastItemIndex: Int,
): Boolean {
return when {
moveOverItemPosition.index == 0 -> true
moveOverItemPosition.index == lastItemIndex -> true
moveOverItem is DraggableItem.GroupPlaceholder -> true
else -> false
}
}
internal fun checkCanMoveTokenOver(item: DraggableItem.Token, moveOverItem: DraggableItem): Boolean {
return when (moveOverItem) {
is DraggableItem.GroupHeader -> false // Token item can not be moved to group item
is DraggableItem.Token -> item.groupId == moveOverItem.groupId // Token item can not be moved over its group
is DraggableItem.GroupPlaceholder -> false
}
}
internal fun PersistentList<DraggableItem>.moveItem(fromIndex: Int, toIndex: Int): PersistentList<DraggableItem> {
val fromItem = this[fromIndex]
return this
.removeAt(fromIndex)
.add(toIndex, fromItem)
}

View file

@ -0,0 +1,79 @@
package com.tangem.feature.wallet.presentation.wallet.state
import com.tangem.core.ui.components.buttons.actions.ActionConfig
import com.tangem.feature.wallet.impl.R
/**
* Wallet manage button state
*
* @param config action config
*
[REDACTED_AUTHOR]
*/
sealed class WalletManageButton(val config: ActionConfig) {
/**
* Buy
*
* @param onClick lambda be invoked when manage button is clicked
*/
data class Buy(val onClick: () -> Unit) : WalletManageButton(
config = ActionConfig(
text = "Buy",
iconResId = R.drawable.ic_plus_24,
onClick = onClick,
),
)
/**
* Send
*
* @param onClick lambda be invoked when manage button is clicked
*/
data class Send(val onClick: () -> Unit) : WalletManageButton(
config = ActionConfig(
text = "Send",
iconResId = R.drawable.ic_arrow_up_24,
onClick = onClick,
),
)
/**
* Receive
*
* @param onClick lambda be invoked when manage button is clicked
*/
data class Receive(val onClick: () -> Unit) : WalletManageButton(
config = ActionConfig(
text = "Receive",
iconResId = R.drawable.ic_arrow_down_24,
onClick = onClick,
),
)
/**
* Exchange
*
* @param onClick lambda be invoked when manage button is clicked
*/
data class Exchange(val onClick: () -> Unit) : WalletManageButton(
config = ActionConfig(
text = "Exchange",
iconResId = R.drawable.ic_exchange_vertical_24,
onClick = onClick,
),
)
/**
* Copy address
*
* @param onClick lambda be invoked when manage button is clicked
*/
data class CopyAddress(val onClick: () -> Unit) : WalletManageButton(
config = ActionConfig(
text = "Copy address",
iconResId = R.drawable.ic_copy_24,
onClick = onClick,
),
)
}

View file

@ -0,0 +1,64 @@
package com.tangem.feature.wallet.presentation.wallet.state
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
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
import com.tangem.core.ui.components.buttons.actions.ActionButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import kotlinx.collections.immutable.ImmutableList
/**
* Wallet manage buttons
*
* @param buttons manage buttons
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun WalletManageButtons(buttons: ImmutableList<WalletManageButton>) {
Row(
modifier = Modifier
.horizontalScroll(state = rememberScrollState())
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
) {
buttons.forEach { button ->
key(button.config.text) {
ActionButton(config = button.config)
}
}
}
}
@Preview
@Composable
private fun Preview_WalletManageButtons_Light(
@PreviewParameter(WalletManageButtonProvider::class) buttons: ImmutableList<WalletManageButton>,
) {
TangemTheme(isDark = false) {
WalletManageButtons(buttons = buttons)
}
}
@Preview
@Composable
private fun Preview_WalletManageButtons_Dark(
@PreviewParameter(WalletManageButtonProvider::class) buttons: ImmutableList<WalletManageButton>,
) {
TangemTheme(isDark = true) {
WalletManageButtons(buttons = buttons)
}
}
private class WalletManageButtonProvider : CollectionPreviewParameterProvider<ImmutableList<WalletManageButton>>(
collection = listOf(WalletPreviewData.manageButtons),
)

View file

@ -0,0 +1,65 @@
package com.tangem.feature.wallet.presentation.wallet.state
import com.tangem.core.ui.components.notifications.NotificationState
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.feature.wallet.impl.R
/**
* Wallet notification component state
*
* @property state state
*
[REDACTED_AUTHOR]
*/
sealed class WalletNotification(open val state: NotificationState) {
/**
* "Backup the card" notification
*
* @property onClick lambda be invoked when notification is clicked
*/
data class NeedToBackup(val onClick: () -> Unit) : WalletNotification(
state = NotificationState.Action(
title = "Backup your card",
iconResId = R.drawable.ic_alert_circle_24,
onClick = onClick,
tint = TangemColorPalette.Amaranth,
),
)
/** "Unreachable networks" notification */
object UnreachableNetworks : WalletNotification(
state = NotificationState.Simple(
title = "Some networks are unreachable",
iconResId = R.drawable.img_attention_20,
tint = null,
),
)
/**
* "Like Tangem App" notification
*
* @property onClick lambda be invoked when notification is clicked
*/
data class LikeTangemApp(val onClick: () -> Unit) : WalletNotification(
state = NotificationState.Action(
title = "Like Tangem App?",
iconResId = R.drawable.ic_star_24,
onClick = onClick,
tint = TangemColorPalette.Tangerine,
),
)
/**
* "Scan the card" notification
*
* @property onClick lambda be invoked when notification is clicked
*/
data class ScanCard(val onClick: () -> Unit) : WalletNotification(
state = NotificationState.Action(
title = "Scan your card to continue",
iconResId = R.drawable.ic_tangem_24,
onClick = onClick,
),
)
}

View file

@ -10,6 +10,7 @@ import kotlinx.collections.immutable.ImmutableList
* @property selectedWallet selected wallet
* @property wallets list of wallets states
* @property contentItems content items
* @property notifications notifications
*
[REDACTED_AUTHOR]
*/
@ -19,16 +20,19 @@ internal sealed class WalletStateHolder(
open val selectedWallet: WalletCardState,
open val wallets: ImmutableList<WalletCardState>,
open val contentItems: ImmutableList<WalletContentItemState>,
open val notifications: ImmutableList<WalletNotification>,
) {
/**
* Multi currency wallet content state
*
* @property onBackClick lambda be invoked when back button is clicked
* @property topBarConfig top bar config
* @property selectedWallet selected wallet
* @property wallets list of wallets states
* @property contentItems content items
* @property onBackClick lambda be invoked when back button is clicked
* @property topBarConfig top bar config
* @property selectedWallet selected wallet
* @property wallets list of wallets states
* @property contentItems content items
* @property notifications notifications
* @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked
*/
data class MultiCurrencyContent(
override val onBackClick: () -> Unit,
@ -36,8 +40,9 @@ internal sealed class WalletStateHolder(
override val selectedWallet: WalletCardState,
override val wallets: ImmutableList<WalletCardState>,
override val contentItems: ImmutableList<WalletContentItemState.MultiCurrencyItem>,
override val notifications: ImmutableList<WalletNotification>,
val onOrganizeTokensClick: () -> Unit,
) : WalletStateHolder(onBackClick, topBarConfig, selectedWallet, wallets, contentItems)
) : WalletStateHolder(onBackClick, topBarConfig, selectedWallet, wallets, contentItems, notifications)
/**
* Single currency wallet content state
@ -47,6 +52,8 @@ internal sealed class WalletStateHolder(
* @property selectedWallet selected wallet
* @property wallets list of wallets states
* @property contentItems content items
* @property notifications notifications
* @property buttons manage buttons
*/
data class SingleCurrencyContent(
override val onBackClick: () -> Unit,
@ -54,5 +61,7 @@ internal sealed class WalletStateHolder(
override val selectedWallet: WalletCardState,
override val wallets: ImmutableList<WalletCardState>,
override val contentItems: ImmutableList<WalletContentItemState.SingleCurrencyItem>,
) : WalletStateHolder(onBackClick, topBarConfig, selectedWallet, wallets, contentItems)
override val notifications: ImmutableList<WalletNotification>,
val buttons: ImmutableList<WalletManageButton>,
) : WalletStateHolder(onBackClick, topBarConfig, selectedWallet, wallets, contentItems, notifications)
}

View file

@ -1,28 +1,26 @@
package com.tangem.feature.wallet.presentation.wallet.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
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 com.tangem.core.ui.components.RoundedActionButton
import com.tangem.core.ui.components.buttons.actions.ActionConfig
import com.tangem.core.ui.components.buttons.actions.RoundedActionButton
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.transactions.Transaction
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
@ -30,9 +28,11 @@ import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.common.component.NetworkGroupItem
import com.tangem.feature.wallet.presentation.common.component.TokenItem
import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState
import com.tangem.feature.wallet.presentation.wallet.state.WalletManageButtons
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletCardsList
import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletTopBar
import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration
/**
* Wallet screen
@ -49,15 +49,29 @@ internal fun WalletScreen(state: WalletStateHolder, modifier: Modifier = Modifie
topBar = { WalletTopBar(config = state.topBarConfig) },
containerColor = TangemTheme.colors.background.secondary,
) { scaffoldPaddings ->
val lastContentItemIndex = remember(state.contentItems) { state.contentItems.lastIndex }
LazyColumn(
modifier = modifier
.padding(scaffoldPaddings)
.padding(paddingValues = scaffoldPaddings)
.fillMaxSize(),
contentPadding = PaddingValues(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing8,
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
item { WalletCardsList(wallets = state.wallets) }
item {
WalletCardsList(
wallets = state.wallets,
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
)
}
if (state is WalletStateHolder.SingleCurrencyContent) {
item { WalletManageButtons(buttons = state.buttons) }
}
items(items = state.notifications, itemContent = { Notification(state = it.state) })
itemsIndexed(
items = state.contentItems,
@ -70,36 +84,19 @@ internal fun WalletScreen(state: WalletStateHolder, modifier: Modifier = Modifie
is WalletContentItemState.SingleCurrencyItem.Transaction -> index
}
},
) { index, item ->
val itemModifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.clipFirstAndLastItems(index, lastContentItemIndex)
when (item) {
is WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle -> {
NetworkGroupItem(networkName = item.networkName, modifier = itemModifier)
}
is WalletContentItemState.MultiCurrencyItem.Token -> {
TokenItem(state = item.state, modifier = itemModifier)
}
is WalletContentItemState.SingleCurrencyItem.Title -> {
SingleCurrencyTitle(config = item, modifier = itemModifier)
}
is WalletContentItemState.SingleCurrencyItem.TransactionGroupTitle -> {
TransactionGroupTitle(config = item, modifier = itemModifier)
}
is WalletContentItemState.SingleCurrencyItem.Transaction -> {
Transaction(state = item.state, modifier = itemModifier)
}
}
}
itemContent = { index, item ->
ContentItem(currentIndex = index, lastIndex = state.contentItems.lastIndex, item = item)
},
)
if (state is WalletStateHolder.MultiCurrencyContent) {
item {
RoundedActionButton(
text = stringResource(id = R.string.organize_tokens_title),
iconResId = R.drawable.ic_filter_24,
onClick = state.onOrganizeTokensClick,
config = ActionConfig(
text = stringResource(id = R.string.organize_tokens_title),
iconResId = R.drawable.ic_filter_24,
onClick = state.onOrganizeTokensClick,
),
modifier = Modifier.padding(top = TangemTheme.dimens.spacing14),
)
}
@ -108,6 +105,29 @@ internal fun WalletScreen(state: WalletStateHolder, modifier: Modifier = Modifie
}
}
@Composable
private fun ContentItem(currentIndex: Int, lastIndex: Int, item: WalletContentItemState) {
val itemModifier = Modifier.walletContentItemDecoration(currentIndex, lastIndex)
when (item) {
is WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle -> {
NetworkGroupItem(networkName = item.networkName, modifier = itemModifier)
}
is WalletContentItemState.MultiCurrencyItem.Token -> {
TokenItem(state = item.state, modifier = itemModifier)
}
is WalletContentItemState.SingleCurrencyItem.Title -> {
SingleCurrencyTitle(config = item, modifier = itemModifier)
}
is WalletContentItemState.SingleCurrencyItem.TransactionGroupTitle -> {
TransactionGroupTitle(config = item, modifier = itemModifier)
}
is WalletContentItemState.SingleCurrencyItem.Transaction -> {
Transaction(state = item.state, modifier = itemModifier)
}
}
}
@Composable
private fun SingleCurrencyTitle(
config: WalletContentItemState.SingleCurrencyItem.Title,
@ -166,31 +186,6 @@ private fun TransactionGroupTitle(
)
}
private fun Modifier.clipFirstAndLastItems(index: Int, lastItemIndex: Int): Modifier = composed {
when (index) {
0 -> {
this
.padding(top = TangemTheme.dimens.spacing14)
.clip(
RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
),
)
}
lastItemIndex -> {
this
.clip(
RoundedCornerShape(
bottomStart = TangemTheme.dimens.radius16,
bottomEnd = TangemTheme.dimens.radius16,
),
)
}
else -> this
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable

View file

@ -23,21 +23,21 @@ import kotlinx.collections.immutable.persistentListOf
/**
* Wallets list
*
* @param wallets list of wallet state
* @param wallets list of wallet state
* @param modifier modifier
*
[REDACTED_AUTHOR]
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun WalletCardsList(wallets: ImmutableList<WalletCardState>) {
internal fun WalletCardsList(wallets: ImmutableList<WalletCardState>, modifier: Modifier = Modifier) {
val horizontalCardPadding = TangemTheme.dimens.spacing16
val itemWidth = LocalConfiguration.current.screenWidthDp.dp - horizontalCardPadding * 2
val lazyListState = rememberLazyListState()
LazyRow(
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
modifier = modifier.background(color = TangemTheme.colors.background.secondary),
state = lazyListState,
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState),
) {

View file

@ -0,0 +1,36 @@
package com.tangem.feature.wallet.presentation.wallet.ui.decorations
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip
import com.tangem.core.ui.res.TangemTheme
/**
[REDACTED_AUTHOR]
*/
internal fun Modifier.walletContentItemDecoration(currentIndex: Int, lastItemIndex: Int): Modifier = composed {
when (currentIndex) {
0 -> {
this
.padding(top = TangemTheme.dimens.spacing14)
.clip(
RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
),
)
}
lastItemIndex -> {
this
.clip(
RoundedCornerShape(
bottomStart = TangemTheme.dimens.radius16,
bottomEnd = TangemTheme.dimens.radius16,
),
)
}
else -> this
}
}

View file

@ -31,6 +31,7 @@ compose-constraint = "1.0.1"
compose-navigation = "2.5.3"
compose-accompanist = "0.30.1"
compose-paging = "1.0.0-alpha18"
compose-reorderable = "0.9.6"
# endregion Compose
# region Other libraries
@ -77,7 +78,7 @@ walletConnectWeb3 = "1.8.0"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "develop-247"
tangemBlockchainSdk = "develop-250"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-266"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds
@ -147,6 +148,7 @@ compose-accompanist-appCompatTheme = { module = "com.google.accompanist:accompan
compose-accompanist-systemUiController = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "compose-accompanist" }
compose-accompanist-webView = { module = "com.google.accompanist:accompanist-webview", version.ref = "compose-accompanist" }
compose-paging = { module = "androidx.paging:paging-compose", version.ref = "compose-paging" }
compose-reorderable = { module = "org.burnoutcrew.composereorderable:reorderable", version.ref = "compose-reorderable" }
# endregion Compose
# region Firebase

View file

@ -1,836 +0,0 @@
build:
maxIssues: 0
config:
validation: true
warningsAsErrors: false
processors:
active: true
console-reports:
active: true
exclude:
- 'ProjectStatisticsReport'
- 'ComplexityReport'
- 'NotificationReport'
- 'FindingsReport'
- 'FileBasedFindingsReport'
# - 'LiteFindingsReport'
output-reports:
active: true
exclude:
- 'HtmlOutputReport'
# - 'TxtOutputReport'
- 'XmlOutputReport'
- 'SarifOutputReport'
- 'MdOutputReport'
comments:
active: false
AbsentOrWrongFileLicense:
active: false
licenseTemplateFile: 'license.template'
licenseTemplateIsRegex: false
CommentOverPrivateFunction:
active: false
CommentOverPrivateProperty:
active: false
DeprecatedBlockTag:
active: false
EndOfSentenceFormat:
active: false
endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
KDocReferencesNonPublicProperty:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**' ]
OutdatedDocumentation:
active: false
matchTypeParameters: true
matchDeclarationsOrder: true
allowParamOnConstructorProperties: false
UndocumentedPublicClass:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**' ]
searchInNestedClass: true
searchInInnerClass: true
searchInInnerObject: true
searchInInnerInterface: true
UndocumentedPublicFunction:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**' ]
UndocumentedPublicProperty:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**' ]
complexity:
active: true
CyclomaticComplexMethod:
active: true
ComplexCondition:
active: true
threshold: 4
ComplexInterface:
active: false
threshold: 10
includeStaticDeclarations: false
includePrivateDeclarations: false
LabeledExpression:
active: false
ignoredLabels: [ ]
LargeClass:
active: true
threshold: 300
LongMethod:
active: true
threshold: 70
LongParameterList:
active: true
functionThreshold: 6
constructorThreshold: 7
ignoreDefaultParameters: true
ignoreDataClasses: true
ignoreAnnotated: [ 'Provides' ]
MethodOverloading:
active: true
threshold: 6
NamedArguments:
active: true
threshold: 3
ignoreArgumentsMatchingNames: false
NestedBlockDepth:
active: true
threshold: 5
NestedScopeFunctions:
active: false
threshold: 1
functions:
- 'kotlin.apply'
- 'kotlin.run'
- 'kotlin.with'
- 'kotlin.let'
- 'kotlin.also'
ReplaceSafeCallChainWithRun:
active: true
StringLiteralDuplication:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**' ]
threshold: 3
ignoreAnnotation: true
excludeStringsWithLessThan5Characters: true
ignoreStringsRegex: '$^'
TooManyFunctions:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**' ]
thresholdInFiles: 20
thresholdInClasses: 20
thresholdInInterfaces: 20
thresholdInObjects: 20
thresholdInEnums: 20
ignoreDeprecated: false
ignorePrivate: true
ignoreOverridden: false
coroutines:
active: true
GlobalCoroutineUsage:
active: true
InjectDispatcher:
active: false #TODO
dispatcherNames:
- 'IO'
- 'Default'
- 'Unconfined'
RedundantSuspendModifier:
active: true
SleepInsteadOfDelay:
active: true
SuspendFunWithCoroutineScopeReceiver:
active: false
SuspendFunWithFlowReturnType:
active: true
empty-blocks:
active: true
EmptyCatchBlock:
active: true
allowedExceptionNameRegex: '_|(ignore|expected).*'
EmptyClassBlock:
active: true
EmptyDefaultConstructor:
active: true
EmptyDoWhileBlock:
active: true
EmptyElseBlock:
active: true
EmptyFinallyBlock:
active: true
EmptyForBlock:
active: true
EmptyFunctionBlock:
active: true
ignoreOverridden: true
EmptyIfBlock:
active: true
EmptyInitBlock:
active: true
EmptyKtFile:
active: true
EmptySecondaryConstructor:
active: true
EmptyTryBlock:
active: true
EmptyWhenBlock:
active: true
EmptyWhileBlock:
active: true
exceptions:
active: true
ExceptionRaisedInUnexpectedLocation:
active: true
methodNames:
- 'equals'
- 'finalize'
- 'hashCode'
- 'toString'
InstanceOfCheckForException:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**' ]
NotImplementedDeclaration:
active: false
ObjectExtendsThrowable:
active: true
PrintStackTrace:
active: true
RethrowCaughtException:
active: true
ReturnFromFinally:
active: true
ignoreLabeled: false
SwallowedException:
active: false
ignoredExceptionTypes:
- 'InterruptedException'
- 'MalformedURLException'
- 'NumberFormatException'
- 'ParseException'
allowedExceptionNameRegex: '_|(ignore|expected).*'
ThrowingExceptionFromFinally:
active: true
ThrowingExceptionInMain:
active: false
ThrowingExceptionsWithoutMessageOrCause:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**' ]
exceptions:
- 'ArrayIndexOutOfBoundsException'
- 'Exception'
- 'IllegalArgumentException'
- 'IllegalMonitorStateException'
- 'IllegalStateException'
- 'IndexOutOfBoundsException'
- 'NullPointerException'
- 'RuntimeException'
- 'Throwable'
ThrowingNewInstanceOfSameException:
active: true
TooGenericExceptionCaught:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**' ]
exceptionNames:
- 'ArrayIndexOutOfBoundsException'
- 'Error'
- 'Exception'
- 'IllegalMonitorStateException'
- 'IndexOutOfBoundsException'
- 'NullPointerException'
- 'RuntimeException'
- 'Throwable'
allowedExceptionNameRegex: '_|(ignore|expected).*'
TooGenericExceptionThrown:
active: true
exceptionNames:
- 'Error'
- 'Exception'
- 'RuntimeException'
- 'Throwable'
naming:
active: true
BooleanPropertyNaming:
active: true
allowedPattern: '^(is|has|are)'
ignoreOverridden: true
ClassNaming:
active: true
classPattern: '[A-Z][a-zA-Z0-9]*'
ConstructorParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
privateParameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
EnumNaming:
active: true
enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
ForbiddenClassName:
active: false
forbiddenName: [ ]
FunctionMaxLength:
active: false
maximumFunctionNameLength: 30
FunctionMinLength:
active: false
minimumFunctionNameLength: 3
FunctionNaming:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**' ]
functionPattern: '[a-z][a-zA-Z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
ignoreAnnotated: [ 'Composable' ]
FunctionParameterNaming:
active: true
parameterPattern: '[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
InvalidPackageDeclaration:
active: true
rootPackage: ''
requireRootInDeclaration: false
LambdaParameterNaming:
active: false
parameterPattern: '[a-z][A-Za-z0-9]*|_'
MatchingDeclarationName: # Same as FileName
active: false
mustBeFirst: true
MemberNameEqualsClassName:
active: false
ignoreOverridden: true
NoNameShadowing:
active: true
NonBooleanPropertyPrefixedWithIs:
active: true
ObjectPropertyNaming:
active: true
constantPattern: '[A-Za-z][_A-Za-z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
PackageNaming:
active: true
packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
TopLevelPropertyNaming:
active: true
constantPattern: '[A-Z][_A-Z0-9]*'
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
VariableMaxLength:
active: false
maximumVariableNameLength: 50
VariableMinLength:
active: false
minimumVariableNameLength: 3
VariableNaming:
active: true
variablePattern: '[a-z][A-Za-z0-9]*'
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
excludeClassPattern: '$^'
ignoreOverridden: true
performance:
active: true
ArrayPrimitive:
active: true
CouldBeSequence:
active: false
threshold: 3
ForEachOnRange:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**' ]
SpreadOperator:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**' ]
UnnecessaryTemporaryInstantiation:
active: true
potential-bugs:
active: true
AvoidReferentialEquality:
active: true
forbiddenTypePatterns:
- 'kotlin.String'
CastToNullableType:
active: true
Deprecation:
active: false
DontDowncastCollectionTypes:
active: true
DoubleMutabilityForCollection:
active: true
mutableTypes:
- 'kotlin.collections.MutableList'
- 'kotlin.collections.MutableMap'
- 'kotlin.collections.MutableSet'
- 'java.util.ArrayList'
- 'java.util.LinkedHashSet'
- 'java.util.HashSet'
- 'java.util.LinkedHashMap'
- 'java.util.HashMap'
ElseCaseInsteadOfExhaustiveWhen:
active: false
EqualsAlwaysReturnsTrueOrFalse:
active: true
EqualsWithHashCodeExist:
active: true
ExitOutsideMain:
active: false
ExplicitGarbageCollectionCall:
active: true
HasPlatformType:
active: true
IgnoredReturnValue:
active: true
returnValueAnnotations:
- '*.CheckResult'
- '*.CheckReturnValue'
ignoreReturnValueAnnotations:
- '*.CanIgnoreReturnValue'
ignoreFunctionCall: [ ]
ImplicitDefaultLocale:
active: true
ImplicitUnitReturnType:
active: false
allowExplicitReturnType: true
InvalidRange:
active: true
IteratorHasNextCallsNextMethod:
active: true
IteratorNotThrowingNoSuchElementException:
active: true
LateinitUsage:
active: false
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**' ]
ignoreAnnotated: [ 'Inject' ]
ignoreOnClassesPattern: ''
MapGetWithNotNullAssertionOperator:
active: true
MissingPackageDeclaration:
active: true
excludes: [ '**/*.kts' ]
NullCheckOnMutableProperty:
active: true
NullableToStringCall:
active: true
UnconditionalJumpStatementInLoop:
active: true
UnnecessaryNotNullOperator:
active: true
UnnecessarySafeCall:
active: true
UnreachableCatchBlock:
active: true
UnreachableCode:
active: true
UnsafeCallOnNullableType:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**' ]
UnsafeCast:
active: true
UnusedUnaryOperator:
active: true
UselessPostfixExpression:
active: true
WrongEqualsTypeParameter:
active: true
style:
active: true
CanBeNonNullable:
active: false
CascadingCallWrapping:
active: false
includeElvis: true
ClassOrdering:
active: true
CollapsibleIfStatements:
active: true
DataClassContainsFunctions:
active: false
conversionFunctionPrefix:
- 'to'
DataClassShouldBeImmutable:
active: false
DestructuringDeclarationWithTooManyEntries:
active: true
maxDestructuringEntries: 3
EqualsNullCall:
active: false
EqualsOnSignatureLine:
active: true
ExplicitCollectionElementAccessMethod:
active: true
ExplicitItLambdaParameter:
active: true
ExpressionBodySyntax:
active: false
includeLineWrapping: true
ForbiddenComment:
active: false
values:
- 'FIXME:'
- 'STOPSHIP:'
- 'TODO:'
allowedPatterns: ''
customMessage: ''
ForbiddenImport:
active: false
imports: [ ]
forbiddenPatterns: ''
ForbiddenMethodCall:
active: false
methods:
- reason: 'print does not allow you to configure the output stream. Use a logger instead.'
value: 'kotlin.io.print'
- reason: 'println does not allow you to configure the output stream. Use a logger instead.'
value: 'kotlin.io.println'
ForbiddenSuppress:
active: false
rules: [ ]
ForbiddenVoid:
active: false
ignoreOverridden: false
ignoreUsageInGenerics: false
FunctionOnlyReturningConstant:
active: true
ignoreOverridableFunction: true
ignoreActualFunction: true
LoopWithTooManyJumpStatements:
active: true
maxJumpCount: 2
MagicNumber:
active: true
excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/*.kts' ]
ignoreNumbers:
- '-1'
- '0'
- '1'
- '2'
ignoreHashCodeFunction: true
ignorePropertyDeclaration: true
ignoreLocalVariableDeclaration: false
ignoreConstantDeclaration: true
ignoreCompanionObjectPropertyDeclaration: true
ignoreAnnotation: false
ignoreNamedArgument: true
ignoreEnums: false
ignoreRanges: false
ignoreExtensionFunctions: true
ignoreAnnotated: [ 'Preview' ]
MandatoryBracesIfStatements:
active: true
MandatoryBracesLoops:
active: true
MaxChainedCallsOnSameLine:
active: true
maxChainedCalls: 5
MaxLineLength:
active: false # Same as MaximumLineLength
maxLineLength: 120
excludePackageStatements: true
excludeImportStatements: true
excludeCommentStatements: false
MayBeConst:
active: true
ModifierOrder:
active: false # Same as ModifierOrdering
MultilineLambdaItParameter:
active: true
NestedClassesVisibility:
active: true
NewLineAtEndOfFile:
active: false # Same as FinalNewline
NoTabs:
active: true
NullableBooleanCheck:
active: true
ObjectLiteralToLambda:
active: true
OptionalAbstractKeyword:
active: true
OptionalUnit:
active: true
OptionalWhenBraces:
active: false
PreferToOverPairSyntax:
active: false
ProtectedMemberInFinalClass:
active: true
RedundantExplicitType:
active: false
RedundantHigherOrderMapUsage:
active: true
RedundantVisibilityModifierRule:
active: true
ReturnCount:
active: false
max: 2
excludedFunctions:
- 'equals'
excludeLabeled: false
excludeReturnFromLambda: true
excludeGuardClauses: false
SafeCast:
active: true
SerialVersionUIDInSerializableClass:
active: true
SpacingBetweenPackageAndImports:
active: true
ThrowsCount:
active: false
max: 2
excludeGuardClauses: false
TrailingWhitespace:
active: false
UnderscoresInNumericLiterals:
active: false
acceptableLength: 4
allowNonStandardGrouping: false
UnnecessaryAbstractClass:
active: true
UnnecessaryAnnotationUseSiteTarget:
active: false
UnnecessaryApply:
active: true
UnnecessaryBackticks:
active: true
UnnecessaryFilter:
active: true
UnnecessaryInheritance:
active: true
UnnecessaryInnerClass:
active: false
UnnecessaryLet:
active: true
UnnecessaryParentheses:
active: true
UntilInsteadOfRangeTo:
active: true
UnusedImports:
active: true
UnusedPrivateClass:
active: true
ignoreAnnotated: [ 'UnusedRequiredComponent' ]
UnusedPrivateMember:
active: true
allowedNames: '(_|ignored|expected|serialVersionUID)'
ignoreAnnotated: [ 'Preview', 'UnusedRequiredComponent' ]
UseAnyOrNoneInsteadOfFind:
active: true
UseArrayLiteralsInAnnotations:
active: true
UseCheckNotNull:
active: true
UseCheckOrError:
active: true
UseDataClass:
active: false
allowVars: false
UseEmptyCounterpart:
active: true
UseIfEmptyOrIfBlank:
active: true
UseIfInsteadOfWhen:
active: false
UseIsNullOrEmpty:
active: true
UseOrEmpty:
active: true
UseRequire:
active: true
UseRequireNotNull:
active: true
UselessCallOnNotNull:
active: true
UtilityClassWithPublicConstructor:
active: true
VarCouldBeVal:
active: true
ignoreLateinitVar: false
WildcardImport:
active: false
excludeImports:
- 'java.util.*'
formatting:
active: true
android: true
AnnotationOnSeparateLine:
active: false
AnnotationSpacing:
active: true
ArgumentListWrapping:
active: true
indentSize: 4
maxLineLength: 120
BlockCommentInitialStarAlignment:
active: true
ChainWrapping:
active: true
CommentSpacing:
active: true
CommentWrapping:
active: false
indentSize: 4
DiscouragedCommentLocation:
active: true
EnumEntryNameCase:
active: true
Filename:
active: true # This rules overlaps with naming>MatchingDeclarationName from the standard rules
mustBeFirst: true
FinalNewline: # This rules overlaps with style>NewLineAtEndOfFile from the standard rules
active: true
insertFinalNewLine: true
FunKeywordSpacing:
active: true
FunctionReturnTypeSpacing:
active: true
FunctionSignature:
active: true
forceMultilineWhenParameterCountGreaterOrEqualThan: 2147483647
functionBodyExpressionWrapping: 'default'
maxLineLength: 120
indentSize: 4
FunctionStartOfBodySpacing:
active: true
FunctionTypeReferenceSpacing:
active: true
ImportOrdering:
active: false
layout: 'java.**,javax.**,kotlin.**,kotlinx.**,android.**,androidx.**,*,^'
Indentation:
active: true
indentSize: 4
KdocWrapping:
active: true
indentSize: 4
MaximumLineLength:
excludes: [ "**/assets/**" ]
active: true # This rules overlaps with style>MaxLineLength from the standard rules
maxLineLength: 120
excludePackageStatements: true
excludeImportStatements: true
excludeCommentStatements: false
ModifierListSpacing:
active: true
ModifierOrdering:
active: true # This rules overlaps with style>ModifierOrder from the standard rules
MultiLineIfElse:
active: true
NoBlankLineBeforeRbrace:
active: true
NoBlankLinesInChainedMethodCalls:
active: true
NoConsecutiveBlankLines:
active: true
NoEmptyClassBody:
active: true
NoEmptyFirstLineInMethodBlock:
active: true
NoLineBreakAfterElse:
active: true
NoLineBreakBeforeAssignment:
active: true
NoMultipleSpaces:
active: true
NoSemicolons:
active: true
NoTrailingSpaces:
active: true
NoUnitReturn:
active: true
NoUnusedImports:
active: true
NoWildcardImports:
active: false
packagesToUseImportOnDemandProperty: 'java.util.*,kotlinx.android.synthetic.**'
NullableTypeSpacing:
active: true
PackageName:
active: true
ParameterListSpacing:
active: true
ParameterListWrapping:
active: true
maxLineLength: 120
SpacingAroundAngleBrackets:
active: true
SpacingAroundColon:
active: true
SpacingAroundComma:
active: true
SpacingAroundCurly:
active: true
SpacingAroundDot:
active: true
SpacingAroundDoubleColon:
active: true
SpacingAroundKeyword:
active: true
SpacingAroundOperators:
active: true
SpacingAroundParens:
active: true
SpacingAroundRangeOperator:
active: true
SpacingAroundUnaryOperator:
active: true
SpacingBetweenDeclarationsWithAnnotations:
active: true
SpacingBetweenDeclarationsWithComments:
active: false
SpacingBetweenFunctionNameAndOpeningParenthesis:
active: true
StringTemplate:
active: true
TrailingCommaOnCallSite:
active: true
useTrailingCommaOnCallSite: true
TrailingCommaOnDeclarationSite:
active: true
useTrailingCommaOnDeclarationSite: true
TypeArgumentListSpacing:
active: true
TypeParameterListSpacing:
active: true
UnnecessaryParenthesesBeforeTrailingLambda:
active: true
Wrapping:
active: true
indentSize: 4
compose:
ReusedModifierInstance:
active: true
UnnecessaryEventHandlerParameter:
active: true
ComposableEventParameterNaming:
active: true
ComposableParametersOrdering:
active: true
ModifierDefaultValue:
active: true
MissingModifierDefaultValue:
active: true
ModifierHeightWithText:
active: true
ModifierParameterPosition:
active: true
PublicComposablePreview:
active: true
TopLevelComposableFunctions:
active: true
ComposeFunctionName:
active: true

View file

@ -22,7 +22,7 @@ private fun DetektExtension.configure(project: Project) {
ignoreFailures = false
autoCorrect = true
buildUponDefaultConfig = true
config.setFrom(project.rootProject.files("plugins/configuration/detekt.yml"))
config.setFrom(project.rootProject.files("tangem-android-tools/detekt-config.yml"))
}
private fun Project.configureDetektPlugins() {

1
tangem-android-tools Submodule

@ -0,0 +1 @@
Subproject commit 03186c35c13d8693d9a4c7dcb6e760fa58984e67