Updated on 2026-08-14
This commit is contained in:
commit
b2e5a764ae
33 changed files with 703 additions and 63 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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"),
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,9 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
|
|||
globalState
|
||||
}
|
||||
}
|
||||
is GlobalAction.SetFeedbackManager -> {
|
||||
globalState.copy(feedbackManager = action.feedbackManager)
|
||||
}
|
||||
else -> globalState
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ data class WarningMessage(
|
|||
Permanent, // нельзя скрыть
|
||||
|
||||
@Json(name = "temporary")
|
||||
Temporary // можно скрыть (кнопка ОК)
|
||||
Temporary, // можно скрыть (кнопка ОК)
|
||||
|
||||
AppRating
|
||||
}
|
||||
|
||||
enum class Location {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 don’t 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 = "Can’t 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 = "Can’t 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")
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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">Can’t 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>
|
||||
|
|
|
|||
4
app/src/main/res/xml/provider_paths.xml
Normal file
4
app/src/main/res/xml/provider_paths.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<files-path name="files" path="/"/>
|
||||
</paths>
|
||||
Loading…
Add table
Add a link
Reference in a new issue