Updated on 2026-08-14

This commit is contained in:
Tangem 2021-03-17 16:13:28 +03:00
commit 427fe69313
72 changed files with 1305 additions and 159 deletions

View file

@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.android.material.snackbar.Snackbar
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.store
@ -30,6 +31,9 @@ abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
store.dispatch(NavigationAction.PopBackTo())
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

View file

@ -141,12 +141,12 @@ sealed class SendAction : SendScreenAction {
data class SendError(override val error: TapError) : SendAction(), ErrorAction
sealed class Dialog : SendAction() {
data class ShowTezosWarningDialog(
data class TezosWarningDialog(
val reduceCallback: () -> Unit,
val sendAllCallback: () -> Unit,
val reduceAmount: BigDecimal,
) : Dialog()
data class SendTransactionFails(val errorMessage: String): Dialog()
object Hide : Dialog()
}
data class SetWarnings(val warningList: List<WarningMessage>) : SendAction()

View file

@ -1,19 +1,17 @@
package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.common.network.Result
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.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
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.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.TransactionExtrasAction
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
@ -50,7 +48,7 @@ internal class AddressPayIdMiddleware {
private fun setAddressAndCheck(data: String, isUserInput: Boolean, dispatch: (Action) -> Unit) {
val potentialPayId = data.toLowerCase()
if (PayIdManager.isPayId(potentialPayId) && isPayIdEnabled()) {
if (isPayIdEnabled() && PayIdManager.isPayId(potentialPayId)) {
dispatch(SetPayIdWalletAddress(potentialPayId, "", isUserInput))
} else {
dispatch(SetWalletAddress(data, isUserInput))
@ -64,7 +62,7 @@ internal class AddressPayIdMiddleware {
val addressPayId = sendState.addressPayIdState.normalFieldValue ?: return
val isUserInput = sendState.addressPayIdState.viewFieldValue.isFromUserInput
if (PayIdManager.isPayId(addressPayId) && isPayIdEnabled()) {
if (isPayIdEnabled() && PayIdManager.isPayId(addressPayId)) {
verifyPayId(addressPayId, wallet, isUserInput, dispatch)
} else {
verifyAddress(addressPayId, wallet, isUserInput, dispatch)
@ -110,7 +108,11 @@ internal class AddressPayIdMiddleware {
}
private fun verifyAddress(address: String, wallet: Wallet, isUserInput: Boolean, dispatch: (Action) -> Unit) {
val addressSchemeSplit = address.split(":")
val addressSchemeSplit = if (wallet.blockchain == Blockchain.BitcoinCash) {
listOf(address)
} else {
address.split(":")
}
val noSchemeAddress = when (addressSchemeSplit.size) {
1 -> address // no scheme
2 -> { // scheme
@ -131,6 +133,9 @@ internal class AddressPayIdMiddleware {
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(wallet, supposedAddress)
if (failReason == null) {
noSchemeAddress.getQueryParameter("amount")?.toBigDecimalOrNull()?.let {
dispatch(AmountAction.SetAmount(it, false))
}
dispatch(SetWalletAddress(supposedAddress, isUserInput))
dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, null))
} else {
@ -141,7 +146,7 @@ internal class AddressPayIdMiddleware {
private fun isValidBlockchainAddressAndNotTheSameAsWallet(wallet: Wallet, address: String): Error? {
return if (wallet.blockchain.validateAddress(address)) {
if (wallet.addresses.all { it.value != address } ) {
if (wallet.addresses.all { it.value != address }) {
null
} else {
Error.ADDRESS_SAME_AS_WALLET
@ -151,14 +156,10 @@ internal class AddressPayIdMiddleware {
}
}
//TODO: move to the blockchainSDK
private fun extractAddressFromShareUri(shareUri: String): String {
val sharePrefix = listOf("bitcoin:", "ethereum:", "xrpl:", "litecoin:", "bnb:")
val prefixes = sharePrefix.filter { shareUri.contains(it) }
return if (prefixes.isEmpty()) shareUri else shareUri.replace(prefixes[0], "")
}
private fun String.removeShareUriQuery(): String = this.substringBefore("?")
private fun String.getQueryParameter(name: String): String? {
return this.substringAfter("?").splitToMap("&", "=")[name]
}
private fun verifyClipboard(input: String?, appState: AppState?, dispatch: DispatchFunction) {
val addressPayId = input ?: return
@ -186,4 +187,11 @@ internal class AddressPayIdMiddleware {
private fun isPayIdEnabled(): Boolean {
return store.state.globalState.configManager?.config?.isSendingToPayIdEnabled ?: false
}
}
fun String.splitToMap(firstDelimiter: String, secondDelimiter: String): Map<String, String> {
return this.split(firstDelimiter)
.map { it.split(secondDelimiter) }
.map { it.first() to it.last().toString() }
.toMap()
}

View file

@ -22,6 +22,7 @@ import com.tangem.tap.features.send.redux.states.SendButtonState
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@ -52,7 +53,7 @@ val sendMiddleware: Middleware<AppState> = { dispatch, appState ->
}
private fun verifyAndSendTransaction(
action: SendActionUi.SendAmountToRecipient, appState: AppState?, dispatch: (Action) -> Unit
action: SendActionUi.SendAmountToRecipient, appState: AppState?, dispatch: (Action) -> Unit,
) {
val sendState = appState?.sendState ?: return
val walletManager = appState.globalState.scanNoteResponse?.walletManager ?: return
@ -68,7 +69,7 @@ private fun verifyAndSendTransaction(
when {
hadTezosError -> {
val reduceAmount = walletManager.wallet.blockchain.minimalAmount()
dispatch(SendAction.Dialog.ShowTezosWarningDialog(reduceCallback = {
dispatch(SendAction.Dialog.TezosWarningDialog(reduceCallback = {
dispatch(AmountAction.SetAmount(typedAmount.value!!.minus(reduceAmount), false))
dispatch(AmountActionUi.CheckAmountToSend)
}, sendAllCallback = {
@ -94,7 +95,7 @@ private fun sendTransaction(
destinationAddress: String,
transactionExtras: TransactionExtrasState,
card: Card,
dispatch: (Action) -> Unit
dispatch: (Action) -> Unit,
) {
dispatch(SendAction.ChangeSendButtonState(SendButtonState.PROGRESS))
var txData = walletManager.createTransaction(amountToSend, feeAmount, destinationAddress)
@ -144,6 +145,8 @@ private fun sendTransaction(
when {
message == null -> {
dispatch(SendAction.SendError(TapError.UnknownError))
updateFeedbackManager(walletManager, amountToSend, feeAmount, destinationAddress, card)
dispatch(SendAction.Dialog.SendTransactionFails("unknown error"))
}
message.contains("50002") -> {
// user was cancelled the operation by closing the Sdk bottom sheet
@ -157,6 +160,8 @@ private fun sendTransaction(
Timber.e(throwable)
FirebaseCrashlytics.getInstance().recordException(throwable)
dispatch(SendAction.SendError(TapError.CustomError(message)))
updateFeedbackManager(walletManager, amountToSend, feeAmount, destinationAddress, card)
dispatch(SendAction.Dialog.SendTransactionFails(message))
}
}
}
@ -168,6 +173,29 @@ private fun sendTransaction(
}
}
private fun updateFeedbackManager(
walletManager: WalletManager,
amountToSend: Amount,
feeAmount: Amount,
destinationAddress: String,
card: Card,
) {
val infoHolder = store.state.globalState.feedbackManager?.infoHolder ?: return
val amountState = store.state.sendState.amountState
infoHolder.cardId = card.cardId
infoHolder.blockchain = walletManager.wallet.blockchain
infoHolder.sourceAddress = walletManager.wallet.address
infoHolder.destinationAddress = destinationAddress
infoHolder.amount = amountToSend.value?.stripZeroPlainString() ?: "0"
infoHolder.fee = feeAmount.value?.stripZeroPlainString() ?: "0"
infoHolder.cardFirmwareVersion = card.firmwareVersion.version
if (amountState.typeOfAmount is AmountType.Token) {
infoHolder.token = amountState.amountToExtract?.currencySymbol ?: ""
}
// infoHolder.transactionHex = ""
}
fun extractErrorsForAmountField(errors: EnumSet<TransactionError>): EnumSet<TransactionError> {
val showIntoAmountField = EnumSet.noneOf(TransactionError::class.java)
errors.forEach {

View file

@ -42,7 +42,8 @@ private class SendReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
val result = when (action) {
is SendAction.ChangeSendButtonState -> sendState.copy(sendButtonState = action.state)
is SendAction.Dialog.ShowTezosWarningDialog -> sendState.copy(dialog = action)
is SendAction.Dialog.TezosWarningDialog -> sendState.copy(dialog = action)
is SendAction.Dialog.SendTransactionFails -> sendState.copy(dialog = action)
is SendAction.Dialog.Hide -> sendState.copy(dialog = null)
is SendAction.SetWarnings -> sendState.copy(sendWarningsList = action.warningList)
else -> return sendState

View file

@ -149,9 +149,14 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
val scannedCode = data?.getStringExtra(ScanQrCodeActivity.SCAN_RESULT) ?: ""
if (scannedCode.isEmpty()) return
store.dispatch(PasteAddressPayId(scannedCode))
store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused))
store.dispatch(FeeAction.RequestFee)
// 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
imvQrCode.postDelayed({
store.dispatch(PasteAddressPayId(scannedCode))
store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused))
store.dispatch(FeeAction.RequestFee)
}, 200)
}
private fun setupAmountLayout() {

View file

@ -0,0 +1,30 @@
package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.feedback.SendTransactionFailedEmail
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class SendTransactionFailsDialog {
companion object {
fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.alert_failed_to_send_transaction_title)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, dialog.errorMessage))
setNeutralButton(R.string.alert_button_send_feedback) { _, _ ->
store.dispatch(GlobalAction.SendFeedback(SendTransactionFailedEmail(dialog.errorMessage)))
}
setPositiveButton(R.string.common_no) { _, _ -> }
setOnDismissListener { store.dispatch(WalletAction.HideDialog) }
}.create()
}
}
}

View file

@ -9,10 +9,10 @@ import com.tangem.wallet.R
class TezosWarningDialog(context: Context) : AlertDialog(context) {
companion object {
fun create(context: Context, showDialogData: SendAction.Dialog.ShowTezosWarningDialog): AlertDialog {
fun create(context: Context, showDialogData: SendAction.Dialog.TezosWarningDialog): AlertDialog {
val reduceAmount = showDialogData.reduceAmount.toPlainString()
return Builder(context).apply {
setTitle(context.getString(R.string.common_warning))
setTitle(R.string.common_warning)
setMessage(context.getString(R.string.xtz_withdrawal_message_warning, reduceAmount))
setNegativeButton(R.string.xtz_withdrawal_message_ignore) { _, _ ->
showDialogData.sendAllCallback()

View file

@ -21,6 +21,7 @@ import com.tangem.tap.features.send.redux.reducers.ReceiptReducer
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.SendTransactionFailsDialog
import com.tangem.tap.features.send.ui.dialogs.TezosWarningDialog
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.store
@ -97,12 +98,18 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
val sendFragment = (fg as? SendFragment) ?: return
when (state.dialog) {
is SendAction.Dialog.ShowTezosWarningDialog -> {
is SendAction.Dialog.TezosWarningDialog -> {
if (dialog == null) {
dialog = TezosWarningDialog.create(fg.requireContext(), state.dialog)
dialog?.show()
}
}
is SendAction.Dialog.SendTransactionFails -> {
if (dialog == null) {
dialog = SendTransactionFailsDialog.create(fg.requireContext(), state.dialog)
dialog?.show()
}
}
else -> {
dialog?.dismiss()
dialog = null
@ -184,22 +191,6 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
val amountToSend = state.viewAmountValue
if (!amountToSend.isFromUserInput) fg.etAmountToSend.update(amountToSend.value)
// fg.tvAmountToSendShadow.text = amountToSend
// if (amountToSend.length > 10) {
// post is needed to wait for text size changes
// fg.tvAmountToSendShadow.post {
// fg.etAmountToSend.setTextSize(TypedValue.COMPLEX_UNIT_PX, fg.tvAmountToSendShadow.textSize - 2)
// fg.etAmountToSend.update(amountToSend)
// if (!state.cursorAtTheSamePosition) fg.etAmountToSend.setSelection(amountToSend.length)
// }
// } else {
// val textSize = fg.resources.getDimension(R.dimen.text_size_amount_to_send)
// fg.tvAmountToSendShadow.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
// fg.etAmountToSend.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
// fg.etAmountToSend.update(amountToSend)
// if (!state.cursorAtTheSamePosition) fg.etAmountToSend.setSelection(amountToSend.length)
// }
fg.tvAmountCurrency.update(state.mainCurrency.currencySymbol)
(fg as? SendFragment)?.saveMainCurrency(state.mainCurrency.type)