Updated on 2026-08-14

This commit is contained in:
Tangem 2021-03-10 17:09:14 +00:00
commit b2e5a764ae
33 changed files with 703 additions and 63 deletions

View file

@ -70,6 +70,8 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.android.material:material:1.2.1'
implementation 'com.google.android.play:core:1.9.1'
implementation 'com.google.android.play:core-ktx:1.8.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
implementation 'com.tangem:blockchain:1.143.0'

View file

@ -81,6 +81,17 @@
</activity>
<activity android:name="com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>

View file

@ -4,6 +4,7 @@ import android.app.Application
import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.google.firebase.remoteconfig.ktx.remoteConfigSettings
import com.tangem.Log
import com.tangem.tap.common.images.PicassoHelper
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
@ -13,6 +14,9 @@ import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader
import com.tangem.tap.domain.configurable.config.FeaturesRemoteLoader
import com.tangem.tap.domain.configurable.warningMessage.RemoteWarningLoader
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.feedback.AdditionalEmailInfo
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.feedback.TangemLogCollector
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.createMoshi
import com.tangem.tap.persistence.PreferencesStorage
@ -46,10 +50,10 @@ class TapApplication : Application() {
preferencesStorage = PreferencesStorage(this)
PicassoHelper.initPicassoWithCaching(this)
initFeedbackManager()
loadConfigs()
}
private fun loadConfigs() {
val moshi = createMoshi()
val localLoader = FeaturesLocalLoader(this, moshi)
@ -59,4 +63,15 @@ class TapApplication : Application() {
val warningsManager = WarningMessagesManager(RemoteWarningLoader(moshi))
warningsManager.load { store.dispatch(GlobalAction.SetWarningManager(warningsManager)) }
}
private fun initFeedbackManager() {
val infoHolder = AdditionalEmailInfo()
infoHolder.updateAppVersion(this)
val logWriter = TangemLogCollector()
Log.addLogger(logWriter)
val feedbackManager = FeedbackManager(infoHolder, this, logWriter)
store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager))
}
}

View file

@ -4,4 +4,8 @@ enum class AnalyticsEvent(val event: String) {
CARD_IS_SCANNED("card_is_scanned"),
TRANSACTION_IS_SENT("transaction_is_sent"),
READY_TO_SCAN("ready_to_scan"),
APP_RATING_DISPLAYED("rate_app_warning_displayed"),
APP_RATING_DISMISS("dismiss_rate_app_warning"),
APP_RATING_NEGATIVE("negative_rate_app_feedback"),
APP_RATING_POSITIVE("positive_rate_app_feedback"),
}

View file

@ -4,5 +4,5 @@ import com.tangem.commands.common.card.Card
interface AnalyticsHandler {
fun triggerEvent(event: AnalyticsEvent, card: Card?)
fun triggerEvent(event: AnalyticsEvent, card: Card? = null)
}

View file

@ -8,8 +8,7 @@ import com.tangem.commands.common.card.Card
object FirebaseAnalyticsHandler : AnalyticsHandler {
override fun triggerEvent(event: AnalyticsEvent, card: Card?) {
Firebase.analytics
.logEvent(event.event, setCardData(card))
Firebase.analytics.logEvent(event.event, setCardData(card))
}
fun logException(name: String, throwable: Throwable) {

View file

@ -5,6 +5,8 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.feedback.EmailData
import com.tangem.tap.features.feedback.FeedbackManager
import org.rekotlin.Action
import java.math.BigDecimal
@ -19,8 +21,12 @@ sealed class GlobalAction : Action {
data class Success(val appCurrency: FiatCurrencyName) : GlobalAction()
}
data class UpdateWalletSignedHashes(val walletSignedHashes: Int?) : GlobalAction()
data class SetConfigManager(val configManager: ConfigManager) : GlobalAction()
data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction()
data class HideWarningMessage(val warning: WarningMessage) : GlobalAction()
data class UpdateSecurityOptions(val securityOption: SecurityOption) : GlobalAction()
data class SetConfigManager(val configManager: ConfigManager) : GlobalAction()
data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction()
data class SetFeedbackManager(val feedbackManager: FeedbackManager): GlobalAction()
data class SendFeedback(val emailData: EmailData): GlobalAction()
}

View file

@ -26,11 +26,17 @@ val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
store.dispatch(WalletAction.SaveCardId)
}
store.dispatch(WalletAction.SetWarnings(it.getWarnings(WarningMessage.Location.MainScreen)))
store.dispatch(SendAction.SetWarnings(it.getWarnings(WarningMessage.Location.SendScreen)))
store.dispatch(WalletAction.Warnings.SetWarnings(
it.getWarnings(WarningMessage.Location.MainScreen)))
store.dispatch(SendAction.SetWarnings(
it.getWarnings(WarningMessage.Location.SendScreen)))
}
}
}
is GlobalAction.SendFeedback -> {
store.state.globalState.feedbackManager?.send(action.emailData)
}
}
nextDispatch(action)
}

View file

@ -57,6 +57,9 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
globalState
}
}
is GlobalAction.SetFeedbackManager -> {
globalState.copy(feedbackManager = action.feedbackManager)
}
else -> globalState
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import org.rekotlin.StateType
import java.math.BigDecimal
@ -20,6 +21,7 @@ data class GlobalState(
val conversionRates: ConversionRates = ConversionRates(emptyMap()),
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val feedbackManager: FeedbackManager? = null,
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY
) : StateType

View file

@ -84,23 +84,10 @@ class TapWalletManager {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.CARD_IS_SCANNED, data.card)
}
TapWorkarounds.updateCard(data.card)
store.state.globalState.warningManager?.setBlockchain(data.walletManager?.wallet?.blockchain)
val configManager = store.state.globalState.configManager
if (TapWorkarounds.isStart2Coin) {
configManager?.turnOff(ConfigManager.isWalletPayIdEnabled)
configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled)
configManager?.turnOff(ConfigManager.isTopUpEnabled)
} else if (data.walletManager?.wallet?.blockchain == Blockchain.Bitcoin
|| data.card.cardData?.blockchainName == Blockchain.Bitcoin.id){
configManager?.turnOff(ConfigManager.isWalletPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
} else {
configManager?.resetToDefault(ConfigManager.isWalletPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
}
updateConfigManager(data)
updateFeedbackManager(data)
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.ResetState)
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
@ -117,9 +104,40 @@ class TapWalletManager {
}
}
private fun updateConfigManager(data: ScanNoteResponse) {
val configManager = store.state.globalState.configManager
if (TapWorkarounds.isStart2Coin) {
configManager?.turnOff(ConfigManager.isWalletPayIdEnabled)
configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled)
configManager?.turnOff(ConfigManager.isTopUpEnabled)
} else if (data.walletManager?.wallet?.blockchain == Blockchain.Bitcoin
|| data.card.cardData?.blockchainName == Blockchain.Bitcoin.id) {
configManager?.turnOff(ConfigManager.isWalletPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
} else {
configManager?.resetToDefault(ConfigManager.isWalletPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
}
}
private fun updateFeedbackManager(data: ScanNoteResponse) {
val card = data.card
val wallet = data.walletManager?.wallet ?: return
val infoHolder = store.state.globalState.feedbackManager?.infoHolder ?: return
infoHolder.cardId = card.cardId
infoHolder.cardFirmwareVersion = card.firmwareVersion.version
infoHolder.signedHashesCount = card.walletSignedHashes?.toString() ?: "0"
infoHolder.sourceAddress = wallet.address
infoHolder.explorerLink = wallet.getExploreUrl(wallet.address)
infoHolder.blockchain = wallet.blockchain
}
suspend fun loadData(data: ScanNoteResponse) {
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.CheckIfWarningNeeded)
store.dispatch(WalletAction.Warnings.CheckIfNeeded)
val artworkId = data.verifyResponse?.artworkInfo?.id
if (data.walletManager != null) {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {

View file

@ -40,7 +40,9 @@ data class WarningMessage(
Permanent, // нельзя скрыть
@Json(name = "temporary")
Temporary // можно скрыть (кнопка ОК)
Temporary, // можно скрыть (кнопка ОК)
AppRating
}
enum class Location {

View file

@ -51,7 +51,8 @@ class WarningMessagesManager(
val foundWarning = findWarning(warning)
return when {
foundWarning == null -> false
foundWarning.type == WarningMessage.Type.Temporary -> {
foundWarning.type == WarningMessage.Type.Temporary
|| foundWarning.type == WarningMessage.Type.AppRating -> {
if (foundWarning.isHidden) {
false
} else {
@ -101,7 +102,19 @@ class WarningMessagesManager(
WarningMessage.Origin.Local
)
fun isAlreadySignedHashesWarning(warning: WarningMessage):Boolean {
fun appRatingWarning(): WarningMessage = WarningMessage(
"",
"",
WarningMessage.Type.AppRating,
WarningMessage.Priority.Info,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.warning_rate_app_title,
R.string.warning_rate_app_message,
WarningMessage.Origin.Local
)
fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean {
return warning.messageResId == R.string.alert_card_signed_transactions
}
}

View file

@ -8,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
@ -15,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.*
@ -115,6 +117,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

@ -139,12 +139,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

@ -21,6 +21,7 @@ import com.tangem.tap.features.send.redux.FeeAction.RequestFee
import com.tangem.tap.features.send.redux.states.SendButtonState
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -50,7 +51,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
@ -66,7 +67,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 = {
@ -92,7 +93,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)
@ -136,19 +137,23 @@ 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
}
// make it easier latter by handling an appropriate enumError or, like on iOS,
// accept a string identifier of the error message
message.contains("Target account is not created. To create account send 1+ XLM.")-> {
message.contains("Target account is not created. To create account send 1+ XLM.") -> {
dispatch(SendAction.SendError(TapError.XmlError.AssetAccountNotCreated))
}
else -> {
Timber.e(throwable)
FirebaseCrashlytics.getInstance().recordException(throwable)
dispatch(SendAction.SendError(TapError.CustomError(message)))
updateFeedbackManager(walletManager, amountToSend, feeAmount, destinationAddress, card)
dispatch(SendAction.Dialog.SendTransactionFails(message))
}
}
}
@ -160,6 +165,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

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

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.wallet.redux
import android.content.Context
import com.tangem.TangemError
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.address.AddressType
@ -24,20 +25,28 @@ sealed class WalletAction : Action {
}
data class LoadWallet(
val wallet: Wallet, val artworkId: String?, val allowTopUp: Boolean
val wallet: Wallet, val artworkId: String?, val allowTopUp: Boolean,
) : WalletAction() {
data class Success(val wallet: Wallet) : WalletAction()
data class NoAccount(val amountToCreateAccount: String) : WalletAction()
data class Failure(val errorMessage: String? = null) : WalletAction()
}
object CheckIfWarningNeeded : WalletAction()
object CheckHashesCountOnline : WalletAction()
object NeedToCheckHashesCountOnline : WalletAction()
object ConfirmHashesCount : WalletAction()
data class SetWarnings(val warningList: List<WarningMessage>) : WalletAction()
object SaveCardId : WalletAction()
object Warnings : WalletAction() {
object CheckIfNeeded : WalletAction()
data class SetWarnings(val warningList: List<WarningMessage>) : WalletAction()
object AppRating : WalletAction() {
object SetNeverToShow : WalletAction()
object RemindLater : WalletAction()
}
}
object UpdateWallet : WalletAction() {
object ScheduleUpdatingWallet : WalletAction()
data class Success(val wallet: Wallet) : WalletAction()
@ -63,6 +72,8 @@ sealed class WalletAction : Action {
}
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()
@ -85,8 +96,12 @@ sealed class WalletAction : Action {
}
}
object ShowQrCode : WalletAction()
object ShowDialog : WalletAction() {
object QrCode : WalletAction()
object ScanFails : WalletAction()
}
object HideDialog : WalletAction()
data class ExploreAddress(val context: Context) : WalletAction()
object CreateWallet : WalletAction()
object EmptyWallet : WalletAction()
@ -102,7 +117,7 @@ sealed class WalletAction : Action {
object SetOnboardingShown : TwinsAction()
data class SetTwinCard(
val secondCardId: String, val number: TwinCardNumber,
val isCreatingTwinCardsAllowed: Boolean
val isCreatingTwinCardsAllowed: Boolean,
) : TwinsAction()
}
}

View file

@ -4,6 +4,7 @@ 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
@ -12,8 +13,10 @@ 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
@ -41,6 +44,7 @@ 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()
@ -97,6 +101,7 @@ class WalletMiddleware {
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 {
@ -136,6 +141,19 @@ class WalletMiddleware {
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)
}
}
}
}
}
}
@ -173,7 +191,7 @@ class WalletMiddleware {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
}
}
is WalletAction.CheckIfWarningNeeded -> {
is WalletAction.Warnings.CheckIfNeeded -> {
val globalState = store.state.globalState
val validator = globalState.scanNoteResponse?.walletManager as? SignatureCountValidator
globalState.scanNoteResponse?.card?.let { card ->
@ -184,7 +202,6 @@ class WalletMiddleware {
}
updateWarningMessages()
}
}
is WalletAction.CheckHashesCountOnline -> checkHashesCountOnline()
is WalletAction.SaveCardId -> {
@ -198,12 +215,31 @@ class WalletMiddleware {
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)
@ -290,7 +326,8 @@ class WalletMiddleware {
private fun updateWarningMessages() {
val warningManager = store.state.globalState.warningManager ?: return
store.dispatch(WalletAction.SetWarnings(warningManager.getWarnings(WarningMessage.Location.MainScreen)))
store.dispatch(WalletAction.Warnings.SetWarnings(
warningManager.getWarnings(WarningMessage.Location.MainScreen)))
}
}

View file

@ -176,7 +176,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
}
newState = newState.copy(cardImage = Artwork(artworkId = artworkUrl))
}
is WalletAction.ShowQrCode -> {
is WalletAction.ShowDialog.QrCode -> {
newState = newState.copy(
walletDialog = WalletDialog.QrDialog(
newState.walletAddresses?.selectedAddress?.shareUrl?.toQrCode(),
@ -185,6 +185,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)
}
@ -215,7 +218,7 @@ 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.NeedToCheckHashesCountOnline ->
newState = newState.copy(hashesCountVerified = false)
is WalletAction.ConfirmHashesCount ->
@ -254,6 +257,13 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
twinCardsState = newState.twinCardsState?.copy(showTwinOnboarding = false)
)
}
is WalletAction.ScanCardFinished -> {
newState = if (action.scanError == null) {
newState.copy(scanCardFailsCounter = 0)
} else {
newState.copy(scanCardFailsCounter = newState.scanCardFailsCounter + 1)
}
}
}
return newState
}
@ -288,7 +298,7 @@ private fun handleTopUpActions(action: WalletAction.TopUpAction, state: TopUpSta
}
private fun onWalletLoaded(
wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null
wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null,
): WalletState {
val fiatCurrencySymbol = store.state.globalState.appCurrency
val token = wallet.getFirstToken()

View file

@ -28,7 +28,8 @@ data class WalletState(
val mainButton: WalletMainButton = WalletMainButton.SendButton(false),
val topUpState: TopUpState = TopUpState(),
val twinCardsState: TwinCardsState? = null,
val mainWarningsList: List<WarningMessage> = mutableListOf()
val mainWarningsList: List<WarningMessage> = mutableListOf(),
val scanCardFailsCounter: Int = 0,
) : StateType {
val showDetails: Boolean =
currencyData.status != com.tangem.tap.features.wallet.ui.BalanceStatus.EmptyCard &&
@ -52,6 +53,7 @@ sealed class WalletDialog {
data class CreatePayIdDialog(val creatingPayIdState: CreatingPayIdState?) : WalletDialog()
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog()
data class TwinsOnboardingFragment(val secondCardId: String): WalletDialog()
object ScanFailsDialog: WalletDialog()
}
enum class ProgressState { Loading, Done, Error }

View file

@ -27,6 +27,7 @@ import com.tangem.tap.features.wallet.redux.*
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.card_balance.*
@ -209,7 +210,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
btn_copy.setOnClickListener { store.dispatch(WalletAction.CopyAddress(requireContext())) }
btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowQrCode) }
btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowDialog.QrCode) }
val buttonTitle = when (state.mainButton) {
is WalletMainButton.SendButton -> R.string.wallet_button_send
@ -302,6 +303,11 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
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(GlobalAction.HideWarningMessage(warning))
store.dispatch(GlobalAction.SendFeedback(RateCanBeBetterEmail()))
}
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
view.btn_really_cool.setOnClickListener {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.APP_RATING_POSITIVE)
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))
}
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
}
}
}
}

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

View file

@ -11,16 +11,18 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import java.util.*
class PreferencesStorage(applicationContext: Application) {
private val preferences: SharedPreferences by lazy {
applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
}
private val preferences: SharedPreferences = applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
val appRatingLaunchObserver: AppRatingLaunchObserver
init {
incrementLaunchCounter()
appRatingLaunchObserver = AppRatingLaunchObserver(preferences, getCountOfLaunches())
}
private val fiatCurrenciesAdapter: JsonAdapter<List<FiatCurrency>> by lazy {
@ -97,4 +99,55 @@ class PreferencesStorage(applicationContext: Application) {
private const val APP_LAUNCH_COUNT_KEY = "launchCount"
}
}
class AppRatingLaunchObserver(
private val preferences: SharedPreferences,
private val launchCounts: Int,
) {
private val K_SHOW_RATING_AT_LAUNCH_COUNT = "showRatingDialogAtLaunchCount"
private val K_FUNDS_FOUND_DATE = "aFundsFoundDate"
private val deferShowing = 20
private val firstShowing = 3
private var fundsFoundDate: Calendar? = null
init {
val dateTimeMs = preferences.getLong(K_FUNDS_FOUND_DATE, -1)
if (dateTimeMs > 0) fundsFoundDate = Calendar.getInstance().apply { timeInMillis = dateTimeMs }
}
fun foundWalletWithFunds() {
if (fundsFoundDate != null) return
fundsFoundDate = Calendar.getInstance()
preferences.edit().putLong(K_FUNDS_FOUND_DATE, fundsFoundDate!!.timeInMillis).apply()
updateNextShowing(launchCounts + firstShowing)
}
fun isReadyToShow(): Boolean {
val fundsDate = fundsFoundDate ?: return false
val diff = Calendar.getInstance().timeInMillis - fundsDate.timeInMillis
val diffInDays = diff / (100 * 60 * 60 * 24)
if (diffInDays >= firstShowing) return true
return launchCounts >= getCounterOfNextShowing()
}
fun applyDelayedShowing() {
if (getCounterOfNextShowing() < launchCounts) updateNextShowing(launchCounts + deferShowing)
}
fun setNeverToShow() {
updateNextShowing(999999999)
}
private fun updateNextShowing(at: Int) {
preferences.edit().putInt(K_SHOW_RATING_AT_LAUNCH_COUNT, at).apply()
}
private fun getCounterOfNextShowing(): Int {
return preferences.getInt(K_SHOW_RATING_AT_LAUNCH_COUNT, firstShowing)
}
}

View file

@ -185,6 +185,19 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_settings_title" />
<TextView
android:id="@+id/tv_send_feedback"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="@string/details_row_title_send_feedback"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_app_currency_title" />
<TextView
android:id="@+id/tv_card_title"
android:layout_width="wrap_content"
@ -196,7 +209,7 @@
android:textColor="@color/colorSecondary"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_app_currency_title" />
app:layout_constraintTop_toBottomOf="@id/tv_send_feedback" />
<View
android:layout_width="match_parent"

View file

@ -20,14 +20,25 @@
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginEnd="8dp"
android:text="Test title"
android:textColor="@android:color/white"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn_close"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/btn_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:tint="@android:color/white"
app:layout_constraintBottom_toBottomOf="@+id/tv_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/tv_title"
app:srcCompat="@drawable/ic_close" />
<TextView
android:id="@+id/tv_message"
android:layout_width="0dp"
@ -54,9 +65,46 @@
android:text="@string/how_to_got_it_button"
android:textAllCaps="false"
android:textColor="@android:color/white"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_message" />
<Button
android:id="@+id/btn_can_be_better"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/warning_button_can_be_better"
android:textAllCaps="false"
android:textColor="@android:color/white"
app:layout_constraintEnd_toStartOf="@+id/btn_really_cool"
app:layout_constraintTop_toBottomOf="@+id/tv_message" />
<Button
android:id="@+id/btn_really_cool"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/warning_button_really_cool"
android:textAllCaps="false"
android:textColor="@android:color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_message" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_controls_temporary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="btn_got_it" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_controls_rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="btn_close, btn_can_be_better, btn_really_cool" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View file

@ -39,5 +39,23 @@ this wallet.
<string name="send_error_invalid_destination_tag" translatable="false">Invalid destination tag. It won\'t be added to the transaction</string>
<string name="send_error_invalid_memo_id" translatable="false">Invalid Memo ID. It won\'t be added to the transaction</string>
<string name="details_section_title_app" translatable="false">App</string>
<string name="details_row_title_send_feedback" translatable="false">Send feedback</string>
<string name="alert_app_feedback_sent_title" translatable="false">Sent successfully</string>
<string name="alert_app_feedback_sent_message" translatable="false">Thank you for your feedback</string>
<string name="alert_negative_app_rate_sent_title" translatable="false">Your suggestions were sent</string>
<string name="alert_negative_app_rate_sent_message" translatable="false">Thank your for your feedback. We will response as soon as possible</string>
<string name="alert_failed_to_send_email_title" translatable="false">Failed to send email</string>
<string name="alert_failed_to_send_email_message" translatable="false">Reason: %s</string>
<string name="alert_failed_to_send_transaction_title" translatable="false">Cant send a transaction</string>
<string name="alert_failed_to_send_transaction_message" translatable="false">Reason: %s. Do you want to send feedback?</string>
<string name="alert_troubleshooting_scan_card_title" translatable="false">Are you having difficulty scanning your card?</string>
<string name="alert_troubleshooting_scan_card_message" translatable="false">Please try to tap the card exactly as shown in the animation or request support.</string>
<string name="alert_button_try_again" translatable="false">Try again</string>
<string name="alert_button_request_support" translatable="false">Request support</string>
<string name="alert_button_send_feedback" translatable="false">Send feedback</string>
<string name="warning_button_really_cool" translatable="false">Really cool!</string>
<string name="warning_button_can_be_better" translatable="false">Can be better</string>
<string name="warning_rate_app_title" translatable="false">One question</string>
<string name="warning_rate_app_message" translatable="false">How do you like Tangem?</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="files" path="/"/>
</paths>