diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b9db373358..8981d0a0b1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,6 +30,13 @@ + + + + + + diff --git a/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt index 7fe3ef0081..965d6caacd 100644 --- a/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt +++ b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt @@ -1,8 +1,11 @@ package com.tangem.tap.common.url import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsClient import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT @@ -12,6 +15,7 @@ import com.tangem.tap.common.extensions.getColorCompat import com.tangem.tap.foregroundActivityObserver import com.tangem.tap.withForegroundActivity import com.tangem.wallet.R +import timber.log.Timber internal class CustomTabsUrlOpener : UrlOpener { @@ -34,6 +38,37 @@ internal class CustomTabsUrlOpener : UrlOpener { ) .build() - customTabsIntent.launchUrl(context, Uri.parse(url)) + customTabsIntent.intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + runCatching { + if (checkCustomTabsAvailability(context, browserIntent)) { + context.startActivity(browserIntent) + } else { + customTabsIntent.launchUrl(context, Uri.parse(url)) + } + }.onFailure { + Timber.e(it.message) + } + } + + /** + * Custom Tabs compatibility check. Returns flag whether custom tabs are supported + * @see "https://developer.chrome.com/docs/android/custom-tabs/howto-custom-tab-check" + */ + private fun checkCustomTabsAvailability(context: Context, browserIntent: Intent): Boolean { + // Get all apps that can handle VIEW intents and Custom Tab service connections. + val resolveInfos = context.packageManager.queryIntentActivities(browserIntent, PackageManager.MATCH_ALL) + + // Extract package names from ResolveInfo objects + val packageNames = mutableListOf() + for (info in resolveInfos) { + packageNames.add(info.activityInfo.packageName) + } + + // Get a package that supports Custom Tabs + val packageName = CustomTabsClient.getPackageName(context, packageNames, true) + + return packageName == null // Custom Tabs are not supported by any browser on the device } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/model/OnrampCurrencyDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/model/OnrampCurrencyDTO.kt index 7dbb49bbdf..a2a62175ec 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/model/OnrampCurrencyDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/model/OnrampCurrencyDTO.kt @@ -12,7 +12,7 @@ data class OnrampCurrencyDTO( val code: String, @Json(name = "image") - val image: String, + val image: String?, @Json(name = "precision") val precision: Int, diff --git a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt index b9427aee10..259ffd2cbb 100644 --- a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt +++ b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt @@ -3,15 +3,10 @@ package com.tangem.core.deeplink.global import com.tangem.core.deeplink.DeepLink class BuyCurrencyDeepLink( - isOnrampFeatureEnabled: Boolean, val onReceive: (externalTxId: String) -> Unit, ) : DeepLink { - override val uri: String = if (isOnrampFeatureEnabled) { - ONRAMP_REDIRECT_DEEPLINK - } else { - BUY_REDIRECT_DEEPLINK - } + override val uri = BUY_REDIRECT_DEEPLINK override fun onReceive(params: Map) { onReceive( @@ -20,7 +15,7 @@ class BuyCurrencyDeepLink( } companion object { - const val ONRAMP_REDIRECT_DEEPLINK = "tangem://onramp-success?" + const val ONRAMP_REDIRECT_DEEPLINK = "https://tangem.com/success?action=dismissBrowser" private const val BUY_REDIRECT_DEEPLINK = "tangem://redirect?action=dismissBrowser" } } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt index ca78dbeca6..3d7221ff2d 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt @@ -32,10 +32,8 @@ import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObject -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrDefault import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.storeObject -import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.onramp.model.* import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.model.error.OnrampError @@ -281,6 +279,7 @@ internal class DefaultOnrampRepository( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, quote: OnrampProviderWithQuote.Data, + isDarkTheme: Boolean, ): OnrampTransaction = withContext(dispatchers.io) { try { val address = requireNotNull( @@ -306,7 +305,7 @@ internal class DefaultOnrampRepository( toAddress = address, redirectUrl = BuyCurrencyDeepLink.ONRAMP_REDIRECT_DEEPLINK, language = null, - theme = getTheme(), + theme = getTheme(isDarkTheme), requestId = requestId, ).bind() }, @@ -415,7 +414,7 @@ internal class DefaultOnrampRepository( fromCurrency = currency, toAmount = quote.toAmount.value, toCurrencyId = cryptoCurrency.id.value, - status = OnrampStatus.Status.Expired, + status = OnrampStatus.Status.Created, externalTxUrl = onrampDataJson.externalTxUrl, externalTxId = onrampDataJson.externalTxId, timestamp = DateTime.now().millis, @@ -442,17 +441,10 @@ internal class DefaultOnrampRepository( ) } - private suspend fun getTheme(): String { - val appTheme = appPreferencesStore.getObjectSyncOrDefault( - key = PreferencesKeys.APP_THEME_MODE_KEY, - default = AppThemeMode.DEFAULT, - ) - return when (appTheme) { - AppThemeMode.FORCE_DARK -> PROVIDER_THEME_DARK - AppThemeMode.FORCE_LIGHT, - AppThemeMode.FOLLOW_SYSTEM, - -> PROVIDER_THEME_LIGHT - } + private fun getTheme(isDarkTheme: Boolean): String = if (isDarkTheme) { + PROVIDER_THEME_DARK + } else { + PROVIDER_THEME_LIGHT } private fun List.removeApplePay(): List = filterNot { it.id == "apple-pay" } @@ -465,7 +457,7 @@ internal class DefaultOnrampRepository( ) = if (error is ApiResponseError.HttpException) { val onrampError = onrampErrorConverter.convert(value = error.errorBody.orEmpty()) if (onrampError is OnrampError.AmountError) { - OnrampQuote.Error( + OnrampQuote.AmountError( paymentMethod = paymentMethod, provider = provider, fromAmount = fromOnrampAmount, @@ -473,7 +465,10 @@ internal class DefaultOnrampRepository( ) } else { Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error") - null + OnrampQuote.Error( + paymentMethod = paymentMethod, + provider = provider, + ) } } else { Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error") diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrency.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrency.kt index 20f225f582..8b5bdbf34b 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrency.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrency.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable data class OnrampCurrency( val name: String, val code: String, - val image: String, + val image: String?, val precision: Int, val unit: String, ) \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt index f0d51802d6..2891d6072d 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt @@ -23,7 +23,7 @@ sealed interface OnrampProviderWithQuote { data class Error( override val provider: OnrampProvider, - val quoteError: OnrampQuote.Error, + val quoteError: OnrampQuote.AmountError, ) : Unavailable } } \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt index cc6a4586c1..033d3f3f87 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt @@ -16,10 +16,15 @@ sealed class OnrampQuote { val maxFromAmount: OnrampAmount, ) : OnrampQuote() - data class Error( + data class AmountError( override val paymentMethod: OnrampPaymentMethod, override val provider: OnrampProvider, val fromAmount: OnrampAmount, val error: OnrampError.AmountError, ) : OnrampQuote() + + data class Error( + override val paymentMethod: OnrampPaymentMethod, + override val provider: OnrampProvider, + ) : OnrampQuote() } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt index 22238ab748..1d6c6700d6 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt @@ -31,8 +31,8 @@ class GetOnrampProviderWithQuoteUseCase( private fun List.quoteWithProvider( provider: OnrampProvider, selectedPaymentMethod: OnrampPaymentMethod, - ): OnrampProviderWithQuote { - val matchedQuote = firstOrNull { it.paymentMethod == selectedPaymentMethod } + ): OnrampProviderWithQuote? { + val matchedQuote = firstOrNull { it.paymentMethod.id == selectedPaymentMethod.id } return when (matchedQuote) { is OnrampQuote.Data -> OnrampProviderWithQuote.Data( provider = matchedQuote.provider, @@ -40,14 +40,23 @@ class GetOnrampProviderWithQuoteUseCase( toAmount = matchedQuote.toAmount, fromAmount = matchedQuote.fromAmount, ) - is OnrampQuote.Error -> Unavailable.Error( + is OnrampQuote.AmountError -> Unavailable.Error( provider = matchedQuote.provider, quoteError = matchedQuote, ) - null -> Unavailable.NotSupportedPaymentMethod( - provider = provider, - availablePaymentMethods = provider.paymentMethods, - ) + is OnrampQuote.Error -> null + null -> { + val availablePaymentMethods = getAvailablePaymentMethods(provider) + Unavailable.NotSupportedPaymentMethod( + provider = provider, + availablePaymentMethods = availablePaymentMethods, + ).takeIf { availablePaymentMethods.isNotEmpty() } + } } } + + private fun List.getAvailablePaymentMethods(provider: OnrampProvider) = provider.paymentMethods + .filter { pm -> + filter { it !is OnrampQuote.Error }.any { it.paymentMethod.id == pm.id } + } } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampQuotesUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampQuotesUseCase.kt index 1da7243f67..24b27d4860 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampQuotesUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampQuotesUseCase.kt @@ -47,9 +47,9 @@ class GetOnrampQuotesUseCase( grouped.value.sortedByDescending { when (it) { is OnrampQuote.Data -> it.toAmount.value - + is OnrampQuote.Error -> null // negative difference to sort both when data and unavailable is present - is OnrampQuote.Error -> { + is OnrampQuote.AmountError -> { when (val error = it.error) { is OnrampError.AmountError.TooSmallError -> it.fromAmount.value - error.requiredAmount is OnrampError.AmountError.TooBigError -> error.requiredAmount - it.fromAmount.value diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampRedirectUrlUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampRedirectUrlUseCase.kt index 34bb9f5202..55ba74a5bf 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampRedirectUrlUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampRedirectUrlUseCase.kt @@ -19,12 +19,14 @@ class GetOnrampRedirectUrlUseCase( userWalletId: UserWalletId, quote: OnrampProviderWithQuote.Data, cryptoCurrency: CryptoCurrency, + isDarkTheme: Boolean, ): Either { return Either.catch { val transaction = repository.getOnrampData( userWalletId = userWalletId, cryptoCurrency = cryptoCurrency, quote = quote, + isDarkTheme = isDarkTheme, ) transactionRepository.storeTransaction(transaction) transaction.redirectUrl diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt index 87e9bff955..302a563b12 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt @@ -24,6 +24,7 @@ interface OnrampRepository { userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, quote: OnrampProviderWithQuote.Data, + isDarkTheme: Boolean, ): OnrampTransaction // cache diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt index de711b188f..41f42b4ccd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt @@ -4,6 +4,7 @@ import arrow.core.NonEmptyList import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TotalFiatBalance +import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.extensions.orZero import java.math.BigDecimal @@ -12,6 +13,7 @@ internal class TokenListFiatBalanceOperations( private val isAnyTokenLoading: Boolean, ) { + @Suppress("LoopWithTooManyJumpStatements") fun calculateFiatBalance(): TotalFiatBalance { var fiatBalance: TotalFiatBalance = TotalFiatBalance.Loading if (isAnyTokenLoading) return fiatBalance @@ -22,14 +24,22 @@ internal class TokenListFiatBalanceOperations( fiatBalance = TotalFiatBalance.Loading break } - is CryptoCurrencyStatus.MissedDerivation, - is CryptoCurrencyStatus.Unreachable, - is CryptoCurrencyStatus.NoAmount, is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.MissedDerivation, -> { fiatBalance = TotalFiatBalance.Failed break } + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, + -> { + if (BlockchainUtils.isIncludeToBalanceOnError(token.currency.network.id.value)) { + fiatBalance = recalculateNoAccountBalance(fiatBalance) + } else { + fiatBalance = TotalFiatBalance.Failed + break + } + } is CryptoCurrencyStatus.NoAccount -> { fiatBalance = recalculateNoAccountBalance(fiatBalance) } diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 4f73123e32..87db7e37d6 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -14,6 +14,7 @@ android { dependencies { /* Project - API */ api(projects.features.markets.api) + api(projects.features.onramp.api) implementation(projects.core.navigation) /* Domain */ diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index 34923a5f2d..ce9e2b7841 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -23,6 +23,7 @@ import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.WarningDialog import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM +import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.utils.Provider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -41,12 +42,15 @@ internal class TokenActionsHandler @AssistedInject constructor( @Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit, private val isDemoCardUseCase: IsDemoCardUseCase, private val messageSender: UiMessageSender, + private val onrampFeatureToggles: OnrampFeatureToggles, ) { - private val disabledActionsInDemoMode = setOf( - TokenActionsBSContentUM.Action.Buy, - TokenActionsBSContentUM.Action.Sell, - ) + private val disabledActionsInDemoMode = buildSet { + if (!onrampFeatureToggles.isFeatureEnabled) { + add(TokenActionsBSContentUM.Action.Buy) + } + add(TokenActionsBSContentUM.Action.Sell) + } fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { onHandleQuickAction( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt index d0cfbe8515..c56ecedb67 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt @@ -10,7 +10,12 @@ internal data class OnrampAmountBlockUM( val secondaryFieldModel: OnrampAmountSecondaryFieldUM, ) -internal data class OnrampCurrencyUM(val code: String, val iconUrl: String, val precision: Int, val onClick: () -> Unit) +internal data class OnrampCurrencyUM( + val code: String, + val iconUrl: String?, + val precision: Int, + val onClick: () -> Unit, +) @Immutable internal sealed interface OnrampAmountSecondaryFieldUM { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt index 5d85cb0561..70d7f41345 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt @@ -75,7 +75,7 @@ internal class OnrampAmountStateFactory( return currentState.copy( amountBlockState = amountState.copy( - secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState), + secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState) ?: amountState.secondaryFieldModel, ), providerBlockState = quote.toProviderBlockState(isBestRate), buyButtonConfig = currentState.buyButtonConfig.copy( @@ -175,19 +175,20 @@ internal class OnrampAmountStateFactory( ) } - private fun OnrampQuote.toSecondaryFieldUiModel(amountState: OnrampAmountBlockUM): OnrampAmountSecondaryFieldUM { + private fun OnrampQuote.toSecondaryFieldUiModel(amountState: OnrampAmountBlockUM): OnrampAmountSecondaryFieldUM? { return when (this) { + is OnrampQuote.Error -> null is OnrampQuote.Data -> { val amount = toAmount.value.format { crypto(symbol = toAmount.symbol, decimals = toAmount.decimals) } OnrampAmountSecondaryFieldUM.Content(stringReference(amount)) } - is OnrampQuote.Error -> this.toSecondaryFieldUiModel(amountState) + is OnrampQuote.AmountError -> this.toSecondaryFieldUiModel(amountState) } } - private fun OnrampQuote.Error.toSecondaryFieldUiModel( + private fun OnrampQuote.AmountError.toSecondaryFieldUiModel( amountState: OnrampAmountBlockUM, ): OnrampAmountSecondaryFieldUM.Error { val amount = error.requiredAmount.format { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 366e4ba313..a7955b8c84 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -196,6 +196,8 @@ internal class OnrampMainComponentModel @Inject constructor( private fun subscribeToQuotesUpdate() { getOnrampQuotesUseCase.invoke() + .distinctUntilChanged() + .conflate() .onEach { maybeQuotes -> maybeQuotes.fold( ifLeft = ::handleOnrampError, @@ -269,7 +271,7 @@ internal class OnrampMainComponentModel @Inject constructor( } private fun handleQuoteResult(quotes: List) { - quotes.filterIsInstance().forEach { errorState -> + quotes.filterIsInstance().forEach { errorState -> sendOnrampErrorAnalytic(errorState.error) } @@ -299,12 +301,12 @@ internal class OnrampMainComponentModel @Inject constructor( paymentMethod = quote.paymentMethod.name, ), ) - } - _state.update { - amountStateFactory.getAmountSecondaryUpdatedState( - quote = quote, - isBestRate = isBestProvider, - ) + _state.update { + amountStateFactory.getAmountSecondaryUpdatedState( + quote = quote, + isBestRate = isBestProvider, + ) + } } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/SelectProviderResult.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/SelectProviderResult.kt index 434138e8ab..3f1364105d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/SelectProviderResult.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/SelectProviderResult.kt @@ -20,6 +20,6 @@ sealed class SelectProviderResult { data class ProviderWithError( override val paymentMethod: OnrampPaymentMethod, override val provider: OnrampProvider, - val quoteError: OnrampQuote.Error, + val quoteError: OnrampQuote.AmountError, ) : SelectProviderResult() } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt index 71cedbb3a5..17904c58e6 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt @@ -1,7 +1,9 @@ package com.tangem.features.onramp.redirect import androidx.activity.compose.BackHandler +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel @@ -22,6 +24,11 @@ internal class DefaultOnrampRedirectComponent @AssistedInject constructor( override fun Content(modifier: Modifier) { BackHandler(onBack = params.onBack) OnrampRedirectContent(modifier = modifier, state = model.state) + + val isDarkTheme = isSystemInDarkTheme() + LaunchedEffect(model.state) { + model.getRedirectUrl(isDarkTheme = isDarkTheme) + } } @AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt index f15be660c5..0ce73d2372 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt @@ -62,16 +62,13 @@ internal class OnrampRedirectModel @Inject constructor( ), ) - init { - getRedirectUrl() - } - - private fun getRedirectUrl() { + fun getRedirectUrl(isDarkTheme: Boolean) { modelScope.launch { getOnrampRedirectUrlUseCase.invoke( userWalletId = params.userWalletId, quote = params.onrampProviderWithQuote, cryptoCurrency = params.cryptoCurrency, + isDarkTheme = isDarkTheme, ) .onLeft(::handleError) .onRight { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt index 71f80e2f05..cb5a0cd776 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt @@ -11,7 +11,7 @@ sealed class OnrampSuccessComponentUM { data class Content( val txId: String, val timestamp: Long, - val currencyImageUrl: String, + val currencyImageUrl: String?, val fromAmount: TextReference, val toAmount: TextReference, val statusBlock: ExpressStatusUM, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/CustomTabsManager.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/CustomTabsManager.kt index 565d27a3b8..af04294b72 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/CustomTabsManager.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/CustomTabsManager.kt @@ -8,6 +8,7 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import java.lang.ref.WeakReference +@Deprecated("Replace with CustomTabsUrlOpener") class CustomTabsManager(private val context: WeakReference) { fun openUrl(url: String) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt index 52eae5de76..d8a3eb4733 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt @@ -50,6 +50,13 @@ internal class DefaultTokenDetailsRouter( } override fun openOnrampSuccess(externalTxId: String) { - router.push(AppRoute.OnrampSuccess(externalTxId)) + // finish current onramp flow and show onramp success screen + val replaceOnrampScreens = router.stack + .filterNot { it is AppRoute.Onramp } + .toMutableList() + + replaceOnrampScreens.add(AppRoute.OnrampSuccess(externalTxId)) + + router.replaceAll(*replaceOnrampScreens.toTypedArray()) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 625959ab86..358d295129 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -308,6 +308,7 @@ internal object TokenDetailsPreviewData { dialogConfig = null, pendingTxs = persistentListOf(), expressTxs = persistentListOf(), + expressTxsToDisplay = persistentListOf(), pullToRefreshConfig = pullToRefreshConfig, bottomSheetConfig = null, isBalanceHidden = false, @@ -338,6 +339,7 @@ internal object TokenDetailsPreviewData { dialogConfig = null, pendingTxs = persistentListOf(), expressTxs = persistentListOf(), + expressTxsToDisplay = persistentListOf(), pullToRefreshConfig = pullToRefreshConfig, bottomSheetConfig = null, isBalanceHidden = false, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 8ece0d02e9..d285c89fc9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -21,6 +21,7 @@ internal data class TokenDetailsState( val stakingBlocksState: StakingBlockUM?, val notifications: ImmutableList, val pendingTxs: PersistentList, + val expressTxsToDisplay: PersistentList, val expressTxs: PersistentList, val txHistoryState: TxHistoryState, val dialogConfig: TokenDetailsDialogConfig?, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 07f98c6f10..f22431609c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -67,6 +67,7 @@ internal class TokenDetailsSkeletonStateConverter( notifications = persistentListOf(), pendingTxs = persistentListOf(), expressTxs = persistentListOf(), + expressTxsToDisplay = persistentListOf(), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt index 92a6a2508c..aa5d0ecc92 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt @@ -98,8 +98,15 @@ internal class ExpressStatusFactory @AssistedInject constructor( if (currentTx is ExpressTransactionStateUM.ExchangeUM && currentTx.activeStatus == ExchangeStatus.Finished) { updateBalance(currentTx.toCryptoCurrency) } + val expressTxsToDisplay = expressTxs.filterNot { + when (it) { + is ExpressTransactionStateUM.ExchangeUM -> false + is ExpressTransactionStateUM.OnrampUM -> it.activeStatus.isHidden + } + }.toPersistentList() return state.copy( expressTxs = expressTxs, + expressTxsToDisplay = expressTxsToDisplay, bottomSheetConfig = currentTx?.let( ::updateStateWithExpressStatusBottomSheet, ) ?: config, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt index eca24c7c13..3e2cbe8813 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt @@ -61,7 +61,7 @@ internal class OnrampStatusFactory @AssistedInject constructor( ifRight = { onrampTxs -> val transactions = onrampTransactionStateConverter.convertList(onrampTxs) transactions.clearHiddenTerminal() - transactions.filterNot { it.activeStatus.isHidden } + transactions }, ifLeft = { persistentListOf() }, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index a22b97d642..a522838fe3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -178,7 +178,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon } expressTransactionsItems( - expressTxs = state.expressTxs, + expressTxs = state.expressTxsToDisplay, modifier = itemModifier, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 480de2e7f2..6690f0a085 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -198,7 +198,6 @@ internal class TokenDetailsViewModel @Inject constructor( viewModel = this, deepLinks = listOf( BuyCurrencyDeepLink( - isOnrampFeatureEnabled = onrampFeatureToggles.isFeatureEnabled, onReceive = ::onBuyCurrencyDeepLink, ), ), @@ -336,6 +335,7 @@ internal class TokenDetailsViewModel @Inject constructor( expressTxStatusTaskScheduler.scheduleTask( viewModelScope, PeriodicTask( + isDelayFirst = false, delay = EXPRESS_STATUS_UPDATE_DELAY, task = { runCatching { @@ -462,9 +462,8 @@ internal class TokenDetailsViewModel @Inject constructor( return } - showErrorIfDemoModeOrElse { - val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse - + val status = cryptoCurrencyStatus ?: return + if (onrampFeatureToggles.isFeatureEnabled) { viewModelScope.launch(dispatchers.main) { reduxStateHolder.dispatch( TradeCryptoAction.Buy( @@ -475,6 +474,19 @@ internal class TokenDetailsViewModel @Inject constructor( ), ) } + } else { + showErrorIfDemoModeOrElse { + viewModelScope.launch(dispatchers.main) { + reduxStateHolder.dispatch( + TradeCryptoAction.Buy( + userWallet = userWallet, + source = OnrampSource.TOKEN_DETAILS, + cryptoCurrencyStatus = status, + appCurrencyCode = selectedAppCurrencyFlow.value.code, + ), + ) + } + } } } @@ -805,7 +817,7 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onExpressTransactionClick(txId: String) { - val expressTxState = internalUiState.value.expressTxs.first { it.info.txId == txId } + val expressTxState = internalUiState.value.expressTxsToDisplay.first { it.info.txId == txId } internalUiState.value = expressStatusFactory.getStateWithExpressStatusBottomSheet(expressTxState) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt index c9abd72ef6..d729196c4f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt @@ -65,7 +65,6 @@ internal class WalletDeepLinksHandler @Inject constructor( if (onrampFeatureToggles.isFeatureEnabled || !userWallet.isMultiCurrency) { add( BuyCurrencyDeepLink( - isOnrampFeatureEnabled = onrampFeatureToggles.isFeatureEnabled, onReceive = { externalTxId -> scope.launch { onBuyCurrencyDeepLink(externalTxId, userWallet) } }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 4d2df2bd9f..6780b00e47 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -130,7 +130,14 @@ internal class DefaultWalletRouter( } override fun openOnrampSuccessScreen(externalTxId: String) { - router.push(AppRoute.OnrampSuccess(externalTxId)) + // finish current onramp flow and show onramp success screen + val replaceOnrampScreens = router.stack + .filterNot { it is AppRoute.Onramp } + .toMutableList() + + replaceOnrampScreens.add(AppRoute.OnrampSuccess(externalTxId)) + + router.replaceAll(*replaceOnrampScreens.toTypedArray()) } override fun openUrl(url: String) { diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 456959fddd..80b0af4a35 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -116,6 +116,17 @@ object BlockchainUtils { return l2BlockchainsList.contains(blockchain) } + /** + * Blockchains not affecting total balance counting on errors + */ + fun isIncludeToBalanceOnError(blockchainId: String): Boolean { + val blockchain = Blockchain.fromId(blockchainId) + return when (blockchain) { + Blockchain.Binance, Blockchain.BinanceTestnet -> true + else -> false + } + } + private fun getNetworkStandardName(blockchain: Blockchain): String { return when (blockchain) { Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ERC20"