Updated on 2026-08-14
This commit is contained in:
commit
4613836a1b
37 changed files with 623 additions and 311 deletions
|
|
@ -40,7 +40,6 @@ internal class DefaultScanCardProcessor(
|
|||
cardId: String?,
|
||||
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
|
||||
onWalletNotCreated: suspend () -> Unit,
|
||||
disclaimerWillShow: () -> Unit,
|
||||
onCancel: suspend () -> Unit,
|
||||
onFailure: suspend (error: TangemError) -> Unit,
|
||||
onSuccess: suspend (scanResponse: ScanResponse) -> Unit,
|
||||
|
|
@ -51,7 +50,6 @@ internal class DefaultScanCardProcessor(
|
|||
cardId = cardId,
|
||||
onProgressStateChange = onProgressStateChange,
|
||||
onWalletNotCreated = onWalletNotCreated,
|
||||
disclaimerWillShow = disclaimerWillShow,
|
||||
onFailure = onFailure,
|
||||
onSuccess = onSuccess,
|
||||
)
|
||||
|
|
@ -62,7 +60,6 @@ internal class DefaultScanCardProcessor(
|
|||
cardId = cardId,
|
||||
onProgressStateChange = onProgressStateChange,
|
||||
onWalletNotCreated = onWalletNotCreated,
|
||||
disclaimerWillShow = disclaimerWillShow,
|
||||
onCancel = onCancel,
|
||||
onFailure = onFailure,
|
||||
onSuccess = onSuccess,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import com.tangem.core.ui.extensions.toWrappedList
|
|||
import com.tangem.core.ui.message.dialog.Dialogs
|
||||
import com.tangem.domain.card.ScanFailsCounter
|
||||
import com.tangem.domain.card.common.util.twinsIsTwinned
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
|
|
@ -32,13 +31,10 @@ import com.tangem.sdk.extensions.localizedDescriptionRes
|
|||
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.mainScope
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -52,7 +48,6 @@ internal class LegacyScanProcessor @Inject constructor(
|
|||
private val appRouter: AppRouter,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase,
|
||||
private val cardRepository: CardRepository,
|
||||
private val onboardingHelper: OnboardingHelper,
|
||||
) {
|
||||
|
||||
|
|
@ -79,7 +74,6 @@ internal class LegacyScanProcessor @Inject constructor(
|
|||
cardId: String?,
|
||||
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
|
||||
onWalletNotCreated: suspend () -> Unit,
|
||||
disclaimerWillShow: () -> Unit,
|
||||
onCancel: suspend () -> Unit,
|
||||
onFailure: suspend (error: TangemError) -> Unit,
|
||||
onSuccess: suspend (scanResponse: ScanResponse) -> Unit,
|
||||
|
|
@ -119,17 +113,11 @@ internal class LegacyScanProcessor @Inject constructor(
|
|||
|
||||
sendAnalytics(analyticsEvent, scanResponse)
|
||||
|
||||
showDisclaimerIfNeed(
|
||||
onScanSuccess(
|
||||
scanResponse = scanResponse,
|
||||
disclaimerWillShow = disclaimerWillShow,
|
||||
nextHandler = { scanResponse2 ->
|
||||
onScanSuccess(
|
||||
scanResponse = scanResponse2,
|
||||
onProgressStateChange = onProgressStateChange,
|
||||
onSuccess = onSuccess,
|
||||
onWalletNotCreated = onWalletNotCreated,
|
||||
)
|
||||
},
|
||||
onProgressStateChange = onProgressStateChange,
|
||||
onSuccess = onSuccess,
|
||||
onWalletNotCreated = onWalletNotCreated,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -144,26 +132,6 @@ internal class LegacyScanProcessor @Inject constructor(
|
|||
Analytics.send(analyticsEvent)
|
||||
}
|
||||
|
||||
private suspend inline fun showDisclaimerIfNeed(
|
||||
scanResponse: ScanResponse,
|
||||
crossinline disclaimerWillShow: () -> Unit = {},
|
||||
crossinline nextHandler: suspend (ScanResponse) -> Unit,
|
||||
) {
|
||||
if (cardRepository.isTangemTOSAccepted()) {
|
||||
nextHandler(scanResponse)
|
||||
} else {
|
||||
scope.launch {
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
|
||||
withContext(Dispatchers.Main.immediate) {
|
||||
disclaimerWillShow()
|
||||
|
||||
appRouter.push(AppRoute.Disclaimer(isTosAccepted = false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun onScanFailure(
|
||||
analyticsSource: AnalyticsParam.ScreensSources,
|
||||
error: TangemError,
|
||||
|
|
|
|||
|
|
@ -13,11 +13,13 @@ import com.tangem.core.analytics.utils.TrackingContextProxy
|
|||
import com.tangem.domain.card.ScanCardException
|
||||
import com.tangem.domain.card.ScanCardUseCase
|
||||
import com.tangem.domain.card.ScanFailsRequester
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
|
||||
import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent
|
||||
import com.tangem.tap.domain.scanCard.chains.*
|
||||
import com.tangem.tap.domain.scanCard.chains.AnalyticsChain
|
||||
import com.tangem.tap.domain.scanCard.chains.CheckForOnboardingChain
|
||||
import com.tangem.tap.domain.scanCard.chains.FailedScansCounterChain
|
||||
import com.tangem.tap.domain.scanCard.chains.ScanChainException
|
||||
import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.scope
|
||||
|
|
@ -35,7 +37,6 @@ internal class UseCaseScanProcessor @Inject constructor(
|
|||
private val appRouter: AppRouter,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase,
|
||||
private val cardRepository: CardRepository,
|
||||
private val onboardingHelper: OnboardingHelper,
|
||||
) {
|
||||
private val scanCardExceptionConverter = ScanCardExceptionConverter()
|
||||
|
|
@ -62,7 +63,6 @@ internal class UseCaseScanProcessor @Inject constructor(
|
|||
cardId: String?,
|
||||
onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
|
||||
onWalletNotCreated: suspend () -> Unit,
|
||||
disclaimerWillShow: () -> Unit,
|
||||
onFailure: suspend (error: TangemError) -> Unit,
|
||||
onSuccess: suspend (scanResponse: ScanResponse) -> Unit,
|
||||
) = progressScope(onProgressStateChange) {
|
||||
|
|
@ -73,7 +73,6 @@ internal class UseCaseScanProcessor @Inject constructor(
|
|||
),
|
||||
)
|
||||
add(AnalyticsChain(Basic.CardWasScanned(analyticsSource)))
|
||||
add(DisclaimerChain(appRouter, cardRepository, disclaimerWillShow))
|
||||
add(CheckForOnboardingChain(trackingContextProxy, wasTwinsOnboardingShownUseCase, onboardingHelper))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
package com.tangem.tap.domain.scanCard.chains
|
||||
|
||||
import arrow.core.right
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.domain.card.ScanCardException
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.core.chain.Chain
|
||||
import com.tangem.domain.core.chain.ResultChain
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
||||
/**
|
||||
* Handles disclaimer display after the card scanning operation.
|
||||
*
|
||||
* @param disclaimerWillShow an optional function to be invoked when a disclaimer is about to be shown. Default is an
|
||||
* empty function.
|
||||
*
|
||||
* @see Chain for more information about the Chain interface.
|
||||
*/
|
||||
internal class DisclaimerChain(
|
||||
private val appRouter: AppRouter,
|
||||
private val cardRepository: CardRepository,
|
||||
private val disclaimerWillShow: () -> Unit = {},
|
||||
) : ResultChain<ScanCardException, ScanResponse>() {
|
||||
|
||||
override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult {
|
||||
return if (cardRepository.isTangemTOSAccepted()) {
|
||||
previousChainResult.right()
|
||||
} else {
|
||||
disclaimerWillShow()
|
||||
|
||||
appRouter.push(route = AppRoute.Disclaimer(isTosAccepted = false))
|
||||
|
||||
previousChainResult.right()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import com.tangem.domain.common.wallets.error.*
|
|||
import com.tangem.domain.hotwallet.repository.HotWalletRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isImported
|
||||
import com.tangem.domain.models.wallet.isLocked
|
||||
import com.tangem.domain.wallets.R
|
||||
import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents
|
||||
|
|
@ -603,10 +604,16 @@ internal class DefaultUserWalletsListRepository(
|
|||
|
||||
private fun trackSignInEvent(userWallet: UserWallet, type: Basic.SignedIn.SignInType) {
|
||||
trackingContextProxy.addContext(userWallet)
|
||||
val isBackedUp = when (userWallet) {
|
||||
is UserWallet.Cold -> userWallet.scanResponse.card.backupStatus?.isActive == true
|
||||
is UserWallet.Hot -> userWallet.backedUp
|
||||
}
|
||||
analyticsEventHandler.send(
|
||||
event = Basic.SignedIn(
|
||||
signInType = type,
|
||||
walletsCount = userWallets.value?.size ?: 0,
|
||||
isImported = userWallet.isImported(),
|
||||
hasBackup = isBackedUp,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import com.tangem.core.ui.message.SnackbarMessage
|
|||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.isImported
|
||||
import com.tangem.domain.models.wallet.isLocked
|
||||
import com.tangem.domain.onboarding.repository.OnboardingRepository
|
||||
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
|
||||
|
|
@ -170,14 +172,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
val userWallets = userWalletsListRepository.userWalletsSync()
|
||||
|
||||
return when {
|
||||
userWallets.isEmpty() -> {
|
||||
val shouldShowTos = !cardRepository.isTangemTOSAccepted()
|
||||
if (shouldShowTos) {
|
||||
AppRoute.Disclaimer(isTosAccepted = false)
|
||||
} else {
|
||||
AppRoute.Home(launchMode = launchMode)
|
||||
}
|
||||
}
|
||||
userWallets.isEmpty() -> AppRoute.Home(launchMode = launchMode)
|
||||
userWallets.any { it.isLocked } -> {
|
||||
AppRoute.Welcome(
|
||||
launchMode = launchMode,
|
||||
|
|
@ -339,10 +334,16 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
|
|||
val userWallets = userWalletsListRepository.userWalletsSync()
|
||||
val selectedWallet = userWalletsListRepository.selectedUserWalletSync() ?: return
|
||||
trackingContextProxy.addContext(selectedWallet)
|
||||
val isBackedUp = when (selectedWallet) {
|
||||
is UserWallet.Cold -> selectedWallet.scanResponse.card.backupStatus?.isActive == true
|
||||
is UserWallet.Hot -> selectedWallet.backedUp
|
||||
}
|
||||
analyticsEventHandler.send(
|
||||
event = Basic.SignedIn(
|
||||
signInType = Basic.SignedIn.SignInType.NoSecurity,
|
||||
walletsCount = userWallets.size,
|
||||
isImported = selectedWallet.isImported(),
|
||||
hasBackup = isBackedUp,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,11 +42,17 @@ sealed class Basic(
|
|||
class SignedIn(
|
||||
signInType: SignInType,
|
||||
walletsCount: Int,
|
||||
isImported: Boolean,
|
||||
hasBackup: Boolean?,
|
||||
) : Basic(
|
||||
event = "Signed in",
|
||||
params = buildMap {
|
||||
put("Sign in type", signInType.value)
|
||||
put("Wallets Count", walletsCount.toString())
|
||||
put("Wallet Type", if (isImported) "Seed Phrase" else "Seedless")
|
||||
if (hasBackup != null) {
|
||||
put("Backuped", if (hasBackup) "Yes" else "No")
|
||||
}
|
||||
},
|
||||
) {
|
||||
enum class SignInType(val value: String) {
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ class MoshiModule {
|
|||
.withSubtype(PaymentAccountStatusValueDM.UnderReview::class.java, "kyc_status")
|
||||
.withSubtype(PaymentAccountStatusValueDM.IssuingCard::class.java, "issuing_card")
|
||||
.withSubtype(PaymentAccountStatusValueDM.ActiveAccount::class.java, "active_account")
|
||||
.withSubtype(PaymentAccountStatusValueDM.DeactivatedAccount::class.java, "deactivated_account")
|
||||
.withSubtype(PaymentAccountStatusValueDM.CardIssueFailed::class.java, "card_issue_failed"),
|
||||
)
|
||||
.add(
|
||||
|
|
|
|||
|
|
@ -54,6 +54,12 @@ sealed interface PaymentAccountStatusValueDM {
|
|||
@Json(name = "customer_id") val customerId: String,
|
||||
) : PaymentAccountStatusValueDM
|
||||
|
||||
@NameLabel("deactivated_account")
|
||||
data class DeactivatedAccount(
|
||||
@Json(name = "deactivated_account") val marker: Boolean = true,
|
||||
@Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM,
|
||||
) : PaymentAccountStatusValueDM
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FiatBalanceDM(
|
||||
@Json(name = "available_balance") val availableBalance: BigDecimal,
|
||||
|
|
|
|||
|
|
@ -150,6 +150,9 @@
|
|||
<string name="balance_hidden_do_not_show_button">Don\'t show again</string>
|
||||
<string name="balance_hidden_got_it_button">Got it</string>
|
||||
<string name="balance_hidden_title">Balances are hidden</string>
|
||||
<string name="banner.swap.firsttime.button">Start Swap</string>
|
||||
<string name="banner.swap.firsttime.subtitletitle">Turn one asset into another in just a few taps</string>
|
||||
<string name="banner.swap.firsttime.title">Make your first swap</string>
|
||||
<string name="beta_mode_warning_message">According to the blockchain developers, Kaspa tokens are currently in beta. Stay tuned for updates!</string>
|
||||
<string name="beta_mode_warning_title">Beta Mode</string>
|
||||
<string name="biometric_disabled_warning_description">Biometrics are turned off on your device, so you can\'t use them to unlock your wallets. Enable biometrics in your device settings to use this method again.</string>
|
||||
|
|
@ -1540,6 +1543,7 @@
|
|||
<string name="swap_approve_description">By approving, you allow the smart contract to use your tokens in future transactions.</string>
|
||||
<string name="swap_fixed_rate">Fixed Rate</string>
|
||||
<string name="swap_give_permission_fee_footer">The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap.</string>
|
||||
<string name="swap_in_progress">Swap in progress</string>
|
||||
<string name="swap_promo_text">Exchange more tokens at better rates directly in your wallet.</string>
|
||||
<string name="swap_promo_title">New Swap Provider Available!</string>
|
||||
<string name="swap_search_suggestion_hint">Looking for something else?\nTry searching or explore another crypto!</string>
|
||||
|
|
@ -1548,11 +1552,11 @@
|
|||
<string name="swap_story_fifth_subtitle">Feel confident with round-the-clock support to help with any issues</string>
|
||||
<string name="swap_story_fifth_title">Always Here</string>
|
||||
<string name="swap_story_first_subtitle">Multiple trusted providers in one place—swap any asset effortlessly in your wallet</string>
|
||||
<string name="swap_story_first_subtitle_v2">Exchange crypto directly in Tangem\nNo additional transfers
\nNo moving funds to exchanges</string>
|
||||
<string name="swap_story_first_subtitle_v2">Exchange crypto directly in Tangem\nNo additional transfers\nNo moving funds to exchanges</string>
|
||||
<string name="swap_story_first_title">Swap With Us</string>
|
||||
<string name="swap_story_first_title_v2">Swap Inside Your Wallet</string>
|
||||
<string name="swap_story_forth_subtitle">No fumbles, no turnovers, no blind spots—your transaction is always protected</string>
|
||||
<string name="swap_story_forth_subtitle_v2">Swaps are executed through trusted providers. Your keys remain in your Tangem wallet at all times.
Clear. Transparent. Self-custodial.</string>
|
||||
<string name="swap_story_forth_subtitle_v2">Swaps are executed through trusted providers. Your keys remain in your Tangem wallet at all times. Clear. Transparent. Self-custodial.</string>
|
||||
<string name="swap_story_forth_title">Impenetrable Defense</string>
|
||||
<string name="swap_story_forth_title_v2">You Stay in Control</string>
|
||||
<string name="swap_story_second_subtitle">Maximize your value with rates sourced from a wide network of trusted providers, always choosing the best one</string>
|
||||
|
|
@ -1562,7 +1566,7 @@
|
|||
<string name="swap_story_third_subtitle">Hassle-free and intuitive, allowing you to swap tokens in just a few taps</string>
|
||||
<string name="swap_story_third_subtitle_v2">Swap across major networks
and thousands of tokens 0% fee on stablecoin-to-stablecoin swaps</string>
|
||||
<string name="swap_story_third_title">Simply Convenient</string>
|
||||
<string name="swap_story_third_title_v2">90+ Blockchains
\n16,000+ Assets</string>
|
||||
<string name="swap_story_third_title_v2">90+ Blockchains\n16,000+ Assets</string>
|
||||
<string name="swap_via_provider">Swap via provider</string>
|
||||
<string name="swap_your_assets_title">Your assets</string>
|
||||
<string name="swapping_alert_cex_description">The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address.</string>
|
||||
|
|
@ -1573,7 +1577,9 @@
|
|||
<string name="swapping_approve_information_text">All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval.</string>
|
||||
<string name="swapping_approve_information_title">Approve</string>
|
||||
<string name="swapping_fee_estimation_error_text">Fee estimation error. Please send feedback to support.</string>
|
||||
<string name="swapping_from_account_title">You send from</string>
|
||||
<string name="swapping_from_title">You swap</string>
|
||||
<string name="swapping_from_title_v2">You send</string>
|
||||
<string name="swapping_high_price_impact_description">Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.</string>
|
||||
<string name="swapping_high_price_impact_text">You may receive significantly less due to low liquidity. Try a smaller amount or another provider.</string>
|
||||
<string name="swapping_high_price_impact_title">High price impact</string>
|
||||
|
|
@ -1582,6 +1588,7 @@
|
|||
<string name="swapping_permission_header">Give Permission</string>
|
||||
<string name="swapping_swap_action">Swap</string>
|
||||
<string name="swapping_swap_action_in_progress">Swapping...</string>
|
||||
<string name="swapping_to_account_title">You receive to</string>
|
||||
<string name="swapping_to_title">You receive</string>
|
||||
<string name="swapping_token_list_title">Choose token</string>
|
||||
<string name="swapping_token_not_available">not available</string>
|
||||
|
|
@ -1661,13 +1668,13 @@
|
|||
<string name="tangempay_card_details_receive_error_description">Technical issues detected. Please try again later or contact support.</string>
|
||||
<string name="tangempay_card_details_receive_error_title">Receive unavailable now</string>
|
||||
<string name="tangempay_card_details_reissue_card">Replace card</string>
|
||||
<string name="tangempay_card_details_rename_card_invalid_description">Only letters and numbers are allowed.</string>
|
||||
<string name="tangempay_card_details_rename_card_invalid_description">Only letters and numbers are allowed</string>
|
||||
<string name="tangempay_card_details_rename_card_invalid_title">Invalid characters</string>
|
||||
<string name="tangempay_card_details_reveal_text">Reveal</string>
|
||||
<string name="tangempay_card_details_show_details">Show details</string>
|
||||
<string name="tangempay_card_details_swap_description">Swap any asset in your portfolio for card</string>
|
||||
<string name="tangempay_card_details_title">Card details</string>
|
||||
<string name="tangempay_card_details_unable_to_rename_card_description">Please try again later.</string>
|
||||
<string name="tangempay_card_details_unable_to_rename_card_description">Please try again later</string>
|
||||
<string name="tangempay_card_details_unfreeze_card">Unfreeze Card</string>
|
||||
<string name="tangempay_card_details_view_pin_code_description">Come back to the app if you forget it.</string>
|
||||
<string name="tangempay_card_details_view_pin_code_title">Your PIN code</string>
|
||||
|
|
@ -1676,7 +1683,7 @@
|
|||
<string name="tangempay_card_details_withdraw_in_progress_description">You can\'t initiate swap or new withdrawal till the current one is finished</string>
|
||||
<string name="tangempay_card_details_withdraw_in_progress_title">Withdrawal in progress</string>
|
||||
<string name="tangempay_card_limit_setup_amount_subtitle">Set a limit from %s</string>
|
||||
<string name="tangempay_card_limit_setup_error_message">We couldn’t set the limit. Please try again.</string>
|
||||
<string name="tangempay_card_limit_setup_error_message">We couldn’t set the limit. Please try again</string>
|
||||
<string name="tangempay_card_page_daily_limit_change">Change</string>
|
||||
<string name="tangempay_card_page_daily_limit_current_limit">Current limit</string>
|
||||
<string name="tangempay_card_page_daily_limit_error_description">We couldn\'t load your daily limit. Please try again.</string>
|
||||
|
|
@ -1753,7 +1760,7 @@
|
|||
<string name="tangempay_title">Tangem Pay</string>
|
||||
<string name="tangempay_usdc_on_polygon_network">USDC on Polygon network</string>
|
||||
<string name="tangempay_use_tangem_device_to_restore_payment_account">Click the button below to restore access</string>
|
||||
<string name="tangempay_withdrawal_note_description">Your on-chain USDC Polygon balance differs from your card balance and updates within 2 business days after a purchase. Funds from refunded purchases won’t be returned your on-chain balance or be available for withdrawal, but will stay on your card balance for purchases.</string>
|
||||
<string name="tangempay_withdrawal_note_description">Funds from refunded purchases won’t be returned your on-chain Polygon balance or be available for withdrawal, but will stay on your card balance for purchases</string>
|
||||
<string name="tangempay_withdrawal_note_title">Please note</string>
|
||||
<string name="tangempay_your_pin_code">Your PIN code</string>
|
||||
<string name="this_is_my_wallet_title">This is my wallet</string>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ android {
|
|||
namespace = "com.tangem.data.visa"
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** Project - Data */
|
||||
|
|
@ -68,4 +72,9 @@ dependencies {
|
|||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Test */
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(projects.common.test)
|
||||
testImplementation(projects.test.core)
|
||||
}
|
||||
|
|
@ -57,6 +57,9 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
customerId = value.customerId,
|
||||
)
|
||||
is PaymentAccountStatusValue.Empty -> PaymentAccountStatusValueDM.Empty()
|
||||
is PaymentAccountStatusValue.Deactivated -> PaymentAccountStatusValueDM.DeactivatedAccount(
|
||||
fiatBalance = value.fiatBalance.toDM(),
|
||||
)
|
||||
// Transient statuses are not persisted
|
||||
is PaymentAccountStatusValue.Loading,
|
||||
is PaymentAccountStatusValue.Error.ExposedDevice,
|
||||
|
|
@ -107,6 +110,10 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
|
|||
kycStatus = value.kycStatus,
|
||||
customerId = value.customerId,
|
||||
)
|
||||
is PaymentAccountStatusValueDM.DeactivatedAccount -> PaymentAccountStatusValue.Deactivated(
|
||||
source = StatusSource.CACHE,
|
||||
fiatBalance = value.fiatBalance.toDomain(),
|
||||
)
|
||||
null -> PaymentAccountStatusValue.Error.Unavailable
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,16 +52,6 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
val account = Account.Payment(userWalletId = params.userWalletId)
|
||||
logger.i("fetch: ${params.userWalletId.stringValue}")
|
||||
|
||||
if (onboardingRepository.isTangemPayDeactivated(params.userWalletId)) {
|
||||
return@catchOn paymentAccountStatusesStore.store(
|
||||
userWalletId = params.userWalletId,
|
||||
status = AccountStatus.Payment(
|
||||
account = account,
|
||||
value = PaymentAccountStatusValue.Empty,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if (deviceSecurity.isSecurityExposed()) {
|
||||
logger.i("fetch security info: rooted: ${deviceSecurity.isRooted}")
|
||||
logger.i("fetch security info: xposed: ${deviceSecurity.isXposed}")
|
||||
|
|
@ -256,21 +246,32 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
private fun CustomerInfo.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue {
|
||||
val cardInfo = this.cardInfo
|
||||
val productInstance = this.productInstance
|
||||
return if (kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty()) {
|
||||
PaymentAccountStatusValue.UnderReview(
|
||||
source = StatusSource.ACTUAL,
|
||||
kycStatus = kycStatus,
|
||||
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
|
||||
)
|
||||
} else if (cardInfo != null && productInstance != null && !customerId.isNullOrEmpty()) {
|
||||
convertToContentState(
|
||||
|
||||
val isDeactivated = productInstance?.status == CustomerInfo.ProductInstance.Status.DEACTIVATED
|
||||
val isFormer = state == CustomerInfo.State.FORMER
|
||||
val fiatBalance = fiatBalance
|
||||
|
||||
return when {
|
||||
kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty() -> {
|
||||
PaymentAccountStatusValue.UnderReview(
|
||||
source = StatusSource.ACTUAL,
|
||||
kycStatus = kycStatus,
|
||||
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
|
||||
)
|
||||
}
|
||||
fiatBalance != null && (isDeactivated || isFormer) -> {
|
||||
PaymentAccountStatusValue.Deactivated(
|
||||
source = StatusSource.ACTUAL,
|
||||
fiatBalance = fiatBalance,
|
||||
)
|
||||
}
|
||||
cardInfo != null && productInstance != null && !customerId.isNullOrEmpty() -> convertToContentState(
|
||||
userWalletId = userWalletId,
|
||||
productInstance = productInstance,
|
||||
cardInfo = cardInfo,
|
||||
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
|
||||
)
|
||||
} else {
|
||||
PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
|
||||
else -> PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -309,7 +310,6 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
|
|||
return when (this) {
|
||||
is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced
|
||||
is VisaApiError.NotPaeraCustomer -> constructNotCreatedOrEmptyStatus(userWalletId)
|
||||
is VisaApiError.Deactivated -> constructNotCreatedOrEmptyStatus(userWalletId)
|
||||
else -> PaymentAccountStatusValue.Error.Unavailable
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.tangem.data.pay.repository
|
|||
import arrow.core.Either
|
||||
import arrow.core.flatMap
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.data.pay.store.PaymentAccountStatusesStore
|
||||
|
|
@ -12,24 +11,25 @@ import com.tangem.datasource.api.pay.models.request.DeeplinkValidityRequest
|
|||
import com.tangem.datasource.api.pay.models.request.OrderRequest
|
||||
import com.tangem.datasource.api.pay.models.request.SetTangemPayEnabledRequest
|
||||
import com.tangem.datasource.api.pay.models.response.CustomerMeResponse
|
||||
import com.tangem.datasource.api.pay.models.response.FiatBalance
|
||||
import com.tangem.datasource.api.pay.models.response.OrderResponse
|
||||
import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore
|
||||
import com.tangem.datasource.local.visa.TangemPayStorage
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.pay.TangemPayEligibilityType
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.CardDisplayName
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimit
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
|
||||
import com.tangem.domain.models.pay.TangemPayEligibilityType
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
|
||||
import com.tangem.domain.pay.model.CustomerInfo
|
||||
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
|
||||
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimit
|
||||
import com.tangem.domain.models.pay.TangemPayCardLimitPeriod
|
||||
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
|
||||
import com.tangem.domain.visa.error.VisaApiError
|
||||
|
|
@ -105,10 +105,8 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
val isFormer = result?.state?.let { CustomerInfo.State.fromString(it) } == CustomerInfo.State.FORMER
|
||||
if (isDeactivated || isFormer) {
|
||||
tangemPayStorage.storeIsTangemPayDeactivated(userWalletId)
|
||||
VisaApiError.Deactivated.left()
|
||||
} else {
|
||||
getCustomerInfo(userWalletId = userWalletId, response = result).right()
|
||||
}
|
||||
getCustomerInfo(userWalletId = userWalletId, response = result).right()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -184,10 +182,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
currencyCode = fiatBalance.currency,
|
||||
depositAddress = response.depositAddress,
|
||||
isPinSet = response.card?.isPinSet == true,
|
||||
fiatBalance = PaymentAccountStatusValue.FiatBalance(
|
||||
availableBalance = fiatBalance.availableBalance,
|
||||
currency = fiatBalance.currency,
|
||||
),
|
||||
fiatBalance = fiatBalance.toDomain(),
|
||||
cryptoBalance = PaymentAccountStatusValue.CryptoBalance(
|
||||
id = cryptoBalance.id,
|
||||
chainId = cryptoBalance.chainId.toLong(),
|
||||
|
|
@ -212,6 +207,7 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
id = instance.id,
|
||||
cardId = instance.cardId,
|
||||
frozenState = cardFrozenState,
|
||||
status = instance.status.toDomain(),
|
||||
displayName = if (displayName != null) CardDisplayName(displayName).getOrElse { null } else null,
|
||||
actualCardLimit = instance.actualCardLimit?.parseCardLimit(),
|
||||
adminCardLimit = instance.adminCardLimit?.parseCardLimit(),
|
||||
|
|
@ -222,6 +218,8 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
productInstance = productInstance,
|
||||
kycStatus = kycStatus,
|
||||
cardInfo = cardInfo,
|
||||
state = response?.state?.let { CustomerInfo.State.fromString(it) } ?: CustomerInfo.State.UNDEFINED,
|
||||
fiatBalance = fiatBalance?.toDomain(),
|
||||
).also {
|
||||
lastFetchedCustomerInfoMap[userWalletId] = it
|
||||
}
|
||||
|
|
@ -310,4 +308,24 @@ internal class DefaultOnboardingRepository @Inject constructor(
|
|||
setHideMainOnboardingBanner(userWalletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun FiatBalance.toDomain() = PaymentAccountStatusValue.FiatBalance(
|
||||
availableBalance = availableBalance,
|
||||
currency = currency,
|
||||
)
|
||||
|
||||
private fun CustomerMeResponse.ProductInstance.Status.toDomain() = when (this) {
|
||||
CustomerMeResponse.ProductInstance.Status.NEW -> ProductInstance.Status.NEW
|
||||
CustomerMeResponse.ProductInstance.Status.READY_FOR_MANUFACTURING -> ProductInstance.Status.READY_FOR_MANUFACTURING
|
||||
CustomerMeResponse.ProductInstance.Status.MANUFACTURING -> ProductInstance.Status.MANUFACTURING
|
||||
CustomerMeResponse.ProductInstance.Status.SENT_TO_DELIVERY -> ProductInstance.Status.SENT_TO_DELIVERY
|
||||
CustomerMeResponse.ProductInstance.Status.DELIVERED -> ProductInstance.Status.DELIVERED
|
||||
CustomerMeResponse.ProductInstance.Status.ACTIVATING -> ProductInstance.Status.ACTIVATING
|
||||
CustomerMeResponse.ProductInstance.Status.ACTIVE -> ProductInstance.Status.ACTIVE
|
||||
CustomerMeResponse.ProductInstance.Status.BLOCKED -> ProductInstance.Status.BLOCKED
|
||||
CustomerMeResponse.ProductInstance.Status.DEACTIVATING -> ProductInstance.Status.DEACTIVATING
|
||||
CustomerMeResponse.ProductInstance.Status.DEACTIVATED -> ProductInstance.Status.DEACTIVATED
|
||||
CustomerMeResponse.ProductInstance.Status.CANCELED -> ProductInstance.Status.CANCELED
|
||||
CustomerMeResponse.ProductInstance.Status.UNKNOWN -> ProductInstance.Status.UNKNOWN
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package com.tangem.data.pay.converter
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.data.pay.entity.TangemPayCurrencyFactory
|
||||
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class PaymentAccountStatusValueDMConverterTest {
|
||||
|
||||
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory = mockk()
|
||||
private val userWalletId = UserWalletId("1234567890ABCDEF")
|
||||
|
||||
private val converter = PaymentAccountStatusValueDMConverter(tangemPayCurrencyFactory)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Convert {
|
||||
|
||||
@Test
|
||||
fun `GIVEN domain Empty WHEN convert THEN returns DM Empty`() {
|
||||
// GIVEN
|
||||
val domain = PaymentAccountStatusValue.Empty
|
||||
|
||||
// WHEN
|
||||
val result = converter.convert(domain)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isInstanceOf(PaymentAccountStatusValueDM.Empty::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN domain Deactivated with balance WHEN convert THEN returns DM DeactivatedAccount with same balance`() {
|
||||
// GIVEN
|
||||
val domain = PaymentAccountStatusValue.Deactivated(
|
||||
source = StatusSource.ACTUAL,
|
||||
fiatBalance = PaymentAccountStatusValue.FiatBalance(
|
||||
availableBalance = BigDecimal("100"),
|
||||
currency = "USD",
|
||||
)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = converter.convert(domain)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isInstanceOf(PaymentAccountStatusValueDM.DeactivatedAccount::class.java)
|
||||
val dm = result as PaymentAccountStatusValueDM.DeactivatedAccount
|
||||
assertThat(dm.fiatBalance.availableBalance).isEqualTo(BigDecimal("100"))
|
||||
assertThat(dm.fiatBalance.currency).isEqualTo("USD")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN domain Loading WHEN convert THEN returns null (transient, not persisted)`() {
|
||||
// GIVEN
|
||||
val domain = PaymentAccountStatusValue.Loading
|
||||
|
||||
// WHEN
|
||||
val result = converter.convert(domain)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN domain Error Unavailable WHEN convert THEN returns null (transient, not persisted)`() {
|
||||
// GIVEN
|
||||
val domain = PaymentAccountStatusValue.Error.Unavailable
|
||||
|
||||
// WHEN
|
||||
val result = converter.convert(domain)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN domain NotCreated WHEN convert THEN returns DM NotCreated`() {
|
||||
// GIVEN
|
||||
val domain = PaymentAccountStatusValue.NotCreated
|
||||
|
||||
// WHEN
|
||||
val result = converter.convert(domain)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isInstanceOf(PaymentAccountStatusValueDM.NotCreated::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ConvertBack {
|
||||
|
||||
@Test
|
||||
fun `GIVEN DM Empty WHEN convertBack THEN returns domain Empty`() {
|
||||
// GIVEN
|
||||
val dm = PaymentAccountStatusValueDM.Empty()
|
||||
|
||||
// WHEN
|
||||
val result = converter.convertBack(userWalletId, dm)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(PaymentAccountStatusValue.Empty)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN DM DeactivatedAccount WHEN convertBack THEN returns domain Deactivated with CACHE source`() {
|
||||
// GIVEN
|
||||
val dm = PaymentAccountStatusValueDM.DeactivatedAccount(
|
||||
fiatBalance = PaymentAccountStatusValueDM.FiatBalanceDM(
|
||||
availableBalance = BigDecimal("200"),
|
||||
currency = "EUR",
|
||||
)
|
||||
)
|
||||
|
||||
// WHEN
|
||||
val result = converter.convertBack(userWalletId, dm)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isInstanceOf(PaymentAccountStatusValue.Deactivated::class.java)
|
||||
val deactivated = result as PaymentAccountStatusValue.Deactivated
|
||||
assertThat(deactivated.source).isEqualTo(StatusSource.CACHE)
|
||||
assertThat(deactivated.fiatBalance.availableBalance).isEqualTo(BigDecimal("200"))
|
||||
assertThat(deactivated.fiatBalance.currency).isEqualTo("EUR")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN null DM WHEN convertBack THEN returns Error Unavailable`() {
|
||||
// GIVEN / WHEN
|
||||
val result = converter.convertBack(userWalletId, null)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(PaymentAccountStatusValue.Error.Unavailable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN DM NotCreated WHEN convertBack THEN returns domain NotCreated`() {
|
||||
// GIVEN
|
||||
val dm = PaymentAccountStatusValueDM.NotCreated()
|
||||
|
||||
// WHEN
|
||||
val result = converter.convertBack(userWalletId, dm)
|
||||
|
||||
// THEN
|
||||
assertThat(result).isEqualTo(PaymentAccountStatusValue.NotCreated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,8 +6,10 @@ import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
|
|||
import com.tangem.domain.account.status.usecase.*
|
||||
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
|
||||
import com.tangem.domain.account.status.utils.CryptoCurrencyMetadataCleaner
|
||||
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import com.tangem.domain.express.ExpressServiceFetcher
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier
|
||||
|
|
@ -51,9 +53,13 @@ internal object AccountStatusUseCaseModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideIsAccountsModeEnabledUseCase(
|
||||
multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
|
||||
multiAccountListSupplier: MultiAccountListSupplier,
|
||||
paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
): IsAccountsModeEnabledUseCase {
|
||||
return IsAccountsModeEnabledUseCase(multiAccountStatusListSupplier = multiAccountStatusListSupplier)
|
||||
return IsAccountsModeEnabledUseCase(
|
||||
multiAccountListSupplier = multiAccountListSupplier,
|
||||
paymentAccountStatusSupplier = paymentAccountStatusSupplier,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -1,49 +1,76 @@
|
|||
package com.tangem.domain.account.status.usecase
|
||||
|
||||
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusProducer
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
/**
|
||||
* Use case to determine if the accounts mode is enabled.
|
||||
* Accounts mode is considered enabled if any [com.tangem.domain.account.models.AccountStatusList] produced for the
|
||||
* user's wallets has more than one [AccountStatus.CryptoPortfolio], or has a [AccountStatus.Payment] with any
|
||||
|
||||
* [PaymentAccountStatusValue.Empty].
|
||||
*
|
||||
* @property multiAccountStatusListSupplier supplier that provides a list of
|
||||
* [com.tangem.domain.account.models.AccountStatusList]s for all user wallets
|
||||
* Accounts mode is considered enabled if any user wallet either:
|
||||
* - contains more than one [Account.CryptoPortfolio], or
|
||||
* - has a [com.tangem.domain.models.account.AccountStatus.Payment] in an active state.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
* Splits the structural check (via [MultiAccountListSupplier]) and the payment-status check
|
||||
* (via [PaymentAccountStatusSupplier] per wallet) so that the structural answer doesn't wait for
|
||||
* slow per-wallet status pipelines on cold start.
|
||||
*/
|
||||
class IsAccountsModeEnabledUseCase(
|
||||
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
|
||||
private val multiAccountListSupplier: MultiAccountListSupplier,
|
||||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||
) {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
operator fun invoke(): Flow<Boolean> {
|
||||
return multiAccountStatusListSupplier.invoke()
|
||||
.map { accountStatusLists -> accountStatusLists.any { it.accountStatuses.isModeEnabled() } }
|
||||
val cryptoMode = multiAccountListSupplier.invoke()
|
||||
.map { lists -> lists.any { it.hasMultipleCryptoPortfolios() } }
|
||||
|
||||
val paymentMode = multiAccountListSupplier.invoke().flatMapLatest { lists ->
|
||||
val walletIdsWithPayment = lists.mapNotNull { list ->
|
||||
if (list.accounts.any { it is Account.Payment }) list.userWalletId else null
|
||||
}
|
||||
|
||||
if (walletIdsWithPayment.isEmpty()) {
|
||||
flowOf(false)
|
||||
} else {
|
||||
val flows = walletIdsWithPayment.map { walletId ->
|
||||
paymentAccountStatusSupplier.invoke(walletId)
|
||||
.map { it.value.isActivePayment() }
|
||||
.onStart { emit(false) }
|
||||
}
|
||||
combine(flows) { results -> results.any { it } }
|
||||
}
|
||||
}
|
||||
|
||||
return combine(cryptoMode, paymentMode) { crypto, payment -> crypto || payment }
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
suspend fun invokeSync(): Boolean {
|
||||
return multiAccountStatusListSupplier.getSyncOrNull(Unit)
|
||||
?.any { it.accountStatuses.isModeEnabled() } == true
|
||||
val accountLists = multiAccountListSupplier.getSyncOrNull(Unit).orEmpty()
|
||||
if (accountLists.any { it.hasMultipleCryptoPortfolios() }) return true
|
||||
|
||||
val walletIdsWithPayment = accountLists.mapNotNull { list ->
|
||||
if (list.accounts.any { it is Account.Payment }) list.userWalletId else null
|
||||
}
|
||||
return walletIdsWithPayment.any { walletId ->
|
||||
paymentAccountStatusSupplier
|
||||
.getSyncOrNull(
|
||||
params = PaymentAccountStatusProducer.Params(walletId),
|
||||
timeMillis = PAYMENT_STATUS_SYNC_TIMEOUT_MS,
|
||||
)
|
||||
?.value
|
||||
?.isActivePayment() == true
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<AccountStatus>.isModeEnabled(): Boolean {
|
||||
var cryptoPortfolioCount = 0
|
||||
for (status in this) {
|
||||
when {
|
||||
status is AccountStatus.CryptoPortfolio && ++cryptoPortfolioCount > 1 -> return true
|
||||
status is AccountStatus.Payment && status.value.isActivePayment() -> return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
private fun AccountList.hasMultipleCryptoPortfolios(): Boolean =
|
||||
accounts.filterIsInstance<Account.CryptoPortfolio>().size > 1
|
||||
|
||||
private fun PaymentAccountStatusValue.isActivePayment(): Boolean {
|
||||
return when (this) {
|
||||
|
|
@ -55,7 +82,12 @@ class IsAccountsModeEnabledUseCase(
|
|||
is PaymentAccountStatusValue.Loaded,
|
||||
is PaymentAccountStatusValue.Loading,
|
||||
is PaymentAccountStatusValue.UnderReview,
|
||||
is PaymentAccountStatusValue.Deactivated,
|
||||
-> true
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val PAYMENT_STATUS_SYNC_TIMEOUT_MS = 1_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,20 @@
|
|||
package com.tangem.domain.account.status.usecase
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.account.models.AccountStatusList
|
||||
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.supplier.MultiAccountListSupplier
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.PaymentAccountStatusValue
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusProducer
|
||||
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.last
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Nested
|
||||
|
|
@ -20,13 +24,17 @@ import org.junit.jupiter.api.TestInstance
|
|||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class IsAccountsModeEnabledUseCaseTest {
|
||||
|
||||
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier = mockk()
|
||||
private val multiAccountListSupplier: MultiAccountListSupplier = mockk()
|
||||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier = mockk()
|
||||
|
||||
private val useCase = IsAccountsModeEnabledUseCase(multiAccountStatusListSupplier = multiAccountStatusListSupplier)
|
||||
private val useCase = IsAccountsModeEnabledUseCase(
|
||||
multiAccountListSupplier = multiAccountListSupplier,
|
||||
paymentAccountStatusSupplier = paymentAccountStatusSupplier,
|
||||
)
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
clearMocks(multiAccountStatusListSupplier)
|
||||
clearMocks(multiAccountListSupplier, paymentAccountStatusSupplier)
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
|
@ -35,118 +43,129 @@ class IsAccountsModeEnabledUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun `returns false when supplier emits empty list`() = runTest {
|
||||
every { multiAccountStatusListSupplier.invoke() } returns flowOf(emptyList())
|
||||
every { multiAccountListSupplier.invoke() } returns flowOf(emptyList())
|
||||
|
||||
val actual = useCase.invoke().first()
|
||||
val actual = useCase.invoke().last()
|
||||
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns false when single crypto portfolio account`() = runTest {
|
||||
val statusList = createAccountStatusList(
|
||||
statuses = listOf(mockCryptoPortfolio()),
|
||||
)
|
||||
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio()))
|
||||
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list))
|
||||
|
||||
val actual = useCase.invoke().first()
|
||||
val actual = useCase.invoke().last()
|
||||
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns true when two crypto portfolio accounts`() = runTest {
|
||||
val statusList = createAccountStatusList(
|
||||
statuses = listOf(mockCryptoPortfolio(), mockCryptoPortfolio()),
|
||||
)
|
||||
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockCryptoPortfolio()))
|
||||
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list))
|
||||
|
||||
val actual = useCase.invoke().first()
|
||||
val actual = useCase.invoke().last()
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns true when payment account is Loaded`() = runTest {
|
||||
val statusList = createAccountStatusList(
|
||||
statuses = listOf(mockCryptoPortfolio(), mockPayment(mockk<PaymentAccountStatusValue.Loaded>())),
|
||||
)
|
||||
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list))
|
||||
mockPaymentStatus(WALLET_ID_1, mockk<PaymentAccountStatusValue.Loaded>())
|
||||
|
||||
val actual = useCase.invoke().first()
|
||||
val actual = useCase.invoke().last()
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns false when payment account is NotCreated`() = runTest {
|
||||
val statusList = createAccountStatusList(
|
||||
statuses = listOf(mockCryptoPortfolio(), mockPayment(PaymentAccountStatusValue.NotCreated)),
|
||||
)
|
||||
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list))
|
||||
mockPaymentStatus(WALLET_ID_1, PaymentAccountStatusValue.NotCreated)
|
||||
|
||||
val actual = useCase.invoke().first()
|
||||
val actual = useCase.invoke().last()
|
||||
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns false when payment account is Empty`() = runTest {
|
||||
val statusList = createAccountStatusList(
|
||||
statuses = listOf(mockCryptoPortfolio(), mockPayment(PaymentAccountStatusValue.Empty)),
|
||||
)
|
||||
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list))
|
||||
mockPaymentStatus(WALLET_ID_1, PaymentAccountStatusValue.Empty)
|
||||
|
||||
val actual = useCase.invoke().first()
|
||||
val actual = useCase.invoke().last()
|
||||
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns true when payment account is UnderReview`() = runTest {
|
||||
val statusList = createAccountStatusList(
|
||||
statuses = listOf(mockCryptoPortfolio(), mockPayment(mockk<PaymentAccountStatusValue.UnderReview>())),
|
||||
)
|
||||
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list))
|
||||
mockPaymentStatus(WALLET_ID_1, mockk<PaymentAccountStatusValue.UnderReview>())
|
||||
|
||||
val actual = useCase.invoke().first()
|
||||
val actual = useCase.invoke().last()
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns true when payment account is IssuingCard`() = runTest {
|
||||
val statusList = createAccountStatusList(
|
||||
statuses = listOf(mockCryptoPortfolio(), mockPayment(mockk<PaymentAccountStatusValue.IssuingCard>())),
|
||||
)
|
||||
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list))
|
||||
mockPaymentStatus(WALLET_ID_1, mockk<PaymentAccountStatusValue.IssuingCard>())
|
||||
|
||||
val actual = useCase.invoke().first()
|
||||
val actual = useCase.invoke().last()
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns true when payment account is Loading`() = runTest {
|
||||
val statusList = createAccountStatusList(
|
||||
statuses = listOf(mockCryptoPortfolio(), mockPayment(PaymentAccountStatusValue.Loading)),
|
||||
)
|
||||
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList))
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list))
|
||||
mockPaymentStatus(WALLET_ID_1, PaymentAccountStatusValue.Loading)
|
||||
|
||||
val actual = useCase.invoke().first()
|
||||
val actual = useCase.invoke().last()
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns true when multiple status lists and one has two crypto portfolios`() = runTest {
|
||||
val statusList1 = createAccountStatusList(statuses = listOf(mockCryptoPortfolio()))
|
||||
val statusList2 = createAccountStatusList(
|
||||
statuses = listOf(mockCryptoPortfolio(), mockCryptoPortfolio()),
|
||||
)
|
||||
every { multiAccountStatusListSupplier.invoke() } returns flowOf(listOf(statusList1, statusList2))
|
||||
fun `returns true when payment account is Deactivated`() = runTest {
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list))
|
||||
mockPaymentStatus(WALLET_ID_1, mockk<PaymentAccountStatusValue.Deactivated>())
|
||||
|
||||
val actual = useCase.invoke().first()
|
||||
val actual = useCase.invoke().last()
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns true when multiple wallets and one has two crypto portfolios`() = runTest {
|
||||
val list1 = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio()))
|
||||
val list2 = createAccountList(WALLET_ID_2, listOf(mockCryptoPortfolio(), mockCryptoPortfolio()))
|
||||
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list1, list2))
|
||||
|
||||
val actual = useCase.invoke().last()
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns true when multiple wallets and one has active payment`() = runTest {
|
||||
val list1 = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio()))
|
||||
val list2 = createAccountList(WALLET_ID_2, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list1, list2))
|
||||
mockPaymentStatus(WALLET_ID_2, mockk<PaymentAccountStatusValue.Loaded>())
|
||||
|
||||
val actual = useCase.invoke().last()
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
|
@ -158,7 +177,7 @@ class IsAccountsModeEnabledUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun `returns false when getSyncOrNull returns null`() = runTest {
|
||||
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns null
|
||||
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns null
|
||||
|
||||
val actual = useCase.invokeSync()
|
||||
|
||||
|
|
@ -167,7 +186,7 @@ class IsAccountsModeEnabledUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun `returns false when getSyncOrNull returns empty list`() = runTest {
|
||||
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns emptyList()
|
||||
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns emptyList()
|
||||
|
||||
val actual = useCase.invokeSync()
|
||||
|
||||
|
|
@ -176,10 +195,8 @@ class IsAccountsModeEnabledUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun `returns false when single crypto portfolio account`() = runTest {
|
||||
val statusList = createAccountStatusList(
|
||||
statuses = listOf(mockCryptoPortfolio()),
|
||||
)
|
||||
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns listOf(statusList)
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio()))
|
||||
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list)
|
||||
|
||||
val actual = useCase.invokeSync()
|
||||
|
||||
|
|
@ -188,10 +205,8 @@ class IsAccountsModeEnabledUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun `returns true when two crypto portfolio accounts`() = runTest {
|
||||
val statusList = createAccountStatusList(
|
||||
statuses = listOf(mockCryptoPortfolio(), mockCryptoPortfolio()),
|
||||
)
|
||||
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns listOf(statusList)
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockCryptoPortfolio()))
|
||||
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list)
|
||||
|
||||
val actual = useCase.invokeSync()
|
||||
|
||||
|
|
@ -200,10 +215,9 @@ class IsAccountsModeEnabledUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun `returns true when payment account is Loaded`() = runTest {
|
||||
val statusList = createAccountStatusList(
|
||||
statuses = listOf(mockCryptoPortfolio(), mockPayment(mockk<PaymentAccountStatusValue.Loaded>())),
|
||||
)
|
||||
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns listOf(statusList)
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list)
|
||||
mockPaymentStatusSync(WALLET_ID_1, mockk<PaymentAccountStatusValue.Loaded>())
|
||||
|
||||
val actual = useCase.invokeSync()
|
||||
|
||||
|
|
@ -212,10 +226,9 @@ class IsAccountsModeEnabledUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun `returns false when payment account is NotCreated`() = runTest {
|
||||
val statusList = createAccountStatusList(
|
||||
statuses = listOf(mockCryptoPortfolio(), mockPayment(PaymentAccountStatusValue.NotCreated)),
|
||||
)
|
||||
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns listOf(statusList)
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list)
|
||||
mockPaymentStatusSync(WALLET_ID_1, PaymentAccountStatusValue.NotCreated)
|
||||
|
||||
val actual = useCase.invokeSync()
|
||||
|
||||
|
|
@ -224,10 +237,9 @@ class IsAccountsModeEnabledUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun `returns false when payment account is Empty`() = runTest {
|
||||
val statusList = createAccountStatusList(
|
||||
statuses = listOf(mockCryptoPortfolio(), mockPayment(PaymentAccountStatusValue.Empty)),
|
||||
)
|
||||
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns listOf(statusList)
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list)
|
||||
mockPaymentStatusSync(WALLET_ID_1, PaymentAccountStatusValue.Empty)
|
||||
|
||||
val actual = useCase.invokeSync()
|
||||
|
||||
|
|
@ -236,10 +248,9 @@ class IsAccountsModeEnabledUseCaseTest {
|
|||
|
||||
@Test
|
||||
fun `returns true when payment account is UnderReview`() = runTest {
|
||||
val statusList = createAccountStatusList(
|
||||
statuses = listOf(mockCryptoPortfolio(), mockPayment(mockk<PaymentAccountStatusValue.UnderReview>())),
|
||||
)
|
||||
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns listOf(statusList)
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list)
|
||||
mockPaymentStatusSync(WALLET_ID_1, mockk<PaymentAccountStatusValue.UnderReview>())
|
||||
|
||||
val actual = useCase.invokeSync()
|
||||
|
||||
|
|
@ -247,12 +258,22 @@ class IsAccountsModeEnabledUseCaseTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `returns true when multiple status lists and one has loaded payment`() = runTest {
|
||||
val statusList1 = createAccountStatusList(statuses = listOf(mockCryptoPortfolio()))
|
||||
val statusList2 = createAccountStatusList(
|
||||
statuses = listOf(mockCryptoPortfolio(), mockPayment(mockk<PaymentAccountStatusValue.Loaded>())),
|
||||
)
|
||||
coEvery { multiAccountStatusListSupplier.getSyncOrNull(Unit, any()) } returns listOf(statusList1, statusList2)
|
||||
fun `returns true when payment account is Deactivated`() = runTest {
|
||||
val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list)
|
||||
mockPaymentStatusSync(WALLET_ID_1, mockk<PaymentAccountStatusValue.Deactivated>())
|
||||
|
||||
val actual = useCase.invokeSync()
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns true when multiple wallets and one has loaded payment`() = runTest {
|
||||
val list1 = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio()))
|
||||
val list2 = createAccountList(WALLET_ID_2, listOf(mockCryptoPortfolio(), mockPaymentAccount()))
|
||||
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list1, list2)
|
||||
mockPaymentStatusSync(WALLET_ID_2, mockk<PaymentAccountStatusValue.Loaded>())
|
||||
|
||||
val actual = useCase.invokeSync()
|
||||
|
||||
|
|
@ -260,13 +281,29 @@ class IsAccountsModeEnabledUseCaseTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun mockCryptoPortfolio(): AccountStatus.CryptoPortfolio = mockk()
|
||||
private fun mockCryptoPortfolio(): Account.CryptoPortfolio = mockk()
|
||||
|
||||
private fun mockPayment(value: PaymentAccountStatusValue): AccountStatus.Payment = mockk {
|
||||
every { this@mockk.value } returns value
|
||||
private fun mockPaymentAccount(): Account.Payment = mockk()
|
||||
|
||||
private fun mockPaymentStatus(walletId: UserWalletId, value: PaymentAccountStatusValue) {
|
||||
val status = mockk<AccountStatus.Payment> { every { this@mockk.value } returns value }
|
||||
every { paymentAccountStatusSupplier.invoke(walletId) } returns flowOf(status)
|
||||
}
|
||||
|
||||
private fun createAccountStatusList(statuses: List<AccountStatus>): AccountStatusList = mockk {
|
||||
every { accountStatuses } returns statuses
|
||||
private fun mockPaymentStatusSync(walletId: UserWalletId, value: PaymentAccountStatusValue) {
|
||||
val status = mockk<AccountStatus.Payment> { every { this@mockk.value } returns value }
|
||||
coEvery {
|
||||
paymentAccountStatusSupplier.getSyncOrNull(PaymentAccountStatusProducer.Params(walletId), any())
|
||||
} returns status
|
||||
}
|
||||
|
||||
private fun createAccountList(walletId: UserWalletId, accounts: List<Account>): AccountList = mockk {
|
||||
every { userWalletId } returns walletId
|
||||
every { this@mockk.accounts } returns accounts
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val WALLET_ID_1 = UserWalletId(stringValue = "011")
|
||||
val WALLET_ID_2 = UserWalletId(stringValue = "022")
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,6 @@ interface ScanCardProcessor {
|
|||
cardId: String? = null,
|
||||
onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {},
|
||||
onWalletNotCreated: suspend () -> Unit = {},
|
||||
disclaimerWillShow: () -> Unit = {},
|
||||
onCancel: suspend () -> Unit = {},
|
||||
onFailure: suspend (error: TangemError) -> Unit = {},
|
||||
onSuccess: suspend (scanResponse: ScanResponse) -> Unit = {},
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ sealed class PaymentAccountStatusValue {
|
|||
-> TotalFiatBalance.Loaded(amount = SerializedBigDecimal.ZERO, source = source)
|
||||
is Loading -> TotalFiatBalance.Loading
|
||||
is Loaded -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source)
|
||||
is Deactivated -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -44,6 +45,7 @@ sealed class PaymentAccountStatusValue {
|
|||
is IssuingCard -> copy(source = source)
|
||||
is Loaded -> copy(source = source)
|
||||
is UnderReview -> copy(source = source)
|
||||
is Deactivated -> copy(source = source)
|
||||
is Loading,
|
||||
is Empty,
|
||||
is NotCreated,
|
||||
|
|
@ -92,6 +94,18 @@ sealed class PaymentAccountStatusValue {
|
|||
@Serializable
|
||||
data class IssuingCard(override val source: StatusSource) : PaymentAccountStatusValue()
|
||||
|
||||
/**
|
||||
* Represents a state where the account is deactivated.
|
||||
*
|
||||
* @property source The source of the status information.
|
||||
* @property fiatBalance The fiat balance details.
|
||||
*/
|
||||
@Serializable
|
||||
data class Deactivated(
|
||||
override val source: StatusSource,
|
||||
val fiatBalance: FiatBalance,
|
||||
) : PaymentAccountStatusValue()
|
||||
|
||||
/**
|
||||
* Represents a state where the payment account is successfully loaded with complete information.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -12,5 +12,6 @@ data class TangemPayDetailsConfig(
|
|||
val cardFrozenState: TangemPayCardFrozenState,
|
||||
val cardNumberEnd: String,
|
||||
val chainId: Int,
|
||||
val isTangemPayDeactivated: Boolean,
|
||||
val displayName: CardDisplayName?,
|
||||
)
|
||||
|
|
@ -57,7 +57,6 @@ sealed class VisaApiError(
|
|||
data object CustomerIsBlocked : VisaApiError(104110210)
|
||||
data object UnknownWithoutCode : VisaApiError(104110999)
|
||||
data class Unknown(override val errorCode: Int) : VisaApiError(errorCode)
|
||||
data object Deactivated : VisaApiError(0)
|
||||
|
||||
fun isUnknown() = this is UnknownWithoutCode || this is Unknown
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ data class CustomerInfo(
|
|||
val productInstance: ProductInstance?,
|
||||
val kycStatus: KycStatus,
|
||||
val cardInfo: CardInfo?,
|
||||
val state: State,
|
||||
val fiatBalance: PaymentAccountStatusValue.FiatBalance?,
|
||||
) {
|
||||
enum class State {
|
||||
NEW,
|
||||
|
|
@ -54,7 +56,23 @@ data class CustomerInfo(
|
|||
val displayName: CardDisplayName?,
|
||||
val actualCardLimit: TangemPayCardLimit?,
|
||||
val adminCardLimit: TangemPayCardLimit?,
|
||||
)
|
||||
val status: Status,
|
||||
) {
|
||||
enum class Status {
|
||||
NEW,
|
||||
READY_FOR_MANUFACTURING,
|
||||
MANUFACTURING,
|
||||
SENT_TO_DELIVERY,
|
||||
DELIVERED,
|
||||
ACTIVATING,
|
||||
ACTIVE,
|
||||
BLOCKED,
|
||||
DEACTIVATING,
|
||||
DEACTIVATED,
|
||||
CANCELED,
|
||||
UNKNOWN,
|
||||
}
|
||||
}
|
||||
|
||||
data class CardInfo(
|
||||
val lastFourDigits: String,
|
||||
|
|
|
|||
|
|
@ -6,5 +6,4 @@ sealed interface TangemPayCustomerInfoError {
|
|||
data object RefreshNeededError : TangemPayCustomerInfoError
|
||||
data object UnknownError : TangemPayCustomerInfoError
|
||||
data object ExposedDeviceError : TangemPayCustomerInfoError
|
||||
data object DeactivatedError : TangemPayCustomerInfoError
|
||||
}
|
||||
|
|
@ -26,8 +26,8 @@ import com.tangem.domain.models.tokenlist.TokenList
|
|||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState
|
||||
import com.tangem.features.commonfeatures.api.choosetoken.model.TokenListUMData
|
||||
import com.tangem.features.commonfeatures.impl.choosetoken.model.ClickIntents
|
||||
import com.tangem.features.commonfeatures.impl.R
|
||||
import com.tangem.features.commonfeatures.impl.choosetoken.model.ClickIntents
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
internal class ChooseTokenListItemConverter(
|
||||
|
|
@ -187,6 +187,7 @@ internal class ChooseTokenListItemConverter(
|
|||
is PaymentAccountStatusValue.UnderReview,
|
||||
PaymentAccountStatusValue.Loading,
|
||||
PaymentAccountStatusValue.Empty,
|
||||
is PaymentAccountStatusValue.Deactivated,
|
||||
-> return null
|
||||
is PaymentAccountStatusValue.Loaded -> status.cryptoCurrencyStatus
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,7 +88,6 @@ internal class CreateWalletStartModelTest {
|
|||
cardId = any(),
|
||||
onProgressStateChange = any(),
|
||||
onWalletNotCreated = any(),
|
||||
disclaimerWillShow = any(),
|
||||
onCancel = any(),
|
||||
onFailure = any(),
|
||||
onSuccess = any()
|
||||
|
|
@ -118,7 +117,6 @@ internal class CreateWalletStartModelTest {
|
|||
cardId = null,
|
||||
onProgressStateChange = any(),
|
||||
onWalletNotCreated = any(),
|
||||
disclaimerWillShow = any(),
|
||||
onCancel = any(),
|
||||
onFailure = any(),
|
||||
onSuccess = any()
|
||||
|
|
@ -148,7 +146,6 @@ internal class CreateWalletStartModelTest {
|
|||
cardId = null,
|
||||
onProgressStateChange = any(),
|
||||
onWalletNotCreated = any(),
|
||||
disclaimerWillShow = any(),
|
||||
onCancel = any(),
|
||||
onFailure = any(),
|
||||
onSuccess = any()
|
||||
|
|
@ -266,7 +263,6 @@ internal class CreateWalletStartModelTest {
|
|||
cardId = null,
|
||||
onProgressStateChange = any(),
|
||||
onWalletNotCreated = any(),
|
||||
disclaimerWillShow = any(),
|
||||
onCancel = any(),
|
||||
onFailure = any(),
|
||||
onSuccess = any()
|
||||
|
|
@ -284,13 +280,12 @@ internal class CreateWalletStartModelTest {
|
|||
cardId = any(),
|
||||
onProgressStateChange = any(),
|
||||
onWalletNotCreated = any(),
|
||||
disclaimerWillShow = any(),
|
||||
onCancel = any(),
|
||||
onFailure = any(),
|
||||
onSuccess = any()
|
||||
)
|
||||
} coAnswers {
|
||||
val onFailure = arg<suspend (TangemError) -> Unit>(7)
|
||||
val onFailure = arg<suspend (TangemError) -> Unit>(6)
|
||||
onFailure.invoke(error)
|
||||
}
|
||||
|
||||
|
|
@ -313,13 +308,12 @@ internal class CreateWalletStartModelTest {
|
|||
cardId = any(),
|
||||
onProgressStateChange = any(),
|
||||
onWalletNotCreated = any(),
|
||||
disclaimerWillShow = any(),
|
||||
onCancel = any(),
|
||||
onFailure = any(),
|
||||
onSuccess = any()
|
||||
)
|
||||
} coAnswers {
|
||||
val onFailure = arg<suspend (TangemError) -> Unit>(7)
|
||||
val onFailure = arg<suspend (TangemError) -> Unit>(6)
|
||||
onFailure.invoke(error)
|
||||
}
|
||||
|
||||
|
|
@ -342,13 +336,12 @@ internal class CreateWalletStartModelTest {
|
|||
cardId = any(),
|
||||
onProgressStateChange = any(),
|
||||
onWalletNotCreated = any(),
|
||||
disclaimerWillShow = any(),
|
||||
onCancel = any(),
|
||||
onFailure = any(),
|
||||
onSuccess = any()
|
||||
)
|
||||
} coAnswers {
|
||||
val onFailure = arg<suspend (TangemError) -> Unit>(7)
|
||||
val onFailure = arg<suspend (TangemError) -> Unit>(6)
|
||||
onFailure.invoke(error)
|
||||
}
|
||||
|
||||
|
|
@ -371,13 +364,12 @@ internal class CreateWalletStartModelTest {
|
|||
cardId = any(),
|
||||
onProgressStateChange = any(),
|
||||
onWalletNotCreated = any(),
|
||||
disclaimerWillShow = any(),
|
||||
onCancel = any(),
|
||||
onFailure = any(),
|
||||
onSuccess = any()
|
||||
)
|
||||
} coAnswers {
|
||||
val onSuccess = arg<suspend (ScanResponse) -> Unit>(8)
|
||||
val onSuccess = arg<suspend (ScanResponse) -> Unit>(7)
|
||||
onSuccess.invoke(testScanResponse)
|
||||
}
|
||||
|
||||
|
|
@ -411,13 +403,12 @@ internal class CreateWalletStartModelTest {
|
|||
cardId = any(),
|
||||
onProgressStateChange = any(),
|
||||
onWalletNotCreated = any(),
|
||||
disclaimerWillShow = any(),
|
||||
onCancel = any(),
|
||||
onFailure = any(),
|
||||
onSuccess = any()
|
||||
)
|
||||
} coAnswers {
|
||||
val onSuccess = arg<suspend (ScanResponse) -> Unit>(8)
|
||||
val onSuccess = arg<suspend (ScanResponse) -> Unit>(7)
|
||||
onSuccess.invoke(testScanResponse)
|
||||
}
|
||||
|
||||
|
|
@ -448,13 +439,12 @@ internal class CreateWalletStartModelTest {
|
|||
cardId = any(),
|
||||
onProgressStateChange = any(),
|
||||
onWalletNotCreated = any(),
|
||||
disclaimerWillShow = any(),
|
||||
onCancel = any(),
|
||||
onFailure = any(),
|
||||
onSuccess = any()
|
||||
)
|
||||
} coAnswers {
|
||||
val onSuccess = arg<suspend (ScanResponse) -> Unit>(8)
|
||||
val onSuccess = arg<suspend (ScanResponse) -> Unit>(7)
|
||||
onSuccess.invoke(testScanResponse)
|
||||
}
|
||||
|
||||
|
|
@ -482,13 +472,12 @@ internal class CreateWalletStartModelTest {
|
|||
cardId = any(),
|
||||
onProgressStateChange = any(),
|
||||
onWalletNotCreated = any(),
|
||||
disclaimerWillShow = any(),
|
||||
onCancel = any(),
|
||||
onFailure = any(),
|
||||
onSuccess = any()
|
||||
)
|
||||
} coAnswers {
|
||||
val onSuccess = arg<suspend (ScanResponse) -> Unit>(8)
|
||||
val onSuccess = arg<suspend (ScanResponse) -> Unit>(7)
|
||||
onSuccess.invoke(testScanResponse)
|
||||
}
|
||||
|
||||
|
|
@ -515,13 +504,12 @@ internal class CreateWalletStartModelTest {
|
|||
cardId = any(),
|
||||
onProgressStateChange = any(),
|
||||
onWalletNotCreated = any(),
|
||||
disclaimerWillShow = any(),
|
||||
onCancel = any(),
|
||||
onFailure = any(),
|
||||
onSuccess = any()
|
||||
)
|
||||
} coAnswers {
|
||||
val onSuccess = arg<suspend (ScanResponse) -> Unit>(8)
|
||||
val onSuccess = arg<suspend (ScanResponse) -> Unit>(7)
|
||||
onSuccess.invoke(testScanResponse)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -133,10 +133,6 @@ internal class UserWalletSaver @Inject constructor(
|
|||
onWalletNotCreated = {
|
||||
continuation.resume(Either.Right(null))
|
||||
},
|
||||
disclaimerWillShow = {
|
||||
continuation.resume(Either.Right(null))
|
||||
router.pop()
|
||||
},
|
||||
onSuccess = {
|
||||
continuation.resume(Either.Right(it))
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ package com.tangem.features.tangempay.entity
|
|||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.themedColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.utils.TangemPayDetailIntents
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -18,25 +20,13 @@ internal class TangemPayDetailsStateFactory(
|
|||
private val intents: TangemPayDetailIntents,
|
||||
private val cardFrozenState: TangemPayCardFrozenState,
|
||||
) {
|
||||
|
||||
@Suppress("LongMethod")
|
||||
fun getInitialState(cardNumberEnd: String): TangemPayDetailsUM {
|
||||
fun getInitialState(isTangemPayDeactivated: Boolean, cardNumberEnd: String): TangemPayDetailsUM {
|
||||
return TangemPayDetailsUM(
|
||||
topBarConfig = TangemPayDetailsTopBarConfig(
|
||||
onBackClick = onBack,
|
||||
onOpenMenu = onOpenMenu,
|
||||
items = persistentListOf(
|
||||
TangemDropdownMenuItem(
|
||||
title = resourceReference(R.string.tangem_pay_terms_limits),
|
||||
textColor = themedColor { TangemTheme.colors.text.primary1 },
|
||||
onClick = intents::onClickTermsAndLimits,
|
||||
),
|
||||
TangemDropdownMenuItem(
|
||||
title = resourceReference(R.string.tangempay_pay_support),
|
||||
textColor = themedColor { TangemTheme.colors.text.primary1 },
|
||||
onClick = intents::onContactSupportClicked,
|
||||
),
|
||||
),
|
||||
items = getTopBarMenuItems(isTangemPayDeactivated),
|
||||
),
|
||||
pullToRefreshConfig = PullToRefreshConfig(
|
||||
isRefreshing = false,
|
||||
|
|
@ -69,6 +59,28 @@ internal class TangemPayDetailsStateFactory(
|
|||
),
|
||||
isBalanceHidden = false,
|
||||
addFundsEnabled = true,
|
||||
accountDeactivatedNotificationConfig = NotificationConfig(
|
||||
title = resourceReference(R.string.tangempay_account_deactivated_message_title),
|
||||
subtitle = resourceReference(R.string.tangempay_account_deactivated_message_subtitle),
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
).takeIf { isTangemPayDeactivated },
|
||||
)
|
||||
}
|
||||
|
||||
private fun getTopBarMenuItems(isTangemPayDeactivated: Boolean): ImmutableList<TangemDropdownMenuItem> {
|
||||
if (isTangemPayDeactivated) return persistentListOf()
|
||||
|
||||
return persistentListOf(
|
||||
TangemDropdownMenuItem(
|
||||
title = resourceReference(R.string.tangem_pay_terms_limits),
|
||||
textColor = themedColor { TangemTheme.colors.text.primary1 },
|
||||
onClick = intents::onClickTermsAndLimits,
|
||||
),
|
||||
TangemDropdownMenuItem(
|
||||
title = resourceReference(R.string.tangempay_pay_support),
|
||||
textColor = themedColor { TangemTheme.colors.text.primary1 },
|
||||
onClick = intents::onContactSupportClicked,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.tangempay.entity
|
|||
|
||||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.visa.model.TangemPayCardFrozenState
|
||||
import com.tangem.features.tangempay.model.CardDataType
|
||||
|
|
@ -13,6 +14,7 @@ internal data class TangemPayDetailsUM(
|
|||
val balanceBlockState: TangemPayDetailsBalanceBlockState,
|
||||
val isBalanceHidden: Boolean,
|
||||
val addFundsEnabled: Boolean,
|
||||
val accountDeactivatedNotificationConfig: NotificationConfig?,
|
||||
)
|
||||
|
||||
internal data class TangemPayCardDetailsUM(
|
||||
|
|
|
|||
|
|
@ -56,10 +56,7 @@ import com.tangem.utils.coroutines.saveIn
|
|||
import com.tangem.utils.logging.TangemLogger
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -94,7 +91,12 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
)
|
||||
|
||||
val uiState: StateFlow<TangemPayDetailsUM>
|
||||
field = MutableStateFlow(stateFactory.getInitialState(cardNumberEnd = params.config.cardNumberEnd))
|
||||
field = MutableStateFlow(
|
||||
stateFactory.getInitialState(
|
||||
isTangemPayDeactivated = params.config.isTangemPayDeactivated,
|
||||
cardNumberEnd = params.config.cardNumberEnd,
|
||||
),
|
||||
)
|
||||
|
||||
private val refreshStateJobHolder = JobHolder()
|
||||
private val fetchBalanceJobHolder = JobHolder()
|
||||
|
|
@ -112,7 +114,9 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
analytics.send(TangemPayAnalyticsEvents.MainScreenOpened())
|
||||
handleBalanceHiding()
|
||||
fetchBalance()
|
||||
subscribeToCardFrozenState()
|
||||
if (!params.config.isTangemPayDeactivated) {
|
||||
subscribeToCardFrozenState()
|
||||
}
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import androidx.compose.ui.unit.sp
|
|||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.components.buttons.HorizontalActionChips
|
||||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
import com.tangem.core.ui.components.buttons.small.TangemIconButton
|
||||
|
|
@ -41,6 +42,7 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi
|
|||
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer
|
||||
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem
|
||||
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.components.text.applyBladeBrush
|
||||
import com.tangem.core.ui.extensions.orMaskWithStars
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
|
|
@ -106,22 +108,38 @@ internal fun TangemPayDetailsScreen(
|
|||
TangemPayDetailsBalanceBlock(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.padding(top = 12.dp)
|
||||
.fillMaxWidth(),
|
||||
state = state.balanceBlockState,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
)
|
||||
SpacerH12()
|
||||
},
|
||||
)
|
||||
with(expressTransactionsComponent) {
|
||||
expressTransactionsContent(
|
||||
state = expressState.transactionsToDisplay,
|
||||
modifier = modifier
|
||||
.padding(start = 16.dp, end = 16.dp, top = 12.dp)
|
||||
.fillMaxWidth(),
|
||||
if (state.accountDeactivatedNotificationConfig != null) {
|
||||
item(
|
||||
key = "DEACTIVATION_MESSAGE",
|
||||
content = {
|
||||
Notification(
|
||||
modifier = modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
config = state.accountDeactivatedNotificationConfig,
|
||||
)
|
||||
SpacerH12()
|
||||
},
|
||||
)
|
||||
}
|
||||
with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryState) }
|
||||
if (state.accountDeactivatedNotificationConfig == null) {
|
||||
with(expressTransactionsComponent) {
|
||||
expressTransactionsContent(
|
||||
state = expressState.transactionsToDisplay,
|
||||
modifier = modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryState) }
|
||||
}
|
||||
}
|
||||
}
|
||||
expressTransactionsBottomSheetState?.content()
|
||||
|
|
@ -401,6 +419,7 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
|
|||
),
|
||||
isBalanceHidden = false,
|
||||
addFundsEnabled = true,
|
||||
accountDeactivatedNotificationConfig = null,
|
||||
),
|
||||
TangemPayDetailsUM(
|
||||
topBarConfig = TangemPayDetailsTopBarConfig(onBackClick = {}, onOpenMenu = {}, items = persistentListOf()),
|
||||
|
|
@ -414,6 +433,7 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<Ta
|
|||
),
|
||||
isBalanceHidden = false,
|
||||
addFundsEnabled = true,
|
||||
accountDeactivatedNotificationConfig = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ internal class WalletTangemPayAnalyticsEventSender @Inject constructor(
|
|||
PaymentAccountStatusValue.Loading,
|
||||
PaymentAccountStatusValue.NotCreated,
|
||||
is PaymentAccountStatusValue.UnderReview,
|
||||
is PaymentAccountStatusValue.Deactivated,
|
||||
-> return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
is PaymentAccountStatusValue.Loading,
|
||||
is PaymentAccountStatusValue.UnderReview,
|
||||
is PaymentAccountStatusValue.Empty,
|
||||
is PaymentAccountStatusValue.Deactivated,
|
||||
-> null
|
||||
}
|
||||
notification?.let(::add)
|
||||
|
|
|
|||
|
|
@ -239,6 +239,7 @@ internal class GetWalletNotificationsFactory @Inject constructor(
|
|||
is PaymentAccountStatusValue.Loading,
|
||||
is PaymentAccountStatusValue.UnderReview,
|
||||
is PaymentAccountStatusValue.Empty,
|
||||
is PaymentAccountStatusValue.Deactivated,
|
||||
-> null
|
||||
}
|
||||
notification?.let(::add)
|
||||
|
|
|
|||
|
|
@ -56,6 +56,32 @@ internal class TangemPayMainBlockConverter(
|
|||
is PaymentAccountStatusValue.Empty -> TangemPayMainUM.Empty
|
||||
is PaymentAccountStatusValue.NotCreated -> TangemPayMainUM.Empty
|
||||
is PaymentAccountStatusValue.Loading -> TangemPayMainUM.Loading
|
||||
is PaymentAccountStatusValue.Deactivated -> TangemPayMainUM.Content(
|
||||
subtitle = TextReference.Res(R.string.tangempay_status_deactivated),
|
||||
isBalanceFlickering = statusValue.source == StatusSource.CACHE,
|
||||
balance = getBalanceText(
|
||||
currencyCode = statusValue.fiatBalance.currency,
|
||||
balance = statusValue.fiatBalance.availableBalance,
|
||||
),
|
||||
balanceSubtitle = stringReference("USDC"), // TODO hardcode for now
|
||||
shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE,
|
||||
onClick = {
|
||||
// Dummy config for deactivated account just to open details screen
|
||||
tangemPayClickIntents.openDetails(
|
||||
userWalletId = value.account.userWalletId,
|
||||
config = TangemPayDetailsConfig(
|
||||
customerId = "",
|
||||
cardId = "",
|
||||
isPinSet = false,
|
||||
cardFrozenState = TangemPayCardFrozenState.Unfrozen,
|
||||
cardNumberEnd = "",
|
||||
chainId = POLYGON_CHAIN_ID,
|
||||
displayName = null,
|
||||
isTangemPayDeactivated = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
is PaymentAccountStatusValue.Loaded -> {
|
||||
val card = statusValue.cards.firstOrNull() ?: return TangemPayMainUM.TemporaryUnavailable
|
||||
TangemPayMainUM.Content(
|
||||
|
|
@ -82,6 +108,7 @@ internal class TangemPayMainBlockConverter(
|
|||
cardNumberEnd = card.lastDigits,
|
||||
chainId = POLYGON_CHAIN_ID,
|
||||
displayName = card.displayName,
|
||||
isTangemPayDeactivated = false,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import com.tangem.domain.walletconnect.WcPairService
|
|||
import com.tangem.domain.walletconnect.model.WcPairRequest
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLinkHandler
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import java.net.URLDecoder
|
||||
|
||||
internal class DefaultWalletConnectDeepLinkHandler @AssistedInject constructor(
|
||||
|
|
@ -18,8 +18,12 @@ internal class DefaultWalletConnectDeepLinkHandler @AssistedInject constructor(
|
|||
) : WalletConnectDeepLinkHandler {
|
||||
|
||||
init {
|
||||
val wcUri = uri.toString()
|
||||
if (uri.toString().isNotBlank()) {
|
||||
val wcUri =
|
||||
// deeplink type "tangem://wc/?uri=wc:..."
|
||||
runCatching { uri.getQueryParameter("uri") }.getOrNull()
|
||||
// direct deeplink "wc:..."
|
||||
?: uri.toString()
|
||||
if (wcUri.isNotBlank()) {
|
||||
try {
|
||||
// It is okay here, we are navigating from outside, and there is no other way to getting UserWallet
|
||||
getSelectedWalletSyncUseCase().fold(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import com.tangem.domain.common.wallets.error.SaveWalletError
|
|||
import com.tangem.domain.common.wallets.error.UnlockWalletError
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.isImported
|
||||
import com.tangem.domain.models.wallet.isLocked
|
||||
import com.tangem.domain.settings.CanUseBiometryUseCase
|
||||
import com.tangem.domain.settings.HotWalletRestrictionManager
|
||||
|
|
@ -198,7 +199,6 @@ internal class WelcomeModel @Inject constructor(
|
|||
analyticsSource = AnalyticsParam.ScreensSources.SignIn,
|
||||
shouldCheckIsAlreadyActivated = true,
|
||||
onWalletNotCreated = {},
|
||||
disclaimerWillShow = { router.pop() },
|
||||
onSuccess = { scanResponse ->
|
||||
val userWallet =
|
||||
coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() ?: return@scan
|
||||
|
|
@ -320,10 +320,16 @@ internal class WelcomeModel @Inject constructor(
|
|||
private suspend fun trackSignInEvent(userWallet: UserWallet, type: Basic.SignedIn.SignInType) {
|
||||
val walletsCount = userWalletsListRepository.userWalletsSync().size
|
||||
trackingContextProxy.addContext(userWallet)
|
||||
val isBackedUp = when (userWallet) {
|
||||
is UserWallet.Cold -> userWallet.scanResponse.card.backupStatus?.isActive == true
|
||||
is UserWallet.Hot -> userWallet.backedUp
|
||||
}
|
||||
analyticsEventHandler.send(
|
||||
event = Basic.SignedIn(
|
||||
signInType = type,
|
||||
walletsCount = walletsCount,
|
||||
isImported = userWallet.isImported(),
|
||||
hasBackup = isBackedUp,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue