Updated on 2026-08-14
This commit is contained in:
commit
2bff9c6767
26 changed files with 624 additions and 416 deletions
|
|
@ -144,8 +144,11 @@ dependencies {
|
|||
implementation("com.squareup.okhttp3:okhttp")
|
||||
implementation("com.squareup.okhttp3:logging-interceptor")
|
||||
|
||||
//
|
||||
// //Crypto
|
||||
// Support
|
||||
implementation("com.zendesk:support:5.0.9")
|
||||
implementation("com.zendesk:messaging:5.2.4")
|
||||
|
||||
//Crypto
|
||||
implementation "com.madgag.spongycastle:core:1.58.0.0"
|
||||
|
||||
// animation
|
||||
|
|
|
|||
|
|
@ -128,6 +128,21 @@
|
|||
|
||||
<activity android:name="com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity" />
|
||||
|
||||
<activity android:name="zendesk.messaging.MessagingActivity"
|
||||
android:theme="@style/ZendeskTheme" />
|
||||
|
||||
<activity android:name="zendesk.support.guide.HelpCenterActivity"
|
||||
android:theme="@style/ZendeskTheme" />
|
||||
|
||||
<activity android:name="zendesk.support.guide.ViewArticleActivity"
|
||||
android:theme="@style/ZendeskTheme" />
|
||||
|
||||
<activity android:name="zendesk.support.request.RequestActivity"
|
||||
android:theme="@style/ZendeskTheme" />
|
||||
|
||||
<activity android:name="zendesk.support.requestlist.RequestListActivity"
|
||||
android:theme="@style/ZendeskTheme" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
|
@ -140,4 +155,4 @@
|
|||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.tap
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application.ActivityLifecycleCallbacks
|
||||
import android.os.Bundle
|
||||
import java.util.WeakHashMap
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class ForegroundActivityObserver {
|
||||
private val activities = WeakHashMap<KClass<out Activity>, Activity>()
|
||||
|
||||
val foregroundActivity: Activity?
|
||||
get() = activities.entries
|
||||
.filterNot { it.value.isDestroyed }
|
||||
.firstOrNull()
|
||||
?.value
|
||||
|
||||
val callbacks get() = Callbacks()
|
||||
|
||||
inner class Callbacks : ActivityLifecycleCallbacks {
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
activities[activity::class] = activity
|
||||
}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
activities.remove(activity::class)
|
||||
}
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
}
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
}
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
}
|
||||
override fun onActivityStopped(activity: Activity) {
|
||||
}
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ForegroundActivityObserver.withForegroundActivity(
|
||||
block: (Activity) -> Unit
|
||||
) {
|
||||
foregroundActivity?.let { block(it) }
|
||||
}
|
||||
|
|
@ -27,11 +27,11 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
|||
import com.tangem.tap.features.shop.redux.ShopAction
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.ActivityMainBinding
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
lateinit var tangemSdk: TangemSdk
|
||||
lateinit var tangemSdkManager: TangemSdkManager
|
||||
|
|
@ -57,7 +57,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
|
|||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
systemActions()
|
||||
store.state.globalState.feedbackManager?.updateActivity(this)
|
||||
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
|
||||
|
||||
tangemSdk = TangemSdk.init(this, TangemSdkManager.config)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
|||
import com.tangem.domain.DomainLayer
|
||||
import com.tangem.network.common.MoshiConverter
|
||||
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
|
||||
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
|
||||
import com.tangem.tap.common.feedback.FeedbackManager
|
||||
import com.tangem.tap.common.images.createCoilImageLoader
|
||||
import com.tangem.tap.common.log.TangemLogCollector
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.appReducer
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
|
|
@ -24,9 +27,6 @@ import com.tangem.tap.domain.configurable.warningMessage.RemoteWarningLoader
|
|||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.domain.tokens.CurrenciesRepository
|
||||
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
|
||||
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.persistence.PreferencesStorage
|
||||
import com.tangem.wallet.BuildConfig
|
||||
|
|
@ -40,6 +40,8 @@ val store = Store(
|
|||
)
|
||||
val logConfig = LogConfig()
|
||||
|
||||
lateinit var foregroundActivityObserver: ForegroundActivityObserver
|
||||
|
||||
lateinit var preferencesStorage: PreferencesStorage
|
||||
lateinit var currenciesRepository: CurrenciesRepository
|
||||
lateinit var walletConnectRepository: WalletConnectRepository
|
||||
|
|
@ -68,6 +70,8 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
)
|
||||
walletConnectRepository = WalletConnectRepository(this)
|
||||
|
||||
foregroundActivityObserver = ForegroundActivityObserver()
|
||||
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
|
||||
initFeedbackManager()
|
||||
loadConfigs()
|
||||
|
||||
|
|
@ -85,16 +89,23 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
val localLoader = FeaturesLocalLoader(this, moshi)
|
||||
val remoteLoader = FeaturesRemoteLoader(moshi)
|
||||
val configManager = ConfigManager(localLoader, remoteLoader)
|
||||
configManager.load {
|
||||
configManager.load { config ->
|
||||
store.dispatch(GlobalAction.SetConfigManager(configManager))
|
||||
shopService = TangemShopService(this, configManager.config.shopify!!)
|
||||
shopService = TangemShopService(
|
||||
application = this,
|
||||
shopifyShop = config.shopify!!
|
||||
)
|
||||
store.state.globalState.feedbackManager?.initChat(
|
||||
context = this,
|
||||
zendeskConfig = config.zendesk!!
|
||||
)
|
||||
}
|
||||
val warningsManager = WarningMessagesManager(RemoteWarningLoader(moshi))
|
||||
warningsManager.load { store.dispatch(GlobalAction.SetWarningManager(warningsManager)) }
|
||||
}
|
||||
|
||||
private fun initFeedbackManager() {
|
||||
val infoHolder = AdditionalEmailInfo()
|
||||
val infoHolder = AdditionalFeedbackInfo()
|
||||
infoHolder.setAppVersion(this)
|
||||
|
||||
val logWriter = TangemLogCollector()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
package com.tangem.tap.common.feedback
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
|
||||
class AdditionalFeedbackInfo {
|
||||
class EmailWalletInfo(
|
||||
var blockchain: Blockchain = Blockchain.Unknown,
|
||||
var address: String = "",
|
||||
var explorerLink: String = "",
|
||||
var host: String = "",
|
||||
var derivationPath: String = "",
|
||||
)
|
||||
|
||||
var appVersion: String = ""
|
||||
|
||||
// card
|
||||
var cardId: String = ""
|
||||
var cardFirmwareVersion: String = ""
|
||||
var cardIssuer: String = ""
|
||||
var cardBlockchain: String = ""
|
||||
|
||||
// wallets
|
||||
internal val walletsInfo = mutableListOf<EmailWalletInfo>()
|
||||
internal val tokens = mutableMapOf<Blockchain, Collection<Token>>()
|
||||
internal var onSendErrorWalletInfo: EmailWalletInfo? = null
|
||||
var signedHashesCount: String = ""
|
||||
|
||||
// device
|
||||
var phoneModel: String = Build.MODEL
|
||||
var osVersion: String = Build.VERSION.SDK_INT.toString()
|
||||
|
||||
// send error
|
||||
var destinationAddress: String = ""
|
||||
var amount: String = ""
|
||||
var fee: String = ""
|
||||
var token: String = ""
|
||||
|
||||
fun setAppVersion(context: Context) {
|
||||
try {
|
||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
appVersion = pInfo.versionName
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun setCardInfo(data: ScanResponse) {
|
||||
cardId = data.card.cardId
|
||||
cardBlockchain = data.walletData?.blockchain ?: ""
|
||||
cardFirmwareVersion = data.card.firmwareVersion.stringValue
|
||||
cardIssuer = data.card.issuer.name
|
||||
signedHashesCount = data.card.wallets
|
||||
.joinToString("; ") { "${it.curve.curve} - ${it.totalSignedHashes}" }
|
||||
}
|
||||
|
||||
fun setWalletsInfo(walletManagers: List<WalletManager>) {
|
||||
walletsInfo.clear()
|
||||
tokens.clear()
|
||||
walletManagers.forEach { manager ->
|
||||
walletsInfo.add(
|
||||
EmailWalletInfo(
|
||||
blockchain = manager.wallet.blockchain,
|
||||
address = getAddress(manager.wallet),
|
||||
explorerLink = getExploreUri(manager.wallet),
|
||||
host = manager.currentHost,
|
||||
derivationPath = manager.wallet.publicKey.derivationPath?.rawPath ?: ""
|
||||
)
|
||||
)
|
||||
if (manager.cardTokens.isNotEmpty()) {
|
||||
tokens[manager.wallet.blockchain] = manager.cardTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateOnSendError(
|
||||
wallet: Wallet,
|
||||
host: String,
|
||||
amountToSend: Amount,
|
||||
feeAmount: Amount,
|
||||
destinationAddress: String,
|
||||
) {
|
||||
onSendErrorWalletInfo = EmailWalletInfo(
|
||||
blockchain = wallet.blockchain,
|
||||
address = getAddress(wallet),
|
||||
explorerLink = getExploreUri(wallet),
|
||||
host = host,
|
||||
derivationPath = wallet.publicKey.derivationPath?.rawPath ?: ""
|
||||
)
|
||||
|
||||
this.destinationAddress = destinationAddress
|
||||
amount = amountToSend.value?.stripZeroPlainString() ?: "0"
|
||||
fee = feeAmount.value?.stripZeroPlainString() ?: "0"
|
||||
token = if (amountToSend.type is AmountType.Token) amountToSend.currencySymbol else ""
|
||||
}
|
||||
|
||||
private fun getAddress(wallet: Wallet): String {
|
||||
return if (wallet.addresses.size == 1) {
|
||||
wallet.address
|
||||
} else {
|
||||
val addresses = wallet.addresses.joinToString(", ") {
|
||||
"${it.type.javaClass.simpleName} - ${it.value}"
|
||||
}
|
||||
"Multiple address: $addresses"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getExploreUri(wallet: Wallet): String {
|
||||
return if (wallet.addresses.size == 1) {
|
||||
wallet.getExploreUrl(wallet.address)
|
||||
} else {
|
||||
val links = wallet.addresses.joinToString(", ") {
|
||||
"${it.type.javaClass.simpleName} - ${wallet.getExploreUrl(it.value)}"
|
||||
}
|
||||
"Multiple explorers links: $links"
|
||||
}
|
||||
}
|
||||
}
|
||||
108
app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt
Normal file
108
app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
package com.tangem.tap.common.feedback
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.domain.common.TapWorkarounds
|
||||
import com.tangem.wallet.R
|
||||
|
||||
interface FeedbackData {
|
||||
val subjectResId: Int
|
||||
val mainMessageResId: Int
|
||||
|
||||
fun getDataCollectionMessageResId(): Int = R.string.feedback_data_collection_message
|
||||
|
||||
fun prepare(infoHolder: AdditionalFeedbackInfo) {}
|
||||
|
||||
fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String
|
||||
|
||||
fun joinTogether(context: Context, infoHolder: AdditionalFeedbackInfo): String {
|
||||
return StringBuilder().apply {
|
||||
append(context.getString(mainMessageResId))
|
||||
appendLine(3)
|
||||
append(context.getString(getDataCollectionMessageResId()))
|
||||
appendLine()
|
||||
append(createOptionalMessage(infoHolder))
|
||||
}.toString()
|
||||
}
|
||||
}
|
||||
|
||||
class RateCanBeBetterEmail : FeedbackData {
|
||||
override val subjectResId: Int = R.string.feedback_subject_rate_negative
|
||||
override val mainMessageResId: Int = R.string.feedback_preface_rate_negative
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
|
||||
.appendCardInfo()
|
||||
.appendLine()
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
|
||||
class ScanFailsEmail : FeedbackData {
|
||||
|
||||
override val subjectResId: Int = R.string.feedback_subject_scan_failed
|
||||
override val mainMessageResId: Int = R.string.feedback_preface_scan_failed
|
||||
|
||||
override fun joinTogether(context: Context, infoHolder: AdditionalFeedbackInfo): String = StringBuilder().apply {
|
||||
append(context.getString(mainMessageResId))
|
||||
appendLine(4)
|
||||
append(createOptionalMessage(infoHolder))
|
||||
}.toString()
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
|
||||
class SendTransactionFailedEmail(
|
||||
val error: String
|
||||
) : FeedbackData {
|
||||
|
||||
override val subjectResId: Int = R.string.feedback_subject_tx_failed
|
||||
override val mainMessageResId: Int = R.string.feedback_preface_tx_failed
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
|
||||
.appendCardInfo()
|
||||
.appendDelimiter()
|
||||
.appendTxFailedBlockchainInfo(error)
|
||||
.appendLine()
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
|
||||
class FeedbackEmail : FeedbackData {
|
||||
override val subjectResId: Int
|
||||
get() = if (isS2CCard) s2cSubject else tangemSubject
|
||||
override val mainMessageResId: Int
|
||||
get() = if (isS2CCard) s2cMainMessage else tangemMainMessage
|
||||
|
||||
private val tangemSubject: Int = R.string.feedback_subject_support_tangem
|
||||
private val tangemMainMessage: Int = R.string.feedback_preface_support
|
||||
|
||||
private val s2cSubject: Int = R.string.feedback_subject_support
|
||||
private val s2cMainMessage: Int = R.string.feedback_preface_support
|
||||
|
||||
private var isS2CCard = false
|
||||
|
||||
override fun prepare(infoHolder: AdditionalFeedbackInfo) {
|
||||
isS2CCard = TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)
|
||||
}
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
|
||||
.appendCardInfo()
|
||||
.appendWalletsInfo()
|
||||
.appendLine()
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
|
||||
class SupportInfo : FeedbackData {
|
||||
override val subjectResId: Int = R.string.details_ask_a_question
|
||||
override val mainMessageResId: Int = R.string.details_ask_a_question
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String {
|
||||
return FeedbackDataBuilder(infoHolder)
|
||||
.appendCardInfo()
|
||||
.appendDelimiter()
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package com.tangem.tap.common.feedback
|
||||
|
||||
class FeedbackDataBuilder(
|
||||
private val infoHolder: AdditionalFeedbackInfo
|
||||
) {
|
||||
val builder = StringBuilder()
|
||||
|
||||
fun appendDelimiter(): FeedbackDataBuilder {
|
||||
builder.appendDelimiter()
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendLine(count: Int = 1): FeedbackDataBuilder {
|
||||
builder.appendLine(count)
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendCardInfo(): FeedbackDataBuilder {
|
||||
builder.appendKeyValue("Card ID", infoHolder.cardId)
|
||||
builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
|
||||
builder.appendKeyValue("Card Blockchain", infoHolder.cardBlockchain)
|
||||
builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendWalletsInfo(): FeedbackDataBuilder {
|
||||
infoHolder.walletsInfo.forEach {
|
||||
builder.appendDelimiter()
|
||||
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
|
||||
builder.appendKeyValue("Host", it.host)
|
||||
builder.appendKeyValue("Wallet address", it.address)
|
||||
builder.appendKeyValue("Derivation path", it.derivationPath)
|
||||
builder.appendKeyValue("Explorer link", it.explorerLink)
|
||||
|
||||
infoHolder.tokens[it.blockchain]?.let { tokens ->
|
||||
builder.append("Tokens:")
|
||||
appendLine()
|
||||
tokens.forEach { token ->
|
||||
builder.appendKeyValue("Name", token.name)
|
||||
builder.appendKeyValue("ID", token.id ?: "[custom token]")
|
||||
builder.appendKeyValue("Contract address", token.contractAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendTxFailedBlockchainInfo(error: String): FeedbackDataBuilder {
|
||||
val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalFeedbackInfo.EmailWalletInfo()
|
||||
builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
|
||||
builder.appendKeyValue("Derivation path", walletInfo.derivationPath)
|
||||
builder.appendKeyValue("Host", walletInfo.host)
|
||||
builder.appendKeyValue("Token", infoHolder.token)
|
||||
builder.appendKeyValue("Error", error)
|
||||
builder.appendDelimiter()
|
||||
builder.appendKeyValue("Source address", walletInfo.address)
|
||||
builder.appendKeyValue("Destination address", infoHolder.destinationAddress)
|
||||
builder.appendKeyValue("Amount", infoHolder.amount)
|
||||
builder.appendKeyValue("Fee", infoHolder.fee)
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendPhoneInfo(): FeedbackDataBuilder {
|
||||
builder.appendKeyValue("Phone model", infoHolder.phoneModel)
|
||||
builder.appendKeyValue("OS version", infoHolder.osVersion)
|
||||
builder.appendKeyValue("App version", infoHolder.appVersion)
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): String = builder.toString()
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendKeyValue(key: String, value: String): StringBuilder {
|
||||
return if (value.isNotBlank()) this.append("$key: $value\n") else this
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n")
|
||||
|
||||
private fun StringBuilder.appendLine(count: Int = 1): StringBuilder {
|
||||
return append(List(count) { "\n" }.joinToString(separator = ""))
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package com.tangem.tap.common.feedback
|
||||
|
||||
import android.content.Context
|
||||
import com.tangem.domain.common.TapWorkarounds
|
||||
import com.tangem.tap.common.extensions.sendEmail
|
||||
import com.tangem.tap.common.log.TangemLogCollector
|
||||
import com.tangem.tap.common.zendesk.ZendeskConfig
|
||||
import com.tangem.tap.foregroundActivityObserver
|
||||
import com.tangem.tap.withForegroundActivity
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.StringWriter
|
||||
import timber.log.Timber
|
||||
import zendesk.configurations.Configuration
|
||||
import zendesk.core.Zendesk
|
||||
import zendesk.support.Support
|
||||
import zendesk.support.request.RequestConfiguration
|
||||
import zendesk.support.requestlist.RequestListActivity
|
||||
import zendesk.support.requestlist.RequestListConfiguration
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class FeedbackManager(
|
||||
val infoHolder: AdditionalFeedbackInfo,
|
||||
private val logCollector: TangemLogCollector,
|
||||
) {
|
||||
fun initChat(
|
||||
context: Context,
|
||||
zendeskConfig: ZendeskConfig,
|
||||
) {
|
||||
Zendesk.INSTANCE.init(
|
||||
/* context = */ context,
|
||||
/* zendeskUrl = */ zendeskConfig.url,
|
||||
/* applicationId = */ zendeskConfig.appId,
|
||||
/* oauthClientId = */ zendeskConfig.clientId,
|
||||
)
|
||||
Support.INSTANCE.init(Zendesk.INSTANCE)
|
||||
}
|
||||
|
||||
fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) {
|
||||
feedbackData.prepare(infoHolder)
|
||||
foregroundActivityObserver.withForegroundActivity { activity ->
|
||||
val fileLog = if (feedbackData is ScanFailsEmail) createLogFile(activity) else null
|
||||
activity.sendEmail(
|
||||
email = getSupportEmail(),
|
||||
subject = activity.getString(feedbackData.subjectResId),
|
||||
message = feedbackData.joinTogether(activity, infoHolder),
|
||||
file = fileLog,
|
||||
onFail = onFail
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun openChat(feedbackData: FeedbackData) {
|
||||
feedbackData.prepare(infoHolder)
|
||||
foregroundActivityObserver.withForegroundActivity { activity ->
|
||||
RequestListActivity.builder()
|
||||
.show(
|
||||
/* context = */ activity,
|
||||
/* configurations = */ buildConfigs(activity, feedbackData)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLogFile(context: Context): 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 the logs file")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildConfigs(
|
||||
context: Context,
|
||||
feedbackData: FeedbackData,
|
||||
): List<Configuration> {
|
||||
return listOf(
|
||||
// Request configuration
|
||||
RequestConfiguration.Builder()
|
||||
.withRequestSubject(context.getString(feedbackData.subjectResId))
|
||||
.config(),
|
||||
// Request list configuration
|
||||
RequestListConfiguration.Builder()
|
||||
.withContactUsButtonVisible(true)
|
||||
.config(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSupportEmail(): String {
|
||||
return if (TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)) {
|
||||
S2C_SUPPORT_EMAIL
|
||||
} else {
|
||||
DEFAULT_SUPPORT_EMAIL
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com"
|
||||
const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.tap.common.log
|
||||
|
||||
import com.tangem.Log
|
||||
import com.tangem.TangemSdkLogger
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
|
||||
class TangemLogCollector : TangemSdkLogger {
|
||||
private val dateFormatter = SimpleDateFormat("HH:mm:ss.SSS")
|
||||
private val logs = mutableListOf<String>()
|
||||
private val mutex = Object()
|
||||
|
||||
override fun log(message: () -> String, level: Log.Level) {
|
||||
val time = dateFormatter.format(Date())
|
||||
synchronized(mutex) {
|
||||
logs.add("$time: ${message()}\n")
|
||||
}
|
||||
}
|
||||
|
||||
fun getLogs(): List<String> = synchronized(mutex) { logs.toList() }
|
||||
|
||||
fun clearLogs() {
|
||||
synchronized(mutex) { logs.clear() }
|
||||
}
|
||||
}
|
||||
|
|
@ -7,14 +7,18 @@ import com.tangem.common.core.TangemError
|
|||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.redux.*
|
||||
import com.tangem.tap.common.feedback.FeedbackData
|
||||
import com.tangem.tap.common.feedback.FeedbackManager
|
||||
import com.tangem.tap.common.redux.DebugErrorAction
|
||||
import com.tangem.tap.common.redux.ErrorAction
|
||||
import com.tangem.tap.common.redux.NotificationAction
|
||||
import com.tangem.tap.common.redux.StateDialog
|
||||
import com.tangem.tap.common.redux.ToastNotificationAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.configurable.config.ConfigManager
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
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
|
||||
|
||||
sealed class GlobalAction : Action {
|
||||
|
|
@ -73,7 +77,8 @@ sealed class GlobalAction : Action {
|
|||
data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction()
|
||||
data class SetAnanlyticHandlers(val analyticsHandlers: GlobalAnalyticsHandler) : GlobalAction()
|
||||
|
||||
data class SendFeedback(val emailData: EmailData) : GlobalAction()
|
||||
data class SendEmail(val feedbackData: FeedbackData) : GlobalAction()
|
||||
data class OpenChat(val feedbackData: FeedbackData) : GlobalAction()
|
||||
data class UpdateFeedbackInfo(val walletManagers: List<WalletManager>) : GlobalAction()
|
||||
|
||||
object ExchangeManager : GlobalAction() {
|
||||
|
|
|
|||
|
|
@ -79,8 +79,11 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
|
|||
}
|
||||
}
|
||||
}
|
||||
is GlobalAction.SendFeedback -> {
|
||||
store.state.globalState.feedbackManager?.send(action.emailData)
|
||||
is GlobalAction.SendEmail -> {
|
||||
store.state.globalState.feedbackManager?.sendEmail(action.feedbackData)
|
||||
}
|
||||
is GlobalAction.OpenChat -> {
|
||||
store.state.globalState.feedbackManager?.openChat(action.feedbackData)
|
||||
}
|
||||
is GlobalAction.UpdateWalletSignedHashes -> {
|
||||
store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures))
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ package com.tangem.tap.common.redux.global
|
|||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.tap.common.analytics.AnalyticsHandler
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.feedback.FeedbackManager
|
||||
import com.tangem.tap.common.redux.StateDialog
|
||||
import com.tangem.tap.domain.PayIdManager
|
||||
import com.tangem.tap.domain.TapWalletManager
|
||||
import com.tangem.tap.domain.configurable.config.ConfigManager
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
import com.tangem.tap.features.feedback.FeedbackManager
|
||||
import com.tangem.tap.features.onboarding.OnboardingManager
|
||||
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
|
||||
import org.rekotlin.StateType
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.tap.common.zendesk
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ZendeskConfig(
|
||||
@Json(name = "zendeskApiKey")
|
||||
val apiKey: String,
|
||||
@Json(name = "zendeskAppId")
|
||||
val appId: String,
|
||||
@Json(name = "zendeskClientId")
|
||||
val clientId: String,
|
||||
@Json(name = "zendeskUrl")
|
||||
val url: String,
|
||||
)
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.tap.domain.configurable.config
|
||||
|
||||
import com.tangem.blockchain.common.BlockchainSdkConfig
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.tap.common.shop.shopify.ShopifyShop
|
||||
import com.tangem.tap.common.zendesk.ZendeskConfig
|
||||
import com.tangem.tap.domain.configurable.Loader
|
||||
|
||||
/**
|
||||
|
|
@ -20,7 +20,8 @@ data class Config(
|
|||
val isTopUpEnabled: Boolean = false,
|
||||
@Deprecated("Not relevant since version 3.23")
|
||||
val isCreatingTwinCardsAllowed: Boolean = false,
|
||||
val shopify: ShopifyShop? = null
|
||||
val shopify: ShopifyShop? = null,
|
||||
val zendesk: ZendeskConfig? = null,
|
||||
)
|
||||
|
||||
class ConfigManager(
|
||||
|
|
@ -33,11 +34,11 @@ class ConfigManager(
|
|||
|
||||
private var defaultConfig = Config()
|
||||
|
||||
fun load(onComplete: VoidCallback? = null) {
|
||||
localLoader.load { config ->
|
||||
setupFeature(config.features)
|
||||
setupKey(config.configValues)
|
||||
onComplete?.invoke()
|
||||
fun load(onComplete: ((config: Config) -> Unit)? = null) {
|
||||
localLoader.load { configModel ->
|
||||
setupFeature(configModel.features)
|
||||
setupKey(configModel.configValues)
|
||||
onComplete?.invoke(config)
|
||||
}
|
||||
// Uncomment to enable remote config
|
||||
// remoteLoader.load { config ->
|
||||
|
|
@ -94,6 +95,7 @@ class ConfigManager(
|
|||
),
|
||||
appsFlyerDevKey = values.appsFlyerDevKey,
|
||||
shopify = values.shopifyShop,
|
||||
zendesk = values.zendesk,
|
||||
)
|
||||
defaultConfig = defaultConfig.copy(
|
||||
coinMarketCapKey = values.coinMarketCapKey,
|
||||
|
|
@ -109,6 +111,7 @@ class ConfigManager(
|
|||
),
|
||||
appsFlyerDevKey = values.appsFlyerDevKey,
|
||||
shopify = values.shopifyShop,
|
||||
zendesk = values.zendesk,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.domain.configurable.config
|
||||
|
||||
import com.tangem.tap.common.shop.shopify.ShopifyShop
|
||||
import com.tangem.tap.common.zendesk.ZendeskConfig
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -24,7 +25,8 @@ class ConfigValueModel(
|
|||
val blockcypherTokens: Set<String>?,
|
||||
val infuraProjectId: String?,
|
||||
val appsFlyerDevKey: String,
|
||||
val shopifyShop: ShopifyShop?
|
||||
val shopifyShop: ShopifyShop?,
|
||||
val zendesk: ZendeskConfig?,
|
||||
)
|
||||
|
||||
class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import androidx.transition.TransitionInflater
|
|||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.tangem.domain.common.getTwinCardIdForUser
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.feedback.FeedbackEmail
|
||||
import com.tangem.tap.common.feedback.SupportInfo
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
|
|
@ -16,7 +18,6 @@ import com.tangem.tap.domain.extensions.isMultiwalletAllowed
|
|||
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.feedback.FeedbackEmail
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -124,7 +125,7 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
|
|||
}
|
||||
|
||||
tvSendFeedback.setOnClickListener {
|
||||
store.dispatch(GlobalAction.SendFeedback(FeedbackEmail()))
|
||||
store.dispatch(GlobalAction.SendEmail(FeedbackEmail()))
|
||||
}
|
||||
|
||||
tvWalletConnect.show(state.scanResponse?.card?.isMultiwalletAllowed == true)
|
||||
|
|
@ -132,6 +133,10 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
|
|||
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
|
||||
}
|
||||
|
||||
tvSupport.setOnClickListener {
|
||||
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
|
||||
}
|
||||
|
||||
llManageSecurity.setOnClickListener {
|
||||
store.dispatch(DetailsAction.ManageSecurity.CheckCurrentSecurityOption(state.scanResponse!!.card))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,380 +0,0 @@
|
|||
package com.tangem.tap.features.feedback
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import com.tangem.Log
|
||||
import com.tangem.TangemSdkLogger
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds
|
||||
import com.tangem.tap.common.extensions.sendEmail
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.wallet.R
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.StringWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class FeedbackManager(
|
||||
val infoHolder: AdditionalEmailInfo,
|
||||
private val logCollector: TangemLogCollector,
|
||||
) {
|
||||
|
||||
private lateinit var activity: Activity
|
||||
|
||||
fun updateActivity(activity: Activity) {
|
||||
this.activity = activity
|
||||
}
|
||||
|
||||
fun send(emailData: EmailData, onFail: ((Exception) -> Unit)? = null) {
|
||||
if (!this::activity.isInitialized) return
|
||||
|
||||
emailData.prepare(infoHolder)
|
||||
val fileLog = if (emailData is ScanFailsEmail) createLogFile() else null
|
||||
activity.sendEmail(
|
||||
email = getSupportEmail(),
|
||||
subject = activity.getString(emailData.subjectResId),
|
||||
message = emailData.joinTogether(activity, infoHolder),
|
||||
file = fileLog,
|
||||
onFail = onFail
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSupportEmail(): String {
|
||||
return if (TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)) {
|
||||
S2C_SUPPORT_EMAIL
|
||||
} else {
|
||||
DEFAULT_SUPPORT_EMAIL
|
||||
}
|
||||
}
|
||||
|
||||
private fun createLogFile(): File? {
|
||||
return try {
|
||||
val file = File(activity.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
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com"
|
||||
const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com"
|
||||
}
|
||||
}
|
||||
|
||||
class TangemLogCollector : TangemSdkLogger {
|
||||
private val dateFormatter = SimpleDateFormat("HH:mm:ss.SSS")
|
||||
private val logs = mutableListOf<String>()
|
||||
private val mutex = Object()
|
||||
|
||||
override fun log(message: () -> String, level: Log.Level) {
|
||||
val time = dateFormatter.format(Date())
|
||||
synchronized(mutex) {
|
||||
logs.add("$time: ${message()}\n")
|
||||
}
|
||||
}
|
||||
|
||||
fun getLogs(): List<String> = synchronized(mutex) { logs.toList() }
|
||||
|
||||
fun clearLogs() {
|
||||
synchronized(mutex) { logs.clear() }
|
||||
}
|
||||
}
|
||||
|
||||
class AdditionalEmailInfo {
|
||||
class EmailWalletInfo(
|
||||
var blockchain: Blockchain = Blockchain.Unknown,
|
||||
var address: String = "",
|
||||
var explorerLink: String = "",
|
||||
var host: String = "",
|
||||
var derivationPath: String = "",
|
||||
)
|
||||
|
||||
var appVersion: String = ""
|
||||
|
||||
// card
|
||||
var cardId: String = ""
|
||||
var cardFirmwareVersion: String = ""
|
||||
var cardIssuer: String = ""
|
||||
var cardBlockchain: String = ""
|
||||
|
||||
// wallets
|
||||
internal val walletsInfo = mutableListOf<EmailWalletInfo>()
|
||||
internal val tokens = mutableMapOf<Blockchain, Collection<Token>>()
|
||||
internal var onSendErrorWalletInfo: EmailWalletInfo? = null
|
||||
var signedHashesCount: String = ""
|
||||
|
||||
// device
|
||||
var phoneModel: String = Build.MODEL
|
||||
var osVersion: String = Build.VERSION.SDK_INT.toString()
|
||||
|
||||
// send error
|
||||
var destinationAddress: String = ""
|
||||
var amount: String = ""
|
||||
var fee: String = ""
|
||||
var token: String = ""
|
||||
|
||||
fun setAppVersion(context: Context) {
|
||||
try {
|
||||
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
|
||||
appVersion = pInfo.versionName
|
||||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun setCardInfo(data: ScanResponse) {
|
||||
cardId = data.card.cardId
|
||||
cardBlockchain = data.walletData?.blockchain ?: ""
|
||||
cardFirmwareVersion = data.card.firmwareVersion.stringValue
|
||||
cardIssuer = data.card.issuer.name
|
||||
signedHashesCount = data.card.wallets
|
||||
.joinToString("; ") { "${it.curve.curve} - ${it.totalSignedHashes}" }
|
||||
}
|
||||
|
||||
fun setWalletsInfo(walletManagers: List<WalletManager>) {
|
||||
walletsInfo.clear()
|
||||
tokens.clear()
|
||||
walletManagers.forEach { manager ->
|
||||
walletsInfo.add(
|
||||
EmailWalletInfo(
|
||||
blockchain = manager.wallet.blockchain,
|
||||
address = getAddress(manager.wallet),
|
||||
explorerLink = getExploreUri(manager.wallet),
|
||||
host = manager.currentHost,
|
||||
derivationPath = manager.wallet.publicKey.derivationPath?.rawPath ?: ""
|
||||
)
|
||||
)
|
||||
if (manager.cardTokens.isNotEmpty()) {
|
||||
tokens[manager.wallet.blockchain] = manager.cardTokens
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateOnSendError(wallet: Wallet, host: String, amountToSend: Amount, feeAmount: Amount, destinationAddress: String) {
|
||||
onSendErrorWalletInfo = EmailWalletInfo(
|
||||
blockchain = wallet.blockchain,
|
||||
address = getAddress(wallet),
|
||||
explorerLink = getExploreUri(wallet),
|
||||
host = host,
|
||||
derivationPath = wallet.publicKey.derivationPath?.rawPath ?: ""
|
||||
)
|
||||
|
||||
this.destinationAddress = destinationAddress
|
||||
amount = amountToSend.value?.stripZeroPlainString() ?: "0"
|
||||
fee = feeAmount.value?.stripZeroPlainString() ?: "0"
|
||||
token = if (amountToSend.type is AmountType.Token) amountToSend.currencySymbol else ""
|
||||
}
|
||||
|
||||
private fun getAddress(wallet: Wallet): String {
|
||||
return if (wallet.addresses.size == 1) {
|
||||
wallet.address
|
||||
} else {
|
||||
val addresses = wallet.addresses.joinToString(", ") {
|
||||
"${it.type.javaClass.simpleName} - ${it.value}"
|
||||
}
|
||||
"Multiple address: $addresses"
|
||||
}
|
||||
}
|
||||
|
||||
private fun getExploreUri(wallet: Wallet): String {
|
||||
return if (wallet.addresses.size == 1) {
|
||||
wallet.getExploreUrl(wallet.address)
|
||||
} else {
|
||||
val links = wallet.addresses.joinToString(", ") {
|
||||
"${it.type.javaClass.simpleName} - ${wallet.getExploreUrl(it.value)}"
|
||||
}
|
||||
"Multiple explorers links: $links"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface EmailData {
|
||||
val subjectResId: Int
|
||||
val mainMessageResId: Int
|
||||
|
||||
fun getDataCollectionMessageResId(): Int = R.string.feedback_data_collection_message
|
||||
|
||||
fun prepare(infoHolder: AdditionalEmailInfo) {}
|
||||
|
||||
fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String
|
||||
|
||||
fun joinTogether(context: Context, infoHolder: AdditionalEmailInfo): String {
|
||||
return StringBuilder().apply {
|
||||
append(context.getString(mainMessageResId))
|
||||
appendLine(3)
|
||||
append(context.getString(getDataCollectionMessageResId()))
|
||||
appendLine()
|
||||
append(createOptionalMessage(infoHolder))
|
||||
}.toString()
|
||||
}
|
||||
}
|
||||
|
||||
class RateCanBeBetterEmail : EmailData {
|
||||
override val subjectResId: Int = R.string.feedback_subject_rate_negative
|
||||
override val mainMessageResId: Int = R.string.feedback_preface_rate_negative
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder)
|
||||
.appendCardInfo()
|
||||
.appendLine()
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
|
||||
class ScanFailsEmail : EmailData {
|
||||
|
||||
override val subjectResId: Int = R.string.feedback_subject_scan_failed
|
||||
override val mainMessageResId: Int = R.string.feedback_preface_scan_failed
|
||||
|
||||
override fun joinTogether(context: Context, infoHolder: AdditionalEmailInfo): String = StringBuilder().apply {
|
||||
append(context.getString(mainMessageResId))
|
||||
appendLine(4)
|
||||
append(createOptionalMessage(infoHolder))
|
||||
}.toString()
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder)
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
|
||||
class SendTransactionFailedEmail(
|
||||
val error: String
|
||||
) : EmailData {
|
||||
|
||||
override val subjectResId: Int = R.string.feedback_subject_tx_failed
|
||||
override val mainMessageResId: Int = R.string.feedback_preface_tx_failed
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder)
|
||||
.appendCardInfo()
|
||||
.appendDelimiter()
|
||||
.appendTxFailedBlockchainInfo(error)
|
||||
.appendLine()
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
|
||||
class FeedbackEmail : EmailData {
|
||||
override val subjectResId: Int
|
||||
get() = if (isS2CCard) s2cSubject else tangemSubject
|
||||
override val mainMessageResId: Int
|
||||
get() = if (isS2CCard) s2cMainMessage else tangemMainMessage
|
||||
|
||||
private val tangemSubject: Int = R.string.feedback_subject_support_tangem
|
||||
private val tangemMainMessage: Int = R.string.feedback_preface_support
|
||||
|
||||
private val s2cSubject: Int = R.string.feedback_subject_support
|
||||
private val s2cMainMessage: Int = R.string.feedback_preface_support
|
||||
|
||||
private var isS2CCard = false
|
||||
|
||||
override fun prepare(infoHolder: AdditionalEmailInfo) {
|
||||
isS2CCard = TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)
|
||||
}
|
||||
|
||||
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder)
|
||||
.appendCardInfo()
|
||||
.appendWalletsInfo()
|
||||
.appendLine()
|
||||
.appendPhoneInfo()
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
class EmailDataBuilder(
|
||||
private val infoHolder: AdditionalEmailInfo
|
||||
) {
|
||||
val builder = StringBuilder()
|
||||
|
||||
fun appendDelimiter(): EmailDataBuilder {
|
||||
builder.appendDelimiter()
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendLine(count: Int = 1): EmailDataBuilder {
|
||||
builder.appendLine(count)
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendCardInfo(): EmailDataBuilder {
|
||||
builder.appendKeyValue("Card ID", infoHolder.cardId)
|
||||
builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
|
||||
builder.appendKeyValue("Card Blockchain", infoHolder.cardBlockchain)
|
||||
builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendWalletsInfo(): EmailDataBuilder {
|
||||
infoHolder.walletsInfo.forEach {
|
||||
builder.appendDelimiter()
|
||||
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
|
||||
builder.appendKeyValue("Host", it.host)
|
||||
builder.appendKeyValue("Wallet address", it.address)
|
||||
builder.appendKeyValue("Derivation path", it.derivationPath)
|
||||
builder.appendKeyValue("Explorer link", it.explorerLink)
|
||||
|
||||
infoHolder.tokens[it.blockchain]?.let { tokens ->
|
||||
builder.append("Tokens:")
|
||||
appendLine()
|
||||
tokens.forEach { token ->
|
||||
builder.appendKeyValue("Name", token.name)
|
||||
builder.appendKeyValue("ID", token.id ?: "[custom token]")
|
||||
builder.appendKeyValue("Contract address", token.contractAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendTxFailedBlockchainInfo(error: String): EmailDataBuilder {
|
||||
val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalEmailInfo.EmailWalletInfo()
|
||||
builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
|
||||
builder.appendKeyValue("Derivation path", walletInfo.derivationPath)
|
||||
builder.appendKeyValue("Host", walletInfo.host)
|
||||
builder.appendKeyValue("Token", infoHolder.token)
|
||||
builder.appendKeyValue("Error", error)
|
||||
builder.appendDelimiter()
|
||||
builder.appendKeyValue("Source address", walletInfo.address)
|
||||
builder.appendKeyValue("Destination address", infoHolder.destinationAddress)
|
||||
builder.appendKeyValue("Amount", infoHolder.amount)
|
||||
builder.appendKeyValue("Fee", infoHolder.fee)
|
||||
return this
|
||||
}
|
||||
|
||||
fun appendPhoneInfo(): EmailDataBuilder {
|
||||
builder.appendKeyValue("Phone model", infoHolder.phoneModel)
|
||||
builder.appendKeyValue("OS version", infoHolder.osVersion)
|
||||
builder.appendKeyValue("App version", infoHolder.appVersion)
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): String = builder.toString()
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendKeyValue(key: String, value: String): StringBuilder {
|
||||
return if (value.isNotBlank()) this.append("$key: $value\n") else this
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n")
|
||||
|
||||
private fun StringBuilder.appendLine(count: Int = 1): StringBuilder {
|
||||
return append(List(count) { "\n" }.joinToString(separator = ""))
|
||||
}
|
||||
|
|
@ -2,8 +2,8 @@ package com.tangem.tap.features.send.ui.dialogs
|
|||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
|
||||
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.store
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -19,7 +19,7 @@ class SendTransactionFailsDialog {
|
|||
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)))
|
||||
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(dialog.errorMessage)))
|
||||
}
|
||||
setPositiveButton(R.string.common_no) { _, _ -> }
|
||||
setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) }
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ import com.tangem.tap.common.extensions.getColor
|
|||
import com.tangem.tap.common.extensions.getString
|
||||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.feedback.RateCanBeBetterEmail
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
|
||||
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
|
||||
|
|
@ -135,7 +135,7 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
|
|||
analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_NEGATIVE)
|
||||
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
|
||||
store.dispatch(GlobalAction.HideWarningMessage(warning))
|
||||
store.dispatch(GlobalAction.SendFeedback(RateCanBeBetterEmail()))
|
||||
store.dispatch(GlobalAction.SendEmail(RateCanBeBetterEmail()))
|
||||
}
|
||||
binding.btnReallyCool.setOnClickListener {
|
||||
val activity = binding.root.context.getActivity() ?: return@setOnClickListener
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ package com.tangem.tap.features.wallet.ui.dialogs
|
|||
import android.content.Context
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.tangem.tap.common.extensions.dispatchDialogHide
|
||||
import com.tangem.tap.common.feedback.ScanFailsEmail
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.feedback.ScanFailsEmail
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ class ScanFailsDialog {
|
|||
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()))
|
||||
store.dispatch(GlobalAction.SendEmail(ScanFailsEmail()))
|
||||
}
|
||||
setNeutralButton(R.string.alert_troubleshooting_scan_card_ok) { _, _ -> }
|
||||
setOnDismissListener { store.dispatchDialogHide() }
|
||||
|
|
|
|||
|
|
@ -302,8 +302,22 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_send_feedback" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_support"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="7dp"
|
||||
android:paddingBottom="7dp"
|
||||
android:text="@string/details_ask_a_question"
|
||||
android:textColor="@color/darkGray6"
|
||||
android:textSize="16sp"
|
||||
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
|
||||
app:drawableTint="@color/darkGray1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_wallet_connect" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
|
|||
|
|
@ -72,4 +72,6 @@
|
|||
<string name="russian_bank_card_warning_title">Карты банков РФ в данный момент не принимаются</string>
|
||||
<string name="russian_bank_card_warning_subtitle">У вас есть карта банка другой страны или платежной системы UnionPay?</string>
|
||||
<string name="common_yes">Да</string>
|
||||
|
||||
<string name="details_ask_a_question">Задать вопрос</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -74,4 +74,6 @@
|
|||
<string name="russian_bank_card_warning_subtitle">Do you have a bank card of another country or a UnionPay card?</string>
|
||||
<string name="common_yes">Yes</string>
|
||||
<string name="common_no">No</string>
|
||||
|
||||
<string name="details_ask_a_question">Ask a question</string>
|
||||
</resources>
|
||||
|
|
|
|||
7
app/src/main/res/values/zendesk_styles.xml
Normal file
7
app/src/main/res/values/zendesk_styles.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="ZendeskTheme" parent="ZendeskSdkTheme.Light">
|
||||
<item name="colorPrimary">@color/darkGray6</item>
|
||||
<item name="colorPrimaryDark">@color/darkGray6</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
@ -22,6 +22,7 @@ allprojects {
|
|||
mavenLocal()
|
||||
maven { url "https://nexus.tangem-tech.com/repository/maven-releases/" }
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven { url 'https://zendesk.jfrog.io/zendesk/repo' }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue