Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-11 11:30:58 +05:00
parent 33c331f529
commit b2b0b3cc72
14 changed files with 144 additions and 79 deletions

View file

@ -44,7 +44,12 @@ class NetworkLogsSaveInterceptor(
throw e
}
if (restrictedForLogURLs.contains(request.url.host + request.url.encodedPath)) {
val host = request.url.host
val path = request.url.encodedPath
val isRestrictedUrl = restrictedForLogURLs.contains(host + path)
val isRestrictedHost = restrictedForLogHosts.any { host.contains(it) }
if (isRestrictedUrl || isRestrictedHost) {
logResponseWithEmptyMessage(response, startNs)
} else {
logResponseMessage(response, startNs)
@ -222,5 +227,8 @@ class NetworkLogsSaveInterceptor(
val restrictedForLogURLs = listOf(
"api.stakek.it/v1/yields/enabled",
)
val restrictedForLogHosts = listOf(
"us.paera.com",
)
}
}

View file

@ -1453,6 +1453,7 @@
<string name="tangempay_failed_to_issue_card">Failed to issue card</string>
<string name="tangempay_failed_to_issue_card_retry_description">A technical error has occurred, please try again by clicking the button below.</string>
<string name="tangempay_failed_to_issue_card_support_description">A technical error has occurred, please contact support.</string>
<string name="tangempay_go_to_support">Go to Support</string>
<string name="tangempay_issue_card_notification_description">It usually takes up to 15 minutes</string>
<string name="tangempay_issue_card_notification_title">Setting up your Tangem Card</string>
<string name="tangempay_issuing_your_card">Issuing your card</string>

View file

@ -2,14 +2,12 @@ package com.tangem.data.pay
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.squareup.moshi.Moshi
import com.tangem.blockchain.blockchains.ethereum.Chain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.core.error.UniversalError
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.pay.util.TangemPayErrorConverter
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
@ -26,8 +24,8 @@ private const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d
private const val TOKEN_DECIMALS = 6
internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor(
@NetworkMoshi moshi: Moshi,
excludedBlockchains: ExcludedBlockchains,
private val errorConverter: TangemPayErrorConverter,
) : TangemPayCryptoCurrencyFactory {
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
@ -37,8 +35,6 @@ internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor(
NetworkFactory(excludedBlockchains)
}
private val errorConverter by lazy(mode = LazyThreadSafetyMode.NONE) { TangemPayErrorConverter(moshi) }
override fun create(userWallet: UserWallet, chainId: Int): Either<UniversalError, CryptoCurrency> {
return catch {
val chain = requireNotNull(Chain.entries.find { it.id == chainId }) { "Can not find chain with $chainId" }

View file

@ -1,8 +1,12 @@
package com.tangem.data.pay.repository
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.catch
import arrow.core.right
import com.tangem.core.error.UniversalError
import com.tangem.data.pay.util.RainCryptoUtil
import com.tangem.data.pay.util.TangemPayErrorConverter
import com.tangem.data.visa.config.VisaLibLoader
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
@ -40,6 +44,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
private val rainCryptoUtil: RainCryptoUtil,
private val storage: TangemPayStorage,
private val cardFrozenStateStore: TangemPayCardFrozenStateStore,
private val errorConverter: TangemPayErrorConverter,
) : TangemPayCardDetailsRepository {
private val pollingScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@ -64,37 +69,39 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
}
override suspend fun revealCardDetails(userWalletId: UserWalletId): Either<UniversalError, TangemPayCardDetails> {
return requestHelper.runWithErrorLogs(TAG) {
val publicKeyBase64 = getPublicKeyBase64()
val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
return catch(
block = {
val publicKeyBase64 = getPublicKeyBase64()
val (secretKeyBytes, sessionId) = rainCryptoUtil.generateSecretKeyAndSessionId(publicKeyBase64)
val result = requestHelper.performRequest(userWalletId = userWalletId) { authHeader ->
tangemPayApi.revealCardDetails(
authHeader = authHeader,
body = CardDetailsRequest(sessionId = sessionId),
)
}.getOrNull()?.result ?: error("Cannot reveal card details")
val result = requestHelper.request(userWalletId) { authHeader ->
tangemPayApi.revealCardDetails(
authHeader = authHeader,
body = CardDetailsRequest(sessionId = sessionId),
val pan = rainCryptoUtil.decryptSecret(
base64Secret = result.pan.secret,
base64Iv = result.pan.iv,
secretKeyBytes = secretKeyBytes,
)
}.result ?: error("Cannot reveal card details")
val pan = rainCryptoUtil.decryptSecret(
base64Secret = result.pan.secret,
base64Iv = result.pan.iv,
secretKeyBytes = secretKeyBytes,
)
val cvv = rainCryptoUtil.decryptSecret(
base64Secret = result.cvv.secret,
base64Iv = result.cvv.iv,
secretKeyBytes = secretKeyBytes,
)
secretKeyBytes.fill(0)
val cvv = rainCryptoUtil.decryptSecret(
base64Secret = result.cvv.secret,
base64Iv = result.cvv.iv,
secretKeyBytes = secretKeyBytes,
)
secretKeyBytes.fill(0)
TangemPayCardDetails(
pan = pan,
cvv = cvv,
expirationYear = result.expirationYear,
expirationMonth = result.expirationMonth,
)
}
TangemPayCardDetails(
pan = pan,
cvv = cvv,
expirationYear = result.expirationYear,
expirationMonth = result.expirationMonth,
).right()
},
catch = { errorConverter.convert(it).left() },
)
}
override suspend fun setPin(userWalletId: UserWalletId, pin: String): Either<UniversalError, SetPinResult> {

View file

@ -5,14 +5,12 @@ import arrow.core.getOrElse
import arrow.core.left
import arrow.core.raise.catch
import arrow.core.right
import com.squareup.moshi.Moshi
import com.squareup.wire.Instant
import com.tangem.data.pay.util.TangemPayErrorConverter
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.pay.TangemPayAuthApi
import com.tangem.datasource.api.pay.models.request.RefreshCustomerWalletAccessTokenRequest
import com.tangem.datasource.api.pay.models.response.TangemPayGetTokensResponse
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.models.wallet.UserWalletId
@ -33,7 +31,7 @@ private const val TAG = "TangemPayRequestPerformer"
@Singleton
internal class TangemPayRequestPerformer @Inject constructor(
@NetworkMoshi moshi: Moshi,
private val errorConverter: TangemPayErrorConverter,
private val environmentConfigStorage: EnvironmentConfigStorage,
private val dispatchers: CoroutineDispatcherProvider,
private val tangemPayAuthApi: TangemPayAuthApi,
@ -42,7 +40,6 @@ internal class TangemPayRequestPerformer @Inject constructor(
private val customerWalletAddresses = ConcurrentHashMap<UserWalletId, String>()
private val tokensMutex = Mutex()
private val errorConverter = TangemPayErrorConverter(moshi)
@Deprecated("Do not use this method")
suspend fun <T : Any> runWithErrorLogs(tag: String, requestBlock: suspend () -> T): Either<VisaApiError, T> {

View file

@ -3,10 +3,16 @@ package com.tangem.data.pay.util
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.pay.models.response.VisaErrorResponse
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.converter.Converter
import javax.inject.Inject
import javax.inject.Singleton
class TangemPayErrorConverter(moshi: Moshi) : Converter<Throwable, VisaApiError> {
@Singleton
internal class TangemPayErrorConverter @Inject constructor(
@NetworkMoshi moshi: Moshi,
) : Converter<Throwable, VisaApiError> {
private val visaErrorAdapter by lazy { moshi.adapter(VisaErrorResponse::class.java) }

View file

@ -57,7 +57,6 @@ sealed interface FeedbackEmailType {
}
sealed class Visa : FeedbackEmailType {
data class FailedIssueCard(override val walletMetaInfo: WalletMetaInfo) : Visa()
data class DirectUserRequest(override val walletMetaInfo: WalletMetaInfo) : Visa()
@ -68,6 +67,8 @@ sealed interface FeedbackEmailType {
override val walletMetaInfo: WalletMetaInfo,
) : Visa()
data class FailedIssueCard(override val walletMetaInfo: WalletMetaInfo) : Visa()
data class DisputeV2(
val item: TangemPayTxHistoryItem,
override val walletMetaInfo: WalletMetaInfo,

View file

@ -12,6 +12,7 @@ internal class FeedbackDataBuilder {
fun addTangemPayTxInfo(item: TangemPayTxHistoryItem) {
builder.append(item.jsonRepresentation)
builder.breakLine()
}
fun addVisaTxInfo(txDetails: VisaTxDetails) {
@ -108,6 +109,26 @@ internal class FeedbackDataBuilder {
builder.appendKeyValue("App version", phoneInfo.appVersion)
}
fun addTangemPayIssueType(type: FeedbackEmailType.Visa) {
val issueType = when (type) {
is FeedbackEmailType.Visa.Activation,
is FeedbackEmailType.Visa.DirectUserRequest,
is FeedbackEmailType.Visa.Dispute,
is FeedbackEmailType.Visa.FeatureIsBeta,
is FeedbackEmailType.Visa.Withdrawal,
-> return
is FeedbackEmailType.Visa.DisputeV2 -> when (type.item) {
is TangemPayTxHistoryItem.Collateral -> "Receive/Withdraw"
is TangemPayTxHistoryItem.Fee,
is TangemPayTxHistoryItem.Spend,
is TangemPayTxHistoryItem.Payment,
-> "Transaction"
}
is FeedbackEmailType.Visa.FailedIssueCard -> "Card issuing"
}
builder.appendKeyValue("Issue type", issueType)
}
fun addBlockchainError(info: BlockchainInfo, error: BlockchainErrorInfo) {
builder.appendKeyValue("Blockchain", info.blockchain)
builder.appendAddresses(

View file

@ -6,6 +6,9 @@ import com.tangem.domain.feedback.models.FeedbackEmail
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.feedback.utils.*
import java.io.File
private const val BINDER_MAX_SIZE_BYTES = 500_000 // Safe limit under Android's 1MB Binder limit
/**
* Get email with feedback for support
@ -29,22 +32,30 @@ class SendFeedbackEmailUseCase(
address = getAddress(type),
subject = emailSubjectResolver.resolve(type),
message = createMessage(type),
file = feedbackRepository.getZipLogFile(),
file = getFile(type),
)
feedbackRepository.sendEmail(email)
}
private suspend fun getFile(type: FeedbackEmailType): File? {
return if (type.isVisaEmail()) {
null
} else {
feedbackRepository.getZipLogFile()
}
}
private fun getAddress(type: FeedbackEmailType): String {
return when {
type is FeedbackEmailType.Visa || type.walletMetaInfo?.isVisa == true -> TANGEM_VISA_SUPPORT_EMAIL
type.isVisaEmail() -> TANGEM_VISA_SUPPORT_EMAIL
type.walletMetaInfo?.isStart2Coin == true -> START2COIN_SUPPORT_EMAIL
else -> TANGEM_SUPPORT_EMAIL
}
}
private suspend fun createMessage(type: FeedbackEmailType): String {
return StringBuilder().apply {
val fullMessage = buildString {
val title = emailMessageTitleResolver.resolve(type)
append(title)
@ -54,7 +65,25 @@ class SendFeedbackEmailUseCase(
val body = emailMessageBodyResolver.resolve(type)
append(body)
}.toString()
}
return truncateMessageIfNeeded(fullMessage)
}
private fun truncateMessageIfNeeded(message: String): String {
val messageBytes = message.toByteArray(Charsets.UTF_8)
return if (messageBytes.size > BINDER_MAX_SIZE_BYTES) {
// Find a safe truncation point (avoid cutting in middle of UTF-8 chars)
val truncatedBytes = messageBytes.sliceArray(0 until BINDER_MAX_SIZE_BYTES)
String(truncatedBytes, Charsets.UTF_8)
} else {
message
}
}
private fun FeedbackEmailType.isVisaEmail(): Boolean {
return this is FeedbackEmailType.Visa || this.walletMetaInfo?.isVisa == true
}
private fun StringBuilder.appendDisclaimerIfNeeded(type: FeedbackEmailType): StringBuilder {

View file

@ -4,7 +4,6 @@ import com.tangem.domain.feedback.FeedbackDataBuilder
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.feedback.models.WalletMetaInfo
import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.domain.visa.model.VisaTxDetails
/**
@ -34,31 +33,39 @@ internal class EmailMessageBodyResolver(
-> addPhoneInfoBody()
is FeedbackEmailType.Visa.Activation -> addUserRequestBody(type.walletMetaInfo)
is FeedbackEmailType.Visa.DirectUserRequest -> addUserRequestBody(type.walletMetaInfo)
is FeedbackEmailType.Visa.FailedIssueCard -> addUserRequestBody(type.walletMetaInfo)
is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody(type.walletMetaInfo, type.visaTxDetails)
is FeedbackEmailType.Visa.DisputeV2 -> addTangemPayRequestBody(type.walletMetaInfo, type.item)
is FeedbackEmailType.Visa.FailedIssueCard -> addTangemPayFailedIssuingCardBody(type)
is FeedbackEmailType.Visa.DisputeV2 -> addTangemPayDisputeRequestBody(type)
is FeedbackEmailType.Visa.Withdrawal -> addTangemPayWithdrawalRequestBody(type)
is FeedbackEmailType.Visa.FeatureIsBeta -> addTangemPayBetaRequestBody(type.walletMetaInfo)
is FeedbackEmailType.Visa.FeatureIsBeta -> addTangemPayBetaRequestBody(type)
}
return build()
}
private suspend fun FeedbackDataBuilder.addTangemPayRequestBody(
walletMetaInfo: WalletMetaInfo,
item: TangemPayTxHistoryItem,
) {
addUserRequestBody(walletMetaInfo)
private fun FeedbackDataBuilder.addTangemPayFailedIssuingCardBody(type: FeedbackEmailType.Visa.FailedIssueCard) {
addTangemPayPhoneInfoBody(type)
addDelimiter()
addTangemPayTxInfo(item)
type.walletMetaInfo.userWalletId?.let { userWalletId ->
addUserWalletId(userWalletId = userWalletId.stringValue)
}
}
private fun FeedbackDataBuilder.addTangemPayBetaRequestBody(walletMetaInfo: WalletMetaInfo) {
addPhoneInfoBody()
private fun FeedbackDataBuilder.addTangemPayDisputeRequestBody(type: FeedbackEmailType.Visa.DisputeV2) {
addTangemPayPhoneInfoBody(type = type)
addDelimiter()
walletMetaInfo.userWalletId?.let { userWalletId ->
addTangemPayTxInfo(type.item)
addDelimiter()
type.walletMetaInfo.userWalletId?.let { userWalletId ->
addUserWalletId(userWalletId = userWalletId.stringValue)
}
}
private fun FeedbackDataBuilder.addTangemPayBetaRequestBody(type: FeedbackEmailType.Visa) {
addTangemPayPhoneInfoBody(type)
addDelimiter()
type.walletMetaInfo?.userWalletId?.let { userWalletId ->
addUserWalletId(userWalletId = userWalletId.stringValue)
addDelimiter()
}
}
@ -122,6 +129,11 @@ internal class EmailMessageBodyResolver(
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
}
private fun FeedbackDataBuilder.addTangemPayPhoneInfoBody(type: FeedbackEmailType.Visa) {
addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo())
addTangemPayIssueType(type)
}
private suspend fun FeedbackDataBuilder.addTransactionSendingProblemBody(walletMetaInfo: WalletMetaInfo) {
addUserWalletMetaInfo(walletMetaInfo)
addDelimiter()

View file

@ -1647,8 +1647,7 @@ internal class SwapModel @Inject constructor(
private fun onTangemPaySupportClick(txId: String?) {
modelScope.launch {
val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId)
.getOrElse { error("CardInfo must be not null") }
val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch
val email = FeedbackEmailType.Visa.Withdrawal(
walletMetaInfo = metaInfo,
providerName = dataState.selectedProvider?.name.orEmpty(),

View file

@ -9,8 +9,6 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryDetailsComponent
import com.tangem.features.tangempay.entity.TangemPayTxHistoryDetailsUM
import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryDetailsConverter
@ -21,10 +19,8 @@ import javax.inject.Inject
@Stable
@ModelScoped
@Suppress("LongParameterList")
internal class TangemPayTxHistoryDetailsModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getUserWalletsUseCase: GetWalletsUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val urlOpener: UrlOpener,
@ -66,12 +62,7 @@ internal class TangemPayTxHistoryDetailsModel @Inject constructor(
private fun dispute() {
modelScope.launch {
val userWalletId = params.userWalletId
val userWallet = getUserWalletsUseCase.invokeSync()
.firstOrNull { it.walletId == userWalletId } ?: return@launch
val walletMetaInfo = getWalletMetaInfoUseCase.invoke(
userWallet.requireColdWallet().scanResponse,
).getOrNull() ?: return@launch
val walletMetaInfo = getWalletMetaInfoUseCase.invoke(params.userWalletId).getOrNull() ?: return@launch
sendFeedbackEmailUseCase.invoke(
FeedbackEmailType.Visa.DisputeV2(

View file

@ -6,6 +6,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -74,6 +75,7 @@ internal fun TangemPayTxHistoryDetailsContent(state: TangemPayTxHistoryDetailsUM
text = state.transactionSubtitle.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
Text(
modifier = Modifier.padding(top = 8.dp),
@ -84,7 +86,7 @@ internal fun TangemPayTxHistoryDetailsContent(state: TangemPayTxHistoryDetailsUM
state.localTransactionText?.let { localTransaction ->
Text(
modifier = Modifier.padding(top = 4.dp),
text = localTransaction,
text = localTransaction.orMaskWithStars(state.isBalanceHidden),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
@ -207,11 +209,11 @@ private class TangemPayTxHistoryDetailsUMProvider : CollectionPreviewParameterPr
dismiss = {},
),
TangemPayTxHistoryDetailsUM(
isBalanceHidden = false,
isBalanceHidden = true,
title = stringReference("12 June • 12:40"),
iconState = ImageReference.Res(R.drawable.ic_category_24),
transactionTitle = stringReference("Starbucks"),
transactionSubtitle = stringReference("Food and drinks"),
transactionSubtitle = stringReference("Food and drinks Food and drinks Food and drinks Food and drinks"),
transactionAmount = "-$5.86",
transactionAmountColor = themedColor { TangemTheme.colors.text.primary1 },
localTransactionText = "€ 5.36",

View file

@ -10,11 +10,9 @@ import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.features.tangempay.TangemPayFeatureToggles
import kotlinx.coroutines.launch
@ -41,7 +39,6 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
private val onboardingRepository: OnboardingRepository,
private val produceInitialDataTangemPay: ProduceTangemPayInitialDataUseCase,
private val getWalletMetainfoUseCase: GetWalletMetaInfoUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val uiMessageSender: UiMessageSender,
@ -93,7 +90,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
body = resourceReference(R.string.tangempay_failed_to_issue_card_support_description)
}
secondaryButton {
text = resourceReference(R.string.common_contact_support)
text = resourceReference(R.string.tangempay_go_to_support)
onClick {
onPaySupportClick()
closeBs()
@ -106,10 +103,8 @@ internal class TangemPayClickIntentsImplementor @Inject constructor(
override fun onPaySupportClick() {
modelScope.launch {
val userWalletId = stateHolder.getSelectedWalletId()
val userWallet = getUserWalletUseCase.invoke(userWalletId).getOrNull() ?: return@launch
val cardInfo = getWalletMetainfoUseCase.invoke(
userWallet.requireColdWallet().scanResponse,
userWalletId = stateHolder.getSelectedWalletId(),
).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(