From 8c080fdb584d78e1b88bbc0a1826b97fb58420ae Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Jul 2022 18:05:02 +0300 Subject: [PATCH 1/6] Updated on 2026-08-14 --- app/build.gradle | 7 +++++-- build.gradle | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 791cb96330..3f12b8e345 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/build.gradle b/build.gradle index 23b9e4d0bc..64f57c2248 100644 --- a/build.gradle +++ b/build.gradle @@ -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' } } } From ba350d0de0c98362f6bb1b708504adf0328c685f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Jul 2022 19:56:44 +0300 Subject: [PATCH 2/6] Updated on 2026-08-14 --- .../tangem/tap/ForegroundActivityObserver.kt | 46 +++++++++++++++++++ .../java/com/tangem/tap/TapApplication.kt | 4 ++ 2 files changed, 50 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt diff --git a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt new file mode 100644 index 0000000000..806769df5f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt @@ -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, 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) } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 2e6c7b534b..372fb621bf 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -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() From 3f2e5700fb8b85431d6b6b34b101d6f7654026a3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Jul 2022 13:31:54 +0300 Subject: [PATCH 3/6] Updated on 2026-08-14 --- .../java/com/tangem/tap/TapApplication.kt | 8 +- .../common/feedback/AdditionalFeedbackInfo.kt | 127 ++++++ .../tap/common/feedback/FeedbackData.kt | 95 +++++ .../common/feedback/FeedbackDataBuilder.kt | 81 ++++ .../tap/common/feedback/FeedbackManager.kt | 72 ++++ .../tap/common/log/TangemLogCollector.kt | 25 ++ .../tap/common/redux/global/GlobalAction.kt | 12 +- .../common/redux/global/GlobalMidlleware.kt | 2 +- .../tap/common/redux/global/GlobalState.kt | 2 +- .../features/details/ui/DetailsFragment.kt | 2 +- .../tap/features/feedback/FeedbackManager.kt | 380 ------------------ .../ui/dialogs/SendTransactionFailsDialog.kt | 2 +- .../ui/adapters/WarningMessagesAdapter.kt | 2 +- .../wallet/ui/dialogs/ScanFailsDialog.kt | 2 +- 14 files changed, 418 insertions(+), 394 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt create mode 100644 app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt create mode 100644 app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt create mode 100644 app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt create mode 100644 app/src/main/java/com/tangem/tap/common/log/TangemLogCollector.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 372fb621bf..61b576837d 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -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 @@ -98,7 +98,7 @@ class TapApplication : Application(), ImageLoaderFactory { } private fun initFeedbackManager() { - val infoHolder = AdditionalEmailInfo() + val infoHolder = AdditionalFeedbackInfo() infoHolder.setAppVersion(this) val logWriter = TangemLogCollector() diff --git a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt new file mode 100644 index 0000000000..ddc7a41b23 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt @@ -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() + internal val tokens = mutableMapOf>() + 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) { + 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" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt new file mode 100644 index 0000000000..4750a444b3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt @@ -0,0 +1,95 @@ +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() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt new file mode 100644 index 0000000000..9e5378e9e5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt @@ -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 = "")) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt new file mode 100644 index 0000000000..3a72ece232 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt @@ -0,0 +1,72 @@ +package com.tangem.tap.common.feedback + +import android.app.Activity +import com.tangem.domain.common.TapWorkarounds +import com.tangem.tap.common.extensions.sendEmail +import com.tangem.tap.common.log.TangemLogCollector +import java.io.File +import java.io.FileWriter +import java.io.StringWriter +import timber.log.Timber + + +/** +[REDACTED_AUTHOR] + */ +class FeedbackManager( + val infoHolder: AdditionalFeedbackInfo, + private val logCollector: TangemLogCollector, +) { + + private lateinit var activity: Activity + + fun updateActivity(activity: Activity) { + this.activity = activity + } + + fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) { + if (!this::activity.isInitialized) return + + feedbackData.prepare(infoHolder) + val fileLog = if (feedbackData is ScanFailsEmail) createLogFile() else null + activity.sendEmail( + email = getSupportEmail(), + subject = activity.getString(feedbackData.subjectResId), + message = feedbackData.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" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/log/TangemLogCollector.kt b/app/src/main/java/com/tangem/tap/common/log/TangemLogCollector.kt new file mode 100644 index 0000000000..c1ae08ebc2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/log/TangemLogCollector.kt @@ -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() + 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 = synchronized(mutex) { logs.toList() } + + fun clearLogs() { + synchronized(mutex) { logs.clear() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index a82d5ba94b..a12a6d20bf 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -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,7 @@ 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 SendFeedback(val feedbackData: FeedbackData) : GlobalAction() data class UpdateFeedbackInfo(val walletManagers: List) : GlobalAction() object ExchangeManager : GlobalAction() { diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index 7f61d63022..55c9b20908 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -80,7 +80,7 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di } } is GlobalAction.SendFeedback -> { - store.state.globalState.feedbackManager?.send(action.emailData) + store.state.globalState.feedbackManager?.sendEmail(action.feedbackData) } is GlobalAction.UpdateWalletSignedHashes -> { store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures)) diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 8588256411..62e5217866 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt index 2789b0069d..ce4eaf8018 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt @@ -9,6 +9,7 @@ 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.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction @@ -16,7 +17,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 diff --git a/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt deleted file mode 100644 index ad5a091c99..0000000000 --- a/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt +++ /dev/null @@ -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() - 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 = 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() - internal val tokens = mutableMapOf>() - 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) { - 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 = "")) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt index 8f4b106e97..2dfadd6362 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt index 9014d5d4ff..820fa4167a 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt index 496a8ff731..4122e0bd06 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt @@ -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 From c445651ca9824e2b992aa9f898f00639197cf6aa Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Jul 2022 00:19:07 +0300 Subject: [PATCH 4/6] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 5 +- .../java/com/tangem/tap/TapApplication.kt | 11 ++- .../tap/common/feedback/FeedbackManager.kt | 95 +++++++++++++------ .../tap/common/zendesk/ZendeskConfig.kt | 16 ++++ .../configurable/config/ConfigManager.kt | 17 ++-- .../domain/configurable/config/JsonModels.kt | 4 +- 6 files changed, 108 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/zendesk/ZendeskConfig.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index a663e66e7a..7593f303e7 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 61b576837d..518ad410c4 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -89,9 +89,16 @@ 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)) } diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt index 3a72ece232..dccb805723 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt @@ -1,14 +1,22 @@ package com.tangem.tap.common.feedback -import android.app.Activity +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] @@ -17,38 +25,47 @@ class FeedbackManager( val infoHolder: AdditionalFeedbackInfo, private val logCollector: TangemLogCollector, ) { - - private lateinit var activity: Activity - - fun updateActivity(activity: Activity) { - this.activity = activity + 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) { - if (!this::activity.isInitialized) return - feedbackData.prepare(infoHolder) - val fileLog = if (feedbackData is ScanFailsEmail) createLogFile() else null - activity.sendEmail( - email = getSupportEmail(), - subject = activity.getString(feedbackData.subjectResId), - message = feedbackData.joinTogether(activity, infoHolder), - file = fileLog, - onFail = onFail - ) - } - - private fun getSupportEmail(): String { - return if (TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)) { - S2C_SUPPORT_EMAIL - } else { - DEFAULT_SUPPORT_EMAIL + 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 + ) } } - private fun createLogFile(): File? { + 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(activity.filesDir, "logs.txt") + val file = File(context.filesDir, "logs.txt") file.delete() file.createNewFile() @@ -60,11 +77,35 @@ class FeedbackManager( logCollector.clearLogs() file } catch (ex: Exception) { - Timber.e(ex, "Can't create a file for email attachment") + Timber.e(ex, "Can't create the logs file") null } } + private fun buildConfigs( + context: Context, + feedbackData: FeedbackData, + ): List { + 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" diff --git a/app/src/main/java/com/tangem/tap/common/zendesk/ZendeskConfig.kt b/app/src/main/java/com/tangem/tap/common/zendesk/ZendeskConfig.kt new file mode 100644 index 0000000000..aad872b7df --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/zendesk/ZendeskConfig.kt @@ -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, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt index 52037f828c..795e76fe06 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt @@ -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, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt index f9e053ee98..fc63ed5e9a 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt @@ -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?, val infuraProjectId: String?, val appsFlyerDevKey: String, - val shopifyShop: ShopifyShop? + val shopifyShop: ShopifyShop?, + val zendesk: ZendeskConfig?, ) class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) { From 1e6dc08081b4705a6733052c1a7c0c10cfaa8454 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Jul 2022 00:23:04 +0300 Subject: [PATCH 5/6] Updated on 2026-08-14 --- .../tangem/tap/common/feedback/FeedbackData.kt | 13 +++++++++++++ .../tap/common/redux/global/GlobalAction.kt | 3 ++- .../tap/common/redux/global/GlobalMidlleware.kt | 5 ++++- .../tap/features/details/ui/DetailsFragment.kt | 7 ++++++- .../ui/dialogs/SendTransactionFailsDialog.kt | 2 +- .../wallet/ui/adapters/WarningMessagesAdapter.kt | 2 +- .../wallet/ui/dialogs/ScanFailsDialog.kt | 2 +- app/src/main/res/layout/fragment_details.xml | 16 +++++++++++++++- app/src/main/res/values-ru/strings_final.xml | 2 ++ app/src/main/res/values/strings_final.xml | 2 ++ 10 files changed, 47 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt index 4750a444b3..dc2a7d07d5 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt @@ -92,4 +92,17 @@ class FeedbackEmail : FeedbackData { .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() + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index a12a6d20bf..4927405ccd 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -77,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 feedbackData: FeedbackData) : GlobalAction() + data class SendEmail(val feedbackData: FeedbackData) : GlobalAction() + data class OpenChat(val feedbackData: FeedbackData) : GlobalAction() data class UpdateFeedbackInfo(val walletManagers: List) : GlobalAction() object ExchangeManager : GlobalAction() { diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index 55c9b20908..17cb291621 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -79,9 +79,12 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di } } } - is GlobalAction.SendFeedback -> { + 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)) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt index ce4eaf8018..71b6baa710 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt @@ -10,6 +10,7 @@ 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 @@ -124,7 +125,7 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber - 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) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt index 820fa4167a..f77b716bc7 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt index 4122e0bd06..91afcc3819 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt @@ -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() } diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index 58ac038846..2171b91e61 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -302,8 +302,22 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_send_feedback" /> + + - \ No newline at end of file + diff --git a/app/src/main/res/values-ru/strings_final.xml b/app/src/main/res/values-ru/strings_final.xml index 0deae6285f..d2028c922e 100644 --- a/app/src/main/res/values-ru/strings_final.xml +++ b/app/src/main/res/values-ru/strings_final.xml @@ -72,4 +72,6 @@ Карты банков РФ в данный момент не принимаются У вас есть карта банка другой страны или платежной системы UnionPay? Да + + Задать вопрос diff --git a/app/src/main/res/values/strings_final.xml b/app/src/main/res/values/strings_final.xml index f5dfbc7abd..adbf1e7604 100644 --- a/app/src/main/res/values/strings_final.xml +++ b/app/src/main/res/values/strings_final.xml @@ -74,4 +74,6 @@ Do you have a bank card of another country or a UnionPay card? Yes No + + Ask a question From cfb002bee764d2996d4394e51c45b0ad21d09089 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Jul 2022 12:05:54 +0300 Subject: [PATCH 6/6] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 17 ++++++++++++++++- app/src/main/res/values/zendesk_styles.xml | 7 +++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/values/zendesk_styles.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 22fda180cd..d180642a75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -128,6 +128,21 @@ + + + + + + + + + + - \ No newline at end of file + diff --git a/app/src/main/res/values/zendesk_styles.xml b/app/src/main/res/values/zendesk_styles.xml new file mode 100644 index 0000000000..ebb4fcab6b --- /dev/null +++ b/app/src/main/res/values/zendesk_styles.xml @@ -0,0 +1,7 @@ + + + +