Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-04 15:03:14 +01:00
commit c8b755729f
61 changed files with 931 additions and 415 deletions

View file

@ -55,6 +55,7 @@
<activity
android:name="com.tangem.tap.MainActivity"
android:configChanges="uiMode"
android:exported="true"
android:launchMode="singleTop"
android:screenOrientation="portrait"

View file

@ -17,6 +17,8 @@ import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode
import androidx.compose.ui.graphics.toArgb
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
@ -37,7 +39,8 @@ import com.tangem.core.navigation.email.EmailSender
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.data.card.sdk.CardSdkLifecycleObserver
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.data.card.sdk.CardSdkOwner
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
@ -108,7 +111,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
lateinit var testerRouter: TesterRouter
@Inject
lateinit var cardSdkLifecycleObserver: CardSdkLifecycleObserver
lateinit var cardSdkOwner: CardSdkOwner
@Inject
lateinit var cardSdkConfigRepository: CardSdkConfigRepository
@ -160,7 +163,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
// TODO: fixme: inject through DI
private val intentProcessor: IntentProcessor = IntentProcessor()
@ -172,9 +175,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
// We need to call it before onCreate to prevent unnecessary activity recreation
installAppTheme()
installAppTheme() // We need to call it before onCreate to prevent unnecessary activity recreation
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
@ -218,7 +222,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private fun installActivityDependencies() {
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
cardSdkLifecycleObserver.onCreate(context = this)
cardSdkOwner.register(activity = this)
tangemSdkManager = injectedTangemSdkManager
appStateHolder.tangemSdkManager = tangemSdkManager
backupService = BackupService.init(cardSdkConfigRepository.sdk, this)
@ -248,14 +252,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private fun installAppTheme() {
appThemeModeFlow = createAppThemeModeFlow()
val mode = runBlocking { appThemeModeFlow.filterNotNull().first() }
val mode = runBlocking { appThemeModeFlow.first() }
updateAppTheme(mode)
}
private fun observeAppThemeModeUpdates() {
appThemeModeFlow
.filterNotNull()
.flowWithLifecycle(lifecycle)
.onEach(::updateAppTheme)
.launchIn(lifecycleScope)
@ -273,15 +276,17 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode?> {
private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode> {
val tangemApplication = application as TangemApplication
return tangemApplication.getAppThemeModeUseCase()
.filterNotNull()
.distinctUntilChanged()
.map { maybeMode ->
maybeMode.getOrElse { AppThemeMode.DEFAULT }
}
.shareIn(
scope = lifecycleScope + Dispatchers.IO,
scope = lifecycleScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000),
)
}
@ -327,7 +332,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
override fun onDestroy() {
store.dispatch(NavigationAction.ActivityDestroyed(WeakReference(this)))
intentProcessor.removeAll()
cardSdkLifecycleObserver.onDestroy(this)
super.onDestroy()
}
@ -338,9 +342,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
private fun updateAppTheme(appThemeMode: AppThemeMode) {
MutableAppThemeModeHolder.value = appThemeMode
MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme()
val mode = when (appThemeMode) {
AppThemeMode.FORCE_DARK -> AppCompatDelegate.MODE_NIGHT_YES
AppThemeMode.FORCE_LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
@ -348,7 +349,32 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
setDefaultNightMode(mode)
delegate.localNightMode = mode
MutableAppThemeModeHolder.value = appThemeMode
MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
/*
* We need to manually change the background color of the activity when the UI mode changes to prevent
* flickering when navigating between fragments.
*
* `android:configChanges="uiMode"` is set in the manifest.
* */
updateAppBackground()
}
private fun updateAppBackground() {
val backgroundColor = if (isDarkTheme()) {
TangemColorPalette.Dark6
} else {
TangemColorPalette.White
}
findViewById<CoordinatorLayout>(R.id.fragment_container).setBackgroundColor(backgroundColor.toArgb())
}
private fun isDarkTheme(): Boolean {
@ -363,12 +389,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
/*
* FIXME: Test functionality. TangemSdk is null on some devices when HomeAction.Read is called
* inside IntentHandler.
*/
cardSdkLifecycleObserver.onCreate(context = this)
lifecycleScope.launch {
intentProcessor.handleIntent(intent, true)
}
@ -450,15 +470,16 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) {
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val backStack = store.state.navigationState.backStack
val isOnInitialScreen = backStack.all { it == AppScreen.Welcome || it == AppScreen.Home }
val isNotScannedBefore = store.state.globalState.scanResponse == null
val isOnboardingServiceNotActive = !store.state.globalState.onboardingState.onboardingStarted
when {
!backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive -> {
!isOnInitialScreen && isNotScannedBefore && isOnboardingServiceNotActive -> {
navigateToInitialScreen(intentWhichStartedActivity)
}
backStackIsEmpty -> {
backStack.isEmpty() -> {
navigateToInitialScreen(intentWhichStartedActivity)
}
else -> Unit

View file

@ -353,6 +353,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
Log.Level.Network,
Log.Level.Error,
Log.Level.Biometric,
Log.Level.Info,
)
return TangemLogCollector(logLevels, LogFormat.StairsFormatter())
}

View file

@ -33,6 +33,7 @@ internal object CardSDKLoggerModule {
Log.Level.Network,
Log.Level.Error,
Log.Level.Biometric,
Log.Level.Info,
)
return TangemCardSDKLogger(

View file

@ -14,15 +14,15 @@ import com.tangem.tap.domain.sdk.TangemSdkManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(ViewModelComponent::class)
@InstallIn(SingletonComponent::class)
internal object CardDomainModule {
@Provides
@ViewModelScoped
@Singleton
fun provideGetBiometricsStatusUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
): GetBiometricsStatusUseCase {
@ -30,7 +30,7 @@ internal object CardDomainModule {
}
@Provides
@ViewModelScoped
@Singleton
fun provideSetAccessCodeRequestPolicyUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
): SetAccessCodeRequestPolicyUseCase {
@ -38,35 +38,35 @@ internal object CardDomainModule {
}
@Provides
@ViewModelScoped
@Singleton
fun provideWasWalletAlreadySignedHashesConfirmedUseCase(cardRepository: CardRepository): WasCardScannedUseCase {
return WasCardScannedUseCase(cardRepository = cardRepository)
}
@Provides
@ViewModelScoped
@Singleton
fun provideSetCardWasScannedUseCase(cardRepository: CardRepository): SetCardWasScannedUseCase {
return SetCardWasScannedUseCase(cardRepository = cardRepository)
}
@Provides
@ViewModelScoped
@Singleton
fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig())
@Provides
@ViewModelScoped
@Singleton
fun provideDerivePublicKeysUseCase(derivationsRepository: DerivationsRepository): DerivePublicKeysUseCase {
return DerivePublicKeysUseCase(derivationsRepository = derivationsRepository)
}
@Provides
@ViewModelScoped
@Singleton
fun provideIsNeedToBackupUseCase(userWalletsListManager: UserWalletsListManager): IsNeedToBackupUseCase {
return IsNeedToBackupUseCase(userWalletsListManager = userWalletsListManager)
}
@Provides
@ViewModelScoped
@Singleton
fun provideGetExtendedPublicKeyForCurrencyUseCase(
derivationsRepository: DerivationsRepository,
): GetExtendedPublicKeyForCurrencyUseCase {
@ -74,13 +74,13 @@ internal object CardDomainModule {
}
@Provides
@ViewModelScoped
@Singleton
fun provideDeleteSavedAccessCodesUseCase(tangemSdkManager: TangemSdkManager): DeleteSavedAccessCodesUseCase {
return DefaultDeleteSavedAccessCodesUseCase(tangemSdkManager)
}
@Provides
@ViewModelScoped
@Singleton
fun provideResetCardUseCase(tangemSdkManager: TangemSdkManager): ResetCardUseCase {
return DefaultResetCardUseCase(tangemSdkManager)
}

View file

@ -18,7 +18,7 @@ class UserWalletIdPreflightReadFilter(private val expectedUserWalletId: UserWall
override fun onCardRead(card: Card, environment: SessionEnvironment) = Unit
override fun onFullCardRead(card: Card, environment: SessionEnvironment) {
val actualUserWalletId = UserWalletIdBuilder.card(card = CardDTO(card)).build()
val actualUserWalletId = UserWalletIdBuilder.card(card = CardDTO(card)).build() ?: return
if (expectedUserWalletId != actualUserWalletId) throw TangemSdkError.WalletNotFound()
}

View file

@ -152,19 +152,29 @@ internal class BiometricUserWalletsListManager(
return CompletionResult.Success(Unit)
}
changeSelectedUserWalletIdIfNeeded(idsToRemove)
if (idsToRemove.size == state.value.userWallets.size) {
return clear()
}
return sensitiveInformationRepository.delete(idsToRemove)
.flatMap { publicInformationRepository.delete(idsToRemove) }
.map { keysRepository.delete(idsToRemove) }
.map {
state.update { prevState ->
val newUserWallets = prevState.userWallets.filter { it.walletId !in idsToRemove }
val remainingWallets = prevState.userWallets.filter { it.walletId !in idsToRemove }
val isSelectedWalletDeleted = prevState.selectedUserWalletId in idsToRemove
val newSelectedUserWallet = findOrSetSelectedWallet(
prevSelectedWalletId = prevState.selectedUserWalletId,
userWallets = remainingWallets,
ignorePrevSelectedWallet = isSelectedWalletDeleted,
)
prevState.copy(
encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in idsToRemove },
userWallets = newUserWallets,
isLocked = newUserWallets.any { it.isLocked },
userWallets = remainingWallets,
isLocked = remainingWallets.any { it.isLocked },
selectedUserWalletId = newSelectedUserWallet?.walletId,
)
}
}
@ -173,10 +183,10 @@ internal class BiometricUserWalletsListManager(
override suspend fun clear(): CompletionResult<Unit> {
return sensitiveInformationRepository.clear()
.flatMap { publicInformationRepository.clear() }
.map { keysRepository.clear() }
.map {
keysRepository.clear()
selectedUserWalletRepository.set(null)
lock()
state.value = State()
}
}
@ -301,47 +311,24 @@ internal class BiometricUserWalletsListManager(
private fun findOrSetSelectedWallet(
prevSelectedWalletId: UserWalletId?,
userWallets: List<UserWallet>,
ignorePrevSelectedWallet: Boolean = false,
): UserWallet? {
val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get()
var possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId)
var possibleSelectedUserWallet: UserWallet? = null
if (!ignorePrevSelectedWallet) {
val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get()
possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId)
}
if (possibleSelectedUserWallet == null || possibleSelectedUserWallet.isLocked) {
possibleSelectedUserWallet = userWallets.firstOrNull { !it.isLocked } ?: userWallets.firstOrNull()
if (possibleSelectedUserWallet != null) {
selectedUserWalletRepository.set(possibleSelectedUserWallet.walletId)
}
}
selectedUserWalletRepository.set(possibleSelectedUserWallet?.walletId)
return possibleSelectedUserWallet
}
private fun changeSelectedUserWalletIdIfNeeded(walletsIdsToRemove: List<UserWalletId>) {
val remainingWallets = state.value.userWallets.filter {
it.walletId !in walletsIdsToRemove
}
val selectedWallet = findSelectedUserWallet()
when {
remainingWallets.isEmpty() -> {
state.update { prevState ->
prevState.copy(
selectedUserWalletId = null,
)
}
selectedUserWalletRepository.set(null)
}
!remainingWallets.contains(selectedWallet) -> {
val newSelectedWallet = remainingWallets.firstOrNull { !it.isLocked }
state.update { prevState ->
prevState.copy(
selectedUserWalletId = newSelectedWallet?.walletId,
)
}
selectedUserWalletRepository.set(newSelectedWallet?.walletId)
}
}
}
private fun findSelectedUserWallet(
userWallets: List<UserWallet> = state.value.userWallets,
selectedUserWalletId: UserWalletId? = state.value.selectedUserWalletId,

View file

@ -214,9 +214,10 @@ internal class ResetCardViewModel @Inject constructor(
return when (val status = card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount
is CardDTO.BackupStatus.CardLinked -> status.cardCount
is CardDTO.BackupStatus.NoBackup -> 0
null -> 0 // Multi-currency wallet without backup function. Example, 4.12
is CardDTO.BackupStatus.CardLinked,
is CardDTO.BackupStatus.NoBackup,
null,
-> 0
}
}

View file

@ -13,8 +13,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.wallet.R
@Suppress("LongMethod")

View file

@ -64,6 +64,7 @@ sealed class AnalyticsParam {
data object Send : ScreensSources("Send")
data object Intro : ScreensSources("Introduction")
data object MyWallets : ScreensSources("My Wallets")
data object Token : ScreensSources("Token")
}
sealed class TxSentFrom(val value: String) {

View file

@ -31,4 +31,7 @@ enum class ExchangeProviderType {
@Json(name = "cex")
CEX,
@Json(name = "dex-bridge")
DEX_BRIDGE,
}

View file

@ -23,34 +23,43 @@ data class ExchangeStatusResponse(
enum class ExchangeStatus {
@Json(name = "new")
NEW,
New,
@Json(name = "waiting")
WAITING,
Waiting,
@Json(name = "confirming")
CONFIRMING,
Confirming,
@Json(name = "exchanging")
EXCHANGING,
Exchanging,
@Json(name = "sending")
SENDING,
Sending,
@Json(name = "finished")
FINISHED,
Finished,
@Json(name = "failed")
FAILED,
Failed,
@Json(name = "refunded")
REFUNDED,
Refunded,
@Json(name = "verifying")
VERIFYING,
Verifying,
@Json(name = "expired")
CANCELLED,
Cancelled,
@Json(name = "waiting-tx-hash")
WaitingTxHash,
@Json(name = "tx-failed")
TxFailed,
@Json(name = "unknown")
Unknown,
}
data class ExchangeStatusError(

View file

@ -53,14 +53,6 @@ object PreferencesKeys {
val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") }
val IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY by lazy {
booleanPreferencesKey(name = "isWalletSwapPromoChangellyShown")
}
val IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY by lazy {
booleanPreferencesKey(name = "isTokenSwapPromoChangellyShown")
}
val IS_WALLET_TRAVALA_PROMO_SHOWN_KEY by lazy {
booleanPreferencesKey(name = "isWalletTravalaPromoShown")
}
@ -93,6 +85,14 @@ object PreferencesKeys {
val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") }
val IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY by lazy {
booleanPreferencesKey(name = "isWalletSwapPromoOkxShown")
}
val IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY by lazy {
booleanPreferencesKey(name = "isTokenSwapPromoOkxShown")
}
fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region")
}

View file

@ -225,6 +225,7 @@
<string name="express_exchange_status_subtitle">Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий.</string>
<string name="express_exchange_status_title">Статус обмена</string>
<string name="express_exchange_status_verifying">Требуется верификация</string>
<string name="express_exchange_status_waiting_tx_hash">Ожидание хеша транзакции</string>
<string name="express_exchange_token_list_subtitle">Список токенов в вашем кошельке</string>
<string name="express_fetch_best_rates">Получение наилучших курсов...</string>
<string name="express_floating_rate">Плавающая ставка</string>
@ -549,6 +550,8 @@
<string name="story_meet_title">Встречайте Tangem</string>
<string name="story_web3_description">Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах</string>
<string name="story_web3_title">Поддержка Web 3.0</string>
<string name="swap_promo_text">Обменивайте больше токенов по лучшим курсам прямо в вашем кошельке.</string>
<string name="swap_promo_title">Новый провайдер обмена!</string>
<string name="swapping_alert_cex_description">В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя</string>
<string name="swapping_alert_dex_description">В сумму включена комиссия провайдера сервиса.</string>
<string name="swapping_alert_title">Комиссии</string>

View file

@ -224,6 +224,7 @@
<string name="express_exchange_status_subtitle">Provider-sourced data. Estimated amount subject to change due to market conditions.</string>
<string name="express_exchange_status_title">Exchange status</string>
<string name="express_exchange_status_verifying">Verification required</string>
<string name="express_exchange_status_waiting_tx_hash">Awaiting transaction hash</string>
<string name="express_exchange_token_list_subtitle">List of all tokens added to your wallet</string>
<string name="express_fetch_best_rates">Fetching best rates...</string>
<string name="express_floating_rate">Floating rate</string>
@ -542,6 +543,8 @@
<string name="story_meet_title">Meet Tangem</string>
<string name="story_web3_description">Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services</string>
<string name="story_web3_title">Web 3.0 Compatible</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="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>
<string name="swapping_alert_dex_description">The amount includes the service provider\'s fee.</string>
<string name="swapping_alert_title">Fees</string>

View file

@ -0,0 +1,163 @@
package com.tangem.core.ui.components.notifications
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.LocalIsInDarkTheme
import com.tangem.core.ui.res.TangemColorPalette.Dark6
import com.tangem.core.ui.res.TangemColorPalette.Light4
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
private val OxkPromoColor = Color(0xFFBCFF2F)
@Composable
fun OkxPromoNotification(config: NotificationConfig, modifier: Modifier = Modifier) {
Column(
modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(Dark6),
) {
Content(config = config)
Button(config = config)
}
}
@Composable
private fun Content(config: NotificationConfig) {
Row {
Icon(
painter = rememberVectorPainter(image = ImageVector.vectorResource(id = R.drawable.img_okx_dex_logo)),
contentDescription = null,
tint = TangemTheme.colors.icon.constant,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12)
.align(Alignment.CenterVertically),
)
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
modifier = Modifier
.weight(1f)
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = config.title.resolveReference(),
style = TangemTheme.typography.button,
color = OxkPromoColor,
)
Text(
text = config.subtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.constantWhite,
)
}
config.onCloseClick?.let {
Icon(
painter = rememberVectorPainter(
image = ImageVector.vectorResource(R.drawable.ic_close_24),
),
contentDescription = null,
tint = TangemTheme.colors.icon.constant,
modifier =
Modifier
.padding(
top = TangemTheme.dimens.spacing8,
end = TangemTheme.dimens.spacing8,
)
.size(TangemTheme.dimens.size20)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(radius = TangemTheme.dimens.radius10),
onClick = it,
),
)
}
}
}
@Composable
private fun Button(config: NotificationConfig) {
val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig
button?.let {
val isDarkMode = LocalIsInDarkTheme.current
TangemButton(
text = button.text.resolveReference(),
icon = TangemButtonIconPosition.Start(button.iconResId ?: R.drawable.ic_exchange_vertical_24),
onClick = button.onClick,
colors = TangemButtonColors(
backgroundColor = if (isDarkMode) Light4 else TangemTheme.colors.button.secondary,
contentColor = Dark6,
disabledBackgroundColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
),
enabled = true,
showProgress = false,
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing12,
),
)
}
}
// region Preview
@Preview(widthDp = 360)
@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun OkxPromoNotification_Preview(
@PreviewParameter(OkxPromoNotificationPreviewProvider::class) data: NotificationConfig,
) {
TangemThemePreview {
OkxPromoNotification(data)
}
}
private class OkxPromoNotificationPreviewProvider : PreviewParameterProvider<NotificationConfig> {
override val values: Sequence<NotificationConfig>
get() = sequenceOf(
NotificationConfig(
title = resourceReference(R.string.swap_promo_title),
subtitle = resourceReference(R.string.swap_promo_text),
iconResId = R.drawable.img_okx_dex_logo,
onCloseClick = {},
),
NotificationConfig(
title = resourceReference(R.string.swap_promo_title),
subtitle = resourceReference(R.string.swap_promo_text),
iconResId = R.drawable.img_okx_dex_logo,
onCloseClick = {},
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.token_swap_promotion_button),
onClick = {},
),
),
)
}
// endregion

View file

@ -101,6 +101,7 @@ class TangemColors internal constructor(
private set
var attention by mutableStateOf(attention)
private set
var constant by mutableStateOf(constant)
fun update(other: Icon) {
primary1 = other.primary1
@ -111,6 +112,7 @@ class TangemColors internal constructor(
accent = other.accent
warning = other.warning
attention = other.attention
constant = other.constant
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.core.ui.screen
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -22,6 +23,18 @@ abstract class ComposeFragment : Fragment(), ComposeScreen {
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
/*
* We need to manually dispatch configuration changes to the Compose view.
*
* `android:configChanges="uiMode"` is set in the manifest.
* */
view?.dispatchConfigurationChanged(newConfig)
}
/**
* Inflates transitions for the fragment. Override this method to customize
* enter and exit transitions for the fragment.

View file

@ -0,0 +1,29 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="1232.1" android:viewportWidth="2516" android:width="49.00901dp">
<path android:fillColor="#FF000000" android:pathData="m740.2,0L23.5,0c-4.4,0 -8.7,1.6 -11.8,4.6 -3.1,2.9 -4.9,6.9 -4.9,11v670.6c0,4.1 1.8,8.1 4.9,11 3.1,2.9 7.4,4.6 11.8,4.6h716.7c4.4,0 8.7,-1.6 11.8,-4.6 3.1,-2.9 4.9,-6.9 4.9,-11L756.9,15.6c0,-4.1 -1.8,-8.1 -4.9,-11 -3.1,-2.9 -7.4,-4.6 -11.8,-4.6ZM506.9,452.3c0,4.1 -1.8,8.1 -4.9,11 -3.1,2.9 -7.4,4.6 -11.8,4.6h-216.7c-4.4,0 -8.7,-1.6 -11.8,-4.6 -3.1,-2.9 -4.9,-6.9 -4.9,-11v-202.8c0,-4.1 1.8,-8.1 4.9,-11 3.1,-2.9 7.4,-4.6 11.8,-4.6h216.7c4.4,0 8.7,1.6 11.8,4.6 3.1,2.9 4.9,6.9 4.9,11v202.8Z"/>
<path android:fillColor="#FF000000" android:pathData="m2240.6,234h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6v-202.8c0,-8.6 -7.5,-15.6 -16.7,-15.6h0Z"/>
<path android:fillColor="#FF000000" android:pathData="m1990.7,0h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6V15.6c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
<path android:fillColor="#FF000000" android:pathData="m2490.8,0h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6V15.6c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
<path android:fillColor="#FF000000" android:pathData="m1990.7,467.9h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6v-202.8c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
<path android:fillColor="#FF000000" android:pathData="m2490.8,467.9h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6v-202.8c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
<path android:fillColor="#FF000000" android:pathData="m1615.3,0h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6V15.6c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
<path android:fillColor="#FF000000" android:pathData="m1615.3,467.9h-216.7c-9.2,0 -16.7,7 -16.7,15.6v202.8c0,8.6 7.5,15.6 16.7,15.6h216.7c9.2,0 16.7,-7 16.7,-15.6v-202.8c0,-8.6 -7.5,-15.6 -16.7,-15.6Z"/>
<path android:fillColor="#FF000000" android:pathData="m1381.9,249.4c0,-4.1 -1.8,-8.1 -4.9,-11 -3.1,-2.9 -7.4,-4.6 -11.8,-4.6h-233.4V15.6c0,-4.1 -1.8,-8.1 -4.9,-11 -3.1,-2.9 -7.4,-4.6 -11.8,-4.6h-216.7c-4.4,0 -8.7,1.6 -11.8,4.6 -3.1,2.9 -4.9,6.9 -4.9,11v670.3c0,4.1 1.8,8.1 4.9,11 3.1,2.9 7.4,4.6 11.8,4.6h216.7c4.4,0 8.7,-1.6 11.8,-4.6 3.1,-2.9 4.9,-6.9 4.9,-11v-218.2h233.4c4.4,0 8.7,-1.6 11.8,-4.6 3.1,-2.9 4.9,-6.9 4.9,-11v-202.8Z"/>
<path android:fillColor="#FF000000" android:pathData="m1675.2,885c33.8,0 64.5,6.8 92,20.4 27.5,13.6 49.2,33.4 65.2,59.5 16,26.1 24,57.2 24,93.2s-8,67.2 -24,93.4c-16,26.3 -37.6,46.2 -65,60 -27.4,13.8 -58.1,20.6 -92.2,20.6h-125.2v-347.1h125.2ZM1666.2,1163.8c34.5,0 60.5,-9.2 78.3,-27.5 17.7,-18.3 26.6,-44.4 26.6,-78.3s-8.9,-59.4 -26.8,-77.5c-17.9,-18.2 -43.9,-27.3 -78,-27.3h-35.1v210.6h35.1Z"/>
<path android:fillColor="#FF000000" android:pathData="m2125.7,1092.2h-135.6v69.7h157v70.2h-238.1v-347.1h232.4v69.7h-151.3v68.3h135.6v69.2Z"/>
<path android:fillColor="#FF000000" android:pathData="m2509.8,885l-121.4,168.4 127.6,178.8h-100.1l-77.3,-112.9 -78.7,112.9h-92.9l127.6,-176.9 -121.4,-170.3h99.1l71.6,104.3 72.6,-104.3h93.4Z"/>
<path android:fillColor="#FF000000" android:pathData="M0,1033.9h1381.6v49h-1381.6z"/>
</vector>

View file

@ -1,6 +1,6 @@
package com.tangem.data.card.di
import com.tangem.data.card.sdk.CardSdkLifecycleObserver
import com.tangem.data.card.sdk.CardSdkOwner
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.data.card.sdk.DefaultCardSdkProvider
import dagger.Binds
@ -19,5 +19,5 @@ internal interface CardSdkModule {
@Binds
@Singleton
fun providerCardSdkLifecycleObserver(defaultCardSdkProvider: DefaultCardSdkProvider): CardSdkLifecycleObserver
fun providerCardSdkLifecycleObserver(defaultCardSdkProvider: DefaultCardSdkProvider): CardSdkOwner
}

View file

@ -1,17 +0,0 @@
package com.tangem.data.card.sdk
import android.content.Context
/**
* Lifecycle observer for creating Card SDK instance
*
[REDACTED_AUTHOR]
*/
interface CardSdkLifecycleObserver {
/** Callback of creating activity [context] */
fun onCreate(context: Context)
/** Callback of destroying activity [context] */
fun onDestroy(context: Context)
}

View file

@ -0,0 +1,13 @@
package com.tangem.data.card.sdk
import androidx.fragment.app.FragmentActivity
/**
* Lifecycle observer for creating Card SDK instance
*
[REDACTED_AUTHOR]
*/
interface CardSdkOwner {
fun register(activity: FragmentActivity)
}

View file

@ -1,12 +1,21 @@
package com.tangem.data.card.sdk
import android.content.Context
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.tangem.Log
import com.tangem.TangemSdk
import com.tangem.common.CardFilter
import com.tangem.common.authentication.AuthenticationManager
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.Config
import com.tangem.sdk.extensions.initWithBiometrics
import com.tangem.common.services.secure.SecureStorage
import com.tangem.crypto.bip39.Wordlist
import com.tangem.sdk.DefaultSessionViewDelegate
import com.tangem.sdk.extensions.*
import com.tangem.sdk.nfc.NfcManager
import com.tangem.sdk.storage.create
import javax.inject.Inject
import javax.inject.Singleton
@ -16,31 +25,94 @@ import javax.inject.Singleton
[REDACTED_AUTHOR]
*/
@Singleton
internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, CardSdkLifecycleObserver {
internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, CardSdkOwner {
override val sdk: TangemSdk
get() = requireNotNull(value = _sdk) { "Impossible to get the TangemSdk when activity is destroyed" }
get() = requireNotNull(value = holder?.sdk) {
"Impossible to get the TangemSdk when activity is destroyed"
}
private var _sdk: TangemSdk? = null
private val observer: LifecycleObserver = Observer()
override fun onCreate(context: Context) {
_sdk = TangemSdk.initWithBiometrics(activity = context as FragmentActivity, config = config)
private var holder: Holder? = null
override fun register(activity: FragmentActivity) {
Log.info { "Tangem SDK owner registered" }
if (holder != null) {
unsubscribeAndCleanup()
}
initialize(activity)
activity.lifecycle.addObserver(observer)
}
override fun onDestroy(context: Context) {
// Commented out to prevent crash on getting sdk when it's null.
// FIXME: We still should find the real cause and fix it properly.
// idea: pass everywhere DefaultCardSdkProvider instead sdk to reach lazy access to sdk property
// _sdk = null
private fun initialize(activity: FragmentActivity) {
val secureStorage = SecureStorage.create(activity)
val nfcManager = TangemSdk.initNfcManager(activity)
val authenticationManager = TangemSdk.initAuthenticationManager(activity)
val keystoreManager = TangemSdk.initKeystoreManager(authenticationManager, secureStorage)
val viewDelegate = DefaultSessionViewDelegate(nfcManager, activity)
viewDelegate.sdkConfig = config
val sdk = TangemSdk(
reader = nfcManager.reader,
viewDelegate = viewDelegate,
secureStorage = secureStorage,
authenticationManager = authenticationManager,
keystoreManager = keystoreManager,
wordlist = Wordlist.getWordlist(activity),
config = config,
)
holder = Holder(
activity = activity,
nfcManager = nfcManager,
authenticationManager = authenticationManager,
sdk = sdk,
)
Log.info { "Tangem SDK initialized" }
}
private fun unsubscribeAndCleanup() {
with(receiver = holder ?: return) {
nfcManager.unsubscribe(activity)
authenticationManager.unsubscribe(activity)
activity.lifecycle.removeObserver(observer)
}
holder = null
Log.info { "Tangem SDK unsubscribed and cleaned up" }
}
inner class Observer : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
Log.info { "Tangem SDK owner destroyed" }
unsubscribeAndCleanup()
}
}
data class Holder(
val activity: FragmentActivity,
val sdk: TangemSdk,
val nfcManager: NfcManager,
val authenticationManager: AuthenticationManager,
)
private companion object {
val config = Config(
linkedTerminal = true,
allowUntrustedCards = true,
filter = CardFilter(
allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(),
allowedCardTypes = FirmwareVersion.FirmwareType.entries.toList(),
maxFirmwareVersion = FirmwareVersion(major = 6, minor = 33),
batchIdFilter = CardFilter.Companion.ItemFilter.Deny(
items = setOf("0027", "0030", "0031", "0035"),

View file

@ -32,8 +32,18 @@ internal class DefaultPromoRepository(
}.getOrNull()
}
override suspend fun getOkxPromoBanner(): PromoBanner? {
return runCatching(dispatchers.io) {
promoResponseConverter.convert(
tangemApi.getPromotionInfo(OKX)
.getOrThrow(),
)
}.getOrNull()
}
private companion object {
private const val CHANGELLY_NAME = "changelly"
private const val TRAVALA = "travala"
private const val OKX = "okx"
}
}

View file

@ -1,8 +1,8 @@
package com.tangem.data.settings
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_TRAVALA_PROMO_SHOWN_KEY
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.datasource.local.preferences.utils.store
@ -16,23 +16,23 @@ class DefaultPromoSettingsRepository(
private val appPreferencesStore: AppPreferencesStore,
) : PromoSettingsRepository {
override fun isReadyToShowWalletSwapPromo(): Flow<Boolean> {
return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY, true)
return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY, true)
}
override fun isReadyToShowTokenSwapPromo(): Flow<Boolean> {
return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY, true)
return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY, true)
}
override suspend fun setNeverToShowWalletSwapPromo() {
appPreferencesStore.store(
key = IS_WALLET_SWAP_PROMO_CHANGELLY_SHOW_KEY,
key = IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY,
value = false,
)
}
override suspend fun setNeverToShowTokenSwapPromo() {
appPreferencesStore.store(
key = IS_TOKEN_SWAP_PROMO_CHANGELLY_SHOW_KEY,
key = IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY,
value = false,
)
}

View file

@ -87,8 +87,11 @@ internal object TokensDataModule {
@Provides
@Singleton
fun provideDefaultMarketCoinsRepository(assetsStore: AssetsStore): MarketCryptoCurrencyRepository {
return DefaultMarketCryptoCurrencyRepository(assetsStore)
fun provideDefaultMarketCoinsRepository(
assetsStore: AssetsStore,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
): MarketCryptoCurrencyRepository {
return DefaultMarketCryptoCurrencyRepository(assetsStore, coroutineDispatcherProvider)
}
@Provides

View file

@ -302,26 +302,28 @@ internal class DefaultCurrenciesRepository(
networkId: Network.ID,
derivationPath: Network.DerivationPath,
): CryptoCurrency.Coin {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true)
return withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true)
fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false)
fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false)
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) {
"Unable to find tokens response for user wallet with provided ID: $userWalletId"
}
val blockchain = Blockchain.fromId(networkId.value)
val blockchainNetworkId = blockchain.toNetworkId()
val coinId = blockchain.toCoinId()
val storedCoin = storedTokens.tokens
.find {
it.networkId == blockchainNetworkId && it.id == coinId && it.derivationPath == derivationPath.value
} ?: error("Coin in this network $networkId not found")
val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.scanResponse)
coin as? CryptoCurrency.Coin ?: error("Unable to create currency")
}
val blockchain = Blockchain.fromId(networkId.value)
val blockchainNetworkId = blockchain.toNetworkId()
val coinId = blockchain.toCoinId()
val storedCoin = storedTokens.tokens
.find {
it.networkId == blockchainNetworkId && it.id == coinId && it.derivationPath == derivationPath.value
} ?: error("Coin in this network $networkId not found")
val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.scanResponse)
return coin as? CryptoCurrency.Coin ?: error("Unable to create currency")
}
override fun isTokensGrouped(userWalletId: UserWalletId): Flow<Boolean> {
@ -382,29 +384,31 @@ internal class DefaultCurrenciesRepository(
}
override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, currency: CryptoCurrency): FeePaidCurrency {
val blockchain = Blockchain.fromId(currency.network.id.value)
return when (val feePaidCurrency = blockchain.feePaidCurrency()) {
FeePaidSdkCurrency.Coin -> FeePaidCurrency.Coin
FeePaidSdkCurrency.SameCurrency -> FeePaidCurrency.SameCurrency
is FeePaidSdkCurrency.Token -> {
val balance = walletManagersFacade.tokenBalance(
userWalletId = userWalletId,
network = currency.network,
name = feePaidCurrency.token.name,
symbol = feePaidCurrency.token.symbol,
contractAddress = feePaidCurrency.token.contractAddress,
decimals = feePaidCurrency.token.decimals,
id = feePaidCurrency.token.id,
)
FeePaidCurrency.Token(
tokenId = getTokenId(network = currency.network, sdkToken = feePaidCurrency.token),
name = feePaidCurrency.token.name,
symbol = feePaidCurrency.token.symbol,
contractAddress = feePaidCurrency.token.contractAddress,
balance = balance,
)
return withContext(dispatchers.io) {
val blockchain = Blockchain.fromId(currency.network.id.value)
when (val feePaidCurrency = blockchain.feePaidCurrency()) {
FeePaidSdkCurrency.Coin -> FeePaidCurrency.Coin
FeePaidSdkCurrency.SameCurrency -> FeePaidCurrency.SameCurrency
is FeePaidSdkCurrency.Token -> {
val balance = walletManagersFacade.tokenBalance(
userWalletId = userWalletId,
network = currency.network,
name = feePaidCurrency.token.name,
symbol = feePaidCurrency.token.symbol,
contractAddress = feePaidCurrency.token.contractAddress,
decimals = feePaidCurrency.token.decimals,
id = feePaidCurrency.token.id,
)
FeePaidCurrency.Token(
tokenId = getTokenId(network = currency.network, sdkToken = feePaidCurrency.token),
name = feePaidCurrency.token.name,
symbol = feePaidCurrency.token.symbol,
contractAddress = feePaidCurrency.token.contractAddress,
balance = balance,
)
}
is FeePaidSdkCurrency.FeeResource -> FeePaidCurrency.FeeResource(currency = feePaidCurrency.currency)
}
is FeePaidSdkCurrency.FeeResource -> FeePaidCurrency.FeeResource(currency = feePaidCurrency.currency)
}
}

View file

@ -5,9 +5,12 @@ import com.tangem.datasource.local.token.AssetsStore
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
class DefaultMarketCryptoCurrencyRepository(
private val assetsStore: AssetsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : MarketCryptoCurrencyRepository {
override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
@ -15,11 +18,16 @@ class DefaultMarketCryptoCurrencyRepository(
}
private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean {
val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
return withContext(dispatchers.io) {
val contractAddress =
(cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE
return assetsStore.getSyncOrNull(userWalletId)?.find {
it.network == cryptoCurrency.network.backendId &&
it.contractAddress.equals(contractAddress, ignoreCase = true)
}?.exchangeAvailable ?: false
val asset = assetsStore.getSyncOrNull(userWalletId)?.find {
it.network == cryptoCurrency.network.backendId &&
it.contractAddress.equals(contractAddress, ignoreCase = true)
}
asset?.exchangeAvailable ?: false
}
}
}

View file

@ -35,16 +35,19 @@ class DefaultTxHistoryRepository(
private val sdkPageConverter by lazy { SdkPageConverter() }
override suspend fun getTxHistoryItemsCount(userWalletId: UserWalletId, currency: CryptoCurrency): Int {
val userWallet = getUserWallet(userWalletId)
val state = walletManagersFacade.getTxHistoryState(
userWalletId = userWallet.walletId,
currency = currency,
)
return when (state) {
is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception)
is TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented
is TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories
is TxHistoryState.Success.HasTransactions -> state.txCount
return withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
val state = walletManagersFacade.getTxHistoryState(
userWalletId = userWallet.walletId,
currency = currency,
)
when (state) {
is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception)
is TxHistoryState.NotImplemented -> throw TxHistoryStateError.TxHistoryNotImplemented
is TxHistoryState.Success.Empty -> throw TxHistoryStateError.EmptyTxHistories
is TxHistoryState.Success.HasTransactions -> state.txCount
}
}
}

View file

@ -5,11 +5,11 @@ import com.tangem.domain.core.utils.lceContent
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch
import kotlin.experimental.ExperimentalTypeInference
/**
@ -25,35 +25,43 @@ typealias LceFlow<E, C> = Flow<Lce<E, C>>
* It provides methods to handle [Lce] instances and raise errors within a [Flow].
*
* @property raise The [LceRaise] instance that this class wraps.
* @property scope The [ProducerScope] that this class operates within.
* @property producerScope The [ProducerScope] instance that this class wraps.
* @property ifLoading The function to call if a loading state is raised.
*/
class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
private val raise: LceRaise<E>,
private val scope: ProducerScope<Lce<E, C>>,
private val producerScope: ProducerScope<Lce<E, C>>,
private val ifLoading: suspend LceFlowScope<E, C>.(C?) -> Unit,
) : Raise<E>, CoroutineScope by scope {
) : Raise<E>, CoroutineScope by producerScope {
/**
* Raises an [Lce] instance within the [ProducerScope].
* It closes the [ProducerScope] after raise.
* Sends a error of type [E] within the [ProducerScope] and then closes it for send.
* All subsequent sends will be ignored.
*
* @param r The [Lce] instance to raise.
* This method blocks the coroutine until a error is handled by the receiver.
*
* If the [ProducerScope] is already closed for send (e.g. after rising another error), it just raises [r]
* without closing.
*
* @param r Error to raise.
*/
override fun raise(r: E): Nothing {
scope.launch(NonCancellable) {
scope.send(r.lceError())
scope.close()
}
producerScope.trySendBlocking(r.lceError())
producerScope.close()
raise.raise(r.lceError())
}
/**
* Sends a content value within the [ProducerScope].
* Sends a [content] value within the [ProducerScope].
*
* If the content is still loading, it calls [ifLoading] lambda to retrieve a state.
* Otherwise, it wraps the content in a [Lce.Content] state.
*
* This method suspends until the [Lce] instance is handled by the receiver.
*
* If the [ProducerScope] is closed for send (e.g. after rising a error), it does nothing.
*
* @param content The content value to send.
* @param isStillLoading A flag indicating whether the content is still loading.
*/
@ -65,11 +73,23 @@ class LceFlowScope<E : Any, C : Any> @PublishedApi internal constructor(
content.lceContent()
}
scope.send(value)
send(value)
}
/**
* Sends an [Lce] instance within the [ProducerScope].
*
* This method suspends until the [Lce] instance is handled by the receiver.
*
* If the [ProducerScope] is closed for send (e.g. after rising a error), it does nothing.
*
* @param value The [Lce] instance to send.
*/
@OptIn(DelicateCoroutinesApi::class)
suspend fun send(value: Lce<E, C>) {
scope.send(value)
if (producerScope.isClosedForSend) return
producerScope.send(value)
}
}
@ -94,7 +114,7 @@ fun <E : Any, C : Any> lceFlow(
lce {
val scope = LceFlowScope(
raise = this@lce,
scope = this@channelFlow,
producerScope = this@channelFlow,
ifLoading = ifLoading,
)

View file

@ -87,11 +87,13 @@ class DefaultWalletManagersFacade(
Blockchain.fromId(it.id.value) to it.derivationPath.value
}
walletManagersStore.remove(userWalletId) { walletManager ->
val wallet = walletManager.wallet
val blockchainToDerivationPath = wallet.blockchain to wallet.publicKey.derivationPath?.rawPath
withContext(dispatchers.io) {
walletManagersStore.remove(userWalletId) { walletManager ->
val wallet = walletManager.wallet
val blockchainToDerivationPath = wallet.blockchain to wallet.publicKey.derivationPath?.rawPath
blockchainToDerivationPath in blockchainsToDerivationPaths
blockchainToDerivationPath in blockchainsToDerivationPaths
}
}
}
@ -110,18 +112,20 @@ class DefaultWalletManagersFacade(
network: Network,
networkTokens: List<CryptoCurrency.Token>,
) {
val walletManager = walletManagersStore.getSyncOrNull(
userWalletId = userWalletId,
blockchain = Blockchain.fromId(network.id.value),
derivationPath = network.derivationPath.value,
) ?: return
val tokensToRemove = sdkTokenConverter.convertList(networkTokens)
withContext(dispatchers.io) {
val walletManager = walletManagersStore.getSyncOrNull(
userWalletId = userWalletId,
blockchain = Blockchain.fromId(network.id.value),
derivationPath = network.derivationPath.value,
) ?: return@withContext
val tokensToRemove = sdkTokenConverter.convertList(networkTokens)
tokensToRemove.forEach { token ->
walletManager.removeToken(token)
tokensToRemove.forEach { token ->
walletManager.removeToken(token)
}
walletManagersStore.store(userWalletId, walletManager)
}
walletManagersStore.store(userWalletId, walletManager)
}
override suspend fun updatePendingTransactions(
@ -576,12 +580,16 @@ class DefaultWalletManagersFacade(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): AssetRequirementsCondition? {
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
if (walletManager !is AssetRequirementsManager || !walletManager.hasRequirements(currencyType)) return null
return withContext(dispatchers.io) {
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
if (walletManager !is AssetRequirementsManager || !walletManager.hasRequirements(currencyType)) {
return@withContext null
}
val condition = walletManager.requirementsCondition(currencyType) ?: return null
return requirementsConditionConverter.convert(condition)
val condition = walletManager.requirementsCondition(currencyType) ?: return@withContext null
requirementsConditionConverter.convert(condition)
}
}
override suspend fun associateAsset(
@ -589,15 +597,18 @@ class DefaultWalletManagersFacade(
currency: CryptoCurrency,
signer: CommonSigner,
): SimpleResult {
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
return withContext(dispatchers.io) {
val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network)
val currencyType = cryptoCurrencyTypeConverter.convert(currency)
if (walletManager !is AssetRequirementsManager) {
return SimpleResult.Failure(
BlockchainSdkError.CustomError("WalletManager is not implemented AssetRequirementsManager"),
)
if (walletManager !is AssetRequirementsManager) {
return@withContext SimpleResult.Failure(
BlockchainSdkError.CustomError("WalletManager is not implemented AssetRequirementsManager"),
)
}
walletManager.fulfillRequirements(currencyType, signer)
}
return walletManager.fulfillRequirements(currencyType, signer)
}
override suspend fun checkUtxoConsolidationAvailability(userWalletId: UserWalletId, network: Network): Boolean {

View file

@ -1,18 +1,43 @@
package com.tangem.domain.tokens.models.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
sealed class TokenSwapPromoAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Swap Promo", event, params, null) {
object Close : TokenSwapPromoAnalyticsEvent(event = "Button - Close")
class Exchange(
token: String,
) : AnalyticsEvent(category = "Promotion", event = event, params = params) {
class NoticePromotionBanner(
source: AnalyticsParam.ScreensSources,
programName: ProgramName,
) : TokenSwapPromoAnalyticsEvent(
event = "Button - Exchange Now",
params = mapOf("Token" to token),
event = "Notice - Promotion Banner",
params = mapOf(
AnalyticsParam.SOURCE to source.value,
"Program Name" to programName.name,
),
)
class PromotionBannerClicked(
source: AnalyticsParam.ScreensSources,
programName: ProgramName,
action: BannerAction,
) : TokenSwapPromoAnalyticsEvent(
event = "Promo Banner Clicked",
params = mapOf(
AnalyticsParam.SOURCE to source.value,
"Program Name" to programName.name,
"Action" to action.action,
),
) {
sealed class BannerAction(val action: String) {
data object Clicked : BannerAction(action = "Clicked")
data object Closed : BannerAction(action = "Closed")
}
}
enum class ProgramName {
Travala,
OKX,
}
}

View file

@ -84,7 +84,7 @@ class GetCurrencyWarningsUseCase(
): Flow<CryptoCurrencyWarning?> {
val currency = currencyStatus.currency
val cryptoStatuses = operations.getCurrenciesStatusesSync()
val promoBanner = promoRepository.getChangellyPromoBanner()
val promoBanner = promoRepository.getOkxPromoBanner()
return combine(
showSwapPromoTokenUseCase().conflate(),
flowOf(marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency)).conflate(),

View file

@ -7,4 +7,6 @@ interface PromoRepository {
suspend fun getChangellyPromoBanner(): PromoBanner?
suspend fun getTravalaPromoBanner(): PromoBanner?
suspend fun getOkxPromoBanner(): PromoBanner?
}

View file

@ -68,6 +68,7 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
return when (type) {
ExchangeProviderType.DEX -> ExchangeProviderTypeDomain.DEX
ExchangeProviderType.CEX -> ExchangeProviderTypeDomain.CEX
ExchangeProviderType.DEX_BRIDGE -> ExchangeProviderTypeDomain.DEX_BRIDGE
}
}
}

View file

@ -11,6 +11,7 @@ data class ExchangeStatusModel(
enum class ExchangeStatus {
New,
Waiting,
WaitingTxHash,
Confirming,
Verifying,
Exchanging,
@ -19,4 +20,5 @@ enum class ExchangeStatus {
Finished,
Refunded,
Cancelled,
Unknown,
}

View file

@ -36,9 +36,10 @@ data class SwapProvider(
val privacyPolicy: String?,
)
enum class ExchangeProviderType {
DEX,
CEX,
enum class ExchangeProviderType(val providerName: String) {
DEX("DEX"),
CEX("CEX"),
DEX_BRIDGE("DEX/Bridge"),
}
/**

View file

@ -13,7 +13,7 @@ import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.extenstions.unwrap
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -65,6 +65,7 @@ internal class SwapInteractorImpl @Inject constructor(
private val walletManagersFacade: WalletManagersFacade,
private val sendTransactionUseCase: SendTransactionUseCase,
private val createTransactionUseCase: CreateTransactionUseCase,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val quotesRepository: QuotesRepository,
private val dispatcher: CoroutineDispatcherProvider,
private val swapTransactionRepository: SwapTransactionRepository,
@ -72,7 +73,6 @@ internal class SwapInteractorImpl @Inject constructor(
private val appCurrencyRepository: AppCurrencyRepository,
private val currenciesRepository: CurrenciesRepository,
private val initialToCurrencyResolver: InitialToCurrencyResolver,
private val demoConfig: DemoConfig,
private val transactionRepository: TransactionRepository,
) : SwapInteractor {
@ -268,7 +268,7 @@ internal class SwapInteractorImpl @Inject constructor(
val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null)
val networkId = fromToken.currency.network.backendId
when (provider.type) {
ExchangeProviderType.DEX -> {
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
manageDex(
networkId = networkId,
fromToken = fromToken,
@ -342,7 +342,7 @@ internal class SwapInteractorImpl @Inject constructor(
)
} else {
provider to getQuotesState(
exchangeProviderType = ExchangeProviderType.DEX,
exchangeProviderType = provider.type,
quoteDataModel = quotes,
amount = amount,
fromToken = fromToken,
@ -568,7 +568,7 @@ internal class SwapInteractorImpl @Inject constructor(
fee: TxFee,
): SwapTransactionState {
val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError
if (demoConfig.isDemoCardId(cardId)) return SwapTransactionState.DemoMode
if (isDemoCardUseCase(cardId)) return SwapTransactionState.DemoMode
return when (swapProvider.type) {
ExchangeProviderType.CEX -> {
@ -588,8 +588,9 @@ internal class SwapInteractorImpl @Inject constructor(
userWalletId = requireNotNull(getSelectedWallet()).walletId,
)
}
ExchangeProviderType.DEX -> {
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
onSwapDex(
provider = swapProvider,
networkId = currencyToSend.currency.network.backendId,
swapData = requireNotNull(swapData),
currencyToSendStatus = currencyToSend,
@ -641,6 +642,7 @@ internal class SwapInteractorImpl @Inject constructor(
}
private suspend fun onSwapDex(
provider: SwapProvider,
networkId: String,
swapData: SwapDataModel,
currencyToSendStatus: CryptoCurrencyStatus,
@ -652,7 +654,8 @@ internal class SwapInteractorImpl @Inject constructor(
val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" }
val amount = SwapAmount(amountDecimal, currencyToSendStatus.currency.decimals)
val derivationPath = currencyToSendStatus.currency.network.derivationPath.value
val dataToSign = (swapData.transaction as ExpressTransactionModel.DEX).txData
val dexTransaction = swapData.transaction as ExpressTransactionModel.DEX
val dataToSign = dexTransaction.txData
val txData = createTransactionUseCase(
amount = amount.value.convertToAmount(currencyToSendStatus.currency),
fee = getFeeForTransaction(
@ -686,6 +689,17 @@ internal class SwapInteractorImpl @Inject constructor(
txHash = txHash,
payInExtraId = swapData.transaction.txExtraId,
)
if (provider.type == ExchangeProviderType.DEX_BRIDGE) {
val timestamp = System.currentTimeMillis()
storeSwapTransaction(
currencyToSend = currencyToSendStatus,
currencyToGet = currencyToGetStatus,
amount = amount,
swapProvider = provider,
swapDataModel = swapData,
timestamp = timestamp,
)
}
storeLastCryptoCurrencyId(currencyToGetStatus.currency)
SwapTransactionState.TxSent(
fromAmount = amountFormatter.formatSwapAmountToUI(
@ -702,16 +716,7 @@ internal class SwapInteractorImpl @Inject constructor(
timestamp = System.currentTimeMillis(),
)
},
ifLeft = {
when (it) {
SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled
is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError
is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError
is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError
is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode
else -> SwapTransactionState.UnknownError
}
},
ifLeft = { handleSendTxError(it) },
)
}
@ -783,14 +788,7 @@ internal class SwapInteractorImpl @Inject constructor(
val derivationPath = currencyToSend.currency.network.derivationPath.value
return result.fold(
ifLeft = {
when (it) {
SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled
is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError
is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError
is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError
is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode
else -> SwapTransactionState.UnknownError
}
handleSendTxError(it)
},
ifRight = { txHash ->
repository.exchangeSent(
@ -836,6 +834,17 @@ internal class SwapInteractorImpl @Inject constructor(
)
}
private fun handleSendTxError(txError: SendTransactionError?): SwapTransactionState {
return when (txError) {
SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled
is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError
is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError
is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError
is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode
else -> SwapTransactionState.UnknownError
}
}
private fun getFeeForTransaction(fee: TxFee, blockchain: Blockchain): Fee {
val feeAmountValue = fee.feeValue
val feeAmount = Amount(
@ -884,8 +893,8 @@ internal class SwapInteractorImpl @Inject constructor(
swapProvider: SwapProvider,
swapDataModel: SwapDataModel,
timestamp: Long,
txExternalUrl: String,
txExternalId: String,
txExternalUrl: String? = null,
txExternalId: String? = null,
) {
swapTransactionRepository.storeTransaction(
userWalletId = UserWalletId(userWalletManager.getWalletId()),
@ -1074,7 +1083,7 @@ internal class SwapInteractorImpl @Inject constructor(
)
when (exchangeProviderType) {
ExchangeProviderType.DEX -> {
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
val state = updatePermissionState(
networkId = networkId,
fromTokenStatus = fromToken,
@ -1409,19 +1418,25 @@ internal class SwapInteractorImpl @Inject constructor(
swapAmount = swapAmount,
spenderAddress = requireNotNull(spenderAddress) { "Spender address is null" },
)
val feeData = try {
transactionManager.getFee(
networkId = networkId,
amountToSend = BigDecimal.ZERO,
currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)),
destinationAddress = fromToken.getContractAddress(),
increaseBy = INCREASE_GAS_LIMIT_BY,
data = transactionData,
derivationPath = derivationPath,
)
} catch (e: Exception) {
Timber.e(e, "Failed to get fee")
null
val userWallet = getSelectedWallet()
val cardId = userWallet?.scanResponse?.card?.cardId
val feeData = if (cardId != null && isDemoCardUseCase(cardId)) {
getDemoFees(fromTokenStatus.currency)
} else {
try {
transactionManager.getFee(
networkId = networkId,
amountToSend = BigDecimal.ZERO,
currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)),
destinationAddress = fromToken.getContractAddress(),
increaseBy = INCREASE_GAS_LIMIT_BY,
data = transactionData,
derivationPath = derivationPath,
)
} catch (e: Exception) {
Timber.e(e, "Failed to get fee")
null
}
}
val feeState = feeData?.let {
when (feeData) {
@ -1805,10 +1820,37 @@ internal class SwapInteractorImpl @Inject constructor(
.toMap()
}
private fun getDemoFees(cryptoCurrency: CryptoCurrency): ProxyFees.MultipleFees {
val demoFee = ProxyAmount(
currencySymbol = cryptoCurrency.symbol,
value = minDemoFee,
decimals = cryptoCurrency.decimals,
)
return ProxyFees.MultipleFees(
minFee = ProxyFee.Common(
gasLimit = 1.toBigInteger(),
fee = demoFee,
),
normalFee = ProxyFee.Common(
gasLimit = 1.toBigInteger(),
fee = demoFee.copy(value = normalDemoFee),
),
priorityFee = ProxyFee.Common(
gasLimit = 1.toBigInteger(),
fee = demoFee.copy(value = priorityDemoFee),
),
)
}
companion object {
@Suppress("UnusedPrivateMember")
private const val INCREASE_GAS_LIMIT_BY = 112 // 12%
private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5%
private const val INFINITY_SYMBOL = ""
private val minDemoFee = "0.0001".toBigDecimal()
private val normalDemoFee = "0.0002".toBigDecimal()
private val priorityDemoFee = "0.0003".toBigDecimal()
}
}

View file

@ -42,6 +42,7 @@ class SwapDomainModule {
getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
@SwapScope sendTransactionUseCase: SendTransactionUseCase,
@SwapScope createTransactionUseCase: CreateTransactionUseCase,
isDemoCardUseCase: IsDemoCardUseCase,
quotesRepository: QuotesRepository,
swapTransactionRepository: SwapTransactionRepository,
appCurrencyRepository: AppCurrencyRepository,
@ -61,6 +62,7 @@ class SwapDomainModule {
getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase,
sendTransactionUseCase = sendTransactionUseCase,
createTransactionUseCase = createTransactionUseCase,
isDemoCardUseCase = isDemoCardUseCase,
quotesRepository = quotesRepository,
walletManagersFacade = walletManagersFacade,
dispatcher = coroutineDispatcherProvider,
@ -69,7 +71,6 @@ class SwapDomainModule {
currencyChecksRepository = currencyChecksRepository,
currenciesRepository = currenciesRepository,
initialToCurrencyResolver = initialToCurrencyResolver,
demoConfig = DemoConfig(),
transactionRepository = transactionRepository,
)
}

View file

@ -1004,13 +1004,15 @@ internal class StateBuilder(
val fromFiatAmount = getFormattedFiatAmount(fromCryptoCurrency.value.fiatRate?.multiply(fromAmount))
val toFiatAmount = getFormattedFiatAmount(toCryptoCurrency.value.fiatRate?.multiply(toAmount))
val shouldShowStatus = providerState.type == ExchangeProviderType.CEX.providerName ||
providerState.type == ExchangeProviderType.DEX_BRIDGE.providerName
return uiState.copy(
successState = SwapSuccessStateHolder(
timestamp = swapTransactionState.timestamp,
txUrl = txUrl,
providerName = stringReference(providerState.name),
providerType = stringReference(providerState.type),
showStatusButton = providerState.type == ExchangeProviderType.CEX.name,
showStatusButton = shouldShowStatus,
providerIcon = providerState.iconUrl,
rate = providerState.subtitle,
fee = stringReference("${fee.feeCryptoFormatted} (${fee.feeFiatFormatted})"),
@ -1092,7 +1094,7 @@ internal class StateBuilder(
): SwapStateHolder {
val message = when (providerType) {
ExchangeProviderType.CEX -> resourceReference(R.string.swapping_alert_cex_description, wrappedList(token))
ExchangeProviderType.DEX -> {
ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> {
val refs = buildList {
if (isPriceImpact) {
add(resourceReference(R.string.swapping_high_price_impact_description))
@ -1548,7 +1550,7 @@ internal class StateBuilder(
id = this.providerId,
name = this.name,
iconUrl = this.imageLarge,
type = this.type.toString(),
type = this.type.providerName,
subtitle = stringReference(rateString),
additionalBadge = badge,
selectionType = selectionType,
@ -1575,7 +1577,7 @@ internal class StateBuilder(
id = this.providerId,
name = this.name,
iconUrl = this.imageLarge,
type = this.type.toString(),
type = this.type.providerName,
subtitle = stringReference(rateString),
additionalBadge = additionalBadge,
selectionType = selectionType,
@ -1612,7 +1614,7 @@ internal class StateBuilder(
id = this.providerId,
name = this.name,
iconUrl = this.imageLarge,
type = this.type.toString(),
type = this.type.providerName,
selectionType = selectionType,
subtitle = alertText,
additionalBadge = ProviderState.AdditionalBadge.Empty,

View file

@ -154,7 +154,7 @@ private val state = SwapSuccessStateHolder(
txUrl = "https://www.google.com/#q=nam",
fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"),
providerName = TextReference.Str("1inch"),
providerType = TextReference.Str(ExchangeProviderType.DEX.name),
providerType = TextReference.Str(ExchangeProviderType.DEX.providerName),
showStatusButton = false,
providerIcon = "",
fromTokenAmount = TextReference.Str("1 000 DAI"),

View file

@ -2,7 +2,9 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
@ -35,12 +37,15 @@ internal class TokenDetailsNotificationsAnalyticsSender(
-> TokenDetailsAnalyticsEvent.Notice.NotEnoughFee(
currency = cryptoCurrency,
)
is TokenDetailsNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
source = AnalyticsParam.ScreensSources.Token,
)
is TokenDetailsNotification.NetworksUnreachable,
is TokenDetailsNotification.ExistentialDeposit,
is TokenDetailsNotification.NetworksNoAccount,
is TokenDetailsNotification.TopUpWithoutReserve,
is TokenDetailsNotification.RentInfo,
is TokenDetailsNotification.SwapPromo,
is TokenDetailsNotification.NetworkShutdown,
is TokenDetailsNotification.HederaAssociateWarning,
is TokenDetailsNotification.KoinosMana,

View file

@ -2,7 +2,10 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.componen
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.features.tokendetails.impl.R
@ -45,16 +48,9 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
val onCloseClick: () -> Unit,
) : TokenDetailsNotification(
config = NotificationConfig(
title = resourceReference(
id = R.string.token_swap_changelly_promotion_title,
formatArgs = wrappedList("0%"),
),
subtitle = resourceReference(
id = R.string.token_swap_changelly_promotion_message,
formatArgs = wrappedList("0%", startDateTime.dayOfMonth, endDateTime.dayOfMonth),
),
iconResId = R.drawable.img_swap_promo,
backgroundResId = R.drawable.img_swap_promo_green_banner_background,
title = resourceReference(id = R.string.swap_promo_title),
subtitle = resourceReference(id = R.string.swap_promo_text),
iconResId = R.drawable.img_okx_dex_logo,
onCloseClick = onCloseClick,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(id = com.tangem.core.ui.R.string.token_swap_promotion_button),

View file

@ -162,6 +162,7 @@ internal class TokenDetailsSwapTransactionsStateConverter(
private fun getStatuses(status: ExchangeStatus?, hasFailed: Boolean = false): ImmutableList<ExchangeStatusState> {
if (status == null) return persistentListOf()
val isWaiting = status == ExchangeStatus.New || status == ExchangeStatus.Waiting
val isWaitingTxHash = status == ExchangeStatus.WaitingTxHash
val isConfirming = status == ExchangeStatus.Confirming
val isVerifying = status == ExchangeStatus.Verifying
val isExchanging = status == ExchangeStatus.Exchanging
@ -174,27 +175,35 @@ internal class TokenDetailsSwapTransactionsStateConverter(
val isExchangingDone = !isExchanging && isConfirmingDone
val isSendingDone = !isSending && !isVerifying && !isFailed && isExchangingDone
return if (status == ExchangeStatus.Cancelled) {
listOf(cancelledStep())
} else {
listOf(
waitStep(isWaiting, isWaitingDone),
confirmStep(isConfirming, isConfirmingDone),
exchangeStep(
isExchanging = isExchanging,
isExchangingDone = isExchangingDone,
isRefunded = isRefunded,
hasFailed = hasFailed,
isVerifying = isVerifying,
isFailed = isFailed,
),
sendStep(
isSending = isSending,
isSendingDone = isSendingDone,
isRefunded = isRefunded,
hasFailed = hasFailed,
),
)
return buildList {
when {
status == ExchangeStatus.Cancelled -> add(cancelledStep())
isWaitingTxHash -> add(waitTxStep())
// ExchangeStatus.Unknown is temporary added for 1inch
status == ExchangeStatus.Unknown -> add(unknownStateStep())
else -> {
add(waitStep(isWaiting, isWaitingDone))
add(confirmStep(isConfirming, isConfirmingDone))
add(
exchangeStep(
isExchanging = isExchanging,
isExchangingDone = isExchangingDone,
isRefunded = isRefunded,
hasFailed = hasFailed,
isVerifying = isVerifying,
isFailed = isFailed,
),
)
add(
sendStep(
isSending = isSending,
isSendingDone = isSendingDone,
isRefunded = isRefunded,
hasFailed = hasFailed,
),
)
}
}
}.toPersistentList()
}
@ -216,6 +225,20 @@ internal class TokenDetailsSwapTransactionsStateConverter(
isDone = isNewDone,
)
private fun waitTxStep() = ExchangeStatusState(
status = ExchangeStatus.Verifying,
text = TextReference.Res(R.string.express_exchange_status_waiting_tx_hash),
isActive = false,
isDone = false,
)
private fun unknownStateStep() = ExchangeStatusState(
status = ExchangeStatus.Failed,
text = TextReference.Res(R.string.express_exchange_status_failed),
isActive = false,
isDone = true,
)
private fun confirmStep(isConfirming: Boolean, isConfirmingDone: Boolean) = ExchangeStatusState(
status = ExchangeStatus.Confirming,
text = when {

View file

@ -32,7 +32,7 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBotto
import com.tangem.core.ui.components.marketprice.MarketPriceBlock
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationWithBackground
import com.tangem.core.ui.components.notifications.OkxPromoNotification
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.components.transactions.txHistoryItems
import com.tangem.core.ui.event.EventEffect
@ -111,7 +111,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
contentType = { it.config::class.java },
itemContent = {
if (it is TokenDetailsNotification.SwapPromo) {
NotificationWithBackground(
OkxPromoNotification(
config = it.config,
modifier = itemModifier.animateItemPlacement(),
)

View file

@ -70,7 +70,7 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC
SpacerH12()
ExchangeProvider(
providerName = TextReference.Str(config.provider.name),
providerType = TextReference.Str(config.provider.type.name),
providerType = TextReference.Str(config.provider.type.providerName),
providerTxId = config.txExternalId,
imageUrl = config.provider.imageLarge,
)

View file

@ -163,6 +163,7 @@ internal class ExchangeStatusFactory(
return when (status) {
ExchangeStatus.New,
ExchangeStatus.Waiting,
ExchangeStatus.WaitingTxHash,
ExchangeStatus.Sending,
ExchangeStatus.Confirming,
ExchangeStatus.Exchanging,

View file

@ -5,6 +5,7 @@ import androidx.paging.cachedIn
import arrow.core.getOrElse
import com.tangem.blockchain.common.address.AddressType
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
import com.tangem.core.ui.clipboard.ClipboardManager
@ -709,14 +710,26 @@ internal class TokenDetailsViewModel @Inject constructor(
override fun onSwapPromoDismiss() {
viewModelScope.launch(dispatchers.main) {
shouldShowSwapPromoTokenUseCase.neverToShow()
analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Close)
analyticsEventsHandler.send(
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Token,
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed,
),
)
}
}
override fun onSwapPromoClick() {
viewModelScope.launch(dispatchers.main) {
shouldShowSwapPromoTokenUseCase.neverToShow()
analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Exchange(cryptoCurrency.symbol))
analyticsEventsHandler.send(
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Token,
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked,
),
)
}
onSwapClick(ScenarioUnavailabilityReason.None)
}

View file

@ -121,38 +121,4 @@ sealed class WalletScreenAnalyticsEvent {
data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
}
sealed class Promotion(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Promotion", event = event, params = params) {
class NoticePromotionBanner(
source: AnalyticsParam.ScreensSources,
programName: String,
) : Promotion(
event = "Notice - Promotion Banner",
params = mapOf(
AnalyticsParam.SOURCE to source.value,
"Program Name" to programName,
),
)
class PromotionBannerClicked(
source: AnalyticsParam.ScreensSources,
programName: String,
action: BannerAction,
) : Promotion(
event = "Promo Banner Clicked",
params = mapOf(
AnalyticsParam.SOURCE to source.value,
"Program Name" to programName,
"Action" to action.action,
),
) {
sealed class BannerAction(val action: String) {
data object Clicked : BannerAction(action = "Clicked")
data object Closed : BannerAction(action = "Closed")
}
}
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
@ -46,16 +46,19 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses
is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem
is WalletNotification.Critical.BackupError -> MainScreen.BackupError
is WalletNotification.TravalaPromo -> WalletScreenAnalyticsEvent.Promotion.NoticePromotionBanner(
is WalletNotification.TravalaPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
programName = "Travala",
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala,
)
is WalletNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
)
is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender]
is WalletNotification.Informational.NoAccount,
is WalletNotification.Warning.LowSignatures,
is WalletNotification.Warning.SomeNetworksUnreachable,
is WalletNotification.Warning.NetworksUnreachable,
is WalletNotification.SwapPromo,
-> null
}
}

View file

@ -6,7 +6,7 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.promo.PromoBanner
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.settings.ShouldShowTravalaPromoWalletUseCase
import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
@ -34,7 +34,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private val getTokenListUseCase: GetTokenListUseCase,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
private val shouldShowTravalaPromoWalletUseCase: ShouldShowTravalaPromoWalletUseCase,
private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase,
private val promoRepository: PromoRepository,
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
private val backupValidator: BackupValidator,
@ -45,18 +45,18 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
val travalaPromoFlow = flow { emit(promoRepository.getTravalaPromoBanner()) }
val promoFlow = flow { emit(promoRepository.getOkxPromoBanner()) }
return combine(
flow = getTokenListUseCase.launch(userWallet.walletId).conflate(),
flow2 = isReadyToShowRateAppUseCase().conflate(),
flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(),
flow4 = shouldShowTravalaPromoWalletUseCase().conflate(),
flow5 = travalaPromoFlow.conflate(),
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowTravalaPromo, promoBanner ->
flow4 = shouldShowSwapPromoWalletUseCase().conflate(),
flow5 = promoFlow.conflate(),
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowPromo, promoBanner ->
readyForRateAppNotification = true
buildList {
addTravalaPromoNotification(shouldShowTravalaPromo, promoBanner, clickIntents)
addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents)
addCriticalNotifications(userWallet)
@ -69,18 +69,16 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
}
}
private fun MutableList<WalletNotification>.addTravalaPromoNotification(
private fun MutableList<WalletNotification>.addSwapPromoNotification(
shouldShowPromo: Boolean,
promoBanner: PromoBanner?,
clickIntents: WalletClickIntents,
) {
promoBanner ?: return
val promoNotification = WalletNotification.TravalaPromo(
val promoNotification = WalletNotification.SwapPromo(
startDateTime = promoBanner.bannerState.timeline.start,
endDateTime = promoBanner.bannerState.timeline.end,
bannerLink = promoBanner.bannerState.link,
onBookNowButtonClick = clickIntents::onTravalaPromoClick,
onCloseClick = clickIntents::onCloseTravalaPromoClick,
onCloseClick = clickIntents::onCloseSwapPromoClick,
)
addIf(
element = promoNotification,

View file

@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo
@ -107,4 +108,18 @@ internal object WalletAdditionalInfoFactory {
WalletAdditionalInfo(hideable = true, content = TextReference.Str(value = amount.orEmpty()))
}
}
private fun UserWallet.getCardsCount(): Int? {
return if (isMultiCurrency) {
when (val status = scanResponse.card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount + 1
is CardDTO.BackupStatus.CardLinked,
is CardDTO.BackupStatus.NoBackup,
null,
-> 1
}
} else {
null
}
}
}

View file

@ -17,6 +17,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -41,6 +42,7 @@ internal class WalletStateController @Inject constructor(
}
fun update(transformer: WalletScreenStateTransformer) {
Timber.d("Applying: ${transformer::class.simpleName}")
mutableUiState.update(function = transformer::transform)
}

View file

@ -2,7 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.model
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.feature.wallet.impl.R
import org.joda.time.DateTime
@ -205,16 +208,9 @@ sealed class WalletNotification(val config: NotificationConfig) {
val onCloseClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(
id = R.string.main_swap_changelly_promotion_title,
formatArgs = wrappedList("0%"),
),
subtitle = resourceReference(
id = R.string.main_swap_changelly_promotion_message,
formatArgs = wrappedList("0%", startDateTime.dayOfMonth, endDateTime.dayOfMonth),
),
iconResId = R.drawable.img_swap_promo,
backgroundResId = R.drawable.img_swap_promo_green_banner_background,
title = resourceReference(id = R.string.swap_promo_title),
subtitle = resourceReference(id = R.string.swap_promo_text),
iconResId = R.drawable.img_okx_dex_logo,
onCloseClick = onCloseClick,
),
)

View file

@ -1,22 +1,32 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
import com.tangem.feature.wallet.presentation.wallet.domain.getCardsCount
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
import timber.log.Timber
import java.math.BigDecimal
internal class SetTokenListErrorTransformer(
userWalletId: UserWalletId,
private val selectedWallet: UserWallet,
private val error: TokenListError,
) : WalletStateTransformer(userWalletId) {
private val appCurrency: AppCurrency,
) : WalletStateTransformer(selectedWallet.walletId) {
override fun transform(prevState: WalletState): WalletState {
return when (error) {
is TokenListError.EmptyTokens -> {
when (prevState) {
is WalletState.MultiCurrency.Content -> {
prevState.copy(tokensListState = WalletTokensListState.Empty)
prevState.copy(
walletCardState = prevState.walletCardState.toLoadedState(),
tokensListState = WalletTokensListState.Empty,
)
}
is WalletState.MultiCurrency.Locked -> {
Timber.w("Impossible to load tokens list for locked wallet")
@ -37,4 +47,21 @@ internal class SetTokenListErrorTransformer(
-> prevState
}
}
private fun WalletCardState.toLoadedState(): WalletCardState {
return WalletCardState.Content(
id = id,
title = title,
additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = selectedWallet),
imageResId = imageResId,
onRenameClick = onRenameClick,
onDeleteClick = onDeleteClick,
balance = BigDecimalFormatter.formatFiatAmount(
fiatAmount = BigDecimal.ZERO,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
),
cardCount = selectedWallet.getCardsCount(),
)
}
}

View file

@ -61,6 +61,11 @@ internal abstract class BasicTokenListSubscriber(
},
flow2 = getSelectedAppCurrencyUseCase().distinctUntilChanged(),
transform = { maybeTokenList, maybeAppCurrency ->
val appCurrency = maybeAppCurrency.getOrElse { e ->
Timber.e("Failed to load app currency: $e")
AppCurrency.Default
}
val tokenList = maybeTokenList.getOrElse(
ifLoading = { maybeContent ->
val isRefreshing = stateHolder.getWalletState(userWallet.walletId)
@ -74,16 +79,17 @@ internal abstract class BasicTokenListSubscriber(
},
ifError = { e ->
Timber.e("Failed to load token list: $e")
SetTokenListErrorTransformer(userWallet.walletId, e)
stateHolder.update(
SetTokenListErrorTransformer(
selectedWallet = userWallet,
error = e,
appCurrency = appCurrency,
),
)
return@combine
},
)
val appCurrency = maybeAppCurrency.getOrElse { e ->
Timber.e("Failed to load app currency: $e")
AppCurrency.Default
}
updateContent(tokenList, appCurrency)
walletWithFundsChecker.check(tokenList)
},

View file

@ -5,7 +5,7 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationWithBackground
import com.tangem.core.ui.components.notifications.OkxPromoNotification
import com.tangem.core.ui.components.notifications.TravalaNotificationWithBackground
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
@ -29,7 +29,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
// TODO develop promo banner general component
when (it) {
is WalletNotification.SwapPromo -> {
NotificationWithBackground(
OkxPromoNotification(
config = it.config,
modifier = modifier.animateItemPlacement(),
)

View file

@ -216,6 +216,14 @@ internal class WalletViewModel @Inject constructor(
is WalletsUpdateActionResolver.Action.DeleteWallet -> deleteWallet(action)
is WalletsUpdateActionResolver.Action.UnlockWallet -> unlockWallet(action)
is WalletsUpdateActionResolver.Action.UpdateWalletCardCount -> {
// refresh loader to use actual user wallet
walletScreenContentLoader.load(
userWallet = action.selectedWallet,
clickIntents = clickIntents,
isRefresh = true,
coroutineScope = viewModelScope,
)
stateHolder.update(transformer = UpdateWalletCardsCountTransformer(action.selectedWallet))
}
is WalletsUpdateActionResolver.Action.UpdateWalletName -> {

View file

@ -1,6 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.extenstions.unwrap
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.settings.NeverToShowWalletsScrollPreview
import com.tangem.domain.tokens.FetchCardTokenListUseCase
@ -37,6 +39,7 @@ internal class WalletClickIntents @Inject constructor(
private val fetchTokenListUseCase: FetchTokenListUseCase,
private val fetchCardTokenListUseCase: FetchCardTokenListUseCase,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val neverToShowWalletsScrollPreview: NeverToShowWalletsScrollPreview,
private val analyticsEventHandler: AnalyticsEventHandler,
private val dispatchers: CoroutineDispatcherProvider,
@ -115,7 +118,13 @@ internal class WalletClickIntents @Inject constructor(
}
maybeFetchResult.onLeft {
stateHolder.update(SetTokenListErrorTransformer(userWalletId = userWallet.walletId, error = it))
stateHolder.update(
SetTokenListErrorTransformer(
selectedWallet = userWallet,
error = it,
appCurrency = getSelectedAppCurrencyUseCase.unwrap(),
),
)
}
stateHolder.update(

View file

@ -14,13 +14,13 @@ import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase
import com.tangem.domain.settings.ShouldShowTravalaPromoWalletUseCase
import com.tangem.domain.tokens.FetchTokenListUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
import com.tangem.domain.wallets.models.UnlockWalletsError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletClickHandler
@ -214,6 +214,13 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
}
override fun onCloseSwapPromoClick() {
analyticsEventHandler.send(
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX,
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed,
),
)
viewModelScope.launch(dispatchers.main) {
shouldShowSwapPromoWalletUseCase.neverToShow()
}
@ -221,10 +228,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
override fun onTravalaPromoClick(link: String?) {
analyticsEventHandler.send(
WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked(
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
programName = "Travala",
action = WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked.BannerAction.Clicked,
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala,
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Clicked,
),
)
link?.let {
@ -236,10 +243,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
override fun onCloseTravalaPromoClick() {
analyticsEventHandler.send(
WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked(
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
programName = "Travala",
action = WalletScreenAnalyticsEvent.Promotion.PromotionBannerClicked.BannerAction.Closed,
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala,
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed,
),
)
viewModelScope.launch(dispatchers.main) {

View file

@ -87,9 +87,9 @@ markdown = "0.7.2"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "release-app_5.12-691"
tangemBlockchainSdk = "release-app_5.12-694"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "release-app_5.12-367"
tangemCardSdk = "release-app_5.12-369"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
# endregion Tangem