Updated on 2026-08-14
This commit is contained in:
parent
bf0dd570e7
commit
291a07c435
14 changed files with 172 additions and 100 deletions
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
26
app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt
Normal file
26
app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.tangem.tap.common.chat.opener
|
||||
|
||||
import android.content.Context
|
||||
|
||||
internal interface ChatOpener {
|
||||
fun open(feedbackDataBuilder: (Context) -> String)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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("", ""),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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) { _, _ ->
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue