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.repository.CardRepository
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.onboarding.SaveTwinsOnboardingShownUseCase
@ -112,6 +113,8 @@ interface ApplicationEntryPoint {
fun getDetailsFeatureToggles(): DetailsFeatureToggles
fun getGetCardInfoUseCase(): GetCardInfoUseCase
fun getUrlOpener(): UrlOpener
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.common.LogConfig
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.onboarding.SaveTwinsOnboardingShownUseCase
@ -183,6 +184,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase
get() = entryPoint.getSaveBlockchainErrorUseCase()
private val getCardInfoUseCase: GetCardInfoUseCase
get() = entryPoint.getGetCardInfoUseCase()
private val detailsFeatureToggles: DetailsFeatureToggles
get() = entryPoint.getDetailsFeatureToggles()
@ -284,6 +288,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
settingsRepository = settingsRepository,
blockchainSDKFactory = blockchainSDKFactory,
saveBlockchainErrorUseCase = saveBlockchainErrorUseCase,
getFeedbackEmailUseCase = getFeedbackEmailUseCase,
getCardInfoUseCase = getCardInfoUseCase,
assetLoader = assetLoader,
detailsFeatureToggles = detailsFeatureToggles,
urlOpener = urlOpener,
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.GetFeedbackEmailUseCase
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.extensions.inject
import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.withForegroundActivity
import kotlinx.coroutines.launch
@ -36,16 +37,24 @@ class LegacyFeedbackManager(
private var sessionFeedbackFile: File? = null
private var sessionLogsFile: File? = null
fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) {
fun sendEmail(feedbackData: FeedbackData, scanResponse: ScanResponse?) {
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(
when (feedbackData) {
is FeedbackEmail -> FeedbackEmailType.DirectUserRequest
is RateCanBeBetterEmail -> FeedbackEmailType.RateCanBeBetter
type = when (feedbackData) {
is FeedbackEmail -> FeedbackEmailType.DirectUserRequest(cardInfo = getCardInfo())
is RateCanBeBetterEmail -> FeedbackEmailType.RateCanBeBetter(cardInfo = getCardInfo())
is ScanFailsEmail -> FeedbackEmailType.ScanningProblem
is SendTransactionFailedEmail -> FeedbackEmailType.TransactionSendingProblem
else -> FeedbackEmailType.DirectUserRequest
is SendTransactionFailedEmail -> {
FeedbackEmailType.TransactionSendingProblem(cardInfo = getCardInfo())
}
else -> FeedbackEmailType.DirectUserRequest(cardInfo = getCardInfo())
},
)
@ -66,12 +75,28 @@ class LegacyFeedbackManager(
subject = activity.getString(feedbackData.subjectResId),
message = feedbackData.joinTogether(activity, infoHolder),
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) {
chatManager.open(
config = config,

View file

@ -1,13 +1,13 @@
package com.tangem.tap.common.feedback
import com.tangem.core.navigation.feedback.FeedbackManager
import com.tangem.core.navigation.feedback.FeedbackType
import com.tangem.domain.feedback.FeedbackManager
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.tap.store
import timber.log.Timber
internal class ProxyFeedbackManager : FeedbackManager {
override fun sendEmail(type: FeedbackType) {
override fun sendEmail(type: FeedbackEmailType) {
val manager = store.state.globalState.feedbackManager
if (manager == null) {
@ -15,14 +15,6 @@ internal class ProxyFeedbackManager : FeedbackManager {
return
}
val data = when (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)
manager.sendEmail(type)
}
}

View file

@ -81,7 +81,7 @@ sealed class GlobalAction : Action {
data class SetWarningManager(val warningManager: WarningMessagesManager) : 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()
object ExchangeManager : GlobalAction() {

View file

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

View file

@ -1,8 +1,11 @@
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.tokens.utils.convertToAmount
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.SendTransactionFailedEmail
import com.tangem.tap.common.redux.AppState
@ -19,7 +22,10 @@ internal object LegacyMiddleware {
{ action ->
when (action) {
is LegacyAction.SendEmailRateCanBeBetter -> {
store.state.globalState.feedbackManager?.sendEmail(RateCanBeBetterEmail())
store.state.globalState.feedbackManager?.sendEmail(
feedbackData = RateCanBeBetterEmail(),
scanResponse = action.scanResponse,
)
}
is LegacyAction.StartOnboardingProcess -> {
store.dispatch(
@ -28,8 +34,28 @@ internal object LegacyMiddleware {
}
is LegacyAction.SendEmailTransactionFailed -> {
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(
SendTransactionFailedEmail(action.errorMessage),
feedbackData = SendTransactionFailedEmail(action.errorMessage),
scanResponse = action.scanResponse,
)
} else {
scope.launch {
@ -46,7 +72,8 @@ internal object LegacyMiddleware {
)
}
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 {
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 {
store.dispatchDialogHide()

View file

@ -1,11 +1,11 @@
package com.tangem.tap.di
import android.content.Context
import com.tangem.core.navigation.feedback.FeedbackManager
import com.tangem.core.navigation.finisher.AppFinisher
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.core.navigation.share.ShareManager
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.finisher.AndroidAppFinisher
import com.tangem.tap.common.settings.IntentSettingsManager

View file

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

View file

@ -142,7 +142,13 @@ internal class DetailsViewModel(
private fun sendFeedback() {
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() {

View file

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

View file

@ -1,7 +1,7 @@
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.util.cardTypesResolver
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
@ -35,7 +35,10 @@ private fun internalReduce(action: Action, state: AppState): TwinCardsState {
)
}
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 -> {
state = state.copy(userWasUnderstandIfWalletRecreate = action.isUnderstand)

View file

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

View file

@ -6,6 +6,7 @@ import android.view.View
import android.view.animation.OvershootInterpolator
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
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.domain.twins.TwinsCardWidget
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.twins.redux.CreateTwinWalletMode
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.TwinCardsStep
import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding
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")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
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.mainScope
import com.tangem.tap.store
import com.tangem.utils.Provider
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding
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) {
val viewBackupCount = flCardsContainer.childCount - 1
@ -507,7 +513,13 @@ class OnboardingWalletFragment :
onOpenChat = {
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
// 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 ->
store.dispatchOpenUrl(uri.toString())

View file

@ -23,7 +23,13 @@ object WalletActivationErrorDialog {
setNegativeButton(R.string.common_support) { _, _ ->
// changed on email support [REDACTED_TASK_KEY]
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() }
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.transaction.TransactionFee
import com.tangem.common.core.TangemSdkError
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
@ -185,12 +186,16 @@ sealed class SendAction : SendScreenAction {
) : Dialog()
sealed class SendTransactionFails : Dialog() {
data class CardSdkError(val error: TangemSdkError) : Dialog()
data class BlockchainSdkError(val error: com.tangem.blockchain.common.BlockchainSdkError) : Dialog()
data class CardSdkError(val error: TangemSdkError, val scanResponse: ScanResponse) : Dialog()
data class BlockchainSdkError(
val error: com.tangem.blockchain.common.BlockchainSdkError,
val scanResponse: ScanResponse,
) : Dialog()
}
data class RequestFeeError(
val error: com.tangem.blockchain.common.BlockchainSdkError,
val scanResponse: ScanResponse,
val onRetry: () -> Unit,
) : Dialog()

View file

@ -1,6 +1,9 @@
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.extensions.Result
import com.tangem.common.extensions.isZero
@ -92,6 +95,7 @@ class RequestFeeMiddleware {
dispatch(
SendAction.Dialog.RequestFeeError(
error = blockchainSdkError,
scanResponse = scanResponse,
onRetry = { dispatch(FeeAction.RequestFee) },
),
)

View file

@ -288,11 +288,25 @@ private fun sendTransaction(
val tangemSdkError = error.tangemError as? TangemSdkError ?: 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 -> {
// 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 -> {
dispatch(
@ -331,7 +345,14 @@ private fun sendTransaction(
)
}
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))
setNegativeButton(R.string.details_row_title_contact_to_support) { _, _ ->
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() }
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.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.sdk.extensions.localizedDescription
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
@ -21,21 +22,21 @@ import com.tangem.wallet.R
*/
object SendTransactionFailsDialog {
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 {
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 {
setTitle(R.string.alert_failed_to_send_transaction_title)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNeutralButton(R.string.details_row_title_contact_to_support) { _, _ ->
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) { _, _ -> }
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.CardSdkConfigRepository
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.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
@ -82,6 +84,8 @@ data class DaggerGraphState(
val blockchainSDKFactory: BlockchainSDKFactory? = null,
val emailSender: EmailSender? = null,
val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase? = null,
val getFeedbackEmailUseCase: GetFeedbackEmailUseCase? = null,
val getCardInfoUseCase: GetCardInfoUseCase? = null,
val assetLoader: AssetLoader? = null,
val detailsFeatureToggles: DetailsFeatureToggles? = 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.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.models)
implementation(projects.domain.wallets)
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.PreferencesKeys
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.domain.feedback.models.*
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.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import timber.log.Timber
@ -25,43 +27,47 @@ import java.io.StringWriter
/**
* Implementation of [FeedbackRepository]
*
* @property appPreferencesStore application preferences store
* @property userWalletsStore user wallets store
* @property walletManagersStore wallet managers store
* @property context context for getting app version
* @property appPreferencesStore application preferences store
* @property userWalletsListManager user wallets list manager
* @property walletManagersStore wallet managers store
* @property context context for getting app version
* @property dispatchers coroutine dispatchers provider
*
[REDACTED_AUTHOR]
*/
internal class DefaultFeedbackRepository(
private val appPreferencesStore: AppPreferencesStore,
private val userWalletsStore: UserWalletsStore,
private val userWalletsListManager: UserWalletsListManager,
private val walletManagersStore: WalletManagersStore,
private val context: Context,
private val dispatchers: CoroutineDispatcherProvider,
) : FeedbackRepository {
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(
selectedUserWalletId = getSelectedUserWallet().walletId.stringValue,
totalUserWallets = userWalletsStore.getAllSyncOrNull()?.size ?: error("No user wallets found"),
selectedUserWalletId = userWalletId?.stringValue ?: "card isn't activated",
totalUserWallets = userWalletsListManager.walletsCount,
)
}
override suspend fun getCardInfo(): CardInfo {
return CardInfoConverter.convert(value = getSelectedUserWallet())
}
override suspend fun getBlockchainInfoList(): List<BlockchainInfo> {
override suspend fun getBlockchainInfoList(userWalletId: UserWalletId): List<BlockchainInfo> {
return walletManagersStore
.getAllSync(userWalletId = getSelectedUserWallet().walletId)
.getAllSync(userWalletId = userWalletId)
.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
.getSyncOrNull(
userWalletId = getSelectedUserWallet().walletId,
userWalletId = userWalletId,
blockchain = Blockchain.fromId(blockchainId),
derivationPath = derivationPath,
)
@ -77,18 +83,18 @@ internal class DefaultFeedbackRepository(
}
override fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) {
val userWallet = userWalletsListManager.selectedUserWalletSync ?: error("UserWallet is not selected")
blockchainsErrors.update {
it.toMutableMap().apply {
put(getSelectedUserWallet().walletId, error)
put(userWallet.walletId, error)
}
}
}
override suspend fun getBlockchainErrorInfo(): BlockchainErrorInfo? {
return blockchainsErrors.value[getSelectedUserWallet().walletId].also {
if (it == null) {
Timber.e("Blockchain error info is null for ${getSelectedUserWallet().walletId}")
}
override suspend fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? {
return blockchainsErrors.value[userWalletId].also {
if (it == null) Timber.e("Blockchain error info is null for $userWalletId")
}
}
@ -99,7 +105,7 @@ internal class DefaultFeedbackRepository(
}
override suspend fun createLogFile(logs: String): File? {
return try {
return runCatching(dispatchers.io) {
val file = File(context.filesDir, LOGS_FILE)
file.delete()
file.createNewFile()
@ -113,8 +119,8 @@ internal class DefaultFeedbackRepository(
fileWriter.close()
file
} catch (ex: Exception) {
Timber.e(ex, "Logs file isn't created")
}.getOrElse {
Timber.e(it, "Logs file isn't created")
null
}
}
@ -130,11 +136,6 @@ internal class DefaultFeedbackRepository(
)
}
private fun getSelectedUserWallet(): UserWallet {
return userWalletsStore.selectedUserWalletOrNull
?: error("UserWallet is not selected")
}
private companion object {
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.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
/**
* Converter from [UserWallet] to [CardInfo]
* Converter from [ScanResponse] to [CardInfo]
*
[REDACTED_AUTHOR]
*/
internal object CardInfoConverter : Converter<UserWallet, CardInfo> {
internal object CardInfoConverter : Converter<ScanResponse, CardInfo> {
override fun convert(value: UserWallet): CardInfo {
return with(value.scanResponse) {
override fun convert(value: ScanResponse): CardInfo {
return with(value) {
CardInfo(
userWalletId = createUserWalletId(scanResponse = value),
cardId = card.cardId,
firmwareVersion = card.firmwareVersion.stringValue,
cardBlockchain = walletData?.blockchain,
signedHashesList = card.wallets.map {
CardInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString())
},
isImported = value.isImported,
isStart2Coin = value.scanResponse.card.isStart2Coin,
isImported = value.card.wallets.any(CardDTO.Wallet::isImported),
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 com.tangem.data.feedback.DefaultFeedbackRepository
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
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.Provides
import dagger.hilt.InstallIn
@ -21,10 +22,17 @@ internal object FeedbackRepositoryModule {
@Singleton
fun provideFeedbackRepository(
appPreferencesStore: AppPreferencesStore,
userWalletsStore: UserWalletsStore,
userWalletsListManager: UserWalletsListManager,
walletManagersStore: WalletManagersStore,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): 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(projects.core.res)
implementation(projects.domain.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)
suspend operator fun invoke(type: FeedbackEmailType): FeedbackEmail {
val cardInfo = feedbackRepository.getCardInfo()
val formattedLogs = AppLogsFormatter().format(appLogs = feedbackRepository.getAppLogs())
return FeedbackEmail(
address = getAddress(cardInfo),
subject = emailSubjectResolver.resolve(type, cardInfo),
message = createMessage(type, cardInfo),
address = getAddress(type.cardInfo),
subject = emailSubjectResolver.resolve(type),
message = createMessage(type),
file = feedbackRepository.createLogFile(logs = formattedLogs),
)
}
private fun getAddress(cardInfo: CardInfo): String {
return if (cardInfo.isStart2Coin) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL
private fun getAddress(cardInfo: CardInfo?): String {
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 {
val title = emailMessageTitleResolver.resolve(type)
append(title)
@ -50,9 +48,7 @@ class GetFeedbackEmailUseCase(
appendDisclaimerIfNeeded(type)
skipLine()
val body = emailMessageBodyResolver.resolve(type, cardInfo)
val body = emailMessageBodyResolver.resolve(type)
append(body)
}.toString()
}
@ -62,6 +58,7 @@ class GetFeedbackEmailUseCase(
this
} else {
append(resources.getString(R.string.feedback_data_collection_message))
skipLine()
}
}
}

View file

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

View file

@ -7,15 +7,19 @@ package com.tangem.domain.feedback.models
*/
sealed interface FeedbackEmailType {
val cardInfo: CardInfo?
/** 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" */
data object RateCanBeBetter : FeedbackEmailType
data class RateCanBeBetter(override val cardInfo: CardInfo) : FeedbackEmailType
/** User has problem with scanning */
data object ScanningProblem : FeedbackEmailType
data object ScanningProblem : FeedbackEmailType {
override val cardInfo: CardInfo? = null
}
/** 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
import com.tangem.domain.feedback.models.*
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.models.UserWalletId
import java.io.File
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 getBlockchainInfo(blockchainId: String, derivationPath: String?): BlockchainInfo?
suspend fun getBlockchainInfoList(userWalletId: UserWalletId): List<BlockchainInfo>
fun getPhoneInfo(): PhoneInfo
suspend fun getBlockchainInfo(
userWalletId: UserWalletId,
blockchainId: String,
derivationPath: String?,
): BlockchainInfo?
fun saveBlockchainErrorInfo(error: BlockchainErrorInfo)
suspend fun getBlockchainErrorInfo(): BlockchainErrorInfo?
suspend fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo?
suspend fun getAppLogs(): List<AppLogModel>

View file

@ -16,25 +16,33 @@ internal class EmailMessageBodyResolver(
private val feedbackRepository: FeedbackRepository,
) {
/** Resolve email message body by [type] using [cardInfo] */
suspend fun resolve(type: FeedbackEmailType, cardInfo: CardInfo): String = with(FeedbackDataBuilder()) {
/** Resolve email message body by [type] */
suspend fun resolve(type: FeedbackEmailType): String = with(FeedbackDataBuilder()) {
when (type) {
FeedbackEmailType.DirectUserRequest -> addUserRequestBody(cardInfo)
FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(cardInfo)
FeedbackEmailType.ScanningProblem -> addScanningProblemBody()
FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(cardInfo)
is FeedbackEmailType.DirectUserRequest -> addUserRequestBody(type.cardInfo)
is FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(type.cardInfo)
is FeedbackEmailType.ScanningProblem -> addScanningProblemBody()
is FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(type.cardInfo)
}
return build()
}
private suspend fun FeedbackDataBuilder.addUserRequestBody(cardInfo: CardInfo) {
addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo())
addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo(cardInfo.userWalletId))
addDelimiter()
addCardInfo(cardInfo)
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())
}
@ -46,9 +54,11 @@ internal class EmailMessageBodyResolver(
addCardInfo(cardInfo)
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 {
feedbackRepository.getBlockchainInfo(
userWalletId = userWalletId,
blockchainId = blockchainError.blockchainId,
derivationPath = blockchainError.derivationPath,
)

View file

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

View file

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

View file

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

View file

@ -28,6 +28,7 @@ dependencies {
/* Project - Domain */
implementation(projects.domain.models)
implementation(projects.domain.feedback)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
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.ui.Modifier
import com.tangem.core.decompose.navigation.DummyRouter
import com.tangem.core.navigation.feedback.DummyFeedbackManager
import com.tangem.core.navigation.url.DummyUrlOpener
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.details.entity.DetailsFooterUM
@ -19,8 +18,7 @@ internal class PreviewDetailsComponent : DetailsComponent {
ItemsBuilder(
router = DummyRouter(),
urlOpener = DummyUrlOpener(),
feedbackManager = DummyFeedbackManager(),
).buldAll(isWalletConnectAvailable = true)
).buildAll(isWalletConnectAvailable = true, onSupportClick = {})
}
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.ParamsContainer
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.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.details.entity.DetailsFooterUM
import com.tangem.features.details.entity.DetailsItemUM
@ -33,6 +37,9 @@ internal class DetailsModel @Inject constructor(
private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase,
private val router: Router,
private val paramsContainer: ParamsContainer,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val feedbackManager: FeedbackManager,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
@ -66,7 +73,22 @@ internal class DetailsModel @Inject constructor(
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>) {

View file

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

View file

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

View file

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

View file

@ -204,7 +204,12 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
viewModelScope.launch(dispatchers.main) {
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()
return getUserWalletUseCase(userWalletId).getOrElse {
Timber.e(