Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-25 20:58:35 +08:00
parent fe1d2342f4
commit 24848624e9
49 changed files with 414 additions and 182 deletions

View file

@ -18,6 +18,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetFeedbackEmailUseCase import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
@ -112,6 +113,8 @@ interface ApplicationEntryPoint {
fun getDetailsFeatureToggles(): DetailsFeatureToggles fun getDetailsFeatureToggles(): DetailsFeatureToggles
fun getGetCardInfoUseCase(): GetCardInfoUseCase
fun getUrlOpener(): UrlOpener fun getUrlOpener(): UrlOpener
fun getShareManager(): ShareManager fun getShareManager(): ShareManager

View file

@ -33,6 +33,7 @@ import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.LogConfig import com.tangem.domain.common.LogConfig
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetFeedbackEmailUseCase import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
@ -183,6 +184,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase
get() = entryPoint.getSaveBlockchainErrorUseCase() get() = entryPoint.getSaveBlockchainErrorUseCase()
private val getCardInfoUseCase: GetCardInfoUseCase
get() = entryPoint.getGetCardInfoUseCase()
private val detailsFeatureToggles: DetailsFeatureToggles private val detailsFeatureToggles: DetailsFeatureToggles
get() = entryPoint.getDetailsFeatureToggles() get() = entryPoint.getDetailsFeatureToggles()
@ -284,6 +288,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
settingsRepository = settingsRepository, settingsRepository = settingsRepository,
blockchainSDKFactory = blockchainSDKFactory, blockchainSDKFactory = blockchainSDKFactory,
saveBlockchainErrorUseCase = saveBlockchainErrorUseCase, saveBlockchainErrorUseCase = saveBlockchainErrorUseCase,
getFeedbackEmailUseCase = getFeedbackEmailUseCase,
getCardInfoUseCase = getCardInfoUseCase,
assetLoader = assetLoader,
detailsFeatureToggles = detailsFeatureToggles, detailsFeatureToggles = detailsFeatureToggles,
urlOpener = urlOpener, urlOpener = urlOpener,
shareManager = shareManager, shareManager = shareManager,

View file

@ -7,13 +7,14 @@ import com.tangem.domain.common.TapWorkarounds
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetFeedbackEmailUseCase import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.chat.ChatManager import com.tangem.tap.common.chat.ChatManager
import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.sendEmail import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.log.TangemLogCollector import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.foregroundActivityObserver import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.tap.withForegroundActivity import com.tangem.tap.withForegroundActivity
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -36,16 +37,24 @@ class LegacyFeedbackManager(
private var sessionFeedbackFile: File? = null private var sessionFeedbackFile: File? = null
private var sessionLogsFile: File? = null private var sessionLogsFile: File? = null
fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) { fun sendEmail(feedbackData: FeedbackData, scanResponse: ScanResponse?) {
if (feedbackManagerFeatureToggles.isLocalLogsEnabled) { if (feedbackManagerFeatureToggles.isLocalLogsEnabled) {
mainScope.launch { scope.launch {
val getCardInfo = suspend {
scanResponse ?: error("ScanResponse must be not null")
store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponse).getOrNull()
?: error("CardInfo must be not null")
}
val email = getFeedbackEmailUseCase( val email = getFeedbackEmailUseCase(
when (feedbackData) { type = when (feedbackData) {
is FeedbackEmail -> FeedbackEmailType.DirectUserRequest is FeedbackEmail -> FeedbackEmailType.DirectUserRequest(cardInfo = getCardInfo())
is RateCanBeBetterEmail -> FeedbackEmailType.RateCanBeBetter is RateCanBeBetterEmail -> FeedbackEmailType.RateCanBeBetter(cardInfo = getCardInfo())
is ScanFailsEmail -> FeedbackEmailType.ScanningProblem is ScanFailsEmail -> FeedbackEmailType.ScanningProblem
is SendTransactionFailedEmail -> FeedbackEmailType.TransactionSendingProblem is SendTransactionFailedEmail -> {
else -> FeedbackEmailType.DirectUserRequest FeedbackEmailType.TransactionSendingProblem(cardInfo = getCardInfo())
}
else -> FeedbackEmailType.DirectUserRequest(cardInfo = getCardInfo())
}, },
) )
@ -66,12 +75,28 @@ class LegacyFeedbackManager(
subject = activity.getString(feedbackData.subjectResId), subject = activity.getString(feedbackData.subjectResId),
message = feedbackData.joinTogether(activity, infoHolder), message = feedbackData.joinTogether(activity, infoHolder),
file = getLogFile(activity), file = getLogFile(activity),
onFail = onFail,
) )
} }
} }
} }
fun sendEmail(type: FeedbackEmailType) {
if (!feedbackManagerFeatureToggles.isLocalLogsEnabled) error("LOCAL_LOGS feature toggle must be enabled")
scope.launch {
val email = getFeedbackEmailUseCase(type = type)
store.inject(DaggerGraphState::emailSender).send(
email = EmailSender.Email(
address = email.address,
subject = email.subject,
message = email.message,
attachment = email.file,
),
)
}
}
fun openChat(config: ChatConfig, feedbackData: FeedbackData) { fun openChat(config: ChatConfig, feedbackData: FeedbackData) {
chatManager.open( chatManager.open(
config = config, config = config,

View file

@ -1,13 +1,13 @@
package com.tangem.tap.common.feedback package com.tangem.tap.common.feedback
import com.tangem.core.navigation.feedback.FeedbackManager import com.tangem.domain.feedback.FeedbackManager
import com.tangem.core.navigation.feedback.FeedbackType import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.tap.store import com.tangem.tap.store
import timber.log.Timber import timber.log.Timber
internal class ProxyFeedbackManager : FeedbackManager { internal class ProxyFeedbackManager : FeedbackManager {
override fun sendEmail(type: FeedbackType) { override fun sendEmail(type: FeedbackEmailType) {
val manager = store.state.globalState.feedbackManager val manager = store.state.globalState.feedbackManager
if (manager == null) { if (manager == null) {
@ -15,14 +15,6 @@ internal class ProxyFeedbackManager : FeedbackManager {
return return
} }
val data = when (type) { manager.sendEmail(type)
is FeedbackType.Feedback -> FeedbackEmail()
is FeedbackType.RateCanBeBetter -> RateCanBeBetterEmail()
is FeedbackType.ScanFails -> ScanFailsEmail()
is FeedbackType.SendTransactionFailed -> SendTransactionFailedEmail(type.error)
is FeedbackType.Support -> FeedbackEmail()
}
manager.sendEmail(data)
} }
} }

View file

@ -81,7 +81,7 @@ sealed class GlobalAction : Action {
data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction() data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction()
data class SetFeedbackManager(val feedbackManager: LegacyFeedbackManager) : GlobalAction() data class SetFeedbackManager(val feedbackManager: LegacyFeedbackManager) : GlobalAction()
data class SendEmail(val feedbackData: FeedbackData) : GlobalAction() data class SendEmail(val feedbackData: FeedbackData, val scanResponse: ScanResponse?) : GlobalAction()
data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction() data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction()
object ExchangeManager : GlobalAction() { object ExchangeManager : GlobalAction() {

View file

@ -69,7 +69,10 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
} }
} }
is GlobalAction.SendEmail -> { is GlobalAction.SendEmail -> {
store.state.globalState.feedbackManager?.sendEmail(action.feedbackData) store.state.globalState.feedbackManager?.sendEmail(
feedbackData = action.feedbackData,
scanResponse = action.scanResponse,
)
} }
is GlobalAction.OpenChat -> { is GlobalAction.OpenChat -> {
val globalState = store.state.globalState val globalState = store.state.globalState

View file

@ -1,8 +1,11 @@
package com.tangem.tap.common.redux.legacy package com.tangem.tap.common.redux.legacy
import com.tangem.blockchain.common.AmountType
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.tokens.utils.convertToAmount
import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.feedback.RateCanBeBetterEmail import com.tangem.tap.common.feedback.RateCanBeBetterEmail
import com.tangem.tap.common.feedback.SendTransactionFailedEmail import com.tangem.tap.common.feedback.SendTransactionFailedEmail
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
@ -19,7 +22,10 @@ internal object LegacyMiddleware {
{ action -> { action ->
when (action) { when (action) {
is LegacyAction.SendEmailRateCanBeBetter -> { is LegacyAction.SendEmailRateCanBeBetter -> {
store.state.globalState.feedbackManager?.sendEmail(RateCanBeBetterEmail()) store.state.globalState.feedbackManager?.sendEmail(
feedbackData = RateCanBeBetterEmail(),
scanResponse = action.scanResponse,
)
} }
is LegacyAction.StartOnboardingProcess -> { is LegacyAction.StartOnboardingProcess -> {
store.dispatch( store.dispatch(
@ -28,8 +34,28 @@ internal object LegacyMiddleware {
} }
is LegacyAction.SendEmailTransactionFailed -> { is LegacyAction.SendEmailTransactionFailed -> {
if (store.inject(DaggerGraphState::feedbackManagerFeatureToggles).isLocalLogsEnabled) { if (store.inject(DaggerGraphState::feedbackManagerFeatureToggles).isLocalLogsEnabled) {
val amount = action.amount?.convertToAmount(action.cryptoCurrency)
store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke(
error = BlockchainErrorInfo(
errorMessage = action.errorMessage,
blockchainId = action.cryptoCurrency.network.id.value,
derivationPath = action.cryptoCurrency.network.derivationPath.value,
destinationAddress = action.destinationAddress.orEmpty(),
tokenSymbol = if (amount?.type is AmountType.Token) {
amount.currencySymbol
} else {
""
},
amount = amount?.value?.stripZeroPlainString() ?: "unknown",
fee = action.fee?.convertToAmount(action.cryptoCurrency)
?.value?.stripZeroPlainString() ?: "unknown",
),
)
store.state.globalState.feedbackManager?.sendEmail( store.state.globalState.feedbackManager?.sendEmail(
SendTransactionFailedEmail(action.errorMessage), feedbackData = SendTransactionFailedEmail(action.errorMessage),
scanResponse = action.scanResponse,
) )
} else { } else {
scope.launch { scope.launch {
@ -46,7 +72,8 @@ internal object LegacyMiddleware {
) )
} }
store.state.globalState.feedbackManager?.sendEmail( store.state.globalState.feedbackManager?.sendEmail(
SendTransactionFailedEmail(action.errorMessage), feedbackData = SendTransactionFailedEmail(action.errorMessage),
scanResponse = null,
) )
} }
} }

View file

@ -65,7 +65,7 @@ internal object ScanFailsDialog {
} }
customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener { customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener {
Analytics.send(Basic.ButtonSupport(sourceAnalytics)) Analytics.send(Basic.ButtonSupport(sourceAnalytics))
store.dispatch(GlobalAction.SendEmail(ScanFailsEmail())) store.dispatch(GlobalAction.SendEmail(feedbackData = ScanFailsEmail(), scanResponse = null))
} }
customView.findViewById<TextView>(R.id.cancel_button)?.setOnClickListener { customView.findViewById<TextView>(R.id.cancel_button)?.setOnClickListener {
store.dispatchDialogHide() store.dispatchDialogHide()

View file

@ -1,11 +1,11 @@
package com.tangem.tap.di package com.tangem.tap.di
import android.content.Context import android.content.Context
import com.tangem.core.navigation.feedback.FeedbackManager
import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.feedback.FeedbackManager
import com.tangem.tap.common.feedback.ProxyFeedbackManager import com.tangem.tap.common.feedback.ProxyFeedbackManager
import com.tangem.tap.common.finisher.AndroidAppFinisher import com.tangem.tap.common.finisher.AndroidAppFinisher
import com.tangem.tap.common.settings.IntentSettingsManager import com.tangem.tap.common.settings.IntentSettingsManager

View file

@ -1,6 +1,7 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import android.content.Context import android.content.Context
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetFeedbackEmailUseCase import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.feedback.repository.FeedbackRepository
@ -15,6 +16,12 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
internal object FeedbackDomainModule { internal object FeedbackDomainModule {
@Provides
@Singleton
fun provideGetCardInfoUseCase(feedbackRepository: FeedbackRepository): GetCardInfoUseCase {
return GetCardInfoUseCase(feedbackRepository = feedbackRepository)
}
@Provides @Provides
@Singleton @Singleton
fun provideGetFeedbackEmailUseCase( fun provideGetFeedbackEmailUseCase(

View file

@ -142,7 +142,13 @@ internal class DetailsViewModel(
private fun sendFeedback() { private fun sendFeedback() {
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Settings)) Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Settings))
store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail())) store.dispatchOnMain(
GlobalAction.SendEmail(
feedbackData = FeedbackEmail(),
scanResponse = userWalletsListManager.selectedUserWalletSync?.scanResponse
?: error("ScanResponse must be not null"),
),
)
} }
private fun navigateToAppSettings() { private fun navigateToAppSettings() {

View file

@ -7,15 +7,20 @@ import androidx.core.view.MenuProvider
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.Basic
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.feedback.SupportInfo import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R import com.tangem.wallet.R
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
class OnboardingMenuProvider : MenuProvider { class OnboardingMenuProvider(
private val scanResponseProvider: Provider<ScanResponse>,
) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.menu_onboarding, menu) menuInflater.inflate(R.menu.menu_onboarding, menu)
} }
@ -24,7 +29,12 @@ class OnboardingMenuProvider : MenuProvider {
R.id.menu_item_chat_support -> { R.id.menu_item_chat_support -> {
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro)) Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
// changed on email support [REDACTED_TASK_KEY] // changed on email support [REDACTED_TASK_KEY]
store.dispatch(GlobalAction.SendEmail(SupportInfo())) store.dispatch(
GlobalAction.SendEmail(
feedbackData = SupportInfo(),
scanResponse = scanResponseProvider(),
),
)
true true
} }
else -> false else -> false

View file

@ -9,6 +9,8 @@ import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.transitions.HomeToOnboardingTransition import com.tangem.tap.common.transitions.HomeToOnboardingTransition
import com.tangem.tap.features.BaseStoreFragment import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.onboarding.OnboardingMenuProvider import com.tangem.tap.features.onboarding.OnboardingMenuProvider
import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentOnboardingMainBinding import com.tangem.wallet.databinding.FragmentOnboardingMainBinding
import com.tangem.wallet.databinding.ViewOnboardingProgressBinding import com.tangem.wallet.databinding.ViewOnboardingProgressBinding
@ -32,7 +34,13 @@ abstract class BaseOnboardingFragment<T> : BaseStoreFragment(R.layout.fragment_o
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar) (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
} }
override fun loadToolbarMenu(): MenuProvider? = OnboardingMenuProvider() override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider(
scanResponseProvider = Provider {
store.state.globalState.onboardingState.onboardingManager?.scanResponse
?: store.state.detailsState.scanResponse
?: error("ScanResponse must be not null")
},
)
protected fun showConfetti(show: Boolean) = with(binding.vConfetti) { protected fun showConfetti(show: Boolean) = with(binding.vConfetti) {
lavConfetti.show(show) lavConfetti.show(show)

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.onboarding.products.twins.redux package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.getTwinCardNumber import com.tangem.domain.common.getTwinCardNumber
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action import org.rekotlin.Action
@ -35,7 +35,10 @@ private fun internalReduce(action: Action, state: AppState): TwinCardsState {
) )
} }
is TwinCardsAction.SetStepOfScreen -> { is TwinCardsAction.SetStepOfScreen -> {
state = state.copy(currentStep = action.step) state = state.copy(
currentStep = action.step,
welcomeOnlyScanResponse = (action.step as? TwinCardsStep.WelcomeOnly)?.scanResponse,
)
} }
is TwinCardsAction.SetUserUnderstand -> { is TwinCardsAction.SetUserUnderstand -> {
state = state.copy(userWasUnderstandIfWalletRecreate = action.isUnderstand) state = state.copy(userWasUnderstandIfWalletRecreate = action.isUnderstand)

View file

@ -1,8 +1,8 @@
package com.tangem.tap.features.onboarding.products.twins.redux package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.WalletManager
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TapError import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance import com.tangem.tap.features.onboarding.OnboardingWalletBalance
@ -29,6 +29,7 @@ data class TwinCardsState(
val balanceNonCriticalError: TapError? = null, val balanceNonCriticalError: TapError? = null,
val balanceCriticalError: TapError? = null, val balanceCriticalError: TapError? = null,
val showConfetti: Boolean = false, val showConfetti: Boolean = false,
val welcomeOnlyScanResponse: ScanResponse? = null,
) : StateType { ) : StateType {
val steps: List<TwinCardsStep> val steps: List<TwinCardsStep>

View file

@ -6,6 +6,7 @@ import android.view.View
import android.view.animation.OvershootInterpolator import android.view.animation.OvershootInterpolator
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import coil.load import coil.load
@ -25,12 +26,14 @@ import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.domain.twins.TwinsCardWidget import com.tangem.tap.domain.twins.TwinsCardWidget
import com.tangem.tap.features.addBackPressHandler import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.OnboardingMenuProvider
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -61,6 +64,15 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment<TwinCardsState>(
} }
} }
override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider(
scanResponseProvider = Provider {
store.state.twinCardsState.welcomeOnlyScanResponse
?: store.state.globalState.onboardingState.onboardingManager?.scanResponse
?: store.state.detailsState.scanResponse
?: error("ScanResponse must be not null")
},
)
@Suppress("MagicNumber") @Suppress("MagicNumber")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View file

@ -46,6 +46,7 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.*
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog
import com.tangem.tap.mainScope import com.tangem.tap.mainScope
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding
import com.tangem.wallet.databinding.LayoutOnboardingSeedPhraseBinding import com.tangem.wallet.databinding.LayoutOnboardingSeedPhraseBinding
@ -115,7 +116,12 @@ class OnboardingWalletFragment :
) )
} }
override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider() override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider(
scanResponseProvider = Provider {
store.state.globalState.onboardingState.onboardingManager?.scanResponse
?: error("ScanResponse must be not null")
},
)
private fun reInitCardsWidgetIfNeeded(backupCardsCounts: Int) = with(binding) { private fun reInitCardsWidgetIfNeeded(backupCardsCounts: Int) = with(binding) {
val viewBackupCount = flCardsContainer.childCount - 1 val viewBackupCount = flCardsContainer.childCount - 1
@ -507,7 +513,13 @@ class OnboardingWalletFragment :
onOpenChat = { onOpenChat = {
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro)) Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
// changed on email support [REDACTED_TASK_KEY] // changed on email support [REDACTED_TASK_KEY]
store.dispatch(GlobalAction.SendEmail(SupportInfo())) store.dispatch(
GlobalAction.SendEmail(
feedbackData = SupportInfo(),
scanResponse = store.state.globalState.onboardingState.onboardingManager?.scanResponse
?: error("ScanResponse must be not null"),
),
)
}, },
onOpenUriClick = { uri -> onOpenUriClick = { uri ->
store.dispatchOpenUrl(uri.toString()) store.dispatchOpenUrl(uri.toString())

View file

@ -23,7 +23,13 @@ object WalletActivationErrorDialog {
setNegativeButton(R.string.common_support) { _, _ -> setNegativeButton(R.string.common_support) { _, _ ->
// changed on email support [REDACTED_TASK_KEY] // changed on email support [REDACTED_TASK_KEY]
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro)) Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
store.dispatch(GlobalAction.SendEmail(SupportInfo())) store.dispatch(
GlobalAction.SendEmail(
feedbackData = SupportInfo(),
scanResponse = store.state.globalState.onboardingState.onboardingManager?.scanResponse
?: error("ScanResponse must be not null"),
),
)
} }
setOnDismissListener { store.dispatchDialogHide() } setOnDismissListener { store.dispatchDialogHide() }
setCancelable(false) setCancelable(false)

View file

@ -7,6 +7,7 @@ import com.tangem.blockchain.common.FeePaidCurrency
import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.core.TangemSdkError import com.tangem.common.core.TangemSdkError
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog import com.tangem.domain.redux.StateDialog
import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
@ -185,12 +186,16 @@ sealed class SendAction : SendScreenAction {
) : Dialog() ) : Dialog()
sealed class SendTransactionFails : Dialog() { sealed class SendTransactionFails : Dialog() {
data class CardSdkError(val error: TangemSdkError) : Dialog() data class CardSdkError(val error: TangemSdkError, val scanResponse: ScanResponse) : Dialog()
data class BlockchainSdkError(val error: com.tangem.blockchain.common.BlockchainSdkError) : Dialog() data class BlockchainSdkError(
val error: com.tangem.blockchain.common.BlockchainSdkError,
val scanResponse: ScanResponse,
) : Dialog()
} }
data class RequestFeeError( data class RequestFeeError(
val error: com.tangem.blockchain.common.BlockchainSdkError, val error: com.tangem.blockchain.common.BlockchainSdkError,
val scanResponse: ScanResponse,
val onRetry: () -> Unit, val onRetry: () -> Unit,
) : Dialog() ) : Dialog()

View file

@ -1,6 +1,9 @@
package com.tangem.tap.features.send.redux.middlewares package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.common.* import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.isZero import com.tangem.common.extensions.isZero
@ -92,6 +95,7 @@ class RequestFeeMiddleware {
dispatch( dispatch(
SendAction.Dialog.RequestFeeError( SendAction.Dialog.RequestFeeError(
error = blockchainSdkError, error = blockchainSdkError,
scanResponse = scanResponse,
onRetry = { dispatch(FeeAction.RequestFee) }, onRetry = { dispatch(FeeAction.RequestFee) },
), ),
) )

View file

@ -288,11 +288,25 @@ private fun sendTransaction(
val tangemSdkError = error.tangemError as? TangemSdkError ?: return@withMainContext val tangemSdkError = error.tangemError as? TangemSdkError ?: return@withMainContext
if (tangemSdkError is TangemSdkError.UserCancelled) return@withMainContext if (tangemSdkError is TangemSdkError.UserCancelled) return@withMainContext
dispatch(SendAction.Dialog.SendTransactionFails.CardSdkError(tangemSdkError)) dispatch(
SendAction.Dialog.SendTransactionFails.CardSdkError(
error = tangemSdkError,
scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager)
.selectedUserWalletSync?.scanResponse
?: error("ScanResponse must be not null"),
),
)
} }
is BlockchainSdkError.CreateAccountUnderfunded -> { is BlockchainSdkError.CreateAccountUnderfunded -> {
// from XLM, XRP, Polkadot // from XLM, XRP, Polkadot
dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error)) dispatch(
SendAction.Dialog.SendTransactionFails.BlockchainSdkError(
error = error,
scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager)
.selectedUserWalletSync?.scanResponse
?: error("ScanResponse must be not null"),
),
)
} }
is BlockchainSdkError.Kaspa.UtxoAmountError -> { is BlockchainSdkError.Kaspa.UtxoAmountError -> {
dispatch( dispatch(
@ -331,7 +345,14 @@ private fun sendTransaction(
) )
} }
else -> { else -> {
dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error)) dispatch(
SendAction.Dialog.SendTransactionFails.BlockchainSdkError(
error = error,
scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager)
.selectedUserWalletSync?.scanResponse
?: error("ScanResponse must be not null"),
),
)
} }
} }
} }

View file

@ -23,7 +23,12 @@ object RequestFeeErrorDialog {
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage)) setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNegativeButton(R.string.details_row_title_contact_to_support) { _, _ -> setNegativeButton(R.string.details_row_title_contact_to_support) { _, _ ->
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send)) Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send))
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage))) store.dispatch(
GlobalAction.SendEmail(
feedbackData = SendTransactionFailedEmail(errorMessage),
scanResponse = dialog.scanResponse,
),
)
} }
setPositiveButton(R.string.common_retry) { _, _ -> dialog.onRetry() } setPositiveButton(R.string.common_retry) { _, _ -> dialog.onRetry() }
setNeutralButton(R.string.common_cancel) { _, _ -> } setNeutralButton(R.string.common_cancel) { _, _ -> }

View file

@ -8,6 +8,7 @@ import com.tangem.common.module.ModuleMessageConverter
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.Basic
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.sdk.extensions.localizedDescription import com.tangem.sdk.extensions.localizedDescription
import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.feedback.SendTransactionFailedEmail import com.tangem.tap.common.feedback.SendTransactionFailedEmail
@ -21,21 +22,21 @@ import com.tangem.wallet.R
*/ */
object SendTransactionFailsDialog { object SendTransactionFailsDialog {
fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.CardSdkError): AlertDialog { fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.CardSdkError): AlertDialog {
return create(context, dialog.error.localizedDescription(context)) return create(context, dialog.error.localizedDescription(context), dialog.scanResponse)
} }
fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.BlockchainSdkError): AlertDialog { fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.BlockchainSdkError): AlertDialog {
val errorConverter = BlockchainSdkErrorConverter(context) val errorConverter = BlockchainSdkErrorConverter(context)
return create(context, errorConverter.convert(dialog.error)) return create(context, errorConverter.convert(dialog.error), dialog.scanResponse)
} }
private fun create(context: Context, errorMessage: String): AlertDialog { private fun create(context: Context, errorMessage: String, scanResponse: ScanResponse): AlertDialog {
return AlertDialog.Builder(context).apply { return AlertDialog.Builder(context).apply {
setTitle(R.string.alert_failed_to_send_transaction_title) setTitle(R.string.alert_failed_to_send_transaction_title)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage)) setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNeutralButton(R.string.details_row_title_contact_to_support) { _, _ -> setNeutralButton(R.string.details_row_title_contact_to_support) { _, _ ->
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send)) Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send))
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage))) store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage), scanResponse))
} }
setPositiveButton(R.string.common_cancel) { _, _ -> } setPositiveButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) } setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) }

View file

@ -16,6 +16,8 @@ import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
@ -82,6 +84,8 @@ data class DaggerGraphState(
val blockchainSDKFactory: BlockchainSDKFactory? = null, val blockchainSDKFactory: BlockchainSDKFactory? = null,
val emailSender: EmailSender? = null, val emailSender: EmailSender? = null,
val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase? = null, val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase? = null,
val getFeedbackEmailUseCase: GetFeedbackEmailUseCase? = null,
val getCardInfoUseCase: GetCardInfoUseCase? = null,
val assetLoader: AssetLoader? = null, val assetLoader: AssetLoader? = null,
val detailsFeatureToggles: DetailsFeatureToggles? = null, val detailsFeatureToggles: DetailsFeatureToggles? = null,
val stakingRouter: StakingRouter? = null, val stakingRouter: StakingRouter? = null,

View file

@ -1,8 +0,0 @@
package com.tangem.core.navigation.feedback
class DummyFeedbackManager : FeedbackManager {
override fun sendEmail(type: FeedbackType) {
/* no-op */
}
}

View file

@ -1,6 +0,0 @@
package com.tangem.core.navigation.feedback
interface FeedbackManager {
fun sendEmail(type: FeedbackType)
}

View file

@ -1,13 +0,0 @@
package com.tangem.core.navigation.feedback
sealed class FeedbackType {
data object RateCanBeBetter : FeedbackType()
data object ScanFails : FeedbackType()
data class SendTransactionFailed(val error: String) : FeedbackType()
data object Feedback : FeedbackType()
data object Support : FeedbackType()
}

View file

@ -37,7 +37,9 @@ dependencies {
implementation(projects.domain.feedback) implementation(projects.domain.feedback)
implementation(projects.domain.legacy) implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.models) implementation(projects.domain.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models) implementation(projects.domain.wallets.models)
implementation(projects.libs.blockchainSdk)
} }

View file

@ -9,12 +9,14 @@ import com.tangem.data.feedback.converters.CardInfoConverter
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.datasource.local.preferences.utils.getObjectMap
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.models.*
import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import timber.log.Timber import timber.log.Timber
@ -25,43 +27,47 @@ import java.io.StringWriter
/** /**
* Implementation of [FeedbackRepository] * Implementation of [FeedbackRepository]
* *
* @property appPreferencesStore application preferences store * @property appPreferencesStore application preferences store
* @property userWalletsStore user wallets store * @property userWalletsListManager user wallets list manager
* @property walletManagersStore wallet managers store * @property walletManagersStore wallet managers store
* @property context context for getting app version * @property context context for getting app version
* @property dispatchers coroutine dispatchers provider
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
internal class DefaultFeedbackRepository( internal class DefaultFeedbackRepository(
private val appPreferencesStore: AppPreferencesStore, private val appPreferencesStore: AppPreferencesStore,
private val userWalletsStore: UserWalletsStore, private val userWalletsListManager: UserWalletsListManager,
private val walletManagersStore: WalletManagersStore, private val walletManagersStore: WalletManagersStore,
private val context: Context, private val context: Context,
private val dispatchers: CoroutineDispatcherProvider,
) : FeedbackRepository { ) : FeedbackRepository {
private val blockchainsErrors = MutableStateFlow<Map<UserWalletId, BlockchainErrorInfo>>(emptyMap()) private val blockchainsErrors = MutableStateFlow<Map<UserWalletId, BlockchainErrorInfo>>(emptyMap())
override suspend fun getUserWalletsInfo(): UserWalletsInfo { override suspend fun getCardInfo(scanResponse: ScanResponse) = CardInfoConverter.convert(value = scanResponse)
override suspend fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo {
return UserWalletsInfo( return UserWalletsInfo(
selectedUserWalletId = getSelectedUserWallet().walletId.stringValue, selectedUserWalletId = userWalletId?.stringValue ?: "card isn't activated",
totalUserWallets = userWalletsStore.getAllSyncOrNull()?.size ?: error("No user wallets found"), totalUserWallets = userWalletsListManager.walletsCount,
) )
} }
override suspend fun getCardInfo(): CardInfo { override suspend fun getBlockchainInfoList(userWalletId: UserWalletId): List<BlockchainInfo> {
return CardInfoConverter.convert(value = getSelectedUserWallet())
}
override suspend fun getBlockchainInfoList(): List<BlockchainInfo> {
return walletManagersStore return walletManagersStore
.getAllSync(userWalletId = getSelectedUserWallet().walletId) .getAllSync(userWalletId = userWalletId)
.map(BlockchainInfoConverter::convert) .map(BlockchainInfoConverter::convert)
} }
override suspend fun getBlockchainInfo(blockchainId: String, derivationPath: String?): BlockchainInfo? { override suspend fun getBlockchainInfo(
userWalletId: UserWalletId,
blockchainId: String,
derivationPath: String?,
): BlockchainInfo? {
return walletManagersStore return walletManagersStore
.getSyncOrNull( .getSyncOrNull(
userWalletId = getSelectedUserWallet().walletId, userWalletId = userWalletId,
blockchain = Blockchain.fromId(blockchainId), blockchain = Blockchain.fromId(blockchainId),
derivationPath = derivationPath, derivationPath = derivationPath,
) )
@ -77,18 +83,18 @@ internal class DefaultFeedbackRepository(
} }
override fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) { override fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) {
val userWallet = userWalletsListManager.selectedUserWalletSync ?: error("UserWallet is not selected")
blockchainsErrors.update { blockchainsErrors.update {
it.toMutableMap().apply { it.toMutableMap().apply {
put(getSelectedUserWallet().walletId, error) put(userWallet.walletId, error)
} }
} }
} }
override suspend fun getBlockchainErrorInfo(): BlockchainErrorInfo? { override suspend fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? {
return blockchainsErrors.value[getSelectedUserWallet().walletId].also { return blockchainsErrors.value[userWalletId].also {
if (it == null) { if (it == null) Timber.e("Blockchain error info is null for $userWalletId")
Timber.e("Blockchain error info is null for ${getSelectedUserWallet().walletId}")
}
} }
} }
@ -99,7 +105,7 @@ internal class DefaultFeedbackRepository(
} }
override suspend fun createLogFile(logs: String): File? { override suspend fun createLogFile(logs: String): File? {
return try { return runCatching(dispatchers.io) {
val file = File(context.filesDir, LOGS_FILE) val file = File(context.filesDir, LOGS_FILE)
file.delete() file.delete()
file.createNewFile() file.createNewFile()
@ -113,8 +119,8 @@ internal class DefaultFeedbackRepository(
fileWriter.close() fileWriter.close()
file file
} catch (ex: Exception) { }.getOrElse {
Timber.e(ex, "Logs file isn't created") Timber.e(it, "Logs file isn't created")
null null
} }
} }
@ -130,11 +136,6 @@ internal class DefaultFeedbackRepository(
) )
} }
private fun getSelectedUserWallet(): UserWallet {
return userWalletsStore.selectedUserWalletOrNull
?: error("UserWallet is not selected")
}
private companion object { private companion object {
const val LOGS_FILE = "logs.txt" const val LOGS_FILE = "logs.txt"
} }

View file

@ -2,28 +2,36 @@ package com.tangem.data.feedback.converters
import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.feedback.models.CardInfo import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
/** /**
* Converter from [UserWallet] to [CardInfo] * Converter from [ScanResponse] to [CardInfo]
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
internal object CardInfoConverter : Converter<UserWallet, CardInfo> { internal object CardInfoConverter : Converter<ScanResponse, CardInfo> {
override fun convert(value: UserWallet): CardInfo { override fun convert(value: ScanResponse): CardInfo {
return with(value.scanResponse) { return with(value) {
CardInfo( CardInfo(
userWalletId = createUserWalletId(scanResponse = value),
cardId = card.cardId, cardId = card.cardId,
firmwareVersion = card.firmwareVersion.stringValue, firmwareVersion = card.firmwareVersion.stringValue,
cardBlockchain = walletData?.blockchain, cardBlockchain = walletData?.blockchain,
signedHashesList = card.wallets.map { signedHashesList = card.wallets.map {
CardInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString()) CardInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString())
}, },
isImported = value.isImported, isImported = value.card.wallets.any(CardDTO.Wallet::isImported),
isStart2Coin = value.scanResponse.card.isStart2Coin, isStart2Coin = value.card.isStart2Coin,
) )
} }
} }
private fun createUserWalletId(scanResponse: ScanResponse): UserWalletId? {
return UserWalletIdBuilder.scanResponse(scanResponse = scanResponse).build()
}
} }

View file

@ -3,9 +3,10 @@ package com.tangem.data.feedback.di
import android.content.Context import android.content.Context
import com.tangem.data.feedback.DefaultFeedbackRepository import com.tangem.data.feedback.DefaultFeedbackRepository
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -21,10 +22,17 @@ internal object FeedbackRepositoryModule {
@Singleton @Singleton
fun provideFeedbackRepository( fun provideFeedbackRepository(
appPreferencesStore: AppPreferencesStore, appPreferencesStore: AppPreferencesStore,
userWalletsStore: UserWalletsStore, userWalletsListManager: UserWalletsListManager,
walletManagersStore: WalletManagersStore, walletManagersStore: WalletManagersStore,
@ApplicationContext context: Context, @ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): FeedbackRepository { ): FeedbackRepository {
return DefaultFeedbackRepository(appPreferencesStore, userWalletsStore, walletManagersStore, context) return DefaultFeedbackRepository(
appPreferencesStore = appPreferencesStore,
userWalletsListManager = userWalletsListManager,
walletManagersStore = walletManagersStore,
context = context,
dispatchers = dispatchers,
)
} }
} }

View file

@ -13,5 +13,6 @@ dependencies {
implementation(deps.jodatime) implementation(deps.jodatime)
implementation(projects.core.res) implementation(projects.core.res)
implementation(projects.domain.models)
implementation(projects.domain.wallets.models) implementation(projects.domain.wallets.models)
} }

View file

@ -0,0 +1,8 @@
package com.tangem.domain.feedback
import com.tangem.domain.feedback.models.FeedbackEmailType
interface FeedbackManager {
fun sendEmail(type: FeedbackEmailType)
}

View file

@ -0,0 +1,23 @@
package com.tangem.domain.feedback
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.models.scan.ScanResponse
/**
* UseCase for creating 'CardInfo'
*
* @property feedbackRepository feedback repository
*
[REDACTED_AUTHOR]
*/
class GetCardInfoUseCase(
private val feedbackRepository: FeedbackRepository,
) {
suspend operator fun invoke(scanResponse: ScanResponse): Either<Throwable, CardInfo> {
return catch { feedbackRepository.getCardInfo(scanResponse) }
}
}

View file

@ -25,23 +25,21 @@ class GetFeedbackEmailUseCase(
private val emailMessageBodyResolver = EmailMessageBodyResolver(feedbackRepository) private val emailMessageBodyResolver = EmailMessageBodyResolver(feedbackRepository)
suspend operator fun invoke(type: FeedbackEmailType): FeedbackEmail { suspend operator fun invoke(type: FeedbackEmailType): FeedbackEmail {
val cardInfo = feedbackRepository.getCardInfo()
val formattedLogs = AppLogsFormatter().format(appLogs = feedbackRepository.getAppLogs()) val formattedLogs = AppLogsFormatter().format(appLogs = feedbackRepository.getAppLogs())
return FeedbackEmail( return FeedbackEmail(
address = getAddress(cardInfo), address = getAddress(type.cardInfo),
subject = emailSubjectResolver.resolve(type, cardInfo), subject = emailSubjectResolver.resolve(type),
message = createMessage(type, cardInfo), message = createMessage(type),
file = feedbackRepository.createLogFile(logs = formattedLogs), file = feedbackRepository.createLogFile(logs = formattedLogs),
) )
} }
private fun getAddress(cardInfo: CardInfo): String { private fun getAddress(cardInfo: CardInfo?): String {
return if (cardInfo.isStart2Coin) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL return if (cardInfo?.isStart2Coin == true) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL
} }
private suspend fun createMessage(type: FeedbackEmailType, cardInfo: CardInfo): String { private suspend fun createMessage(type: FeedbackEmailType): String {
return StringBuilder().apply { return StringBuilder().apply {
val title = emailMessageTitleResolver.resolve(type) val title = emailMessageTitleResolver.resolve(type)
append(title) append(title)
@ -50,9 +48,7 @@ class GetFeedbackEmailUseCase(
appendDisclaimerIfNeeded(type) appendDisclaimerIfNeeded(type)
skipLine() val body = emailMessageBodyResolver.resolve(type)
val body = emailMessageBodyResolver.resolve(type, cardInfo)
append(body) append(body)
}.toString() }.toString()
} }
@ -62,6 +58,7 @@ class GetFeedbackEmailUseCase(
this this
} else { } else {
append(resources.getString(R.string.feedback_data_collection_message)) append(resources.getString(R.string.feedback_data_collection_message))
skipLine()
} }
} }
} }

View file

@ -1,6 +1,9 @@
package com.tangem.domain.feedback.models package com.tangem.domain.feedback.models
import com.tangem.domain.wallets.models.UserWalletId
data class CardInfo( data class CardInfo(
val userWalletId: UserWalletId?,
val cardId: String, val cardId: String,
val firmwareVersion: String, val firmwareVersion: String,
val cardBlockchain: String?, val cardBlockchain: String?,

View file

@ -7,15 +7,19 @@ package com.tangem.domain.feedback.models
*/ */
sealed interface FeedbackEmailType { sealed interface FeedbackEmailType {
val cardInfo: CardInfo?
/** User initiate request yourself. Example, button on DetailsScreen or OnboardingScreen */ /** User initiate request yourself. Example, button on DetailsScreen or OnboardingScreen */
data object DirectUserRequest : FeedbackEmailType data class DirectUserRequest(override val cardInfo: CardInfo) : FeedbackEmailType
/** User rate the app as "can be better" */ /** User rate the app as "can be better" */
data object RateCanBeBetter : FeedbackEmailType data class RateCanBeBetter(override val cardInfo: CardInfo) : FeedbackEmailType
/** User has problem with scanning */ /** User has problem with scanning */
data object ScanningProblem : FeedbackEmailType data object ScanningProblem : FeedbackEmailType {
override val cardInfo: CardInfo? = null
}
/** User has problem with sending transaction */ /** User has problem with sending transaction */
data object TransactionSendingProblem : FeedbackEmailType data class TransactionSendingProblem(override val cardInfo: CardInfo) : FeedbackEmailType
} }

View file

@ -1,23 +1,29 @@
package com.tangem.domain.feedback.repository package com.tangem.domain.feedback.repository
import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.models.*
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWalletId
import java.io.File import java.io.File
interface FeedbackRepository { interface FeedbackRepository {
suspend fun getUserWalletsInfo(): UserWalletsInfo suspend fun getCardInfo(scanResponse: ScanResponse): CardInfo
suspend fun getCardInfo(): CardInfo suspend fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo
suspend fun getBlockchainInfoList(): List<BlockchainInfo> suspend fun getBlockchainInfoList(userWalletId: UserWalletId): List<BlockchainInfo>
suspend fun getBlockchainInfo(blockchainId: String, derivationPath: String?): BlockchainInfo?
fun getPhoneInfo(): PhoneInfo fun getPhoneInfo(): PhoneInfo
suspend fun getBlockchainInfo(
userWalletId: UserWalletId,
blockchainId: String,
derivationPath: String?,
): BlockchainInfo?
fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) fun saveBlockchainErrorInfo(error: BlockchainErrorInfo)
suspend fun getBlockchainErrorInfo(): BlockchainErrorInfo? suspend fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo?
suspend fun getAppLogs(): List<AppLogModel> suspend fun getAppLogs(): List<AppLogModel>

View file

@ -16,25 +16,33 @@ internal class EmailMessageBodyResolver(
private val feedbackRepository: FeedbackRepository, private val feedbackRepository: FeedbackRepository,
) { ) {
/** Resolve email message body by [type] using [cardInfo] */ /** Resolve email message body by [type] */
suspend fun resolve(type: FeedbackEmailType, cardInfo: CardInfo): String = with(FeedbackDataBuilder()) { suspend fun resolve(type: FeedbackEmailType): String = with(FeedbackDataBuilder()) {
when (type) { when (type) {
FeedbackEmailType.DirectUserRequest -> addUserRequestBody(cardInfo) is FeedbackEmailType.DirectUserRequest -> addUserRequestBody(type.cardInfo)
FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(cardInfo) is FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(type.cardInfo)
FeedbackEmailType.ScanningProblem -> addScanningProblemBody() is FeedbackEmailType.ScanningProblem -> addScanningProblemBody()
FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(cardInfo) is FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(type.cardInfo)
} }
return build() return build()
} }
private suspend fun FeedbackDataBuilder.addUserRequestBody(cardInfo: CardInfo) { private suspend fun FeedbackDataBuilder.addUserRequestBody(cardInfo: CardInfo) {
addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo()) addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo(cardInfo.userWalletId))
addDelimiter() addDelimiter()
addCardInfo(cardInfo) addCardInfo(cardInfo)
addDelimiter() addDelimiter()
addBlockchainInfoList(blockchainInfoList = feedbackRepository.getBlockchainInfoList())
addDelimiter() if (cardInfo.userWalletId != null) {
val blockchainInfoList = feedbackRepository.getBlockchainInfoList(cardInfo.userWalletId)
if (blockchainInfoList.isNotEmpty()) {
addBlockchainInfoList(blockchainInfoList = blockchainInfoList)
addDelimiter()
}
}
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
} }
@ -46,9 +54,11 @@ internal class EmailMessageBodyResolver(
addCardInfo(cardInfo) addCardInfo(cardInfo)
addDelimiter() addDelimiter()
val blockchainError = feedbackRepository.getBlockchainErrorInfo() val userWalletId = requireNotNull(cardInfo.userWalletId) { "UserWalletId must be not null" }
val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId)
val blockchainInfo = blockchainError?.let { val blockchainInfo = blockchainError?.let {
feedbackRepository.getBlockchainInfo( feedbackRepository.getBlockchainInfo(
userWalletId = userWalletId,
blockchainId = blockchainError.blockchainId, blockchainId = blockchainError.blockchainId,
derivationPath = blockchainError.derivationPath, derivationPath = blockchainError.derivationPath,
) )

View file

@ -16,10 +16,10 @@ internal class EmailMessageTitleResolver(private val resources: Resources) {
/** Resolve email message title by [type] */ /** Resolve email message title by [type] */
fun resolve(type: FeedbackEmailType): String { fun resolve(type: FeedbackEmailType): String {
return when (type) { return when (type) {
FeedbackEmailType.DirectUserRequest -> R.string.feedback_preface_support is FeedbackEmailType.DirectUserRequest -> R.string.feedback_preface_support
FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative
FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed
FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_preface_tx_failed is FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_preface_tx_failed
} }
.let(resources::getString) .let(resources::getString)
} }

View file

@ -2,7 +2,6 @@ package com.tangem.domain.feedback.utils
import android.content.res.Resources import android.content.res.Resources
import com.tangem.domain.feedback.R import com.tangem.domain.feedback.R
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.feedback.models.FeedbackEmailType
/** /**
@ -14,19 +13,19 @@ import com.tangem.domain.feedback.models.FeedbackEmailType
*/ */
internal class EmailSubjectResolver(private val resources: Resources) { internal class EmailSubjectResolver(private val resources: Resources) {
/** Resolve email message body by [type] using [cardInfo] */ /** Resolve email message body by [type] */
fun resolve(type: FeedbackEmailType, cardInfo: CardInfo): String { fun resolve(type: FeedbackEmailType): String {
return when (type) { return when (type) {
FeedbackEmailType.DirectUserRequest -> { is FeedbackEmailType.DirectUserRequest -> {
if (cardInfo.isStart2Coin) { if (type.cardInfo.isStart2Coin) {
R.string.feedback_subject_support R.string.feedback_subject_support
} else { } else {
R.string.feedback_subject_support_tangem R.string.feedback_subject_support_tangem
} }
} }
FeedbackEmailType.RateCanBeBetter -> R.string.feedback_subject_rate_negative is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_subject_rate_negative
FeedbackEmailType.ScanningProblem -> R.string.feedback_subject_scan_failed is FeedbackEmailType.ScanningProblem -> R.string.feedback_subject_scan_failed
FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_subject_tx_failed is FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_subject_tx_failed
} }
.let(resources::getString) .let(resources::getString)
} }

View file

@ -8,7 +8,7 @@ import java.math.BigDecimal
sealed interface LegacyAction : Action { sealed interface LegacyAction : Action {
object SendEmailRateCanBeBetter : LegacyAction data class SendEmailRateCanBeBetter(val scanResponse: ScanResponse) : LegacyAction
/** /**
* Initiate an onboarding process. * Initiate an onboarding process.
@ -34,5 +34,6 @@ sealed interface LegacyAction : Action {
val fee: BigDecimal?, val fee: BigDecimal?,
val destinationAddress: String?, val destinationAddress: String?,
val errorMessage: String, val errorMessage: String,
val scanResponse: ScanResponse,
) : LegacyAction ) : LegacyAction
} }

View file

@ -28,6 +28,7 @@ dependencies {
/* Project - Domain */ /* Project - Domain */
implementation(projects.domain.models) implementation(projects.domain.models)
implementation(projects.domain.feedback)
implementation(projects.domain.wallets) implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models) implementation(projects.domain.wallets.models)
implementation(projects.domain.card) implementation(projects.domain.card)

View file

@ -3,7 +3,6 @@ package com.tangem.features.details.component.preview
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.tangem.core.decompose.navigation.DummyRouter import com.tangem.core.decompose.navigation.DummyRouter
import com.tangem.core.navigation.feedback.DummyFeedbackManager
import com.tangem.core.navigation.url.DummyUrlOpener import com.tangem.core.navigation.url.DummyUrlOpener
import com.tangem.features.details.component.DetailsComponent import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.details.entity.DetailsFooterUM import com.tangem.features.details.entity.DetailsFooterUM
@ -19,8 +18,7 @@ internal class PreviewDetailsComponent : DetailsComponent {
ItemsBuilder( ItemsBuilder(
router = DummyRouter(), router = DummyRouter(),
urlOpener = DummyUrlOpener(), urlOpener = DummyUrlOpener(),
feedbackManager = DummyFeedbackManager(), ).buildAll(isWalletConnectAvailable = true, onSupportClick = {})
).buldAll(isWalletConnectAvailable = true)
} }
private val previewFooter = DetailsFooterUM( private val previewFooter = DetailsFooterUM(

View file

@ -5,7 +5,11 @@ import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.feedback.FeedbackManager
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.details.component.DetailsComponent import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.details.entity.DetailsFooterUM import com.tangem.features.details.entity.DetailsFooterUM
import com.tangem.features.details.entity.DetailsItemUM import com.tangem.features.details.entity.DetailsItemUM
@ -33,6 +37,9 @@ internal class DetailsModel @Inject constructor(
private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase, private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase,
private val router: Router, private val router: Router,
private val paramsContainer: ParamsContainer, private val paramsContainer: ParamsContainer,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val feedbackManager: FeedbackManager,
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
) : Model() { ) : Model() {
@ -66,7 +73,22 @@ internal class DetailsModel @Inject constructor(
false false
} }
items.value = itemsBuilder.buldAll(isWalletConnectAvailable) items.value = itemsBuilder.buildAll(
isWalletConnectAvailable = isWalletConnectAvailable,
onSupportClick = ::sendFeedback,
)
}
private fun sendFeedback() {
modelScope.launch {
val scanResponse = getSelectedWalletSyncUseCase().getOrNull()?.scanResponse
?: error("Selected wallet is null")
val cardInfo = getCardInfoUseCase(scanResponse = scanResponse).getOrNull()
?: error("CardInfo must be not null")
feedbackManager.sendEmail(type = FeedbackEmailType.DirectUserRequest(cardInfo))
}
} }
private suspend fun updateState(items: ImmutableList<DetailsItemUM>) { private suspend fun updateState(items: ImmutableList<DetailsItemUM>) {

View file

@ -3,8 +3,6 @@ package com.tangem.features.details.utils
import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.feedback.FeedbackManager
import com.tangem.core.navigation.feedback.FeedbackType
import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.components.block.model.BlockUM
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
@ -21,18 +19,18 @@ import javax.inject.Inject
internal class ItemsBuilder @Inject constructor( internal class ItemsBuilder @Inject constructor(
private val router: Router, private val router: Router,
private val urlOpener: UrlOpener, private val urlOpener: UrlOpener,
private val feedbackManager: FeedbackManager,
) { ) {
suspend fun buldAll(isWalletConnectAvailable: Boolean): ImmutableList<DetailsItemUM> = buildList { fun buildAll(isWalletConnectAvailable: Boolean, onSupportClick: () -> Unit): ImmutableList<DetailsItemUM> =
buildWalletConnectBlock(isWalletConnectAvailable)?.let(::add) buildList {
buildUserWalletListBlock().let(::add) buildWalletConnectBlock(isWalletConnectAvailable)?.let(::add)
buildShopBlock().let(::add) buildUserWalletListBlock().let(::add)
buildSettingsBlock().let(::add) buildShopBlock().let(::add)
buildSupportBlock().let(::add) buildSettingsBlock().let(::add)
}.toImmutableList() buildSupportBlock(onSupportClick).let(::add)
}.toImmutableList()
private suspend fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean): DetailsItemUM? { private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean): DetailsItemUM? {
return if (isWalletConnectAvailable) { return if (isWalletConnectAvailable) {
DetailsItemUM.WalletConnect( DetailsItemUM.WalletConnect(
onClick = { router.push(AppRoute.WalletConnectSessions) }, onClick = { router.push(AppRoute.WalletConnectSessions) },
@ -83,7 +81,7 @@ internal class ItemsBuilder @Inject constructor(
}.toImmutableList(), }.toImmutableList(),
) )
private fun buildSupportBlock(): DetailsItemUM = DetailsItemUM.Basic( private fun buildSupportBlock(onClick: () -> Unit): DetailsItemUM = DetailsItemUM.Basic(
id = "support", id = "support",
items = persistentListOf( items = persistentListOf(
DetailsItemUM.Basic.Item( DetailsItemUM.Basic.Item(
@ -91,7 +89,7 @@ internal class ItemsBuilder @Inject constructor(
block = BlockUM( block = BlockUM(
text = resourceReference(R.string.details_send_feedback), text = resourceReference(R.string.details_send_feedback),
iconRes = R.drawable.ic_comment_24, iconRes = R.drawable.ic_comment_24,
onClick = { feedbackManager.sendEmail(FeedbackType.Feedback) }, onClick = onClick,
), ),
), ),
DetailsItemUM.Basic.Item( DetailsItemUM.Basic.Item(

View file

@ -74,6 +74,7 @@ dependencies {
implementation(projects.domain.card) implementation(projects.domain.card)
implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models) implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.feedback)
implementation(projects.domain.qrScanning) implementation(projects.domain.qrScanning)
implementation(projects.domain.qrScanning.models) implementation(projects.domain.qrScanning.models)
implementation(projects.domain.settings) implementation(projects.domain.settings)

View file

@ -530,6 +530,7 @@ internal class SendViewModel @Inject constructor(
fee = feeValue, fee = feeValue,
destinationAddress = recipient, destinationAddress = recipient,
errorMessage = errorMessage, errorMessage = errorMessage,
scanResponse = userWallet.scanResponse,
), ),
) )
} }

View file

@ -204,7 +204,12 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
viewModelScope.launch(dispatchers.main) { viewModelScope.launch(dispatchers.main) {
neverToSuggestRateAppUseCase() neverToSuggestRateAppUseCase()
reduxStateHolder.dispatch(LegacyAction.SendEmailRateCanBeBetter) reduxStateHolder.dispatch(
LegacyAction.SendEmailRateCanBeBetter(
scanResponse = getSelectedUserWallet()?.scanResponse
?: error("ScanResponse must be not null"),
),
)
} }
} }
@ -250,7 +255,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
} }
} }
private suspend fun getSelectedUserWallet(): UserWallet? { private fun getSelectedUserWallet(): UserWallet? {
val userWalletId = stateHolder.getSelectedWalletId() val userWalletId = stateHolder.getSelectedWalletId()
return getUserWalletUseCase(userWalletId).getOrElse { return getUserWalletUseCase(userWalletId).getOrElse {
Timber.e( Timber.e(