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 com.tangem.commands.common.card.Card
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.network.coinmarketcap.FiatCurrency
@ -19,6 +20,7 @@ sealed class DetailsAction : Action {
val scanNoteResponse: ScanNoteResponse,
val wallets: List<Wallet>,
val isCreatingTwinWalletAllowed: Boolean?,
val cardTou: CardTou,
val fiatCurrencyName: FiatCurrencyName,
val fiatCurrencies: List<FiatCurrencyName>? = null,
) : DetailsAction()

View file

@ -75,6 +75,7 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: Deta
),
securityScreenState = SecurityScreenState(currentOption = securityOption),
createTwinWalletState = twinsState,
cardTermsOfUseUrl = action.cardTou.getUrl(action.card)
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.details.redux
import android.net.Uri
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.entities.Button
@ -18,13 +19,14 @@ data class DetailsState(
val eraseWalletState: EraseWalletState? = null,
val confirmScreenState: ConfirmScreenState? = null,
val securityScreenState: SecurityScreenState? = null,
val createTwinWalletState: CreateTwinWalletState? = null
val createTwinWalletState: CreateTwinWalletState? = null,
val cardTermsOfUseUrl: Uri? = null,
) : StateType
data class CardInfo(
val cardId: String,
val issuer: String,
val signedHashes: Int
val signedHashes: Int,
)
enum class EraseWalletState { Allowed, NotAllowedByCard, NotEmpty }
@ -33,7 +35,7 @@ data class SecurityScreenState(
val currentOption: SecurityOption = SecurityOption.LongTap,
val selectedOption: SecurityOption = currentOption,
val allowedOptions: EnumSet<SecurityOption> = EnumSet.allOf(SecurityOption::class.java),
val buttonProceed: Button = Button(true)
val buttonProceed: Button = Button(true),
)
enum class SecurityOption { LongTap, PassCode, AccessCode }

View file

@ -18,7 +18,7 @@ class CreateTwinWalletMiddleware {
fun handle(action: DetailsAction.CreateTwinWalletAction) {
when (action) {
is DetailsAction.CreateTwinWalletAction.ShowWarning -> {
val wallet = store.state.detailsState.wallets.getOrNull(0)
val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet
if (wallet == null) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWalletWarning))
return

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.details.ui
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
@ -7,6 +8,7 @@ import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.twins.getTwinCardIdForUser
import com.tangem.tap.domain.twins.isTwinCard
@ -14,6 +16,7 @@ import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.feedback.FeedbackEmail
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details.*
@ -79,6 +82,13 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
tv_disclaimer.setOnClickListener { store.dispatch(DetailsAction.ShowDisclaimer) }
tv_card_tou.show(state.cardTermsOfUseUrl != null)
tv_card_tou.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = state.cardTermsOfUseUrl
startActivity(intent)
}
if (state.createTwinWalletState != null) {
if (state.createTwinWalletState.allowRecreatingWallet == true) {
tv_erase_wallet.show()
@ -106,6 +116,9 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
tv_app_currency_title.setOnClickListener {
store.dispatch(DetailsAction.AppCurrencyAction.ChooseAppCurrency)
}
tv_send_feedback.setOnClickListener {
store.dispatch(GlobalAction.SendFeedback(FeedbackEmail()))
}
tv_security_title.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)

View file

@ -0,0 +1,206 @@
package com.tangem.tap.features.feedback
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.tangem.LogMessage
import com.tangem.LoggerInterface
import com.tangem.blockchain.common.Blockchain
import timber.log.Timber
import java.io.File
import java.io.FileWriter
import java.io.StringWriter
/**
[REDACTED_AUTHOR]
*/
class FeedbackManager(
val infoHolder: AdditionalEmailInfo,
private val context: Context,
private val logCollector: TangemLogCollector,
private val email: String = "support@tangem.com",
) {
fun send(emailData: EmailData) {
val fileLog = if (emailData is ScanFailsEmail) createLogFile() else null
sendTo(email, emailData.subject, emailData.joinTogether(infoHolder), fileLog)
}
private fun sendTo(email: String, subject: String, message: String, fileLog: File? = null) {
val intent = Intent(Intent.ACTION_SEND).apply {
data = Uri.parse("mailto:")
type = "text/plain"
putExtra(Intent.EXTRA_EMAIL, arrayOf(email))
putExtra(Intent.EXTRA_SUBJECT, subject)
putExtra(Intent.EXTRA_TEXT, message)
fileLog?.let {
val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", it)
putExtra(Intent.EXTRA_STREAM, uri)
}
}
try {
val chooserIntent = Intent.createChooser(intent, "Send mail...")
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
ContextCompat.startActivity(context, chooserIntent, null)
} catch (ex: ActivityNotFoundException) {
Timber.e(ex)
}
}
private fun createLogFile(): File? {
return try {
val file = File(context.filesDir, "logs.txt")
file.delete()
file.createNewFile()
val stringWriter = StringWriter()
logCollector.getLogs().forEach { stringWriter.append(it) }
val fileWriter = FileWriter(file)
fileWriter.write(stringWriter.toString())
fileWriter.close()
logCollector.clearLogs()
file
} catch (ex: Exception) {
Timber.e(ex, "Can't create a file for email attachment")
null
}
}
}
class TangemLogCollector : LoggerInterface {
private val logs = mutableListOf<String>()
override fun e(logTag: String, message: String) {}
override fun i(logTag: String, message: String) {}
override fun v(logTag: String, message: String) {}
override fun write(message: LogMessage) {
logs.add(message.message)
}
fun getLogs(): List<String> = logs.toList()
fun clearLogs() {
logs.clear()
}
}
class AdditionalEmailInfo {
var cardId: String = ""
var cardFirmwareVersion: String = ""
var blockchain: Blockchain = Blockchain.Unknown
var phoneModel: String = Build.MODEL
var osVersion: String = Build.VERSION.SDK_INT.toString()
var appVersion: String = ""
var token: String = ""
var sourceAddress: String = ""
var destinationAddress: String = ""
var amount: String = ""
var fee: String = ""
// var transactionHex: String = ""
var signedHashesCount: String = ""
var explorerLink: String = ""
// var outputsCount: String = ""
fun updateAppVersion(context: Context) {
try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
appVersion = pInfo.versionName
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
}
}
interface EmailData {
val subject: String
val mainMessage: String
fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String
fun joinTogether(infoHolder: AdditionalEmailInfo): String {
return "$mainMessage\n\n\n\n\n" +
"Following information is optional. You can erase it if you dont want to share it.\n" +
createOptionalMessage(infoHolder)
}
}
class RateCanBeBetterEmail : EmailData {
override val subject: String = "My suggestions"
override val mainMessage: String = "Tell us what functions you are missing, and we will try to help you."
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
return StringBuilder().apply {
appendKeyValue("Card ID", infoHolder.cardId)
appendKeyValue("Blockchain", infoHolder.blockchain.fullName)
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
appendKeyValue("App version", infoHolder.appVersion)
}.toString()
}
}
class ScanFailsEmail : EmailData {
override val subject: String = "Cant scan a card"
override val mainMessage: String = "Please tell us what card do you have?"
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
return StringBuilder().apply {
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
appendKeyValue("App version", infoHolder.appVersion)
}.toString()
}
}
class SendTransactionFailedEmail(private val error: String) : EmailData {
override val subject: String = "Cant send a transaction"
override val mainMessage: String = "Please tell us more about your issue. Every small detail can help."
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
return StringBuilder().apply {
appendKeyValue("Error", error)
appendKeyValue("Card ID", infoHolder.cardId)
appendKeyValue("Blockchain", infoHolder.blockchain.fullName)
appendKeyValue("Token", infoHolder.token)
appendKeyValue("Source address", infoHolder.sourceAddress)
appendKeyValue("Destination address", infoHolder.destinationAddress)
appendKeyValue("Amount", infoHolder.amount)
appendKeyValue("Fee", infoHolder.fee)
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
appendKeyValue("App version", infoHolder.appVersion)
appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
// appendKeyValue("Transaction HEX", infoHolder.transactionHex)
}.toString()
}
}
class FeedbackEmail : EmailData {
override val subject: String = "Tangem Tap feedback"
override val mainMessage: String = "Hi Tangem,"
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
return StringBuilder().apply {
appendKeyValue("Card ID", infoHolder.cardId)
appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
appendKeyValue("Blockchain", infoHolder.blockchain.fullName)
appendKeyValue("Wallet address", infoHolder.sourceAddress)
appendKeyValue("Explorer link", infoHolder.explorerLink)
// appendKeyValue("Outputs count", infoHolder.outputsCount)
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
}.toString()
}
}
fun StringBuilder.appendKeyValue(key: String, value: String): StringBuilder {
return this.append("$key: $value\n")
}

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)

View file

@ -2,6 +2,9 @@ package com.tangem.tap.features.wallet.redux
import android.content.Context
import com.tangem.blockchain.common.*
import com.tangem.TangemError
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.address.AddressType
import com.tangem.commands.common.card.Card
import com.tangem.tap.common.redux.NotificationAction
@ -30,7 +33,8 @@ sealed class WalletAction : Action {
data class SetArtworkId(val artworkId: String?) : WalletAction()
// sealed class ProcessWallet : WalletAction() {
// sealed class ProcessWallet : WalletAction() {
// data class LoadWallet(val artworkId: String?, val allowTopUp: Boolean
// ) : ProcessWallet() {
// data class Success(val wallet: Wallet) : ProcessWallet()
@ -45,7 +49,6 @@ sealed class WalletAction : Action {
// }
// }
sealed class MultiWallet : WalletAction() {
data class SetIsMultiwalletAllowed(val isMultiwalletAllowed: Boolean) : MultiWallet()
data class AddWalletManagers(val walletManagers: List<WalletManager>) : MultiWallet()
@ -68,7 +71,15 @@ sealed class WalletAction : Action {
object SaveCardId : CheckSignedHashes()
}
data class SetWarnings(val warningList: List<WarningMessage>) : WalletAction()
object Warnings : WalletAction() {
object CheckIfNeeded : WalletAction()
data class SetWarnings(val warningList: List<WarningMessage>) : WalletAction()
object AppRating : WalletAction() {
object SetNeverToShow : WalletAction()
object RemindLater : WalletAction()
}
}
data class UpdateWallet(val currency: CryptoCurrencyName? = null) : WalletAction() {
object ScheduleUpdatingWallet : WalletAction()
@ -88,40 +99,59 @@ sealed class WalletAction : Action {
object Failure : WalletAction()
}
data class CopyAddress(val address: String, val context: Context) : WalletAction() {
object Success : WalletAction(), NotificationAction {
override val messageResource = R.string.wallet_notification_address_copied
}
}
data class ShareAddress(val address: String, val context: Context) : WalletAction()
object ShowQrCode : WalletAction()
object HideDialog : WalletAction()
data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction()
object CreateWallet : WalletAction()
object EmptyWallet : WalletAction()
object Scan : WalletAction()
class ScanCardFinished(val scanError: TangemError? = null) : WalletAction()
data class Send(val amount: Amount? = null) : WalletAction() {
data class ChooseCurrency(val amounts: List<Amount>?) : WalletAction()
object Cancel : WalletAction()
}
sealed class TopUpAction : WalletAction() {
data class TopUp(val context: Context, val toolbarColor: Int) : TopUpAction()
}
object CreatePayId : WalletAction() {
data class CompleteCreatingPayId(val payId: String) : WalletAction()
data class Success(val payId: String) : WalletAction()
object EmptyField : WalletAction(), ErrorAction {
override val error = TapError.PayIdEmptyField
}
data class ChangeSelectedAddress(val type: AddressType) : WalletAction()
data class CopyAddress(val address: String, val context: Context) : WalletAction() {
object Success : WalletAction(), NotificationAction {
override val messageResource = R.string.wallet_notification_address_copied
}
}
sealed class TwinsAction : WalletAction() {
object ShowOnboarding : TwinsAction()
object SetOnboardingShown : TwinsAction()
data class SetTwinCard(
val secondCardId: String, val number: TwinCardNumber,
val isCreatingTwinCardsAllowed: Boolean
) : TwinsAction()
}
}
data class ShareAddress(val address: String, val context: Context) : WalletAction()
object ShowDialog : WalletAction() {
object QrCode : WalletAction()
object ScanFails : WalletAction()
}
object HideDialog : WalletAction()
data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction()
object CreateWallet : WalletAction()
object EmptyWallet : WalletAction()
object Scan : WalletAction()
data class Send(val amount: Amount? = null) : WalletAction() {
data class ChooseCurrency(val amounts: List<Amount>?) : WalletAction()
object Cancel : WalletAction()
}
sealed class TopUpAction : WalletAction() {
data class TopUp(val context: Context, val toolbarColor: Int) : TopUpAction()
}
data class ChangeSelectedAddress(val type: AddressType) : WalletAction()
sealed class TwinsAction : WalletAction() {
object ShowOnboarding : TwinsAction()
object SetOnboardingShown : TwinsAction()
data class SetTwinCard(
val secondCardId: String, val number: TwinCardNumber,
val isCreatingTwinCardsAllowed: Boolean,
) : TwinsAction()
}
}

View file

@ -0,0 +1,356 @@
package com.tangem.tap.features.wallet.redux
import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import com.tangem.TangemSdkError
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.commands.common.card.Card
import com.tangem.commands.common.card.CardType
import com.tangem.commands.common.network.Result
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.getType
import com.tangem.common.extensions.toHexString
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.isGreaterThan
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.TopUpHelper
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.NetworkStateChanged
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.Middleware
import java.math.BigDecimal
class WalletMiddleware {
private val topUpMiddleware = TopUpMiddleware()
val walletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is WalletAction.TopUpAction -> topUpMiddleware.handle(action)
is WalletAction.LoadWallet -> {
scope.launch {
store.state.globalState.tapWalletManager.loadWalletData()
}
}
is WalletAction.LoadPayId -> {
scope.launch {
store.state.globalState.tapWalletManager.loadPayId()
}
}
is WalletAction.LoadFiatRate -> {
scope.launch {
store.state.globalState.tapWalletManager.loadFiatRate(store.state.globalState.appCurrency)
}
}
is WalletAction.CreateWallet -> {
if (store.state.walletState.twinCardsState != null) {
store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning(
store.state.globalState.scanNoteResponse?.card?.cardId?.let {
TwinsHelper.getTwinCardNumber(it)
},
CreateTwinWallet.CreateWallet
))
} else {
scope.launch {
val result = tangemSdkManager.createWallet(
store.state.globalState.scanNoteResponse?.card?.cardId
)
when (result) {
is CompletionResult.Success -> {
store.state.globalState.tapWalletManager
.onCardScanned(result.data)
}
}
}
}
}
is WalletAction.UpdateWallet -> {
if (store.state.walletState.state == ProgressState.Done) {
scope.launch { store.state.globalState.tapWalletManager.updateWallet() }
}
}
is WalletAction.UpdateWallet.Success -> setupWalletUpdate(action.wallet)
is WalletAction.LoadWallet.Success -> {
store.dispatch(WalletAction.CheckHashesCountOnline)
if (!store.state.walletState.updatingWallet) setupWalletUpdate(action.wallet)
tryToShowAppRatingWarning(action.wallet)
}
is WalletAction.CreatePayId.CompleteCreatingPayId -> {
scope.launch {
val cardId = store.state.globalState.scanNoteResponse?.card?.cardId
val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet
val publicKey = store.state.globalState.scanNoteResponse?.card?.cardPublicKey
if (cardId != null && wallet != null && publicKey != null) {
val result = PayIdManager().setPayId(
cardId, publicKey.toHexString(),
action.payId, wallet.address, wallet.blockchain
)
withContext(Dispatchers.Main) {
when (result) {
is Result.Success ->
store.dispatch(WalletAction.CreatePayId.Success(action.payId))
is Result.Failure -> {
val error = result.error as? TapError
?: TapError.PayIdCreatingError
store.dispatch(WalletAction.CreatePayId.Failure(error))
}
}
}
}
}
}
is WalletAction.Scan -> {
scope.launch {
val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler)
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card)
store.state.globalState.tapWalletManager
.onCardScanned(result.data, true)
if (store.state.walletState.twinCardsState != null) {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding))
}
}
store.dispatch(WalletAction.ScanCardFinished())
}
is CompletionResult.Failure -> {
if (result.error !is TangemSdkError.UserCancelled) {
// Weird things... If you run the code below without coroutines,
// then rescanning will be impossible
scope.launch(Dispatchers.Main) {
store.dispatch(WalletAction.ScanCardFinished(result.error))
if (store.state.walletState.scanCardFailsCounter >= 2) {
store.dispatch(WalletAction.ShowDialog.ScanFails)
}
}
}
}
}
}
}
is WalletAction.LoadData -> {
scope.launch {
store.state.globalState.scanNoteResponse?.let {
store.state.globalState.tapWalletManager.loadData(it)
}
}
}
is NetworkStateChanged -> {
store.state.globalState.scanNoteResponse?.let { scanNoteResponse ->
store.dispatch(WalletAction.CheckHashesCountOnline)
scope.launch {
store.state.globalState.tapWalletManager.onCardScanned(scanNoteResponse)
}
}
}
is WalletAction.CopyAddress -> {
store.state.walletState.walletAddresses?.selectedAddress?.address?.let {
action.context.copyToClipboard(it)
store.dispatch(WalletAction.CopyAddress.Success)
}
}
is WalletAction.ExploreAddress -> {
val uri = Uri.parse(store.state.walletState.walletAddresses?.selectedAddress?.exploreUrl)
val intent = Intent(Intent.ACTION_VIEW, uri)
ContextCompat.startActivity(action.context, intent, null)
}
is WalletAction.Send -> {
val newAction = prepareSendAction(action.amount)
store.dispatch(newAction)
if (newAction is PrepareSendScreen) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
}
}
is WalletAction.Warnings.CheckIfNeeded -> {
val globalState = store.state.globalState
val validator = globalState.scanNoteResponse?.walletManager as? SignatureCountValidator
globalState.scanNoteResponse?.card?.let { card ->
store.state.globalState.warningManager?.removeWarnings(WarningMessage.Origin.Local)
if (card.getType() != CardType.Release) addWarningMessage(WarningMessagesManager.devCardWarning())
if (!preferencesStorage.wasCardScannedBefore(card.cardId)) {
checkIfWarningNeeded(card, validator)?.let { addWarningMessage(it) }
}
updateWarningMessages()
}
}
is WalletAction.CheckHashesCountOnline -> checkHashesCountOnline()
is WalletAction.SaveCardId -> {
val cardId = store.state.globalState.scanNoteResponse?.card?.cardId
cardId?.let { preferencesStorage.saveScannedCardId(it) }
}
is WalletAction.TwinsAction.SetTwinCard -> {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) store.dispatch(WalletAction.TwinsAction.ShowOnboarding)
}
is WalletAction.TwinsAction.SetOnboardingShown -> {
preferencesStorage.saveTwinsOnboardingShown()
}
is WalletAction.Warnings.AppRating.RemindLater -> {
preferencesStorage.appRatingLaunchObserver.applyDelayedShowing()
}
is WalletAction.Warnings.AppRating.SetNeverToShow -> {
preferencesStorage.appRatingLaunchObserver.setNeverToShow()
}
}
next(action)
}
}
}
private fun tryToShowAppRatingWarning(wallet: Wallet) {
val nonZeroWalletsCount = wallet.amounts.filter {
it.value.value?.isGreaterThan(BigDecimal.ZERO) ?: false
}.size
if (nonZeroWalletsCount > 0) {
preferencesStorage.appRatingLaunchObserver.foundWalletWithFunds()
}
if (preferencesStorage.appRatingLaunchObserver.isReadyToShow()) {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_DISPLAYED)
addWarningMessage(WarningMessagesManager.appRatingWarning(), true)
}
}
private fun setupWalletUpdate(wallet: Wallet) {
if (!wallet.recentTransactions.toPendingTransactions(wallet.address).isNullOrEmpty()) {
store.dispatch(WalletAction.UpdateWallet.ScheduleUpdatingWallet)
scope.launch(Dispatchers.IO) {
delay(10000)
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.UpdateWallet)
}
}
}
}
private fun prepareSendAction(amount: Amount?): Action {
return if (amount != null) {
if (amount.type is AmountType.Token) {
PrepareSendScreen(store.state.walletState.wallet?.amounts?.get(AmountType.Coin), amount)
} else {
PrepareSendScreen(amount)
}
} else {
val amounts = store.state.walletState.wallet?.amounts?.toSendableAmounts()
if (amounts?.size ?: 0 > 1) {
WalletAction.Send.ChooseCurrency(amounts)
} else {
val amountToSend = amounts?.first()
PrepareSendScreen(amountToSend)
}
}
}
private fun checkIfWarningNeeded(
card: Card, signatureCountValidator: SignatureCountValidator? = null,
): WarningMessage? {
if (card.isTwinCard()) return null
return if (signatureCountValidator == null) {
if (card.walletSignedHashes ?: 0 > 0) {
WarningMessagesManager.alreadySignedHashesWarning()
} else {
store.dispatch(WalletAction.SaveCardId)
null
}
} else {
store.dispatch(WalletAction.NeedToCheckHashesCountOnline)
null
}
}
private fun checkHashesCountOnline() {
if (store.state.walletState.hashesCountVerified != false) return
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return
val card = store.state.globalState.scanNoteResponse?.card
if (card == null || preferencesStorage.wasCardScannedBefore(card.cardId)) return
if (card.isTwinCard()) return
val validator = store.state.globalState.scanNoteResponse?.walletManager
as? SignatureCountValidator
scope.launch {
val result = validator?.validateSignatureCount(card.walletSignedHashes ?: 0)
withContext(Dispatchers.Main) {
when (result) {
SimpleResult.Success -> {
store.dispatch(WalletAction.ConfirmHashesCount)
store.dispatch(WalletAction.SaveCardId)
}
is SimpleResult.Failure ->
if (result.error is BlockchainSdkError.SignatureCountNotMatched) {
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true)
} else if (card.walletSignedHashes ?: 0 > 0) {
addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true)
}
}
}
}
}
private fun addWarningMessage(warning: WarningMessage, autoUpdate: Boolean = false) {
store.state.globalState.warningManager?.addWarning(warning)
if (autoUpdate) updateWarningMessages()
}
private fun updateWarningMessages() {
val warningManager = store.state.globalState.warningManager ?: return
store.dispatch(WalletAction.Warnings.SetWarnings(
warningManager.getWarnings(WarningMessage.Location.MainScreen)))
}
}
private class TopUpMiddleware {
fun handle(action: WalletAction.TopUpAction) {
when (action) {
is WalletAction.TopUpAction.TopUp -> {
val config = store.state.globalState.configManager?.config ?: return
val addresses = store.state.walletState.walletAddresses ?: return
if (addresses.list.isEmpty()) return
val defaultAddress = addresses.list[0].address
val url = TopUpHelper.getUrl(
store.state.walletState.currencyData.currencySymbol!!,
defaultAddress,
config.moonPayApiKey,
config.moonPayApiSecretKey
)
val customTabsIntent = CustomTabsIntent.Builder()
.setToolbarColor(action.toolbarColor)
.build()
customTabsIntent.launchUrl(action.context, Uri.parse(url));
}
}
}
}

View file

@ -213,7 +213,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
}
newState = newState.copy(cardImage = Artwork(artworkId = artworkUrl))
}
is WalletAction.ShowQrCode -> {
is WalletAction.ShowDialog.QrCode -> {
val selectedWalletData = newState.getWalletData(newState.selectedWallet)
newState = newState.copy(
walletDialog = WalletDialog.QrDialog(
@ -223,6 +223,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
)
}
is WalletAction.ShowDialog.ScanFails -> {
newState = newState.copy(walletDialog = WalletDialog.ScanFailsDialog)
}
is WalletAction.HideDialog -> {
newState = newState.copy(walletDialog = null)
}
@ -232,7 +235,10 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
}
is WalletAction.Send.Cancel -> newState = newState.copy(walletDialog = null)
is WalletAction.SetWarnings -> newState = newState.copy(mainWarningsList = action.warningList)
is WalletAction.Warnings.SetWarnings -> newState = newState.copy(mainWarningsList = action.warningList)
is WalletAction.TopUpAction -> {
newState = newState.copy(topUpState = handleTopUpActions(action, newState.topUpState))
}
is WalletAction.TopUpAction -> return newState
is WalletAction.ChangeSelectedAddress -> {
val selectedWalletData = newState.getWalletData(newState.selectedWallet)
@ -246,6 +252,13 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
newState = newState.copy(wallets = wallets)
}
is WalletAction.ScanCardFinished -> {
newState = if (action.scanError == null) {
newState.copy(scanCardFailsCounter = 0)
} else {
newState.copy(scanCardFailsCounter = newState.scanCardFailsCounter + 1)
}
}
}
return newState
}

View file

@ -25,6 +25,7 @@ data class WalletState(
val walletDialog: WalletDialog? = null,
val twinCardsState: TwinCardsState? = null,
val mainWarningsList: List<WarningMessage> = mutableListOf(),
val scanCardFailsCounter: Int = 0,
val wallets: List<WalletData> = emptyList(),
val walletManagers: List<WalletManager> = emptyList(),
val isMultiwalletAllowed: Boolean = false,
@ -101,6 +102,7 @@ sealed class WalletDialog {
) : WalletDialog()
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog()
object ScanFailsDialog: WalletDialog()
}
enum class ProgressState { Loading, Done, Error }

View file

@ -19,6 +19,8 @@ import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.adapters.SpacesItemDecoration
@ -26,6 +28,10 @@ import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView
import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView
import com.tangem.tap.features.wallet.ui.wallet.WalletView
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
import com.tangem.tap.features.wallet.ui.dialogs.PayIdDialog
import com.tangem.tap.features.wallet.ui.dialogs.QrDialog
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_wallet.*
@ -160,6 +166,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
scanNoteResponse.card, scanNoteResponse,
store.state.walletState.walletManagers.map { it.wallet },
store.state.globalState.configManager?.config?.isCreatingTwinCardsAllowed,
CardTou(),
store.state.globalState.appCurrency
))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
@ -176,3 +183,78 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
}
//TODO: handle addition of testnets
//class MultipleAddressUiHelper {
// companion object {
// fun typeToId(type: AddressType): Int {
// return when (type) {
// is BitcoinAddressType.Legacy -> R.id.chip_legacy
// is BitcoinAddressType.Segwit -> R.id.chip_default
// is CardanoAddressType.Byron -> R.id.chip_legacy
// is CardanoAddressType.Shelley -> R.id.chip_default
// else -> View.NO_ID
// }
// }
//
// fun idToType(id: Int, blockchain: Blockchain?): AddressType? {
// return when (id) {
// R.id.chip_default -> {
// when (blockchain) {
// Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> BitcoinAddressType.Segwit
// Blockchain.CardanoShelley -> CardanoAddressType.Shelley
// else -> null
// }
// }
// R.id.chip_legacy -> {
// when (blockchain) {
// Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> BitcoinAddressType.Legacy
// Blockchain.CardanoShelley -> CardanoAddressType.Byron
// else -> null
// }
// }
// else -> null
// }
// }
// }
//}
//TODO: handle scan failed dialog:
//private fun handleDialogs(walletDialog: WalletDialog?) {
// when (walletDialog) {
// is WalletDialog.QrDialog -> {
// if (walletDialog.qrCode != null && walletDialog.shareUrl != null) {
// if (dialog == null) dialog = QrDialog(requireContext()).apply {
// this.showQr(
// walletDialog.qrCode, walletDialog.shareUrl, walletDialog.currencyName
// )
// }
// }
// }
// is WalletDialog.CreatePayIdDialog -> {
// when (walletDialog.creatingPayIdState) {
// CreatingPayIdState.EnterPayId -> {
// if (dialog == null) dialog = PayIdDialog(requireContext()).apply {
// this.show()
// }
// (dialog as? PayIdDialog)?.stopProgress()
// }
// CreatingPayIdState.Waiting -> (dialog as? PayIdDialog)?.showProgress()
// }
// }
// is WalletDialog.SelectAmountToSendDialog -> {
// if (dialog == null) dialog = AmountToSendDialog(requireContext()).apply {
// this.show(walletDialog.amounts)
// }
// }
// is WalletDialog.ScanFailsDialog -> {
// if (dialog == null) dialog = ScanFailsDialog.create(requireContext()).apply {
// this.show()
// }
// }
// null -> {
// dialog?.dismiss()
// dialog = null
// }
// }
//}

View file

@ -8,10 +8,16 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ItemDecoration
import com.google.android.play.core.review.ReviewManagerFactory
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
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.redux.global.GlobalAction
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.features.feedback.RateCanBeBetterEmail
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.layout_warning.view.*
@ -40,7 +46,7 @@ class WarningMessageVH(val view: View) : RecyclerView.ViewHolder(view) {
fun bind(warning: WarningMessage) {
setBgColor(warning.priority)
setText(warning)
setupOkButton(warning)
setupControlButtons(warning)
}
private fun setText(warning: WarningMessage) {
@ -59,10 +65,50 @@ class WarningMessageVH(val view: View) : RecyclerView.ViewHolder(view) {
view.card_view.setCardBackgroundColor(view.context.resources.getColor(color))
}
private fun setupOkButton(warning: WarningMessage) {
view.btn_got_it.show(warning.type == WarningMessage.Type.Temporary)
view.btn_got_it.setOnClickListener {
store.dispatch(GlobalAction.HideWarningMessage(warning))
private fun setupControlButtons(warning: WarningMessage) {
when (warning.type) {
WarningMessage.Type.Permanent -> {
view.group_controls_temporary.hide()
view.group_controls_rating.hide()
}
WarningMessage.Type.Temporary -> {
view.group_controls_rating.hide()
view.group_controls_temporary.show()
view.btn_got_it.setOnClickListener { store.dispatch(GlobalAction.HideWarningMessage(warning)) }
}
WarningMessage.Type.AppRating -> {
view.group_controls_temporary.hide()
view.group_controls_rating.show()
view.btn_close.setOnClickListener {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_DISMISS)
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(WalletAction.Warnings.AppRating.RemindLater)
}
view.btn_can_be_better.setOnClickListener {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_NEGATIVE)
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(GlobalAction.SendFeedback(RateCanBeBetterEmail()))
}
view.btn_really_cool.setOnClickListener {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_POSITIVE)
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
val context = view.context
val reviewManager = ReviewManagerFactory.create(context)
val flow = reviewManager.requestReviewFlow()
flow.addOnCompleteListener {
if (it.isSuccessful) {
// val info = it.result
// Toast.makeText(context, "success", Toast.LENGTH_SHORT).show()
} else {
// Toast.makeText(context, "fail", Toast.LENGTH_SHORT).show()
}
}.addOnFailureListener {
// Toast.makeText(context, "failure", Toast.LENGTH_SHORT).show()
}
store.dispatch(GlobalAction.HideWarningMessage(warning))
}
}
}
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.tap.features.wallet.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.ScanFailsEmail
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class ScanFailsDialog {
companion object {
fun create(context: Context): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.common_warning))
setMessage(R.string.alert_troubleshooting_scan_card_title)
setPositiveButton(R.string.alert_button_request_support) { _, _ ->
store.dispatch(GlobalAction.SendFeedback(ScanFailsEmail()))
}
setNegativeButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatch(WalletAction.HideDialog) }
}.create()
}
}
}