Updated on 2026-08-14

This commit is contained in:
Tangem 2023-02-15 17:33:48 +03:00
parent bf0dd570e7
commit 291a07c435
14 changed files with 172 additions and 100 deletions

View file

@ -25,6 +25,7 @@ import com.tangem.tap.common.analytics.filters.BasicTopUpFilter
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.chat.ChatManager
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.images.createCoilImageLoader
@ -55,11 +56,9 @@ import com.tangem.tap.persistence.CardBalanceStateAdapter
import com.tangem.tap.persistence.PreferencesStorage
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.wallet.BuildConfig
import com.zendesk.logger.Logger
import dagger.hilt.android.HiltAndroidApp
import org.rekotlin.Store
import timber.log.Timber
import zendesk.chat.Chat
import javax.inject.Inject
lateinit var store: Store<AppState>
@ -188,7 +187,7 @@ class TapApplication : Application(), ImageLoaderFactory {
private fun initWithConfigDependency(config: Config) {
shopService = TangemShopService(this, config.shopify!!)
initAnalytics(this, config)
initFeedbackManager(this, preferencesStorage, config)
initFeedbackManager(this, preferencesStorage, foregroundActivityObserver)
}
private fun initAnalytics(application: Application, config: Config) {
@ -210,7 +209,11 @@ class TapApplication : Application(), ImageLoaderFactory {
factory.build(Analytics, buildData)
}
private fun initFeedbackManager(context: Context, preferencesStorage: PreferencesStorage, config: Config) {
private fun initFeedbackManager(
context: Context,
preferencesStorage: PreferencesStorage,
foregroundActivityObserver: ForegroundActivityObserver,
) {
fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply {
appVersion = try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
@ -243,12 +246,8 @@ class TapApplication : Application(), ImageLoaderFactory {
val feedbackManager = FeedbackManager(
infoHolder = additionalFeedbackInfo,
logCollector = tangemLogCollector,
preferencesStorage = preferencesStorage,
chatManager = ChatManager(preferencesStorage, foregroundActivityObserver),
)
feedbackManager.chatInitializer = { zendeskConfig ->
Chat.INSTANCE.init(context, zendeskConfig.accountKey, zendeskConfig.appId)
Logger.setLoggable(LogConfig.zendesk)
}
store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager))
}

View file

@ -1,8 +1,10 @@
package com.tangem.tap.common.zendesk
package com.tangem.tap.common.chat
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
sealed interface ChatConfig
@JsonClass(generateAdapter = true)
data class ZendeskConfig(
@Json(name = "zendeskApiKey")
@ -15,4 +17,8 @@ data class ZendeskConfig(
val accountKey: String,
@Json(name = "zendeskUrl")
val url: String,
)
) : ChatConfig
data class SprinklrConfig(
val appId: String,
) : ChatConfig

View file

@ -0,0 +1,26 @@
package com.tangem.tap.common.chat
import android.content.Context
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.tap.common.chat.opener.ChatOpener
import com.tangem.tap.common.chat.opener.implementation.SprinklrChatOpener
import com.tangem.tap.common.chat.opener.implementation.ZendeskChatOpener
import com.tangem.tap.persistence.PreferencesStorage
class ChatManager(
private val preferencesStorage: PreferencesStorage,
private val foregroundActivityObserver: ForegroundActivityObserver,
) {
private val openers = mutableMapOf<ChatConfig, ChatOpener>()
fun open(config: ChatConfig, feedbackDataBuilder: (Context) -> String) {
val opener = openers.getOrPut(config) {
when (config) {
is SprinklrConfig -> SprinklrChatOpener()
is ZendeskConfig -> ZendeskChatOpener(config, preferencesStorage, foregroundActivityObserver)
}
}
opener.open(feedbackDataBuilder)
}
}

View file

@ -0,0 +1,7 @@
package com.tangem.tap.common.chat.opener
import android.content.Context
internal interface ChatOpener {
fun open(feedbackDataBuilder: (Context) -> String)
}

View file

@ -0,0 +1,10 @@
package com.tangem.tap.common.chat.opener.implementation
import android.content.Context
import com.tangem.tap.common.chat.opener.ChatOpener
internal class SprinklrChatOpener : ChatOpener {
override fun open(feedbackDataBuilder: (Context) -> String) {
// TODO: Open activity with Sprinklr WebView, will be in further MRs
}
}

View file

@ -0,0 +1,83 @@
package com.tangem.tap.common.chat.opener.implementation
import android.content.Context
import android.os.Build
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.LogConfig
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.tap.common.chat.ZendeskConfig
import com.tangem.tap.common.chat.opener.ChatOpener
import com.tangem.tap.persistence.PreferencesStorage
import com.tangem.tap.withForegroundActivity
import com.tangem.wallet.R
import com.zendesk.logger.Logger
import zendesk.chat.Chat
import zendesk.chat.ChatConfiguration
import zendesk.chat.ChatEngine
import zendesk.chat.ChatProvidersConfiguration
import zendesk.chat.VisitorInfo
import zendesk.configurations.Configuration
import zendesk.messaging.MessagingActivity
internal class ZendeskChatOpener(
private val config: ZendeskConfig,
private val preferencesStorage: PreferencesStorage,
private val foregroundActivityObserver: ForegroundActivityObserver,
) : ChatOpener {
private var isInitialized = false
override fun open(feedbackDataBuilder: (Context) -> String) {
foregroundActivityObserver.withForegroundActivity { activity ->
initZendeskIfNeeded(activity.applicationContext)
setChatVisitorInfo()
setChatVisitorNote(feedbackDataBuilder(activity))
showMessagingActivity(activity)
}
}
private fun initZendeskIfNeeded(context: Context) {
if (isInitialized) return
isInitialized = true
Chat.INSTANCE.init(context, config.accountKey, config.appId)
Logger.setLoggable(LogConfig.zendesk)
}
private fun setChatVisitorInfo() {
if (preferencesStorage.chatFirstLaunchTime == null) {
preferencesStorage.chatFirstLaunchTime = System.currentTimeMillis()
}
val chatUserId = (preferencesStorage.chatFirstLaunchTime.toString() + Build.MODEL).hashCode()
val visitorInfo = VisitorInfo.builder()
.withName("User $chatUserId")
.build()
Chat.INSTANCE.chatProvidersConfiguration = ChatProvidersConfiguration.builder()
.withVisitorInfo(visitorInfo)
.build()
}
private fun setChatVisitorNote(note: String) {
Chat.INSTANCE.providers()
?.profileProvider()
?.setVisitorNote(note)
}
private fun showMessagingActivity(context: Context) {
Analytics.send(com.tangem.tap.common.analytics.events.Chat.ScreenOpened())
MessagingActivity.builder()
.withMultilineResponseOptionsEnabled(false)
.withBotLabelStringRes(R.string.chat_bot_name)
.withBotAvatarDrawable(R.mipmap.ic_launcher)
.withEngines(ChatEngine.engine())
.show(context, buildChatConfig())
}
private fun buildChatConfig(): Configuration {
return ChatConfiguration.builder()
.withOfflineFormEnabled(true)
.withAgentAvailabilityEnabled(true)
.withPreChatFormEnabled(false)
.build()
}
}

View file

@ -1,24 +1,14 @@
package com.tangem.tap.common.feedback
import android.content.Context
import android.os.Build
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds
import com.tangem.tap.common.chat.ChatConfig
import com.tangem.tap.common.chat.ChatManager
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.persistence.PreferencesStorage
import com.tangem.tap.withForegroundActivity
import com.tangem.wallet.R
import timber.log.Timber
import zendesk.chat.Chat
import zendesk.chat.ChatConfiguration
import zendesk.chat.ChatEngine
import zendesk.chat.ChatProvidersConfiguration
import zendesk.chat.VisitorInfo
import zendesk.configurations.Configuration
import zendesk.messaging.MessagingActivity
import java.io.File
import java.io.FileWriter
import java.io.StringWriter
@ -29,19 +19,8 @@ import java.io.StringWriter
class FeedbackManager(
val infoHolder: AdditionalFeedbackInfo,
private val logCollector: TangemLogCollector,
private val preferencesStorage: PreferencesStorage,
private val chatManager: ChatManager,
) {
private var lastUsedConfigForInitialization: ZendeskConfig? = null
var chatInitializer: ((ZendeskConfig) -> Unit)? = null
fun initChat(zendeskConfig: ZendeskConfig) {
// prevent double initialization with the same config
if (lastUsedConfigForInitialization == zendeskConfig) return
lastUsedConfigForInitialization = zendeskConfig
chatInitializer?.invoke(zendeskConfig)
}
fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) {
feedbackData.prepare(infoHolder)
@ -57,12 +36,12 @@ class FeedbackManager(
}
}
fun openChat(feedbackData: FeedbackData) {
feedbackData.prepare(infoHolder)
foregroundActivityObserver.withForegroundActivity { activity ->
setChatVisitorInfo()
setChatVisitorNote(activity, feedbackData)
showMessagingActivity(activity)
fun openChat(config: ChatConfig, feedbackData: FeedbackData) {
chatManager.open(config) { context ->
feedbackData.run {
prepare(infoHolder)
joinTogether(context, infoHolder)
}
}
}
@ -85,47 +64,6 @@ class FeedbackManager(
}
}
private fun setChatVisitorInfo() {
if (preferencesStorage.chatFirstLaunchTime == null) {
preferencesStorage.chatFirstLaunchTime = System.currentTimeMillis()
}
val chatUserId = (preferencesStorage.chatFirstLaunchTime.toString() + Build.MODEL).hashCode()
val visitorInfo = VisitorInfo.builder()
.withName("User $chatUserId")
.build()
Chat.INSTANCE.chatProvidersConfiguration = ChatProvidersConfiguration.builder()
.withVisitorInfo(visitorInfo)
.build()
}
private fun setChatVisitorNote(
context: Context,
feedbackData: FeedbackData,
) {
Chat.INSTANCE.providers()
?.profileProvider()
?.setVisitorNote(feedbackData.joinTogether(context, infoHolder))
}
private fun showMessagingActivity(context: Context) {
Analytics.send(com.tangem.tap.common.analytics.events.Chat.ScreenOpened())
MessagingActivity.builder()
.withMultilineResponseOptionsEnabled(false)
.withBotLabelStringRes(R.string.chat_bot_name)
.withBotAvatarDrawable(R.mipmap.ic_launcher)
.withEngines(ChatEngine.engine())
.show(context, buildChatConfig())
}
private fun buildChatConfig(): Configuration {
return ChatConfiguration.builder()
.withOfflineFormEnabled(true)
.withAgentAvailabilityEnabled(true)
.withPreChatFormEnabled(false)
.build()
}
private fun getSupportEmail(): String {
return if (TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)) {
S2C_SUPPORT_EMAIL

View file

@ -5,6 +5,7 @@ import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.chat.ChatConfig
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackData
import com.tangem.tap.common.feedback.FeedbackManager
@ -13,7 +14,6 @@ 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.common.zendesk.ZendeskConfig
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
@ -87,7 +87,7 @@ sealed class GlobalAction : Action {
data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction()
data class SendEmail(val feedbackData: FeedbackData) : GlobalAction()
data class OpenChat(val feedbackData: FeedbackData, val zendeskConfig: ZendeskConfig? = null) : GlobalAction()
data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction()
data class UpdateFeedbackInfo(val walletManagers: List<WalletManager>) : GlobalAction()
object ExchangeManager : GlobalAction() {

View file

@ -9,6 +9,7 @@ import com.tangem.domain.common.LogConfig
import com.tangem.domain.common.ProductType
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.chat.SprinklrConfig
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
@ -109,17 +110,16 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
val scanResponse = globalState.scanResponse ?: globalState.onboardingState.onboardingManager?.scanResponse
// if config not set -> try to get it based on a scanResponse.productType
val unsafeZendeskConfig = action.zendeskConfig ?: when {
scanResponse?.isSaltPay() == true -> config.saltPayConfig?.zendesk
val unsafeChatConfig = action.chatConfig ?: when {
scanResponse?.isSaltPay() == true -> config.saltPayConfig?.sprinklrAppID?.let(::SprinklrConfig)
else -> config.zendesk
}
val zendeskConfig = unsafeZendeskConfig.guard {
val chatConfig = unsafeChatConfig.guard {
store.dispatchDebugErrorNotification("ZendeskConfig not initialized")
return
}
feedbackManager.initChat(zendeskConfig)
feedbackManager.openChat(action.feedbackData)
feedbackManager.openChat(chatConfig, action.feedbackData)
}
is GlobalAction.UpdateWalletSignedHashes -> {
store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures))

View file

@ -5,8 +5,8 @@ import com.tangem.blockchain.common.BlockchairCredentials
import com.tangem.blockchain.common.GetBlockCredentials
import com.tangem.blockchain.common.NowNodeCredentials
import com.tangem.blockchain.common.QuickNodeCredentials
import com.tangem.tap.common.chat.ZendeskConfig
import com.tangem.tap.common.shop.shopify.ShopifyShop
import com.tangem.tap.common.zendesk.ZendeskConfig
import com.tangem.tap.domain.configurable.Loader
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig

View file

@ -1,7 +1,7 @@
package com.tangem.tap.domain.configurable.config
import com.tangem.tap.common.chat.ZendeskConfig
import com.tangem.tap.common.shop.shopify.ShopifyShop
import com.tangem.tap.common.zendesk.ZendeskConfig
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayConfig
/**

View file

@ -1,20 +1,19 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay
import com.tangem.tap.common.zendesk.ZendeskConfig
import org.spongycastle.util.encoders.Base64.toBase64String
/**
[REDACTED_AUTHOR]
*/
data class SaltPayConfig(
val zendesk: ZendeskConfig,
val sprinklrAppID: String,
val kycProvider: KYCProvider,
val credentials: Credentials,
) {
companion object {
fun stub(): SaltPayConfig {
return SaltPayConfig(
zendesk = ZendeskConfig("", "", "", "", ""),
sprinklrAppID = "",
kycProvider = KYCProvider("", "", "", ""),
credentials = Credentials("", ""),
)

View file

@ -4,6 +4,7 @@ import android.app.Dialog
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.common.extensions.guard
import com.tangem.tap.common.chat.SprinklrConfig
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
@ -20,11 +21,14 @@ class NoFundsForActivationDialog {
setTitle(R.string.saltpay_error_no_gas_title)
setMessage(R.string.saltpay_error_no_gas_message)
setPositiveButton(R.string.chat_button_title) { _, _ ->
val config = store.state.globalState.configManager?.config?.saltPayConfig?.zendesk.guard {
store.dispatchDebugErrorNotification("SaltPayConfig not initialized")
return@setPositiveButton
}
store.dispatch(GlobalAction.OpenChat(SupportInfo(), config))
val sprinklrAppId = store.state.globalState.configManager?.config
?.saltPayConfig
?.sprinklrAppID
.guard {
store.dispatchDebugErrorNotification("SaltPayConfig not initialized")
return@setPositiveButton
}
store.dispatch(GlobalAction.OpenChat(SupportInfo(), SprinklrConfig(sprinklrAppId)))
}
setNegativeButton(R.string.common_cancel) { _, _ ->
}