Updated on 2026-08-14

This commit is contained in:
Tangem 2024-11-29 16:39:09 +03:00
parent 78770cae44
commit 503bac327d
11 changed files with 178 additions and 27 deletions

View file

@ -18,6 +18,7 @@ import com.arkivanov.decompose.router.slot.navigate
import com.arkivanov.decompose.router.stack.*
import com.arkivanov.decompose.value.Value
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
@ -25,6 +26,7 @@ import com.tangem.core.decompose.navigation.inner.InnerNavigation
import com.tangem.core.decompose.navigation.inner.InnerNavigationState
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.MultiWalletAccessCodeComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWalletBackupComponent
@ -46,6 +48,7 @@ import kotlinx.coroutines.launch
internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: OnboardingMultiWalletComponent.Params,
private val analyticsHandler: AnalyticsEventHandler,
) : OnboardingMultiWalletComponent, AppComponentContext by context {
private val model: OnboardingMultiWalletModel = getOrCreateModel(params)
@ -182,6 +185,7 @@ internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor
}
Done -> {
// final step - navigate to parent
analyticsHandler.send(OnboardingEvent.Finished)
val userWallet = childParams.multiWalletState.value.resultUserWallet ?: return
params.onDone(userWallet)
}

View file

@ -0,0 +1,68 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
sealed class OnboardingEvent(
category: String,
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category, event, params) {
data object Started : OnboardingEvent("Onboarding", "Onboarding Started")
data object Finished : OnboardingEvent("Onboarding", "Onboarding Finished")
sealed class CreateWallet(
event: String,
params: Map<String, String> = mapOf(),
) : OnboardingEvent("Onboarding / Create Wallet", event, params) {
data object ScreenOpened : CreateWallet("Create Wallet Screen Opened")
data object ButtonCreateWallet : CreateWallet("Button - Create Wallet")
class WalletCreatedSuccessfully(
creationType: WalletCreationType = WalletCreationType.PrivateKey,
seedPhraseLength: Int? = null,
) : CreateWallet(
event = "Wallet Created Successfully",
params = buildMap {
put("Creation Type", creationType.value)
if (seedPhraseLength != null) {
put("Seed Phrase Length", seedPhraseLength.toString())
}
},
)
sealed class WalletCreationType(val value: String) {
data object PrivateKey : WalletCreationType(value = "Private Key")
data object NewSeed : WalletCreationType(value = "New Seed")
data object SeedImport : WalletCreationType(value = "Seed Import")
}
}
sealed class Backup(
event: String,
params: Map<String, String> = mapOf(),
) : OnboardingEvent("Onboarding / Backup", event, params) {
data object ScreenOpened : Backup("Backup Screen Opened")
data object Started : Backup("Backup Started")
data object Skipped : Backup("Backup Skipped")
data object SettingAccessCodeStarted : Backup("Setting Access Code Started")
data object AccessCodeEntered : Backup("Access Code Entered")
data object AccessCodeReEntered : Backup("Access Code Re-entered")
class Finished(cardsCount: Int) : Backup(
event = "Backup Finished",
params = mapOf("Cards count" to "$cardsCount"),
)
data object ResetCancelEvent : Backup(
event = "Reset Card Notification",
params = mapOf("Option" to "Cancel"),
)
data object ResetPerformEvent : Backup(
event = "Reset Card Notification",
params = mapOf("Option" to "Reset"),
)
}
}

View file

@ -1,9 +1,11 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.model
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui.state.MultiWalletAccessCodeUM
import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState
@ -23,6 +25,7 @@ internal class MultiWalletAccessCodeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val backupServiceHolder: BackupServiceHolder,
private val analyticsHandler: AnalyticsEventHandler,
) : Model() {
private val _uiState = MutableStateFlow(getInitialState())
@ -32,6 +35,8 @@ internal class MultiWalletAccessCodeModel @Inject constructor(
val onDismiss = MutableSharedFlow<Unit>()
init {
analyticsHandler.send(OnboardingEvent.Backup.SettingAccessCodeStarted)
params.multiWalletState.update {
it.copy(accessCode = null)
}
@ -111,6 +116,8 @@ internal class MultiWalletAccessCodeModel @Inject constructor(
it.copy(atLeast4CharError = true)
}
}
analyticsHandler.send(OnboardingEvent.Backup.AccessCodeEntered)
}
MultiWalletAccessCodeUM.Step.ConfirmAccessCode -> {
if (checkAccessCode()) {
@ -123,6 +130,8 @@ internal class MultiWalletAccessCodeModel @Inject constructor(
}
modelScope.launch { onDismiss.emit(Unit) }
}
analyticsHandler.send(OnboardingEvent.Backup.AccessCodeReEntered)
}
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -10,6 +11,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.scan.ProductType
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWalletBackupComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.backupCardAttestationFailedDialog
@ -33,6 +35,7 @@ class MultiWalletBackupModel @Inject constructor(
private val backupServiceHolder: BackupServiceHolder,
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val tangemSdkManager: TangemSdkManager,
private val analyticsHandler: AnalyticsEventHandler,
) : Model() {
@Suppress("UnusedPrivateMember")
@ -57,6 +60,15 @@ class MultiWalletBackupModel @Inject constructor(
val uiState: StateFlow<MultiWalletBackupUM> = _uiState
val eventFlow = MutableSharedFlow<MultiWalletBackupComponent.Event>()
init {
// for wallet 1 this event is sent in Wallet1ChooseOptionModel
if (scanResponse.productType == ProductType.Wallet2 || scanResponse.productType == ProductType.Ring) {
analyticsHandler.send(OnboardingEvent.Backup.ScreenOpened)
}
analyticsHandler.send(OnboardingEvent.Backup.Started)
}
private fun getInitState(): MultiWalletBackupUM {
return when (backupService.currentState) {
BackupService.State.Preparing -> {
@ -89,9 +101,6 @@ class MultiWalletBackupModel @Inject constructor(
if (primaryCard != null) {
backupService.setPrimaryCard(primaryCard)
addBackupCardWithService()
} else {
// TODO wallet 2 ??? or from main?
// BackupAction.StartAddingPrimaryCard
}
}
@ -146,6 +155,8 @@ class MultiWalletBackupModel @Inject constructor(
}
}
modelScope.launch { eventFlow.emit(MultiWalletBackupComponent.Event.Done) }
analyticsHandler.send(OnboardingEvent.Backup.Finished(cardsCount = state.value.backupCardsNumber + 1))
}
private fun addBackupCardWithService() {
@ -189,7 +200,12 @@ class MultiWalletBackupModel @Inject constructor(
st.copy(
dialog = resetBackupCardDialog(
onReset = { resetBackupCard(cardId = error.cardId) },
onDismiss = { _uiState.update { it.copy(dialog = null) } },
onDismiss = {
_uiState.update { it.copy(dialog = null) }
},
onDismissClick = {
analyticsHandler.send(OnboardingEvent.Backup.ResetCancelEvent)
},
),
)
}
@ -211,6 +227,8 @@ class MultiWalletBackupModel @Inject constructor(
}
private fun resetBackupCard(cardId: String) {
analyticsHandler.send(OnboardingEvent.Backup.ResetPerformEvent)
modelScope.launch {
tangemSdkManager.resetToFactorySettings(
cardId = cardId,

View file

@ -4,16 +4,20 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.state.MultiWalletBackupUMDialog
internal fun resetBackupCardDialog(onReset: () -> Unit, onDismiss: () -> Unit) = MultiWalletBackupUMDialog(
title = resourceReference(R.string.common_attention),
bodyText = resourceReference(R.string.onboarding_linking_error_card_with_wallets),
confirmText = resourceReference(R.string.common_cancel),
cancelText = resourceReference(R.string.card_settings_action_sheet_reset),
warningCancelColor = true,
onDismiss = onDismiss,
onConfirm = onDismiss,
onCancel = { onReset(); onDismiss() },
)
internal fun resetBackupCardDialog(onReset: () -> Unit, onDismiss: () -> Unit, onDismissClick: () -> Unit) =
MultiWalletBackupUMDialog(
title = resourceReference(R.string.common_attention),
bodyText = resourceReference(R.string.onboarding_linking_error_card_with_wallets),
confirmText = resourceReference(R.string.common_cancel),
cancelText = resourceReference(R.string.card_settings_action_sheet_reset),
warningCancelColor = true,
onDismiss = {
onDismiss()
onDismissClick()
},
onConfirm = onDismiss,
onCancel = { onReset(); onDismiss() },
)
internal fun backupCardAttestationFailedDialog(onDismiss: () -> Unit) = MultiWalletBackupUMDialog(
title = resourceReference(R.string.common_error),

View file

@ -1,5 +1,6 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.chooseoption.model
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -9,6 +10,7 @@ import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableSharedFlow
@ -23,18 +25,25 @@ internal class Wallet1ChooseOptionModel @Inject constructor(
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
private val cardRepository: CardRepository,
private val saveWalletUseCase: SaveWalletUseCase,
private val analyticsHandler: AnalyticsEventHandler,
) : Model() {
private val params = paramsContainer.require<MultiWalletChildParams>()
private var skipClicked = false
val returnToParentFlow = MutableSharedFlow<Unit>()
init {
analyticsHandler.send(OnboardingEvent.Backup.ScreenOpened)
}
fun onSkipClick() {
// TODO show confirmation dialog
if (skipClicked) return
skipClicked = true
analyticsHandler.send(OnboardingEvent.Backup.Skipped)
modelScope.launch {
val scanResponse = params.multiWalletState.value.currentScanResponse

View file

@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.mo
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -11,6 +12,7 @@ import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDialog
@ -24,6 +26,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList")
@ComponentScoped
internal class MultiWalletCreateWalletModel @Inject constructor(
paramsContainer: ParamsContainer,
@ -32,6 +35,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor(
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val cardRepository: CardRepository,
private val analyticsHandler: AnalyticsEventHandler,
) : Model() {
private val params = paramsContainer.require<MultiWalletChildParams>()
@ -50,7 +54,10 @@ internal class MultiWalletCreateWalletModel @Inject constructor(
} else {
resourceReference(R.string.onboarding_create_wallet_body)
},
onCreateWalletClick = { createWallet(false) },
onCreateWalletClick = {
analyticsHandler.send(OnboardingEvent.CreateWallet.ButtonCreateWallet)
createWallet(false)
},
showOtherOptionsButton = params.parentParams.withSeedPhraseFlow,
onOtherOptionsClick = {
modelScope.launch {
@ -64,6 +71,10 @@ internal class MultiWalletCreateWalletModel @Inject constructor(
val uiState: StateFlow<MultiWalletCreateWalletUM> = _uiState
val onDone = MutableSharedFlow<OnboardingMultiWalletState.Step>()
init {
analyticsHandler.send(OnboardingEvent.CreateWallet.ScreenOpened)
}
private fun createWallet(shouldReset: Boolean) {
modelScope.launch {
val result = tangemSdkManager.createProductWallet(
@ -91,8 +102,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor(
onDone.emit(OnboardingMultiWalletState.Step.ChooseBackupOption)
}
// TODO
// Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
analyticsHandler.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully())
}
is CompletionResult.Failure -> {

View file

@ -79,7 +79,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
private fun writePrimaryCard() {
val backupService = backupServiceHolder.backupService.get() ?: return
// store.dispatchOnMain(BackupAction.SetHasRing(hasRing = isRing))
// TODO store.dispatchOnMain(BackupAction.SetHasRing(hasRing = isRing))
val primaryCardBatchId = backupService.primaryCardBatchId ?: return
val isRing = isRing(primaryCardBatchId)
@ -108,7 +108,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
private fun writeBackupCard(cardIndex: Int) {
val backupService = backupServiceHolder.backupService.get() ?: return
// store.dispatchOnMain(BackupAction.SetHasRing(hasRing = isRing))
// TODO store.dispatchOnMain(BackupAction.SetHasRing(hasRing = isRing))
val backupCardBatchId = backupService.backupCardsBatchIds.getOrNull(cardIndex) ?: return
val isRing = isRing(backupCardBatchId)
@ -120,7 +120,6 @@ internal class MultiWalletFinalizeModel @Inject constructor(
is CompletionResult.Success -> {
val backupValidator = BackupValidator()
if (backupValidator.isValidBackupStatus(CardDTO(result.data)).not()) {
// TODO store.dispatchOnMain(BackupAction.ErrorInBackupCard)
walletHasBackupError = true
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.mode
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -11,6 +12,7 @@ import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.GenerateSeedPhraseUiStateBuilder
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.ImportSeedPhraseUiStateBuilder
@ -41,6 +43,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor(
private val cardRepository: CardRepository,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val analyticsHandler: AnalyticsEventHandler,
) : Model() {
private val params = paramsContainer.require<MultiWalletChildParams>()
@ -76,6 +79,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor(
state.value.generatedWords12 ?: return@importWallet
},
passphrase = null,
generatedSeedPhrase = true,
)
},
// =============================================
@ -85,7 +89,13 @@ internal class MultiWalletSeedPhraseModel @Inject constructor(
modelScope = modelScope,
mnemonicRepository = mnemonicRepository,
updateUiState = { block -> updateUiStateSpecific(block) },
importWallet = ::importWallet,
importWallet = { mnemonic, passphrase ->
importWallet(
mnemonic = mnemonic,
passphrase = passphrase,
generatedSeedPhrase = false,
)
},
)
val uiState = _uiState.asStateFlow()
@ -159,7 +169,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor(
// =============================================
// | DON'T FIXME !!! MODIFY WITH CAUTION !!! |
// =============================================
private fun importWallet(mnemonic: Mnemonic, passphrase: String?) {
private fun importWallet(mnemonic: Mnemonic, passphrase: String?, generatedSeedPhrase: Boolean) {
val currentState = state.value
if (currentState.readyToImport.not()) return
@ -175,7 +185,17 @@ internal class MultiWalletSeedPhraseModel @Inject constructor(
when (result) {
is CompletionResult.Success -> {
// TODO on success send analytics event
analyticsHandler.send(
OnboardingEvent.CreateWallet.WalletCreatedSuccessfully(
creationType = if (generatedSeedPhrase) {
OnboardingEvent.CreateWallet.WalletCreationType.NewSeed
} else {
OnboardingEvent.CreateWallet.WalletCreationType.SeedImport
},
seedPhraseLength = mnemonic.mnemonicComponents.size,
),
)
multiWalletState.update {
it.copy(
currentScanResponse = it.currentScanResponse.copy(

View file

@ -1,5 +1,6 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.model
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -7,6 +8,7 @@ import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.usecase.GetCardImageUseCase
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent
import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams
import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.interruptBackupDialog
import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM
@ -20,6 +22,7 @@ internal class OnboardingMultiWalletModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val analyticsHandler: AnalyticsEventHandler,
) : Model() {
private val params = paramsContainer.require<OnboardingMultiWalletComponent.Params>()
private val getCardImageUseCase = GetCardImageUseCase()
@ -38,10 +41,7 @@ internal class OnboardingMultiWalletModel @Inject constructor(
val uiState = _uiState.asStateFlow()
init {
// TODO add analytics
// if (!manager.isActivationStarted(notNullCard.cardId)) {
// Analytics.send(Onboarding.Started())
// }
analyticsHandler.send(OnboardingEvent.Started)
initScreenTitle()
loadCardArtwork()
subscribeToBackups()

View file

@ -6,6 +6,8 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
@ -23,11 +25,19 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor(
@Assisted val params: OnboardingStepperComponent.Params,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val analyticsHandler: AnalyticsEventHandler,
) : OnboardingStepperComponent, AppComponentContext by context {
override val state = instanceKeeper.getOrCreateSimple { MutableStateFlow(params.initState) }
private fun openSupport() {
analyticsHandler.send(
AnalyticsEvent(
category = "Onboarding",
event = "Button - Chat",
),
)
componentScope.launch {
val cardInfo = getCardInfoUseCase(params.scanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo))