Updated on 2026-08-14
This commit is contained in:
parent
94a4486443
commit
96ce2fc718
22 changed files with 784 additions and 57 deletions
|
|
@ -1,11 +1,7 @@
|
||||||
package com.tangem.tap.di.domain
|
package com.tangem.tap.di.domain
|
||||||
|
|
||||||
import com.tangem.domain.onramp.repositories.LegacyTopUpRepository
|
|
||||||
import com.tangem.domain.onramp.*
|
import com.tangem.domain.onramp.*
|
||||||
import com.tangem.domain.onramp.repositories.HotCryptoRepository
|
import com.tangem.domain.onramp.repositories.*
|
||||||
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
|
|
||||||
import com.tangem.domain.onramp.repositories.OnrampRepository
|
|
||||||
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
|
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
|
@ -237,4 +233,32 @@ internal object OnrampDomainModule {
|
||||||
fun provideGetLegacyTopUpUrlUseCase(legacyTopUpRepository: LegacyTopUpRepository): GetLegacyTopUpUrlUseCase {
|
fun provideGetLegacyTopUpUrlUseCase(legacyTopUpRepository: LegacyTopUpRepository): GetLegacyTopUpUrlUseCase {
|
||||||
return GetLegacyTopUpUrlUseCase(legacyTopUpRepository)
|
return GetLegacyTopUpUrlUseCase(legacyTopUpRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideGetOnrampAllOffersUseCase(
|
||||||
|
onrampRepository: OnrampRepository,
|
||||||
|
onrampErrorResolver: OnrampErrorResolver,
|
||||||
|
settingsRepository: SettingsRepository,
|
||||||
|
): GetOnrampAllOffersUseCase {
|
||||||
|
return GetOnrampAllOffersUseCase(
|
||||||
|
onrampRepository = onrampRepository,
|
||||||
|
errorResolver = onrampErrorResolver,
|
||||||
|
settingsRepository = settingsRepository,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideGetOnrampOffersUseCase(
|
||||||
|
onrampRepository: OnrampRepository,
|
||||||
|
onrampErrorResolver: OnrampErrorResolver,
|
||||||
|
onrampTransactionRepository: OnrampTransactionRepository,
|
||||||
|
): GetOnrampOffersUseCase {
|
||||||
|
return GetOnrampOffersUseCase(
|
||||||
|
onrampRepository = onrampRepository,
|
||||||
|
errorResolver = onrampErrorResolver,
|
||||||
|
onrampTransactionRepository = onrampTransactionRepository,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -876,7 +876,10 @@
|
||||||
<string name="onramp_payment_method_subtitle">Payment method</string>
|
<string name="onramp_payment_method_subtitle">Payment method</string>
|
||||||
<string name="onramp_provider_max_amount">Available up to %s</string>
|
<string name="onramp_provider_max_amount">Available up to %s</string>
|
||||||
<string name="onramp_provider_min_amount">Available from %s</string>
|
<string name="onramp_provider_min_amount">Available from %s</string>
|
||||||
<string name="onramp_providers_count">%d providers</string>
|
<plurals name="onramp_providers_count">
|
||||||
|
<item quantity="one">%d provider</item>
|
||||||
|
<item quantity="other">%d providers</item>
|
||||||
|
</plurals>
|
||||||
<string name="onramp_providers_subtitle">Providers</string>
|
<string name="onramp_providers_subtitle">Providers</string>
|
||||||
<string name="onramp_recently_used_title">Recently used</string>
|
<string name="onramp_recently_used_title">Recently used</string>
|
||||||
<string name="onramp_recommended_title">Recommended</string>
|
<string name="onramp_recommended_title">Recommended</string>
|
||||||
|
|
|
||||||
9
core/ui/src/main/res/drawable/ic_best_rate_16.xml
Normal file
9
core/ui/src/main/res/drawable/ic_best_rate_16.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:pathData="M8,11.848L11.332,13.901C11.857,14.225 12.506,13.744 12.366,13.134L11.482,9.264L14.426,6.66C14.89,6.25 14.642,5.472 14.031,5.419L10.155,5.084L8.639,1.432C8.4,0.856 7.6,0.856 7.361,1.432L5.845,5.084L1.969,5.419C1.358,5.472 1.11,6.25 1.574,6.66L4.518,9.264L3.634,13.134C3.494,13.744 4.142,14.225 4.668,13.901L8,11.848Z"
|
||||||
|
android:fillColor="#0099FF"/>
|
||||||
|
</vector>
|
||||||
10
core/ui/src/main/res/drawable/ic_fastest_16.xml
Normal file
10
core/ui/src/main/res/drawable/ic_fastest_16.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:pathData="M10.495,1.6C10.535,1.462 10.357,1.368 10.265,1.478L3.851,9.114C3.778,9.2 3.84,9.333 3.953,9.333H6.953L5.505,14.398C5.466,14.537 5.643,14.631 5.735,14.521L12.149,6.885C12.222,6.798 12.161,6.666 12.047,6.666H9.048L10.495,1.6Z"
|
||||||
|
android:fillColor="#FFB71B"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
package com.tangem.domain.onramp.model
|
package com.tangem.domain.onramp.model
|
||||||
|
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
data class OnrampOffersBlock(
|
data class OnrampOffersBlock(
|
||||||
val category: OnrampOfferCategory,
|
val category: OnrampOfferCategory,
|
||||||
val offers: List<OnrampOffer>,
|
val offers: List<OnrampOffer>,
|
||||||
val isVisible: Boolean = offers.isNotEmpty(),
|
val hasMoreOffers: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class OnrampOffer(
|
data class OnrampOffer(
|
||||||
val quote: OnrampQuote,
|
val quote: OnrampQuote,
|
||||||
|
val rateDif: BigDecimal?,
|
||||||
val advantages: OnrampOfferAdvantages = OnrampOfferAdvantages.Default,
|
val advantages: OnrampOfferAdvantages = OnrampOfferAdvantages.Default,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import com.tangem.domain.onramp.model.OnrampQuote
|
||||||
import com.tangem.domain.onramp.model.error.OnrampError
|
import com.tangem.domain.onramp.model.error.OnrampError
|
||||||
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
|
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
|
||||||
import com.tangem.domain.onramp.repositories.OnrampRepository
|
import com.tangem.domain.onramp.repositories.OnrampRepository
|
||||||
|
import com.tangem.domain.onramp.utils.calculateRateDif
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
@ -38,6 +39,7 @@ class GetOnrampAllOffersUseCase(
|
||||||
val isGooglePayAvailable = settingsRepository.isGooglePayAvailability()
|
val isGooglePayAvailable = settingsRepository.isGooglePayAvailability()
|
||||||
|
|
||||||
val overallBestRateQuote = validQuotes.maxByOrNull { it.toAmount.value }
|
val overallBestRateQuote = validQuotes.maxByOrNull { it.toAmount.value }
|
||||||
|
val bestRate = overallBestRateQuote?.toAmount?.value
|
||||||
|
|
||||||
val offersByPaymentMethod = validQuotes.groupBy { it.paymentMethod }
|
val offersByPaymentMethod = validQuotes.groupBy { it.paymentMethod }
|
||||||
|
|
||||||
|
|
@ -48,7 +50,8 @@ class GetOnrampAllOffersUseCase(
|
||||||
} else {
|
} else {
|
||||||
OnrampOfferAdvantages.Default
|
OnrampOfferAdvantages.Default
|
||||||
}
|
}
|
||||||
OnrampOffer(quote = quote, advantages = advantages)
|
val rateDif = calculateRateDif(quote.toAmount.value, bestRate)
|
||||||
|
OnrampOffer(quote = quote, rateDif = rateDif, advantages = advantages)
|
||||||
}
|
}
|
||||||
|
|
||||||
val groupBestRateOfferData = methodQuotes.maxByOrNull { it.toAmount.value }
|
val groupBestRateOfferData = methodQuotes.maxByOrNull { it.toAmount.value }
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import com.tangem.domain.onramp.model.error.OnrampError
|
||||||
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
|
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
|
||||||
import com.tangem.domain.onramp.repositories.OnrampRepository
|
import com.tangem.domain.onramp.repositories.OnrampRepository
|
||||||
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
|
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
|
||||||
|
import com.tangem.domain.onramp.utils.calculateRateDif
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
@ -43,8 +44,12 @@ class GetOnrampOffersUseCase(
|
||||||
val validQuotes = quotes.filterIsInstance<OnrampQuote.Data>()
|
val validQuotes = quotes.filterIsInstance<OnrampQuote.Data>()
|
||||||
if (validQuotes.isEmpty()) return emptyList()
|
if (validQuotes.isEmpty()) return emptyList()
|
||||||
|
|
||||||
|
val bestRateQuote = validQuotes.maxByOrNull { it.toAmount.value }
|
||||||
|
val bestRate = bestRateQuote?.toAmount?.value
|
||||||
|
|
||||||
val offers = validQuotes.map { quote ->
|
val offers = validQuotes.map { quote ->
|
||||||
OnrampOffer(quote = quote)
|
val rateDif = calculateRateDif(quote.toAmount.value, bestRate)
|
||||||
|
OnrampOffer(quote = quote, rateDif = rateDif)
|
||||||
}
|
}
|
||||||
|
|
||||||
val recentOffer = findRecentOffer(offers, transactions)
|
val recentOffer = findRecentOffer(offers, transactions)
|
||||||
|
|
@ -107,6 +112,15 @@ class GetOnrampOffersUseCase(
|
||||||
fastestOffer: OnrampOffer?,
|
fastestOffer: OnrampOffer?,
|
||||||
allOffers: List<OnrampOffer>,
|
allOffers: List<OnrampOffer>,
|
||||||
): List<OnrampOffersBlock> {
|
): List<OnrampOffersBlock> {
|
||||||
|
val recommendedOffers = buildRecommendedOffers(
|
||||||
|
recentOffer = recentOffer,
|
||||||
|
bestRateOffer = bestRateOffer,
|
||||||
|
fastestOffer = fastestOffer,
|
||||||
|
)
|
||||||
|
|
||||||
|
val shownOffersCount = (if (recentOffer != null) 1 else 0) + recommendedOffers.size
|
||||||
|
val hasMoreOffers = allOffers.size > shownOffersCount
|
||||||
|
|
||||||
return buildList {
|
return buildList {
|
||||||
if (recentOffer != null) {
|
if (recentOffer != null) {
|
||||||
add(
|
add(
|
||||||
|
|
@ -119,23 +133,20 @@ class GetOnrampOffersUseCase(
|
||||||
bestRateOffer,
|
bestRateOffer,
|
||||||
fastestOffer,
|
fastestOffer,
|
||||||
),
|
),
|
||||||
|
rateDif = if (bestRateOffer != null) recentOffer.rateDif else null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
hasMoreOffers = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val recommendedOffers = buildRecommendedOffers(
|
|
||||||
recentOffer = recentOffer,
|
|
||||||
bestRateOffer = bestRateOffer,
|
|
||||||
fastestOffer = fastestOffer,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (recommendedOffers.isNotEmpty() && hasOnlyOneMethodAndProvider(allOffers).not()) {
|
if (recommendedOffers.isNotEmpty() && hasOnlyOneMethodAndProvider(allOffers).not()) {
|
||||||
add(
|
add(
|
||||||
OnrampOffersBlock(
|
OnrampOffersBlock(
|
||||||
category = OnrampOfferCategory.Recommended,
|
category = OnrampOfferCategory.Recommended,
|
||||||
offers = recommendedOffers,
|
offers = recommendedOffers,
|
||||||
|
hasMoreOffers = hasMoreOffers,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -167,17 +178,32 @@ class GetOnrampOffersUseCase(
|
||||||
return buildList {
|
return buildList {
|
||||||
if (isSameOffer(bestRateOffer, fastestOffer)) {
|
if (isSameOffer(bestRateOffer, fastestOffer)) {
|
||||||
bestRateOffer?.let { offer ->
|
bestRateOffer?.let { offer ->
|
||||||
add(offer.copy(advantages = OnrampOfferAdvantages.BestRate))
|
add(
|
||||||
|
offer.copy(
|
||||||
|
advantages = OnrampOfferAdvantages.BestRate,
|
||||||
|
rateDif = null,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (bestRateOffer != null && !isSameOffer(bestRateOffer, recentOffer)) {
|
if (bestRateOffer != null && !isSameOffer(bestRateOffer, recentOffer)) {
|
||||||
add(bestRateOffer.copy(advantages = OnrampOfferAdvantages.BestRate))
|
add(
|
||||||
|
bestRateOffer.copy(
|
||||||
|
advantages = OnrampOfferAdvantages.BestRate,
|
||||||
|
rateDif = null,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fastestOffer != null && !isSameOffer(fastestOffer, recentOffer) &&
|
if (fastestOffer != null && !isSameOffer(fastestOffer, recentOffer) &&
|
||||||
!isSameOffer(fastestOffer, bestRateOffer)
|
!isSameOffer(fastestOffer, bestRateOffer)
|
||||||
) {
|
) {
|
||||||
add(fastestOffer.copy(advantages = OnrampOfferAdvantages.Fastest))
|
add(
|
||||||
|
fastestOffer.copy(
|
||||||
|
advantages = OnrampOfferAdvantages.Fastest,
|
||||||
|
rateDif = if (bestRateOffer != null) fastestOffer.rateDif else null,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.tangem.domain.onramp.utils
|
||||||
|
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
internal fun calculateRateDif(currentTokenRate: BigDecimal, bestRate: BigDecimal?): BigDecimal? {
|
||||||
|
if (bestRate == null) return null
|
||||||
|
return BigDecimal.ONE - currentTokenRate / bestRate
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.tangem.features.onramp.alloffers
|
||||||
|
|
||||||
|
import com.tangem.core.decompose.factory.ComponentFactory
|
||||||
|
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
|
||||||
|
internal interface AllOffersComponent : ComposableBottomSheetComponent {
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val userWallet: UserWallet,
|
||||||
|
val cryptoCurrency: CryptoCurrency,
|
||||||
|
val onDismiss: () -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface Factory : ComponentFactory<Params, AllOffersComponent>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.tangem.features.onramp.alloffers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import com.tangem.core.decompose.context.AppComponentContext
|
||||||
|
import com.tangem.core.decompose.model.getOrCreateModel
|
||||||
|
import com.tangem.features.onramp.alloffers.model.AllOffersModel
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
|
||||||
|
internal class DefaultAllOffersComponent @AssistedInject constructor(
|
||||||
|
@Assisted context: AppComponentContext,
|
||||||
|
@Assisted params: AllOffersComponent.Params,
|
||||||
|
) : AllOffersComponent, AppComponentContext by context {
|
||||||
|
|
||||||
|
private val model: AllOffersModel = getOrCreateModel(params)
|
||||||
|
|
||||||
|
override fun dismiss() {
|
||||||
|
model.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun BottomSheet() {
|
||||||
|
// TODO to be continued in [REDACTED_TASK_KEY]
|
||||||
|
}
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory : AllOffersComponent.Factory {
|
||||||
|
override fun create(context: AppComponentContext, params: AllOffersComponent.Params): DefaultAllOffersComponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.tangem.features.onramp.alloffers.di
|
||||||
|
|
||||||
|
import com.tangem.core.decompose.di.ModelComponent
|
||||||
|
import com.tangem.core.decompose.model.Model
|
||||||
|
import com.tangem.features.onramp.alloffers.model.AllOffersModel
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.multibindings.ClassKey
|
||||||
|
import dagger.multibindings.IntoMap
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(ModelComponent::class)
|
||||||
|
internal interface AllOffersComponentModelModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ClassKey(AllOffersModel::class)
|
||||||
|
fun bindAllOffersModel(model: AllOffersModel): Model
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.tangem.features.onramp.alloffers.di
|
||||||
|
|
||||||
|
import com.tangem.features.onramp.alloffers.AllOffersComponent
|
||||||
|
import com.tangem.features.onramp.alloffers.DefaultAllOffersComponent
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
internal interface AllOffersComponentModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
fun bindAllOffersComponentFactory(factory: DefaultAllOffersComponent.Factory): AllOffersComponent.Factory
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.tangem.features.onramp.alloffers.model
|
||||||
|
|
||||||
|
import com.tangem.core.decompose.model.Model
|
||||||
|
import com.tangem.core.decompose.model.ParamsContainer
|
||||||
|
import com.tangem.features.onramp.alloffers.AllOffersComponent
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class AllOffersModel @Inject constructor(
|
||||||
|
override val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
paramsContainer: ParamsContainer,
|
||||||
|
) : Model() {
|
||||||
|
|
||||||
|
private val params: AllOffersComponent.Params = paramsContainer.require()
|
||||||
|
|
||||||
|
fun dismiss() {
|
||||||
|
params.onDismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import com.tangem.core.decompose.context.AppComponentContext
|
||||||
import com.tangem.core.decompose.context.childByContext
|
import com.tangem.core.decompose.context.childByContext
|
||||||
import com.tangem.core.decompose.model.getOrCreateModel
|
import com.tangem.core.decompose.model.getOrCreateModel
|
||||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||||
|
import com.tangem.features.onramp.alloffers.AllOffersComponent
|
||||||
import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent
|
import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent
|
||||||
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainBottomSheetConfig
|
import com.tangem.features.onramp.mainv2.entity.OnrampV2MainBottomSheetConfig
|
||||||
import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel
|
import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel
|
||||||
|
|
@ -26,6 +27,7 @@ internal class DefaultOnrampV2MainComponent @AssistedInject constructor(
|
||||||
@Assisted private val params: OnrampV2MainComponent.Params,
|
@Assisted private val params: OnrampV2MainComponent.Params,
|
||||||
private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory,
|
private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory,
|
||||||
private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory,
|
private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory,
|
||||||
|
private val allOffersComponentFactory: AllOffersComponent.Factory,
|
||||||
) : OnrampV2MainComponent, AppComponentContext by appComponentContext {
|
) : OnrampV2MainComponent, AppComponentContext by appComponentContext {
|
||||||
|
|
||||||
private val model: OnrampV2MainComponentModel = getOrCreateModel(params)
|
private val model: OnrampV2MainComponentModel = getOrCreateModel(params)
|
||||||
|
|
@ -67,6 +69,14 @@ internal class DefaultOnrampV2MainComponent @AssistedInject constructor(
|
||||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
OnrampV2MainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create(
|
||||||
|
context = childByContext(componentContext),
|
||||||
|
params = AllOffersComponent.Params(
|
||||||
|
userWallet = model.userWallet,
|
||||||
|
cryptoCurrency = params.cryptoCurrency,
|
||||||
|
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,5 @@ class DefaultOnrampV2MainFeatureToggle(
|
||||||
private val featureTogglesManager: FeatureTogglesManager,
|
private val featureTogglesManager: FeatureTogglesManager,
|
||||||
) : OnrampV2MainFeatureToggle {
|
) : OnrampV2MainFeatureToggle {
|
||||||
override val isOnrampNewMainEnabled: Boolean
|
override val isOnrampNewMainEnabled: Boolean
|
||||||
get() = featureTogglesManager.isFeatureEnabled("NEW_ONRAMP_RECEIVE_ENABLED")
|
get() = featureTogglesManager.isFeatureEnabled("NEW_ONRAMP_MAIN_ENABLED")
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.features.onramp.mainv2.entity
|
package com.tangem.features.onramp.mainv2.entity
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
|
@ -20,22 +21,32 @@ internal sealed interface OnrampOffersBlockUM {
|
||||||
|
|
||||||
data class Content(
|
data class Content(
|
||||||
override val isBlockVisible: Boolean,
|
override val isBlockVisible: Boolean,
|
||||||
val offers: ImmutableList<OnrampOfferUM>,
|
val recentOffer: OnrampOfferUM?,
|
||||||
|
val recommended: ImmutableList<OnrampOfferUM>,
|
||||||
|
val onrampAllOffersButtonConfig: OnrampAllOffersButtonConfig?,
|
||||||
) : OnrampOffersBlockUM
|
) : OnrampOffersBlockUM
|
||||||
}
|
}
|
||||||
|
|
||||||
internal data class OnrampOfferUM(
|
internal data class OnrampOfferUM(
|
||||||
val category: OnrampOfferCategory,
|
val category: OnrampOfferCategoryUM,
|
||||||
val advantages: OnrampOfferAdvantages,
|
val advantages: OnrampOfferAdvantagesUM,
|
||||||
val paymentMethod: OnrampPaymentMethod,
|
val paymentMethod: OnrampPaymentMethod,
|
||||||
val providerId: String,
|
val providerId: String,
|
||||||
val providerName: String,
|
val providerName: String,
|
||||||
|
val rate: String,
|
||||||
|
val diff: TextReference?,
|
||||||
|
val onBuyClicked: () -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
internal enum class OnrampOfferCategory {
|
internal enum class OnrampOfferCategoryUM {
|
||||||
RecentlyUsed, Recommended
|
RecentlyUsed, Recommended
|
||||||
}
|
}
|
||||||
|
|
||||||
internal enum class OnrampOfferAdvantages {
|
internal enum class OnrampOfferAdvantagesUM {
|
||||||
Default, BestRate, Fastest
|
Default, BestRate, Fastest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal data class OnrampAllOffersButtonConfig(
|
||||||
|
val title: TextReference,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
)
|
||||||
|
|
@ -10,4 +10,7 @@ sealed interface OnrampV2MainBottomSheetConfig {
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object CurrenciesList : OnrampV2MainBottomSheetConfig
|
data object CurrenciesList : OnrampV2MainBottomSheetConfig
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object AllOffers : OnrampV2MainBottomSheetConfig
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
package com.tangem.features.onramp.mainv2.entity.factory
|
||||||
|
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
|
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||||
|
import com.tangem.core.ui.format.bigdecimal.format
|
||||||
|
import com.tangem.core.ui.format.bigdecimal.percent
|
||||||
|
import com.tangem.domain.onramp.model.*
|
||||||
|
import com.tangem.features.onramp.impl.R
|
||||||
|
import com.tangem.features.onramp.main.entity.OnrampIntents
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.*
|
||||||
|
import com.tangem.utils.Provider
|
||||||
|
import com.tangem.utils.StringsSigns.MINUS
|
||||||
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
|
internal class OnrampOffersStateFactory(
|
||||||
|
private val currentStateProvider: Provider<OnrampV2MainComponentUM>,
|
||||||
|
private val onrampIntents: OnrampIntents,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun getOnShowOffersState(offers: List<OnrampOffersBlock>): OnrampV2MainComponentUM {
|
||||||
|
val currentState = currentStateProvider.invoke()
|
||||||
|
return when (currentState) {
|
||||||
|
is OnrampV2MainComponentUM.Content -> {
|
||||||
|
currentState.copy(
|
||||||
|
offersBlockState = mapOnrampOffersBlockToUM(offers),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is OnrampV2MainComponentUM.InitialLoading -> {
|
||||||
|
currentState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapOnrampOffersBlockToUM(offersBlocks: List<OnrampOffersBlock>): OnrampOffersBlockUM.Content {
|
||||||
|
val allOffersUM = mutableListOf<OnrampOfferUM>()
|
||||||
|
offersBlocks.map { block ->
|
||||||
|
block.offers.forEach { offer ->
|
||||||
|
when (val currentQuote = offer.quote) {
|
||||||
|
is OnrampQuote.AmountError,
|
||||||
|
is OnrampQuote.Error,
|
||||||
|
-> Unit
|
||||||
|
is OnrampQuote.Data -> {
|
||||||
|
allOffersUM.add(
|
||||||
|
OnrampOfferUM(
|
||||||
|
category = mapOfferCategoryDTOtoUM(block.category),
|
||||||
|
advantages = mapOfferAdvantagesDTOtoUM(offer.advantages),
|
||||||
|
paymentMethod = currentQuote.paymentMethod,
|
||||||
|
providerId = currentQuote.provider.id,
|
||||||
|
providerName = currentQuote.provider.info.name,
|
||||||
|
rate = currentQuote.toAmount.value.format {
|
||||||
|
crypto(
|
||||||
|
symbol = currentQuote.toAmount.symbol,
|
||||||
|
decimals = currentQuote.toAmount.decimals,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
diff = offer.rateDif?.let { diff ->
|
||||||
|
stringReference("$MINUS${diff.format { percent() }}")
|
||||||
|
},
|
||||||
|
onBuyClicked = {
|
||||||
|
onrampIntents.onBuyClick(
|
||||||
|
quote = OnrampProviderWithQuote.Data(
|
||||||
|
provider = offer.quote.provider,
|
||||||
|
paymentMethod = currentQuote.paymentMethod,
|
||||||
|
toAmount = currentQuote.toAmount,
|
||||||
|
fromAmount = currentQuote.fromAmount,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return OnrampOffersBlockUM.Content(
|
||||||
|
isBlockVisible = false,
|
||||||
|
recentOffer = allOffersUM.firstOrNull { it.category == OnrampOfferCategoryUM.RecentlyUsed },
|
||||||
|
recommended = allOffersUM.filter { it.category == OnrampOfferCategoryUM.Recommended }.toPersistentList(),
|
||||||
|
onrampAllOffersButtonConfig = if (offersBlocks.any { it.hasMoreOffers }) {
|
||||||
|
OnrampAllOffersButtonConfig(
|
||||||
|
title = TextReference.Res(R.string.onramp_all_offers_button_title),
|
||||||
|
onClick = { onrampIntents.openProviders() },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapOfferCategoryDTOtoUM(category: OnrampOfferCategory): OnrampOfferCategoryUM {
|
||||||
|
return when (category) {
|
||||||
|
OnrampOfferCategory.Recent -> OnrampOfferCategoryUM.RecentlyUsed
|
||||||
|
OnrampOfferCategory.Recommended -> OnrampOfferCategoryUM.Recommended
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapOfferAdvantagesDTOtoUM(advantages: OnrampOfferAdvantages): OnrampOfferAdvantagesUM {
|
||||||
|
return when (advantages) {
|
||||||
|
OnrampOfferAdvantages.Default -> OnrampOfferAdvantagesUM.Default
|
||||||
|
OnrampOfferAdvantages.BestRate -> OnrampOfferAdvantagesUM.BestRate
|
||||||
|
OnrampOfferAdvantages.Fastest -> OnrampOfferAdvantagesUM.Fastest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ import com.tangem.features.onramp.main.entity.OnrampLastUpdate
|
||||||
import com.tangem.features.onramp.mainv2.OnrampV2MainComponent
|
import com.tangem.features.onramp.mainv2.OnrampV2MainComponent
|
||||||
import com.tangem.features.onramp.mainv2.entity.*
|
import com.tangem.features.onramp.mainv2.entity.*
|
||||||
import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory
|
import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.factory.OnrampOffersStateFactory
|
||||||
import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2AmountStateFactory
|
import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2AmountStateFactory
|
||||||
import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2StateFactory
|
import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2StateFactory
|
||||||
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
|
import com.tangem.features.onramp.utils.sendOnrampErrorEvent
|
||||||
|
|
@ -32,7 +33,7 @@ import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList", "LargeClass")
|
||||||
internal class OnrampV2MainComponentModel @Inject constructor(
|
internal class OnrampV2MainComponentModel @Inject constructor(
|
||||||
override val dispatchers: CoroutineDispatcherProvider,
|
override val dispatchers: CoroutineDispatcherProvider,
|
||||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||||
|
|
@ -44,6 +45,7 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
||||||
private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase,
|
private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase,
|
||||||
private val fetchPairsUseCase: OnrampFetchPairsUseCase,
|
private val fetchPairsUseCase: OnrampFetchPairsUseCase,
|
||||||
private val amountInputManager: InputManager,
|
private val amountInputManager: InputManager,
|
||||||
|
private val getOnrampOffersUseCase: GetOnrampOffersUseCase,
|
||||||
paramsContainer: ParamsContainer,
|
paramsContainer: ParamsContainer,
|
||||||
getWalletsUseCase: GetWalletsUseCase,
|
getWalletsUseCase: GetWalletsUseCase,
|
||||||
) : Model(), OnrampIntents {
|
) : Model(), OnrampIntents {
|
||||||
|
|
@ -58,6 +60,13 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
||||||
OnrampAmountButtonUMStateFactory()
|
OnrampAmountButtonUMStateFactory()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val onrampOffersStateFactory: OnrampOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
OnrampOffersStateFactory(
|
||||||
|
currentStateProvider = Provider { _state.value },
|
||||||
|
onrampIntents = this,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private val stateFactory = OnrampV2StateFactory(
|
private val stateFactory = OnrampV2StateFactory(
|
||||||
currentStateProvider = Provider { _state.value },
|
currentStateProvider = Provider { _state.value },
|
||||||
cryptoCurrency = params.cryptoCurrency,
|
cryptoCurrency = params.cryptoCurrency,
|
||||||
|
|
@ -94,6 +103,7 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
||||||
subscribeToAmountChanges()
|
subscribeToAmountChanges()
|
||||||
subscribeToCountryAndCurrencyUpdates()
|
subscribeToCountryAndCurrencyUpdates()
|
||||||
subscribeToQuotesUpdate()
|
subscribeToQuotesUpdate()
|
||||||
|
subscribeOnOffers()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|
@ -117,11 +127,19 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBuyClick(quote: OnrampProviderWithQuote.Data) {
|
override fun onBuyClick(quote: OnrampProviderWithQuote.Data) {
|
||||||
// TODO in [REDACTED_TASK_KEY] to be continued
|
val currentContentState = state.value as? OnrampV2MainComponentUM.Content ?: return
|
||||||
|
analyticsEventHandler.send(
|
||||||
|
OnrampAnalyticsEvent.OnBuyClick(
|
||||||
|
providerName = quote.provider.info.name,
|
||||||
|
currency = currentContentState.amountBlockState.currencyUM.code,
|
||||||
|
tokenSymbol = params.cryptoCurrency.symbol,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
params.openRedirectPage(quote)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openProviders() {
|
override fun openProviders() {
|
||||||
// TODO in [REDACTED_TASK_KEY] to be continued
|
bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.AllOffers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRefresh() {
|
override fun onRefresh() {
|
||||||
|
|
@ -172,6 +190,20 @@ internal class OnrampV2MainComponentModel @Inject constructor(
|
||||||
router.pop()
|
router.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun subscribeOnOffers() = modelScope.launch {
|
||||||
|
getOnrampOffersUseCase.invoke(
|
||||||
|
userWalletId = userWallet.walletId,
|
||||||
|
cryptoCurrencyId = params.cryptoCurrency.id,
|
||||||
|
).collectLatest { maybeOffers ->
|
||||||
|
maybeOffers.fold(
|
||||||
|
ifLeft = ::handleOnrampError,
|
||||||
|
ifRight = { offers ->
|
||||||
|
_state.update { onrampOffersStateFactory.getOnShowOffersState(offers) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun subscribeToAmountChanges() = modelScope.launch {
|
private fun subscribeToAmountChanges() = modelScope.launch {
|
||||||
amountInputManager.query
|
amountInputManager.query
|
||||||
.filter(String::isNotEmpty)
|
.filter(String::isNotEmpty)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,42 @@
|
||||||
package com.tangem.features.onramp.mainv2.ui
|
package com.tangem.features.onramp.mainv2.ui
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawWithCache
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import coil.compose.SubcomposeAsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import com.tangem.core.ui.components.*
|
||||||
|
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||||
|
import com.tangem.core.ui.extensions.*
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
|
import com.tangem.domain.onramp.model.OnrampPaymentMethod
|
||||||
|
import com.tangem.domain.onramp.model.PaymentMethodType
|
||||||
|
import com.tangem.features.onramp.impl.R
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM
|
||||||
|
import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM
|
||||||
import com.tangem.features.onramp.mainv2.entity.OnrampOffersBlockUM
|
import com.tangem.features.onramp.mainv2.entity.OnrampOffersBlockUM
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun OnrampOffersContent(state: OnrampOffersBlockUM) {
|
internal fun OnrampOffersContent(state: OnrampOffersBlockUM) {
|
||||||
|
|
@ -23,10 +52,330 @@ internal fun OnrampOffersContent(state: OnrampOffersBlockUM) {
|
||||||
),
|
),
|
||||||
label = "Offers block animation",
|
label = "Offers block animation",
|
||||||
) {
|
) {
|
||||||
Text(
|
if (state is OnrampOffersBlockUM.Content) {
|
||||||
text = "Some offers",
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
style = TangemTheme.typography.head,
|
state.recentOffer?.let { recentOffer ->
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 12.dp),
|
||||||
|
text = stringResourceSafe(R.string.onramp_recently_used_title),
|
||||||
|
style = TangemTheme.typography.subtitle2,
|
||||||
|
color = TangemTheme.colors.text.tertiary,
|
||||||
|
)
|
||||||
|
|
||||||
|
SpacerH(8.dp)
|
||||||
|
|
||||||
|
Offer(recentOffer)
|
||||||
|
|
||||||
|
SpacerH(16.dp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 12.dp),
|
||||||
|
text = stringResourceSafe(R.string.onramp_recommended_title),
|
||||||
|
style = TangemTheme.typography.subtitle2,
|
||||||
|
color = TangemTheme.colors.text.tertiary,
|
||||||
|
)
|
||||||
|
|
||||||
|
SpacerH(8.dp)
|
||||||
|
|
||||||
|
state.recommended.fastForEach {
|
||||||
|
Offer(it)
|
||||||
|
SpacerH(8.dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.onrampAllOffersButtonConfig?.let {
|
||||||
|
SpacerH(12.dp)
|
||||||
|
SecondaryButton(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
text = it.title.resolveReference(),
|
||||||
|
onClick = it.onClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Offer(onrampOfferUM: OnrampOfferUM, modifier: Modifier = Modifier) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = TangemTheme.colors.background.action,
|
||||||
|
shape = RoundedCornerShape(14.dp),
|
||||||
|
)
|
||||||
|
.padding(12.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
|
OfferHeader(advantage = onrampOfferUM.advantages)
|
||||||
|
RateBlock(rate = onrampOfferUM.rate, diff = onrampOfferUM.diff)
|
||||||
|
}
|
||||||
|
SpacerWMax()
|
||||||
|
SecondaryButton(
|
||||||
|
size = TangemButtonSize.RoundedAction,
|
||||||
|
text = stringResourceSafe(R.string.common_buy),
|
||||||
|
onClick = onrampOfferUM.onBuyClicked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SpacerH(10.dp)
|
||||||
|
HorizontalDivider(color = TangemTheme.colors.stroke.primary)
|
||||||
|
SpacerH(12.dp)
|
||||||
|
PaymentBlockInOffer(
|
||||||
|
paymentMethod = onrampOfferUM.paymentMethod,
|
||||||
|
providerName = onrampOfferUM.providerName,
|
||||||
)
|
)
|
||||||
// TODO in [REDACTED_TASK_KEY] to be continued
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun OfferHeader(advantage: OnrampOfferAdvantagesUM) {
|
||||||
|
when (advantage) {
|
||||||
|
OnrampOfferAdvantagesUM.Default -> {
|
||||||
|
Text(
|
||||||
|
text = stringResourceSafe(R.string.onramp_title_you_get),
|
||||||
|
style = TangemTheme.typography.caption2,
|
||||||
|
color = TangemTheme.colors.text.tertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
OnrampOfferAdvantagesUM.BestRate -> {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_best_rate_16),
|
||||||
|
tint = TangemTheme.colors.icon.accent,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResourceSafe(R.string.express_provider_best_rate),
|
||||||
|
style = TangemTheme.typography.caption1,
|
||||||
|
color = TangemTheme.colors.icon.accent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OnrampOfferAdvantagesUM.Fastest -> {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_fastest_16),
|
||||||
|
tint = TangemTheme.colors.icon.attention,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResourceSafe(R.string.onramp_offer_type_fastet),
|
||||||
|
style = TangemTheme.typography.caption1,
|
||||||
|
color = TangemTheme.colors.icon.attention,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RateBlock(rate: String, diff: TextReference?) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = rate,
|
||||||
|
style = TangemTheme.typography.subtitle1,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
diff?.let {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = TangemTheme.colors.text.warning.copy(alpha = 0.1f),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
)
|
||||||
|
.padding(horizontal = 4.dp),
|
||||||
|
text = it.resolveReference(),
|
||||||
|
style = TangemTheme.typography.caption1,
|
||||||
|
color = TangemTheme.colors.text.warning,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PaymentBlockInOffer(paymentMethod: OnrampPaymentMethod, providerName: String) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_clock_24),
|
||||||
|
tint = TangemTheme.colors.icon.informative,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
SpacerW(2.dp)
|
||||||
|
|
||||||
|
TimingBlock(speed = paymentMethod.type.getProcessingSpeed())
|
||||||
|
|
||||||
|
SpacerW(6.dp)
|
||||||
|
|
||||||
|
DrawDot(color = TangemTheme.colors.text.tertiary)
|
||||||
|
|
||||||
|
SpacerW(6.dp)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = providerName,
|
||||||
|
style = TangemTheme.typography.caption2,
|
||||||
|
color = TangemTheme.colors.text.tertiary,
|
||||||
|
)
|
||||||
|
|
||||||
|
SpacerWMax()
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResourceSafe(R.string.onramp_pay_with),
|
||||||
|
style = TangemTheme.typography.caption2,
|
||||||
|
color = TangemTheme.colors.text.tertiary,
|
||||||
|
)
|
||||||
|
|
||||||
|
SpacerW(4.dp)
|
||||||
|
|
||||||
|
SubcomposeAsyncImage(
|
||||||
|
modifier = Modifier.sizeIn(maxWidth = 38.dp, maxHeight = 16.dp),
|
||||||
|
model = ImageRequest.Builder(context = LocalContext.current)
|
||||||
|
.data(paymentMethod.imageUrl)
|
||||||
|
.crossfade(enable = true)
|
||||||
|
.allowHardware(false)
|
||||||
|
.build(),
|
||||||
|
loading = {
|
||||||
|
TextShimmer(
|
||||||
|
style = TangemTheme.typography.body1,
|
||||||
|
modifier = Modifier.width(40.dp),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TimingBlock(speed: PaymentMethodType.PaymentSpeed) {
|
||||||
|
val timingText = when (speed) {
|
||||||
|
PaymentMethodType.PaymentSpeed.Instant -> {
|
||||||
|
stringResourceSafe(id = R.string.onramp_instant_status)
|
||||||
|
}
|
||||||
|
PaymentMethodType.PaymentSpeed.FewMin -> {
|
||||||
|
stringResourceSafe(
|
||||||
|
id = R.string.onramp_timing_minutes,
|
||||||
|
FEW_MINS_VALUE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
PaymentMethodType.PaymentSpeed.FewDays -> {
|
||||||
|
pluralStringResourceSafe(
|
||||||
|
id = R.plurals.onramp_timing_days,
|
||||||
|
count = FEW_DAYS_VALUE,
|
||||||
|
FEW_DAYS_VALUE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
PaymentMethodType.PaymentSpeed.PlentyDays -> {
|
||||||
|
pluralStringResourceSafe(
|
||||||
|
id = R.plurals.onramp_timing_days,
|
||||||
|
count = PLENTY_DAYS_VALUE,
|
||||||
|
PLENTY_DAYS_VALUE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
PaymentMethodType.PaymentSpeed.Unknown -> ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = timingText,
|
||||||
|
style = TangemTheme.typography.caption2,
|
||||||
|
color = TangemTheme.colors.text.tertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DrawDot(color: Color) {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(4.dp)
|
||||||
|
.drawWithCache {
|
||||||
|
val radius = size.minDimension / 2f
|
||||||
|
onDrawBehind {
|
||||||
|
drawCircle(
|
||||||
|
color = color,
|
||||||
|
radius = radius,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// will be hardcoded till server will be ready to provide this values
|
||||||
|
private const val FEW_MINS_VALUE = "3-5"
|
||||||
|
private const val FEW_DAYS_VALUE = 3
|
||||||
|
private const val PLENTY_DAYS_VALUE = 5
|
||||||
|
|
||||||
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
|
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun OnrampOffersContentPreview() {
|
||||||
|
val state = OnrampOffersBlockUM.Content(
|
||||||
|
isBlockVisible = true,
|
||||||
|
recentOffer = OnrampOfferUM(
|
||||||
|
category = OnrampOfferCategoryUM.RecentlyUsed,
|
||||||
|
advantages = OnrampOfferAdvantagesUM.Default,
|
||||||
|
paymentMethod = OnrampPaymentMethod(
|
||||||
|
id = "card",
|
||||||
|
name = "Card",
|
||||||
|
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||||
|
type = PaymentMethodType.CARD,
|
||||||
|
),
|
||||||
|
providerId = "providerId3",
|
||||||
|
providerName = "Simplex",
|
||||||
|
rate = "0,00045334 BTC",
|
||||||
|
diff = stringReference("–27%"),
|
||||||
|
onBuyClicked = {},
|
||||||
|
),
|
||||||
|
recommended = persistentListOf(
|
||||||
|
OnrampOfferUM(
|
||||||
|
category = OnrampOfferCategoryUM.Recommended,
|
||||||
|
advantages = OnrampOfferAdvantagesUM.BestRate,
|
||||||
|
paymentMethod = OnrampPaymentMethod(
|
||||||
|
id = "card",
|
||||||
|
name = "Card",
|
||||||
|
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||||
|
type = PaymentMethodType.CARD,
|
||||||
|
),
|
||||||
|
providerId = "providerId1",
|
||||||
|
providerName = "Simplex",
|
||||||
|
rate = "0,0245334 BTC",
|
||||||
|
diff = null,
|
||||||
|
onBuyClicked = {},
|
||||||
|
),
|
||||||
|
OnrampOfferUM(
|
||||||
|
category = OnrampOfferCategoryUM.Recommended,
|
||||||
|
advantages = OnrampOfferAdvantagesUM.Fastest,
|
||||||
|
paymentMethod = OnrampPaymentMethod(
|
||||||
|
id = "card",
|
||||||
|
name = "Card",
|
||||||
|
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png",
|
||||||
|
type = PaymentMethodType.CARD,
|
||||||
|
),
|
||||||
|
providerId = "providerId2",
|
||||||
|
providerName = "Simplex",
|
||||||
|
rate = "0,00145334 BTC",
|
||||||
|
diff = stringReference("–0.07%"),
|
||||||
|
onBuyClicked = {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onrampAllOffersButtonConfig = null,
|
||||||
|
)
|
||||||
|
TangemThemePreview {
|
||||||
|
OnrampOffersContent(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.tangem.features.onramp.mainv2.ui
|
package com.tangem.features.onramp.mainv2.ui
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
|
@ -51,6 +52,7 @@ internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modif
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.background(
|
.background(
|
||||||
color = TangemTheme.colors.background.action,
|
color = TangemTheme.colors.background.action,
|
||||||
shape = RoundedCornerShape(size = TangemTheme.dimens.radius16),
|
shape = RoundedCornerShape(size = TangemTheme.dimens.radius16),
|
||||||
|
|
@ -61,16 +63,14 @@ internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modif
|
||||||
) {
|
) {
|
||||||
OnrampHeaderTitle()
|
OnrampHeaderTitle()
|
||||||
|
|
||||||
SpacerH(12.dp)
|
|
||||||
|
|
||||||
OnrampAmountField(
|
OnrampAmountField(
|
||||||
amountField = state.amountBlockState.amountFieldModel,
|
amountField = state.amountBlockState.amountFieldModel,
|
||||||
currencyCode = state.amountBlockState.currencyUM.code,
|
currencyCode = state.amountBlockState.currencyUM.code,
|
||||||
)
|
)
|
||||||
|
|
||||||
SpacerH(8.dp)
|
AnimatedVisibility(!state.offersBlockState.isBlockVisible) {
|
||||||
|
OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel)
|
||||||
OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel)
|
}
|
||||||
|
|
||||||
SpacerH(20.dp)
|
SpacerH(20.dp)
|
||||||
|
|
||||||
|
|
@ -115,7 +115,8 @@ private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: Strin
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.focusRequester(requester)
|
.focusRequester(requester)
|
||||||
.padding(
|
.padding(
|
||||||
top = TangemTheme.dimens.spacing24,
|
top = TangemTheme.dimens.spacing8,
|
||||||
|
bottom = TangemTheme.dimens.spacing4,
|
||||||
start = TangemTheme.dimens.spacing12,
|
start = TangemTheme.dimens.spacing12,
|
||||||
end = TangemTheme.dimens.spacing12,
|
end = TangemTheme.dimens.spacing12,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ import com.tangem.core.decompose.context.childByContext
|
||||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||||
import com.tangem.features.onramp.component.OnrampComponent
|
import com.tangem.features.onramp.component.OnrampComponent
|
||||||
import com.tangem.features.onramp.main.OnrampMainComponent
|
import com.tangem.features.onramp.main.OnrampMainComponent
|
||||||
|
import com.tangem.features.onramp.mainv2.OnrampV2MainComponent
|
||||||
|
import com.tangem.features.onramp.mainv2.OnrampV2MainFeatureToggle
|
||||||
import com.tangem.features.onramp.redirect.OnrampRedirectComponent
|
import com.tangem.features.onramp.redirect.OnrampRedirectComponent
|
||||||
import com.tangem.features.onramp.root.entity.OnrampChild
|
import com.tangem.features.onramp.root.entity.OnrampChild
|
||||||
import com.tangem.features.onramp.settings.OnrampSettingsComponent
|
import com.tangem.features.onramp.settings.OnrampSettingsComponent
|
||||||
|
|
@ -24,13 +26,15 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
|
|
||||||
@Suppress("UnusedPrivateMember")
|
@Suppress("LongParameterList")
|
||||||
internal class DefaultOnrampComponent @AssistedInject constructor(
|
internal class DefaultOnrampComponent @AssistedInject constructor(
|
||||||
@Assisted context: AppComponentContext,
|
@Assisted context: AppComponentContext,
|
||||||
@Assisted private val params: OnrampComponent.Params,
|
@Assisted private val params: OnrampComponent.Params,
|
||||||
private val settingsComponentFactory: OnrampSettingsComponent.Factory,
|
private val settingsComponentFactory: OnrampSettingsComponent.Factory,
|
||||||
private val onrampMainComponentFactory: OnrampMainComponent.Factory,
|
private val onrampMainComponentFactory: OnrampMainComponent.Factory,
|
||||||
|
private val onrampMainV2ComponentFactory: OnrampV2MainComponent.Factory,
|
||||||
private val onrampRedirectComponentFactory: OnrampRedirectComponent.Factory,
|
private val onrampRedirectComponentFactory: OnrampRedirectComponent.Factory,
|
||||||
|
private val onrampV2MainFeatureToggle: OnrampV2MainFeatureToggle,
|
||||||
) : OnrampComponent, AppComponentContext by context {
|
) : OnrampComponent, AppComponentContext by context {
|
||||||
|
|
||||||
private val navigation = StackNavigation<OnrampChild>()
|
private val navigation = StackNavigation<OnrampChild>()
|
||||||
|
|
@ -66,24 +70,44 @@ internal class DefaultOnrampComponent @AssistedInject constructor(
|
||||||
onBack = navigation::pop,
|
onBack = navigation::pop,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
OnrampChild.Main -> onrampMainComponentFactory.create(
|
OnrampChild.Main -> if (onrampV2MainFeatureToggle.isOnrampNewMainEnabled) {
|
||||||
context = childByContext(componentContext),
|
onrampMainV2ComponentFactory.create(
|
||||||
params = OnrampMainComponent.Params(
|
context = childByContext(componentContext),
|
||||||
userWalletId = params.userWalletId,
|
params = OnrampV2MainComponent.Params(
|
||||||
cryptoCurrency = params.cryptoCurrency,
|
userWalletId = params.userWalletId,
|
||||||
openSettings = { navigation.push(OnrampChild.Settings) },
|
cryptoCurrency = params.cryptoCurrency,
|
||||||
source = params.source,
|
openSettings = { navigation.push(OnrampChild.Settings) },
|
||||||
openRedirectPage = {
|
source = params.source,
|
||||||
navigation.push(
|
openRedirectPage = {
|
||||||
OnrampChild.RedirectPage(
|
navigation.push(
|
||||||
quote = it,
|
OnrampChild.RedirectPage(
|
||||||
cryptoCurrency = params.cryptoCurrency,
|
quote = it,
|
||||||
),
|
cryptoCurrency = params.cryptoCurrency,
|
||||||
)
|
),
|
||||||
},
|
)
|
||||||
launchSepa = params.launchSepa,
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
onrampMainComponentFactory.create(
|
||||||
|
context = childByContext(componentContext),
|
||||||
|
params = OnrampMainComponent.Params(
|
||||||
|
userWalletId = params.userWalletId,
|
||||||
|
cryptoCurrency = params.cryptoCurrency,
|
||||||
|
openSettings = { navigation.push(OnrampChild.Settings) },
|
||||||
|
source = params.source,
|
||||||
|
openRedirectPage = {
|
||||||
|
navigation.push(
|
||||||
|
OnrampChild.RedirectPage(
|
||||||
|
quote = it,
|
||||||
|
cryptoCurrency = params.cryptoCurrency,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
launchSepa = params.launchSepa,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
is OnrampChild.RedirectPage -> onrampRedirectComponentFactory.create(
|
is OnrampChild.RedirectPage -> onrampRedirectComponentFactory.create(
|
||||||
context = childByContext(componentContext),
|
context = childByContext(componentContext),
|
||||||
params = OnrampRedirectComponent.Params(
|
params = OnrampRedirectComponent.Params(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue