diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index 8c5ddc6502..db4247ca10 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -10,6 +10,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +@Suppress("TooManyFunctions") @Module @InstallIn(SingletonComponent::class) internal object OnrampDomainModule { @@ -125,4 +126,35 @@ internal object OnrampDomainModule { fun provideGetOnrampQuotesUseCase(onrampRepository: OnrampRepository): GetOnrampQuotesUseCase { return GetOnrampQuotesUseCase(onrampRepository) } + + @Provides + @Singleton + fun provideGetOnrampSelectedPaymentMethodUseCase( + onrampRepository: OnrampRepository, + ): GetOnrampSelectedPaymentMethodUseCase { + return GetOnrampSelectedPaymentMethodUseCase(onrampRepository) + } + + @Provides + @Singleton + fun provideGetOnrampProviderWithQuoteUseCase( + onrampRepository: OnrampRepository, + ): GetOnrampProviderWithQuoteUseCase { + return GetOnrampProviderWithQuoteUseCase(onrampRepository) + } + + @Provides + @Singleton + fun provideOnrampSaveSelectedPaymentMethod(onrampRepository: OnrampRepository): OnrampSaveSelectedPaymentMethod { + return OnrampSaveSelectedPaymentMethod(onrampRepository) + } + + @Provides + @Singleton + fun provideGetOnrampRedirectUrlUseCase( + onrampRepository: OnrampRepository, + transactionRepository: OnrampTransactionRepository, + ): GetOnrampRedirectUrlUseCase { + return GetOnrampRedirectUrlUseCase(onrampRepository, transactionRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index b68935424e..be04d0a426 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -56,14 +56,14 @@ object TradeCryptoMiddleware { private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) { val isOnrampEnabled = store.inject(DaggerGraphState::onrampFeatureToggles).isFeatureEnabled if (isOnrampEnabled) { - proceedWithOnramp(action.cryptoCurrencyStatus.currency) + proceedWithOnramp(action.userWallet.walletId, action.cryptoCurrencyStatus.currency) } else { proceedWithLegacyBuyAction(state, action) } } - private fun proceedWithOnramp(cryptoCurrency: CryptoCurrency) { - store.dispatchNavigationAction { push(AppRoute.Onramp(cryptoCurrency)) } + private fun proceedWithOnramp(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency) { + store.dispatchNavigationAction { push(AppRoute.Onramp(userWalletId = userWalletId, currency = cryptoCurrency)) } } private fun proceedWithLegacyBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) { diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 46a9c70161..59331b1445 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -197,7 +197,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.Onramp -> { route.asComponentChild( contextProvider = contextProvider(route, contextFactory), - params = OnrampComponent.Params(route.currency), + params = OnrampComponent.Params(userWalletId = route.userWalletId, cryptoCurrency = route.currency), componentFactory = onrampComponentFactory, ) } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index da4f005d32..dbc94e9340 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -295,7 +295,10 @@ sealed class AppRoute(val path: String) : Route { } @Serializable - data class Onramp(val currency: CryptoCurrency) : AppRoute(path = "/onramp/${currency.symbol}"), RouteBundleParams { + data class Onramp( + val userWalletId: UserWalletId, + val currency: CryptoCurrency, + ) : AppRoute(path = "/onramp/${userWalletId.stringValue}/${currency.symbol}"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampDataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampDataResponse.kt index 3b27e36068..3146bb435d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampDataResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampDataResponse.kt @@ -20,6 +20,18 @@ data class OnrampDataResponse( val signature: String, ) +@JsonClass(generateAdapter = true) +data class OnrampDataJson( + @Json(name = "widgetUrl") + val widgetUrl: String, + + @Json(name = "externalTxId") + val externalTxId: String, + + @Json(name = "externalTxUrl") + val externalTxUrl: String?, +) + @JsonClass(generateAdapter = true) data class OnrampTxDetails( @Json(name = "fromCurrencyCode") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampQuoteResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampQuoteResponse.kt index 509352375a..46ca99bbb6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampQuoteResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampQuoteResponse.kt @@ -39,8 +39,8 @@ data class OnrampQuoteResponse( val maxFromAmount: String, @Json(name = "minToAmount") - val minToAmount: String, + val minToAmount: String?, @Json(name = "maxToAmount") - val maxToAmount: String, + val maxToAmount: String?, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/onramp/paymentmethods/OnrampPaymentMethodsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/onramp/paymentmethods/OnrampPaymentMethodsStore.kt index fc1eca40d4..6626726313 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/onramp/paymentmethods/OnrampPaymentMethodsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/onramp/paymentmethods/OnrampPaymentMethodsStore.kt @@ -1,11 +1,14 @@ package com.tangem.datasource.local.onramp.paymentmethods import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO +import kotlinx.coroutines.flow.Flow interface OnrampPaymentMethodsStore { suspend fun getSyncOrNull(key: String): List? + fun get(key: String): Flow> + suspend fun store(key: String, value: List) suspend fun contains(key: String): Boolean diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index d5b6d3041c..0ce8ca7272 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -296,7 +296,7 @@ Empfohlen Gekauft %s Kauf %s - Kauf %s... + Kauf %s… Keine Token gefunden. Bitte versuche eine andere Anfrage ID: %s Transaktions-ID kopiert @@ -590,8 +590,11 @@ Einstellungen Du hast keinen Zugriff auf deine Kamera gewährt Kamerazugriff verweigert + Kein Memo erforderlich %1$s ( %2$s ) im %3$s Netzwerk + Das Senden einer anderen Währung führt zu deren irreversiblem Verlust. Sende nur %s an diese Adresse. Der Versand einer anderen Währung führt zu ihrem unwiderruflichen Verlust. + Sende nur %s an diese Adresse. Überweise Geld von einem anderen Wallet oder einer anderen Börse Teilnehmen Die Informationen zum Empfehlungsprogramm konnten nicht geladen werden. Bitte versuche es später noch einmal. diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index c65acf41fa..38c53c5104 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -432,7 +432,6 @@ El valor teórico total de una criptomoneda si todas las monedas que podrían existir estuvieran en circulación, incluidas las que actualmente no circulan. Valoración totalmente diluida Dato de Genesis - Vacío Alto Titulares El cambio en el número de poseedores de tokens dentro de un período de tiempo específico @@ -443,7 +442,6 @@ El cambio en la cantidad de liquidez disponible para el token durante el período de tiempo especificado Liquidez Índice de liquidez - Vacío Listado en Bajo Capital. de mercado @@ -517,12 +515,12 @@ Creando un backup Leer más sobre seed phrase - Vacío + Escriba estas %d palabras en el orden que se indica a continuación y guárdelas en un lugar seguro y secreto. Su seed phrase - Vacío + %d palabras Para importar su billetera, ingrese su seed phrase en el campo de abajo diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 1d37bbc265..e6010bdeda 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -370,7 +370,7 @@ Choisissez un portefeuille Le portefeuille ne prend pas en charge plus d\'un réseau Pour acheter, échanger ou recevoir cet actif, ajoutez-le à votre portefeuille - Cet actif n\'est plus pris en charge dans le portefeuille + Cet actif n\'est pas pris en charge dans le portefeuille Cet actif n\'est pas disponible pour ce portefeuille Ajouter au portfolio Ajoutez un jeton @@ -432,7 +432,6 @@ La valeur théorique totale d\'une crypto-monnaie si toutes les pièces qui pourraient exister étaient en circulation, y compris celles qui ne circulent pas actuellement Valorisation entièrement diluée Date de la Genesis - Vide Haut Détenteurs L\'évolution du nombre de détenteurs de jetons au cours d\'une période donnée @@ -443,7 +442,6 @@ Le changement dans la quantité de liquidité disponible pour le jeton pendant la période spécifiée Liquidité Indice de liquidité - Vide Listé sur Faible Cap. boursière @@ -517,12 +515,12 @@ Sauvegarde en cours En savoir plus sur les seed phrases - Vide + Écrivez ces %d mots dans l\'ordre indiqué ci-dessous et conservez-les dans un endroit sûr et secret. Votre seed phrase - Vide + %d mots Pour importer votre portefeuille, entrez votre seed phrase dans le champ ci-dessous @@ -607,9 +605,9 @@ pour le portefeuille %d pour les portefeuilles %d - Obtiendras ^^%1$s^^ pour chaque portefeuille acheté par votre ami sur votre adresse de réseau %2$s %3$s ^^30 jours après^^ + Obtiendrez ^^%1$s^^ pour chaque portefeuille acheté par votre ami sur votre adresse de réseau %2$s %3$s ^^30 jours après^^ Vous - Obtiendrez + Obtiendra lors de l\'achat d\'un portefeuille sur tangem.com %s réduction Votre ami diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 9f3c536b40..fa0b45908d 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -582,8 +582,11 @@ 設定 カメラへのアクセスを許可していません。 カメラへのアクセスが拒否されました + メモ不要 %3$sネットワーク上の%1$s ( %2$s ) + 他の暗号資産を送信すると、取り返しのつかない損失が発生します。 このアドレスには%sのみを送金してください。他のトークンを送信すると、取り返しのつかない損失が発生します。 + このアドレスには%sのみを送信してください。 ウォレットや取引所から資金を送金する 参加する 紹介プログラムに関する情報を読み込めませんでした。しばらくしてからもう一度お試しください。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 94815eb793..dfbc4e21cc 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -297,6 +297,8 @@ Недоступно для этой пары Требуется разрешение Рекомендовано + Покупка %s + Покупка %s... Токены не найдены. Пожалуйста, попробуйте другой запрос ID: %s ID транзакции скопирован @@ -889,6 +891,7 @@ Обменять Недоступно для покупки Недоступно для продажи + Недоступно для обмена с %s контракт: %s У вас еще нет транзакций Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию. diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index b425f91e95..25e1a4419d 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -301,7 +301,7 @@ Потрібен дозвіл Рекомендовано Купив %s - Купівля %s... + Купівля %s Купівля %s... Токенів не знайдено. Будь ласка, спробуйте інший запит ID: %s diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 60cf7b6306..5ee169bd92 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ Token not found in your portfolio? Check the Markets to find and add it for purchase - Token not found in your portfolio? Check the Markets to find and add it for sell + Token not found in your portfolio? Check the Markets to find and add it for selling Choose the Token Token not found in your portfolio? Check the Markets to find and add it for swap There are no available tokens to swap with the selected token. Please choose another one. @@ -565,6 +565,7 @@ Access code restore Identical cards Access code + Providers facilitate transactions, ensuring smooth and efficient token exchanges Search by country Unavailable Search by currency @@ -590,8 +591,11 @@ Settings You have not given access to your camera Camera access denied + No memo required %1$s (%2$s) on %3$s network + Sending any other currency will result in its irreversible loss. Send only %s to this address. Sending any other currency will result in its irreversible loss. + Send only %s to this address. Transfer funds from any wallet or exchange Participate Failed to load the information about the referral program. Please try again later. diff --git a/data/onramp/build.gradle.kts b/data/onramp/build.gradle.kts index 2745c0d132..51477ea895 100644 --- a/data/onramp/build.gradle.kts +++ b/data/onramp/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { /** Domain modules */ implementation(projects.domain.onramp) + implementation(projects.domain.legacy) // region DI 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 e6d6c8e177..aa7e56fae3 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 @@ -1,5 +1,7 @@ package com.tangem.data.onramp +import com.squareup.moshi.Moshi +import com.tangem.blockchain.extensions.toBigDecimalOrDefault import com.tangem.data.common.api.safeApiCall import com.tangem.data.onramp.converters.CountryConverter import com.tangem.data.onramp.converters.CurrencyConverter @@ -12,13 +14,16 @@ import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.TangemExpressValues import com.tangem.datasource.api.express.models.response.ExchangeProvider +import com.tangem.datasource.api.express.models.response.ExchangeProviderType import com.tangem.datasource.api.onramp.OnrampApi import com.tangem.datasource.api.onramp.models.common.OnrampDestinationDTO import com.tangem.datasource.api.onramp.models.request.OnrampPairsRequest +import com.tangem.datasource.api.onramp.models.response.OnrampDataJson import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO +import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore @@ -28,20 +33,26 @@ import com.tangem.datasource.local.preferences.utils.getObject import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.domain.onramp.model.* +import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.withContext +import org.joda.time.DateTime import timber.log.Timber import java.math.BigDecimal +import java.util.UUID -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass", "TooManyFunctions") internal class DefaultOnrampRepository( private val onrampApi: OnrampApi, private val expressApi: TangemExpressApi, @@ -51,12 +62,16 @@ internal class DefaultOnrampRepository( private val pairsStore: OnrampPairsStore, private val quotesStore: OnrampQuotesStore, private val quotesErrorConverter: OnrampQuotesErrorConverter, + private val walletManagersFacade: WalletManagersFacade, + private val dataSignatureVerifier: DataSignatureVerifier, + moshi: Moshi, ) : OnrampRepository { private val currencyConverter = CurrencyConverter() private val countryConverter = CountryConverter(currencyConverter) private val statusConverter = StatusConverter() private val paymentMethodsConverter = PaymentMethodConverter() + private val onrampDataAdapter = moshi.adapter(OnrampDataJson::class.java) override suspend fun getCurrencies(): List = withContext(dispatchers.io) { onrampApi.getCurrencies() @@ -194,14 +209,19 @@ internal class DefaultOnrampRepository( providerId = provider.id, ).bind() OnrampQuote.Data( - fromAmount = convertToBigDecimal(response.fromAmount), - toAmount = convertToBigDecimal(response.toAmount), - minFromAmount = convertToBigDecimal(response.minFromAmount), - maxFromAmount = convertToBigDecimal(response.maxFromAmount), + fromAmount = OnrampAmount( + value = response.fromAmount + .toBigDecimalOrDefault() + .movePointLeft(currency.precision), + decimals = currency.precision, + symbol = amount.currencySymbol, + ), + toAmount = convertToAmount(response.toAmount, cryptoCurrency), + minFromAmount = convertToAmount(response.minFromAmount, cryptoCurrency), + maxFromAmount = convertToAmount(response.maxFromAmount, cryptoCurrency), paymentMethod = paymentMethod, provider = provider, ) - null }, onError = { error -> if (error is ApiResponseError.HttpException) { @@ -230,6 +250,69 @@ internal class DefaultOnrampRepository( return quotesStore.get(QUOTES_KEY) } + override suspend fun getQuotesSync(): List? { + return quotesStore.getSyncOrNull(QUOTES_KEY) + } + + override suspend fun getOnrampData( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + quote: OnrampProviderWithQuote.Data, + ): OnrampTransaction = withContext(dispatchers.io) { + try { + val address = requireNotNull( + value = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network), + lazyMessage = { "Address must not be null" }, + ) + val fromAmountString = quote.fromAmount.value.movePointRight(quote.fromAmount.decimals).toString() + val currency = requireNotNull(getDefaultCurrencySync()) { "Default currency must not be null" } + val country = requireNotNull(getDefaultCountrySync()) { "Default country must not be null" } + val data = safeApiCall( + call = { + onrampApi.getData( + fromCurrencyCode = currency.code, + toContractAddress = cryptoCurrency.getContractAddress(), + toNetwork = cryptoCurrency.network.backendId, + paymentMethod = quote.paymentMethod.id, + countryCode = country.code, + fromAmount = fromAmountString, + toDecimals = cryptoCurrency.decimals, + providerId = quote.provider.id, + toAddress = address, + redirectUrl = "tangem://redirect?action=dismissBrowser", + language = null, + theme = null, + requestId = UUID.randomUUID().toString(), + ).bind() + }, + onError = { e -> + Timber.e(e) + throw e + }, + ) + if (dataSignatureVerifier.verifySignature(data.signature, data.dataJson)) { + val dataJson = requireNotNull(onrampDataAdapter.fromJson(data.dataJson)) { + "Can not parse dataJson. ${data.dataJson}" + } + createOnrampTransaction( + txId = data.txId, + onrampDataJson = dataJson, + userWalletId = userWalletId, + currency = currency, + provider = quote.provider, + cryptoCurrency = cryptoCurrency, + fromAmount = quote.fromAmount.value, + toAmount = quote.toAmount.value, + ) + } else { + error("Express signature invalid") + } + } catch (e: Exception) { + Timber.e(e) + throw e + } + } + override suspend fun getPaymentMethods(): List { val paymentMethods = requireNotNull(paymentMethodsStore.getSyncOrNull(PAYMENT_METHODS_KEY)) { "Onramp payment methods is absent in storage" @@ -237,6 +320,17 @@ internal class DefaultOnrampRepository( return paymentMethodsConverter.convertList(paymentMethods) } + override suspend fun saveSelectedPaymentMethod(paymentMethod: OnrampPaymentMethod) { + val dto = paymentMethodsConverter.convertBack(paymentMethod) + paymentMethodsStore.store(SELECTED_PAYMENT_METHOD_KEY, listOf(dto)) + } + + override fun getSelectedPaymentMethod(): Flow { + return paymentMethodsStore + .get(SELECTED_PAYMENT_METHOD_KEY) + .mapNotNull { paymentMethods -> paymentMethods.firstOrNull()?.let(paymentMethodsConverter::convert) } + } + override suspend fun clearCache() = withContext(NonCancellable) { paymentMethodsStore.clear() pairsStore.clear() @@ -264,18 +358,53 @@ internal class DefaultOnrampRepository( pairsStore.store(PAIRS_KEY, onrampPairs) } + private fun createOnrampTransaction( + txId: String, + onrampDataJson: OnrampDataJson, + userWalletId: UserWalletId, + currency: OnrampCurrency, + provider: OnrampProvider, + cryptoCurrency: CryptoCurrency, + fromAmount: BigDecimal, + toAmount: BigDecimal, + ): OnrampTransaction { + return OnrampTransaction( + txId = txId, + userWalletId = userWalletId, + fromAmount = fromAmount, + fromCurrency = currency, + toAmount = toAmount, + toCurrencyId = cryptoCurrency.id.value, + status = OnrampStatus.Status.Expired, + externalTxUrl = onrampDataJson.externalTxUrl, + externalTxId = onrampDataJson.externalTxId, + timestamp = DateTime.now().millis, + providerName = provider.info.name, + providerImageUrl = provider.info.imageLarge, + providerType = ExchangeProviderType.ONRAMP.name, + redirectUrl = onrampDataJson.widgetUrl, + ) + } + private fun CryptoCurrency.getContractAddress(): String = when (this) { is CryptoCurrency.Coin -> TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE is CryptoCurrency.Token -> this.contractAddress } - private fun convertToBigDecimal(value: String): BigDecimal = - requireNotNull(value.toBigDecimalOrNull()) { "Can not parse $value to BigDecimal" } + private fun convertToAmount(value: String, cryptoCurrency: CryptoCurrency): OnrampAmount { + val cryptoValue = requireNotNull(value.toBigDecimalOrNull()) { "Can not parse $value to BigDecimal" } + return OnrampAmount( + value = cryptoValue.movePointLeft(cryptoCurrency.decimals), + symbol = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + ) + } private fun List.removeApplePay(): List = filterNot { it.id == "apple-pay" } private companion object { const val PAYMENT_METHODS_KEY = "onramp_payment_methods" + const val SELECTED_PAYMENT_METHOD_KEY = "onramp_selected_payment_method" const val PAIRS_KEY = "onramp_pairs" const val QUOTES_KEY = "onramp_quotes" } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/PaymentMethodConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/PaymentMethodConverter.kt index b45273c12e..f29bfe88ae 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/PaymentMethodConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/PaymentMethodConverter.kt @@ -2,12 +2,18 @@ package com.tangem.data.onramp.converters import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO import com.tangem.domain.onramp.model.OnrampPaymentMethod -import com.tangem.utils.converter.Converter +import com.tangem.utils.converter.TwoWayConverter -internal class PaymentMethodConverter : Converter { +internal class PaymentMethodConverter : TwoWayConverter { override fun convert(value: PaymentMethodDTO): OnrampPaymentMethod = OnrampPaymentMethod( id = value.id, name = value.name, imageUrl = value.image, ) + + override fun convertBack(value: OnrampPaymentMethod): PaymentMethodDTO = PaymentMethodDTO( + id = value.id, + name = value.name, + image = value.imageUrl, + ) } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/TransactionConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/TransactionConverter.kt index 5373cba864..9b29c49779 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/TransactionConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/TransactionConverter.kt @@ -25,7 +25,7 @@ internal class TransactionConverter : TwoWayConverter, ) +@Serializable data class OnrampProviderInfo(val name: String, val imageLarge: 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 new file mode 100644 index 0000000000..4499df29bd --- /dev/null +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.onramp.model + +import kotlinx.serialization.Serializable + +sealed interface OnrampProviderWithQuote { + + val provider: OnrampProvider + + @Serializable + data class Data( + override val provider: OnrampProvider, + val paymentMethod: OnrampPaymentMethod, + val toAmount: OnrampAmount, + val fromAmount: OnrampAmount, + ) : OnrampProviderWithQuote + + sealed interface Unavailable : OnrampProviderWithQuote { + + data class NotSupportedPaymentMethod( + override val provider: OnrampProvider, + val availablePaymentMethods: List, + ) : Unavailable + + data class AvailableFrom(override val provider: OnrampProvider, val amount: OnrampAmount) : Unavailable + data class AvailableUpTo(override val provider: OnrampProvider, val amount: OnrampAmount) : 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 565aab17c5..9c2ab7307f 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 @@ -1,8 +1,5 @@ package com.tangem.domain.onramp.model -import com.tangem.domain.tokens.model.Amount -import java.math.BigDecimal - sealed class OnrampQuote { abstract val paymentMethod: OnrampPaymentMethod @@ -11,10 +8,10 @@ sealed class OnrampQuote { data class Data( override val paymentMethod: OnrampPaymentMethod, override val provider: OnrampProvider, - val fromAmount: BigDecimal, - val toAmount: BigDecimal, - val minFromAmount: BigDecimal, - val maxFromAmount: BigDecimal, + val fromAmount: OnrampAmount, + val toAmount: OnrampAmount, + val minFromAmount: OnrampAmount, + val maxFromAmount: OnrampAmount, ) : OnrampQuote() sealed class Error : OnrampQuote() { @@ -22,13 +19,13 @@ sealed class OnrampQuote { data class AmountTooSmallError( override val paymentMethod: OnrampPaymentMethod, override val provider: OnrampProvider, - val amount: Amount, + val amount: OnrampAmount, ) : Error() data class AmountTooBigError( override val paymentMethod: OnrampPaymentMethod, override val provider: OnrampProvider, - val amount: Amount, + val amount: OnrampAmount, ) : Error() } } \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt index d2403d003e..acf88799f9 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt @@ -36,4 +36,5 @@ data class OnrampTransaction( val providerName: String, // todo onramp fix after SwapProvider moved to own module val providerImageUrl: String, // todo onramp fix after SwapProvider moved to own module val providerType: String, // todo onramp fix after SwapProvider moved to own module + val redirectUrl: String, ) \ 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 new file mode 100644 index 0000000000..e1de3cdca5 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt @@ -0,0 +1,68 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.model.OnrampProvider +import com.tangem.domain.onramp.model.OnrampProviderWithQuote +import com.tangem.domain.onramp.model.OnrampProviderWithQuote.Unavailable +import com.tangem.domain.onramp.model.OnrampQuote +import com.tangem.domain.onramp.repositories.OnrampRepository + +class GetOnrampProviderWithQuoteUseCase(private val repository: OnrampRepository) { + + suspend operator fun invoke(paymentMethod: OnrampPaymentMethod): Either> { + return Either.catch { + val quotes: List = requireNotNull(repository.getQuotesSync()) { "Quotes must not be null" } + quotes + .groupBy { it.provider } + .mapNotNull { (provider, quotes) -> + quotes.quoteWithProvider(provider = provider, selectedPaymentMethod = paymentMethod) + } + } + } + + private fun List.quoteWithProvider( + provider: OnrampProvider, + selectedPaymentMethod: OnrampPaymentMethod, + ): OnrampProviderWithQuote? { + val quoteData = this.filterIsInstance() + val matchedPaymentMethodQuote = quoteData.firstOrNull { it.paymentMethod == selectedPaymentMethod } + + if (matchedPaymentMethodQuote != null) { + return OnrampProviderWithQuote.Data( + provider = matchedPaymentMethodQuote.provider, + paymentMethod = matchedPaymentMethodQuote.paymentMethod, + toAmount = matchedPaymentMethodQuote.toAmount, + fromAmount = matchedPaymentMethodQuote.fromAmount, + ) + } + + val paymentMethodNotSupported = quoteData.filterNot { it.paymentMethod == selectedPaymentMethod } + if (paymentMethodNotSupported.isNotEmpty()) { + return Unavailable.NotSupportedPaymentMethod( + provider = provider, + availablePaymentMethods = paymentMethodNotSupported.map(OnrampQuote.Data::paymentMethod), + ) + } + + val amountError = this + .filterIsInstance() + .firstOrNull { it.paymentMethod == selectedPaymentMethod } + + return when { + amountError != null -> { + when (amountError) { + is OnrampQuote.Error.AmountTooBigError -> Unavailable.AvailableUpTo( + provider = amountError.provider, + amount = amountError.amount, + ) + is OnrampQuote.Error.AmountTooSmallError -> Unavailable.AvailableFrom( + provider = amountError.provider, + amount = amountError.amount, + ) + } + } + else -> null + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..3c7762b989 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampRedirectUrlUseCase.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampProviderWithQuote +import com.tangem.domain.onramp.repositories.OnrampRepository +import com.tangem.domain.onramp.repositories.OnrampTransactionRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +class GetOnrampRedirectUrlUseCase( + private val repository: OnrampRepository, + private val transactionRepository: OnrampTransactionRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + quote: OnrampProviderWithQuote.Data, + cryptoCurrency: CryptoCurrency, + ): Either { + return Either.catch { + val transaction = repository.getOnrampData( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + quote = quote, + ) + transactionRepository.storeTransaction(transaction) + transaction.redirectUrl + } + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampSelectedPaymentMethodUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampSelectedPaymentMethodUseCase.kt new file mode 100644 index 0000000000..23daf16d80 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampSelectedPaymentMethodUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.repositories.OnrampRepository +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +class GetOnrampSelectedPaymentMethodUseCase(private val repository: OnrampRepository) { + + operator fun invoke(): EitherFlow { + return repository.getSelectedPaymentMethod() + .map> { it.right() } + .catch { emit(it.left()) } + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveSelectedPaymentMethod.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveSelectedPaymentMethod.kt new file mode 100644 index 0000000000..69f40194af --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveSelectedPaymentMethod.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.repositories.OnrampRepository + +class OnrampSaveSelectedPaymentMethod(private val repository: OnrampRepository) { + + suspend operator fun invoke(paymentMethod: OnrampPaymentMethod): Either { + return Either.catch { repository.saveSelectedPaymentMethod(paymentMethod) } + } +} \ No newline at end of file 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 7ffb44d177..313e735b18 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 @@ -1,10 +1,13 @@ package com.tangem.domain.onramp.repositories import com.tangem.domain.onramp.model.* +import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow +@Suppress("TooManyFunctions") interface OnrampRepository { // api suspend fun getCurrencies(): List @@ -14,6 +17,11 @@ interface OnrampRepository { suspend fun fetchPaymentMethodsIfAbsent() suspend fun fetchPairs(currency: OnrampCurrency, country: OnrampCountry, cryptoCurrency: CryptoCurrency) suspend fun fetchQuotes(cryptoCurrency: CryptoCurrency, amount: Amount) + suspend fun getOnrampData( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + quote: OnrampProviderWithQuote.Data, + ): OnrampTransaction // cache suspend fun saveDefaultCurrency(currency: OnrampCurrency) @@ -23,6 +31,9 @@ interface OnrampRepository { suspend fun getDefaultCountrySync(): OnrampCountry? fun getDefaultCountry(): Flow suspend fun getPaymentMethods(): List + suspend fun saveSelectedPaymentMethod(paymentMethod: OnrampPaymentMethod) + fun getSelectedPaymentMethod(): Flow fun getQuotes(): Flow> + suspend fun getQuotesSync(): List? suspend fun clearCache() } \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt index 52be1340e5..ac7c7c7800 100644 --- a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt @@ -3,10 +3,11 @@ package com.tangem.features.onramp.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId interface OnrampComponent : ComposableContentComponent { - data class Params(val cryptoCurrency: CryptoCurrency) + data class Params(val userWalletId: UserWalletId, val cryptoCurrency: CryptoCurrency) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt index 8e4f11228c..a5c15531e0 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt @@ -16,6 +16,7 @@ import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig import com.tangem.features.onramp.main.model.OnrampMainComponentModel import com.tangem.features.onramp.main.ui.OnrampMainComponentContent +import com.tangem.features.onramp.providers.SelectProviderComponent import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -26,6 +27,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( @Assisted params: OnrampMainComponent.Params, private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory, private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory, + private val selectProviderComponentFactory: SelectProviderComponent.Factory, ) : OnrampMainComponent, AppComponentContext by appComponentContext { private val model: OnrampMainComponentModel = getOrCreateModel(params) @@ -60,6 +62,14 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( context = childByContext(componentContext), params = SelectCurrencyComponent.Params(model.bottomSheetNavigation::dismiss), ) + is OnrampMainBottomSheetConfig.ProvidersList -> selectProviderComponentFactory.create( + context = childByContext(componentContext), + params = SelectProviderComponent.Params( + onProviderClick = model::onProviderSelected, + onDismiss = model.bottomSheetNavigation::dismiss, + selectedPaymentMethod = config.selectedPaymentMethod, + ), + ) } @AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt index de585fef2d..b8998352d9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt @@ -2,11 +2,16 @@ package com.tangem.features.onramp.main import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.onramp.model.OnrampProviderWithQuote import com.tangem.domain.tokens.model.CryptoCurrency internal interface OnrampMainComponent : ComposableContentComponent { - data class Params(val cryptoCurrency: CryptoCurrency, val openSettings: () -> Unit) + data class Params( + val cryptoCurrency: CryptoCurrency, + val openSettings: () -> Unit, + val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit, + ) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt index bc8946cbb5..ef55baf857 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt @@ -1,9 +1,11 @@ package com.tangem.features.onramp.main.entity +import com.tangem.domain.onramp.model.OnrampProviderWithQuote + interface OnrampIntents { fun onAmountValueChanged(value: String) fun openSettings() fun openCurrenciesList() - fun onBuyClick() + fun onBuyClick(quote: OnrampProviderWithQuote.Data) fun openProviders() } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt index 397816b229..0a6f2b8fa1 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt @@ -1,6 +1,7 @@ package com.tangem.features.onramp.main.entity import com.tangem.domain.onramp.model.OnrampCountry +import com.tangem.domain.onramp.model.OnrampPaymentMethod import kotlinx.serialization.Serializable @Serializable @@ -10,4 +11,7 @@ internal sealed interface OnrampMainBottomSheetConfig { @Serializable data object CurrenciesList : OnrampMainBottomSheetConfig + + @Serializable + data class ProvidersList(val selectedPaymentMethod: OnrampPaymentMethod) : OnrampMainBottomSheetConfig } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt index c390cb9a7e..afd1b4e88e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt @@ -18,7 +18,6 @@ internal sealed interface OnrampMainComponentUM { val currency: String, val onClose: () -> Unit, val openSettings: () -> Unit, - val onBuyClick: () -> Unit, ) : OnrampMainComponentUM { override val topBarConfig: OnrampMainTopBarUM = OnrampMainTopBarUM( title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")), @@ -36,7 +35,7 @@ internal sealed interface OnrampMainComponentUM { override val buyButtonConfig: BuyButtonConfig = BuyButtonConfig( text = resourceReference(R.string.common_buy), - onClick = onBuyClick, + onClick = {}, enabled = false, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt index 997292dfb8..5cd08fe2fa 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt @@ -3,7 +3,7 @@ package com.tangem.features.onramp.main.entity import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.extensions.TextReference -data class OnrampMainTopBarUM( +internal data class OnrampMainTopBarUM( val title: TextReference, val startButtonUM: TopAppBarButtonUM, val endButtonUM: TopAppBarButtonUM, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt index 2d34e8e66d..10469ddea3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt @@ -1,11 +1,12 @@ package com.tangem.features.onramp.main.entity +import com.tangem.domain.onramp.model.OnrampPaymentMethod + sealed class OnrampProviderBlockUM { data object Empty : OnrampProviderBlockUM() data object Loading : OnrampProviderBlockUM() data class Content( - val paymentMethodIconUrl: String, - val paymentMethodName: String, + val paymentMethod: OnrampPaymentMethod, val providerName: String, val isBestRate: Boolean, val onClick: () -> Unit, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt index b138b4f5a9..054e19aa36 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -30,7 +30,6 @@ internal class OnrampStateFactory( currency = currency, onClose = onClose, openSettings = onrampIntents::openSettings, - onBuyClick = onrampIntents::onBuyClick, ) } 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 d1c630786f..ff6681a318 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 @@ -7,6 +7,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.domain.onramp.model.OnrampProviderWithQuote import com.tangem.domain.onramp.model.OnrampQuote import com.tangem.domain.tokens.model.AmountType import com.tangem.features.onramp.impl.R @@ -55,6 +56,7 @@ internal class OnrampAmountStateFactory( return currentState.copy( amountBlockState = amountState.copy(secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading), + buyButtonConfig = currentState.buyButtonConfig.copy(enabled = false), ) } @@ -68,13 +70,52 @@ internal class OnrampAmountStateFactory( secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState), ), providerBlockState = quote.toProviderBlockState(), + buyButtonConfig = currentState.buyButtonConfig.copy( + enabled = quote is OnrampQuote.Data, + onClick = { + if (quote is OnrampQuote.Data) { + onrampIntents.onBuyClick( + OnrampProviderWithQuote.Data( + provider = quote.provider, + paymentMethod = quote.paymentMethod, + toAmount = quote.toAmount, + fromAmount = quote.fromAmount, + ), + ) + } + }, + ), + ) + } + + fun getAmountSecondaryUpdatedState(quoteWithProvider: OnrampProviderWithQuote.Data): OnrampMainComponentUM { + val currentState = currentStateProvider() + if (currentState !is OnrampMainComponentUM.Content) return currentState + + val amountState = currentState.amountBlockState + val amount = quoteWithProvider.toAmount.value.format { + crypto(symbol = quoteWithProvider.toAmount.symbol, decimals = quoteWithProvider.toAmount.decimals) + } + return currentState.copy( + amountBlockState = amountState.copy( + secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(stringReference(amount)), + ), + providerBlockState = OnrampProviderBlockUM.Content( + paymentMethod = quoteWithProvider.paymentMethod, + providerName = quoteWithProvider.provider.info.name, + isBestRate = true, + onClick = onrampIntents::openProviders, + ), + buyButtonConfig = currentState.buyButtonConfig.copy( + enabled = true, + onClick = { onrampIntents.onBuyClick(quoteWithProvider) }, + ), ) } private fun OnrampQuote.toProviderBlockState(): OnrampProviderBlockUM { return OnrampProviderBlockUM.Content( - paymentMethodIconUrl = paymentMethod.imageUrl, - paymentMethodName = paymentMethod.name, + paymentMethod = paymentMethod, providerName = provider.info.name, isBestRate = true, onClick = onrampIntents::openProviders, @@ -84,11 +125,8 @@ internal class OnrampAmountStateFactory( private fun OnrampQuote.toSecondaryFieldUiModel(amountState: OnrampAmountBlockUM): OnrampAmountSecondaryFieldUM { return when (this) { is OnrampQuote.Data -> { - val amount = toAmount.format { - crypto( - symbol = amountState.amountFieldModel.cryptoAmount.currencySymbol, - decimals = amountState.amountFieldModel.cryptoAmount.decimals, - ) + val amount = toAmount.value.format { + crypto(symbol = toAmount.symbol, decimals = toAmount.decimals) } OnrampAmountSecondaryFieldUM.Content(stringReference(amount)) } 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 f8d3afd97d..14ffd88063 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 @@ -8,10 +8,12 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.domain.onramp.* import com.tangem.domain.onramp.model.OnrampAvailability import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.domain.onramp.model.OnrampProviderWithQuote import com.tangem.features.onramp.main.OnrampMainComponent import com.tangem.features.onramp.main.entity.OnrampIntents import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory import com.tangem.features.onramp.utils.InputManager @@ -60,6 +62,10 @@ internal class OnrampMainComponentModel @Inject constructor( subscribeToAmountChanges() } + fun onProviderSelected(providerWithQuote: OnrampProviderWithQuote.Data) { + _state.update { amountStateFactory.getAmountSecondaryUpdatedState(providerWithQuote) } + } + private fun checkResidenceCountry() { modelScope.launch { checkOnrampAvailabilityUseCase.invoke(params.cryptoCurrency) @@ -125,8 +131,8 @@ internal class OnrampMainComponentModel @Inject constructor( params.openSettings() } - override fun onBuyClick() { - TODO("Not yet implemented") + override fun onBuyClick(quote: OnrampProviderWithQuote.Data) { + params.openRedirectPage(quote) } override fun openCurrenciesList() { @@ -134,6 +140,13 @@ internal class OnrampMainComponentModel @Inject constructor( } override fun openProviders() { + val providerState = (state.value as? OnrampMainComponentUM.Content)?.providerBlockState ?: return + val providerContentState = providerState as? OnrampProviderBlockUM.Content ?: return + bottomSheetNavigation.activate( + OnrampMainBottomSheetConfig.ProvidersList( + selectedPaymentMethod = providerContentState.paymentMethod, + ), + ) } override fun onDestroy() { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt index 0ebdc14d95..cc1fe8f7be 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt @@ -49,7 +49,7 @@ private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: .size(size = TangemTheme.dimens.size40) .clip(TangemTheme.shapes.roundedCorners8), model = ImageRequest.Builder(context = LocalContext.current) - .data(state.paymentMethodIconUrl) + .data(state.paymentMethod.imageUrl) .crossfade(enable = true) .allowHardware(false) .build(), @@ -67,7 +67,7 @@ private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: color = TangemTheme.colors.text.primary1, ), ) { - append(state.paymentMethodName) + append(state.paymentMethod.name) } }, color = TangemTheme.colors.text.tertiary, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/entity/PaymentMethodUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/entity/PaymentMethodUM.kt index 08214001de..24c79bdaa6 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/entity/PaymentMethodUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/entity/PaymentMethodUM.kt @@ -1,6 +1,9 @@ package com.tangem.features.onramp.paymentmethod.entity -data class PaymentMethodUM( +import kotlinx.serialization.Serializable + +@Serializable +internal data class PaymentMethodUM( val id: String, val imageUrl: String, val name: String, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/DefaultSelectProviderComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/DefaultSelectProviderComponent.kt new file mode 100644 index 0000000000..e59e4722e9 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/DefaultSelectProviderComponent.kt @@ -0,0 +1,95 @@ +package com.tangem.features.onramp.providers + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onramp.paymentmethod.SelectPaymentMethodComponent +import com.tangem.features.onramp.providers.entity.ProviderListBottomSheetConfig +import com.tangem.features.onramp.providers.model.SelectProviderModel +import com.tangem.features.onramp.providers.ui.SelectProviderBottomSheet +import com.tangem.features.onramp.providers.ui.SelectProviderBottomSheetContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultSelectProviderComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: SelectProviderComponent.Params, + private val selectPaymentMethodComponentFactory: SelectPaymentMethodComponent.Factory, +) : SelectProviderComponent, AppComponentContext by context { + + private val model: SelectProviderModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = null, + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val state by model.state.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() + + val bottomSheetConfig = remember(key1 = this) { + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ) + } + SelectProviderBottomSheet( + config = bottomSheetConfig, + content = { + SelectProviderBottomSheetContent( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = TangemTheme.dimens.spacing16), + state = state, + ) + }, + ) + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + config: ProviderListBottomSheetConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = when (config) { + is ProviderListBottomSheetConfig.PaymentMethods -> selectPaymentMethodComponentFactory.create( + context = childByContext(componentContext), + params = SelectPaymentMethodComponent.Params( + selectedMethodId = config.selectedMethodId, + paymentMethods = config.paymentMethodsUM, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + } + + @AssistedFactory + interface Factory : SelectProviderComponent.Factory { + override fun create( + context: AppComponentContext, + params: SelectProviderComponent.Params, + ): DefaultSelectProviderComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/SelectProviderComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/SelectProviderComponent.kt new file mode 100644 index 0000000000..1e12daef48 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/SelectProviderComponent.kt @@ -0,0 +1,17 @@ +package com.tangem.features.onramp.providers + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.model.OnrampProviderWithQuote + +internal interface SelectProviderComponent : ComposableBottomSheetComponent { + + data class Params( + val onProviderClick: (OnrampProviderWithQuote.Data) -> Unit, + val onDismiss: () -> Unit, + val selectedPaymentMethod: OnrampPaymentMethod, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/di/SelectProviderComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/di/SelectProviderComponentModule.kt new file mode 100644 index 0000000000..ac7e9c9db1 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/di/SelectProviderComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.providers.di + +import com.tangem.features.onramp.providers.DefaultSelectProviderComponent +import com.tangem.features.onramp.providers.SelectProviderComponent +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 SelectProviderComponentModule { + + @Binds + @Singleton + fun bindSelectProviderComponentFactory( + factory: DefaultSelectProviderComponent.Factory, + ): SelectProviderComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/di/SelectProviderModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/di/SelectProviderModelModule.kt new file mode 100644 index 0000000000..812d6bd45d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/di/SelectProviderModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.providers.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.providers.model.SelectProviderModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface SelectProviderModelModule { + + @Binds + @IntoMap + @ClassKey(SelectProviderModel::class) + fun bindSelectProviderModel(model: SelectProviderModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListBottomSheetConfig.kt new file mode 100644 index 0000000000..966e361c38 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListBottomSheetConfig.kt @@ -0,0 +1,14 @@ +package com.tangem.features.onramp.providers.entity + +import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodUM +import kotlinx.serialization.Serializable + +@Serializable +internal sealed interface ProviderListBottomSheetConfig { + + @Serializable + data class PaymentMethods( + val selectedMethodId: String, + val paymentMethodsUM: List, + ) : ProviderListBottomSheetConfig +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListItemUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListItemUM.kt new file mode 100644 index 0000000000..9a1105f0f9 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListItemUM.kt @@ -0,0 +1,24 @@ +package com.tangem.features.onramp.providers.entity + +import com.tangem.core.ui.extensions.TextReference + +internal sealed interface ProviderListItemUM { + + val providerId: String + + data class Available( + override val providerId: String, + val imageUrl: String, + val name: String, + val rate: String, + val isSelected: Boolean, + val onClick: () -> Unit, + ) : ProviderListItemUM + + data class Unavailable( + override val providerId: String, + val imageUrl: String, + val name: String, + val subtitle: TextReference, + ) : ProviderListItemUM +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListPaymentMethodUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListPaymentMethodUM.kt new file mode 100644 index 0000000000..ab8e11e89c --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListPaymentMethodUM.kt @@ -0,0 +1,8 @@ +package com.tangem.features.onramp.providers.entity + +internal data class ProviderListPaymentMethodUM( + val id: String, + val name: String, + val imageUrl: String, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListUM.kt new file mode 100644 index 0000000000..6b8b8764c2 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListUM.kt @@ -0,0 +1,8 @@ +package com.tangem.features.onramp.providers.entity + +import kotlinx.collections.immutable.ImmutableList + +internal data class ProviderListUM( + val paymentMethod: ProviderListPaymentMethodUM, + val providers: ImmutableList, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt new file mode 100644 index 0000000000..cbab9bf39e --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt @@ -0,0 +1,170 @@ +package com.tangem.features.onramp.providers.model + +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.onramp.GetOnrampPaymentMethodsUseCase +import com.tangem.domain.onramp.GetOnrampProviderWithQuoteUseCase +import com.tangem.domain.onramp.GetOnrampSelectedPaymentMethodUseCase +import com.tangem.domain.onramp.OnrampSaveSelectedPaymentMethod +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.model.OnrampProviderWithQuote +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodUM +import com.tangem.features.onramp.providers.SelectProviderComponent +import com.tangem.features.onramp.providers.entity.ProviderListBottomSheetConfig +import com.tangem.features.onramp.providers.entity.ProviderListItemUM +import com.tangem.features.onramp.providers.entity.ProviderListPaymentMethodUM +import com.tangem.features.onramp.providers.entity.ProviderListUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@ComponentScoped +internal class SelectProviderModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val getSelectedPaymentMethodsUseCase: GetOnrampPaymentMethodsUseCase, + private val getOnrampSelectedPaymentMethodUseCase: GetOnrampSelectedPaymentMethodUseCase, + private val getOnrampProviderWithQuoteUseCase: GetOnrampProviderWithQuoteUseCase, + private val saveSelectedPaymentMethod: OnrampSaveSelectedPaymentMethod, + paramsContainer: ParamsContainer, +) : Model() { + + val state: StateFlow get() = _state.asStateFlow() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val params: SelectProviderComponent.Params = paramsContainer.require() + private val _state = MutableStateFlow(getInitialState()) + + init { + getProviders(params.selectedPaymentMethod) + subscribeToPaymentMethodUpdates() + } + + private fun subscribeToPaymentMethodUpdates() { + getOnrampSelectedPaymentMethodUseCase.invoke() + .onEach { it.getOrNull()?.let(::getProviders) } + .launchIn(modelScope) + } + + private fun getProviders(paymentMethod: OnrampPaymentMethod) { + modelScope.launch { + getOnrampProviderWithQuoteUseCase.invoke(paymentMethod) + .onRight { quotes -> + _state.update { state -> state.copy(providers = quotes.toProvidersListItems()) } + } + .onLeft { Timber.e(it) } + } + } + + private fun getInitialState(): ProviderListUM { + return ProviderListUM( + paymentMethod = ProviderListPaymentMethodUM( + id = params.selectedPaymentMethod.id, + name = params.selectedPaymentMethod.name, + imageUrl = params.selectedPaymentMethod.imageUrl, + onClick = ::openPaymentMethods, + ), + providers = emptyList().toImmutableList(), + ) + } + + private fun openPaymentMethods() { + modelScope.launch { + val methods = getSelectedPaymentMethodsUseCase.invoke().getOrNull().orEmpty() + bottomSheetNavigation.activate( + ProviderListBottomSheetConfig.PaymentMethods( + selectedMethodId = _state.value.paymentMethod.id, + paymentMethodsUM = methods.toPaymentUMList(), + ), + ) + } + } + + private fun List.toPaymentUMList(): List = map { method -> + PaymentMethodUM( + id = method.id, + imageUrl = method.imageUrl, + name = method.name, + onSelect = { onPaymentMethodSelected(method) }, + ) + } + + private fun onPaymentMethodSelected(paymentMethod: OnrampPaymentMethod) { + modelScope.launch { + saveSelectedPaymentMethod.invoke(paymentMethod) + _state.update { state -> + val paymentMethodUM = state.paymentMethod.copy( + id = paymentMethod.id, + name = paymentMethod.name, + imageUrl = paymentMethod.imageUrl, + ) + state.copy(paymentMethod = paymentMethodUM) + } + bottomSheetNavigation.dismiss() + } + } + + private fun List.toProvidersListItems() = mapIndexed { index, quote -> + when (quote) { + is OnrampProviderWithQuote.Data -> { + val rate = quote.toAmount.value.format { + crypto(symbol = quote.toAmount.symbol, decimals = quote.toAmount.decimals) + } + ProviderListItemUM.Available( + providerId = quote.provider.id, + imageUrl = quote.provider.info.imageLarge, + name = quote.provider.info.name, + rate = rate, + isSelected = index == 0, + onClick = { + params.onProviderClick(quote) + params.onDismiss() + }, + ) + } + is OnrampProviderWithQuote.Unavailable.AvailableFrom -> { + val amount = quote.amount.value.format { + crypto(symbol = quote.amount.symbol, decimals = quote.amount.decimals) + } + ProviderListItemUM.Unavailable( + providerId = quote.provider.id, + imageUrl = quote.provider.info.imageLarge, + name = quote.provider.info.name, + subtitle = resourceReference(R.string.express_provider_min_amount, wrappedList(amount)), + ) + } + is OnrampProviderWithQuote.Unavailable.AvailableUpTo -> { + val amount = quote.amount.value.format { + crypto(symbol = quote.amount.symbol, decimals = quote.amount.decimals) + } + ProviderListItemUM.Unavailable( + providerId = quote.provider.id, + imageUrl = quote.provider.info.imageLarge, + name = quote.provider.info.name, + subtitle = resourceReference(R.string.express_provider_max_amount, wrappedList(amount)), + ) + } + is OnrampProviderWithQuote.Unavailable.NotSupportedPaymentMethod -> { + ProviderListItemUM.Unavailable( + providerId = quote.provider.id, + imageUrl = quote.provider.info.imageLarge, + name = quote.provider.info.name, + subtitle = stringReference( + "Available with ${quote.availablePaymentMethods.joinToString { it.name }}", + ), + ) + } + } + }.toImmutableList() +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt new file mode 100644 index 0000000000..6c79dfaa11 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt @@ -0,0 +1,199 @@ +package com.tangem.features.onramp.providers.ui + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.util.fastForEach +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.providers.entity.ProviderListItemUM +import com.tangem.features.onramp.providers.entity.ProviderListPaymentMethodUM +import com.tangem.features.onramp.providers.entity.ProviderListUM + +@Composable +internal fun SelectProviderBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) { + TangemBottomSheet( + config = config, + titleText = resourceReference(R.string.express_choose_providers_title), + content = { content() }, + ) +} + +@Composable +internal fun SelectProviderBottomSheetContent(state: ProviderListUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier.verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(id = R.string.onramp_choose_provider_title_hint), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + PaymentMethodBlock( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing20, bottom = TangemTheme.dimens.spacing16), + state = state.paymentMethod, + ) + state.providers.fastForEach { provider -> + key(provider.providerId) { ProviderItem(state = provider) } + } + } +} + +@Composable +private fun PaymentMethodBlock(state: ProviderListPaymentMethodUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .border( + width = TangemTheme.dimens.size1, + color = TangemTheme.colors.stroke.secondary, + shape = RoundedCornerShape(TangemTheme.dimens.radius16), + ) + .clickable(onClick = state.onClick) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + SubcomposeAsyncImage( + modifier = Modifier + .size(size = TangemTheme.dimens.size40) + .clip(TangemTheme.shapes.roundedCorners8), + model = ImageRequest.Builder(context = LocalContext.current) + .data(state.imageUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, + contentDescription = null, + ) + Column(modifier = Modifier.weight(1F)) { + Text( + text = stringResource(id = R.string.onramp_pay_with), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + Text( + text = state.name, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.primary1, + ) + } + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_chevron_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } +} + +@Composable +private fun ProviderItem(state: ProviderListItemUM, modifier: Modifier = Modifier) { + when (state) { + is ProviderListItemUM.Available -> AvailableProviderItem( + modifier = modifier + .clickable(onClick = state.onClick) + .padding(all = TangemTheme.dimens.spacing12), + state = state, + ) + is ProviderListItemUM.Unavailable -> UnavailableProviderItem( + modifier = modifier.padding(all = TangemTheme.dimens.spacing12), + state = state, + ) + } +} + +@Composable +private fun AvailableProviderItem(state: ProviderListItemUM.Available, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + SubcomposeAsyncImage( + modifier = Modifier + .size(size = TangemTheme.dimens.size40) + .clip(TangemTheme.shapes.roundedCorners8), + model = ImageRequest.Builder(context = LocalContext.current) + .data(state.imageUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, + contentDescription = null, + ) + Text( + modifier = Modifier.weight(1F), + text = state.name, + style = TangemTheme.typography.subtitle2, + color = if (state.isSelected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary, + ) + Text( + text = state.rate, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } +} + +private const val UNAVAILABLE_ALPHA = 0.4F + +@Composable +private fun UnavailableProviderItem(state: ProviderListItemUM.Unavailable, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + SubcomposeAsyncImage( + modifier = Modifier + .size(size = TangemTheme.dimens.size40) + .clip(TangemTheme.shapes.roundedCorners8) + .alpha(UNAVAILABLE_ALPHA), + model = ImageRequest.Builder(context = LocalContext.current) + .data(state.imageUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, + contentDescription = null, + ) + Column(modifier = Modifier.weight(1F)) { + Text( + text = state.name, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.secondary, + ) + Text( + text = state.subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } +} \ 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 new file mode 100644 index 0000000000..71cedbb3a5 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt @@ -0,0 +1,34 @@ +package com.tangem.features.onramp.redirect + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.onramp.redirect.model.OnrampRedirectModel +import com.tangem.features.onramp.redirect.ui.OnrampRedirectContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultOnrampRedirectComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: OnrampRedirectComponent.Params, +) : OnrampRedirectComponent, AppComponentContext by appComponentContext { + + private val model: OnrampRedirectModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + BackHandler(onBack = params.onBack) + OnrampRedirectContent(modifier = modifier, state = model.state) + } + + @AssistedFactory + interface Factory : OnrampRedirectComponent.Factory { + override fun create( + context: AppComponentContext, + params: OnrampRedirectComponent.Params, + ): DefaultOnrampRedirectComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/OnrampRedirectComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/OnrampRedirectComponent.kt new file mode 100644 index 0000000000..9a7dcceb3b --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/OnrampRedirectComponent.kt @@ -0,0 +1,19 @@ +package com.tangem.features.onramp.redirect + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.onramp.model.OnrampProviderWithQuote +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +internal interface OnrampRedirectComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + val onBack: () -> Unit, + val cryptoCurrency: CryptoCurrency, + val onrampProviderWithQuote: OnrampProviderWithQuote.Data, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/di/OnrampRedirectComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/di/OnrampRedirectComponentModule.kt new file mode 100644 index 0000000000..4a3a0d5c78 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/di/OnrampRedirectComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.redirect.di + +import com.tangem.features.onramp.redirect.DefaultOnrampRedirectComponent +import com.tangem.features.onramp.redirect.OnrampRedirectComponent +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 OnrampRedirectComponentModule { + + @Binds + @Singleton + fun bindOnrampRedirectComponentFactory( + factory: DefaultOnrampRedirectComponent.Factory, + ): OnrampRedirectComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/di/OnrampRedirectModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/di/OnrampRedirectModelModule.kt new file mode 100644 index 0000000000..21547db511 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/di/OnrampRedirectModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.redirect.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.redirect.model.OnrampRedirectModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface OnrampRedirectModelModule { + + @Binds + @IntoMap + @ClassKey(OnrampRedirectModel::class) + fun bindOnrampRedirectModel(model: OnrampRedirectModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/entity/OnrampRedirectTopBarUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/entity/OnrampRedirectTopBarUM.kt new file mode 100644 index 0000000000..b7761dec03 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/entity/OnrampRedirectTopBarUM.kt @@ -0,0 +1,6 @@ +package com.tangem.features.onramp.redirect.entity + +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.TextReference + +data class OnrampRedirectTopBarUM(val title: TextReference, val startButtonUM: TopAppBarButtonUM) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/entity/OnrampRedirectUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/entity/OnrampRedirectUM.kt new file mode 100644 index 0000000000..22db516242 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/entity/OnrampRedirectUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onramp.redirect.entity + +import com.tangem.core.ui.extensions.TextReference + +internal data class OnrampRedirectUM( + val topBarConfig: OnrampRedirectTopBarUM, + val providerImageUrl: String, + val title: TextReference, + val subtitle: TextReference, +) \ No newline at end of file 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 new file mode 100644 index 0000000000..8d7b3dc545 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt @@ -0,0 +1,69 @@ +package com.tangem.features.onramp.redirect.model + +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.onramp.GetOnrampRedirectUrlUseCase +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.redirect.OnrampRedirectComponent +import com.tangem.features.onramp.redirect.entity.OnrampRedirectTopBarUM +import com.tangem.features.onramp.redirect.entity.OnrampRedirectUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +internal class OnrampRedirectModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val urlOpener: UrlOpener, + private val getOnrampRedirectUrlUseCase: GetOnrampRedirectUrlUseCase, + router: Router, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: OnrampRedirectComponent.Params = paramsContainer.require() + val state = OnrampRedirectUM( + topBarConfig = OnrampRedirectTopBarUM( + title = combinedReference( + resourceReference(R.string.common_buy), + stringReference(" ${params.cryptoCurrency.name}"), + ), + startButtonUM = TopAppBarButtonUM( + iconRes = R.drawable.ic_close_24, + onIconClicked = router::pop, + enabled = true, + ), + ), + providerImageUrl = params.onrampProviderWithQuote.provider.info.imageLarge, + title = resourceReference( + R.string.onramp_redirecting_to_provider_title, + wrappedList(params.onrampProviderWithQuote.provider.info.name), + ), + subtitle = resourceReference( + R.string.onramp_redirecting_to_provider_subtitle, + wrappedList(params.onrampProviderWithQuote.provider.info.name), + ), + ) + + init { + getRedirectUrl() + } + + private fun getRedirectUrl() { + modelScope.launch { + getOnrampRedirectUrlUseCase.invoke( + userWalletId = params.userWalletId, + quote = params.onrampProviderWithQuote, + cryptoCurrency = params.cryptoCurrency, + ) + .onLeft { Timber.e(it) } + .onRight { urlOpener.openUrl(it) } + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/ui/OnrampRedirectContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/ui/OnrampRedirectContent.kt new file mode 100644 index 0000000000..384f8f83e7 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/ui/OnrampRedirectContent.kt @@ -0,0 +1,133 @@ +package com.tangem.features.onramp.redirect.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.WindowInsetsZero +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.redirect.entity.OnrampRedirectUM + +@Composable +internal fun OnrampRedirectContent(state: OnrampRedirectUM, modifier: Modifier = Modifier) { + Scaffold( + modifier = modifier.imePadding(), + contentWindowInsets = WindowInsetsZero, + containerColor = TangemTheme.colors.background.secondary, + topBar = { + TangemTopAppBar( + modifier = Modifier.statusBarsPadding(), + startButton = state.topBarConfig.startButtonUM, + title = state.topBarConfig.title.resolveReference(), + ) + }, + content = { innerPadding -> + Content( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .padding(horizontal = TangemTheme.dimens.spacing16), + state = state, + ) + }, + ) +} + +@Composable +private fun Content(state: OnrampRedirectUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + LogoBlock(providerImageUrl = state.providerImageUrl) + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing24), + text = state.title.resolveReference(), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + text = state.subtitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + } +} + +@Composable +private fun LogoBlock(providerImageUrl: String, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + modifier = Modifier + .size(TangemTheme.dimens.size64) + .clip(TangemTheme.shapes.roundedCorners8) + .background(TangemTheme.colors.background.primary) + .padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing12), + painter = painterResource(id = R.drawable.ic_tangem_24), + contentDescription = null, + ) + LoadingDots() + SubcomposeAsyncImage( + modifier = Modifier + .size(size = TangemTheme.dimens.size64) + .clip(TangemTheme.shapes.roundedCorners8), + model = ImageRequest.Builder(context = LocalContext.current) + .data(providerImageUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, + contentDescription = null, + ) + } +} + +@Composable +private fun LoadingDots(modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size6) + .clip(CircleShape) + .background(color = TangemTheme.colors.icon.accent.copy(alpha = 0.75F)), + ) + Box( + modifier = Modifier + .size(TangemTheme.dimens.size6) + .clip(CircleShape) + .background(color = TangemTheme.colors.icon.accent.copy(alpha = 0.45F)), + ) + Box( + modifier = Modifier + .size(TangemTheme.dimens.size8) + .clip(CircleShape) + .background(color = TangemTheme.colors.icon.accent), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt index 36fd583a12..0c6a0c9b77 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt @@ -17,6 +17,7 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.onramp.component.OnrampComponent import com.tangem.features.onramp.main.OnrampMainComponent +import com.tangem.features.onramp.redirect.OnrampRedirectComponent import com.tangem.features.onramp.root.entity.OnrampChild import com.tangem.features.onramp.settings.OnrampSettingsComponent import dagger.assisted.Assisted @@ -29,6 +30,7 @@ internal class DefaultOnrampComponent @AssistedInject constructor( @Assisted private val params: OnrampComponent.Params, private val settingsComponentFactory: OnrampSettingsComponent.Factory, private val onrampMainComponentFactory: OnrampMainComponent.Factory, + private val onrampRedirectComponentFactory: OnrampRedirectComponent.Factory, ) : OnrampComponent, AppComponentContext by context { private val navigation = StackNavigation() @@ -65,6 +67,23 @@ internal class DefaultOnrampComponent @AssistedInject constructor( params = OnrampMainComponent.Params( cryptoCurrency = params.cryptoCurrency, openSettings = { navigation.push(OnrampChild.Settings) }, + openRedirectPage = { + navigation.push( + OnrampChild.RedirectPage( + quote = it, + cryptoCurrency = params.cryptoCurrency, + ), + ) + }, + ), + ) + is OnrampChild.RedirectPage -> onrampRedirectComponentFactory.create( + context = childByContext(componentContext), + params = OnrampRedirectComponent.Params( + userWalletId = params.userWalletId, + onBack = navigation::pop, + cryptoCurrency = config.cryptoCurrency, + onrampProviderWithQuote = config.quote, ), ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/entity/OnrampChild.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/entity/OnrampChild.kt index 994a986713..73dc3b48db 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/entity/OnrampChild.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/entity/OnrampChild.kt @@ -1,5 +1,7 @@ package com.tangem.features.onramp.root.entity +import com.tangem.domain.onramp.model.OnrampProviderWithQuote +import com.tangem.domain.tokens.model.CryptoCurrency import kotlinx.serialization.Serializable @Serializable @@ -9,4 +11,7 @@ internal sealed class OnrampChild { @Serializable data object Settings : OnrampChild() + + @Serializable + data class RedirectPage(val quote: OnrampProviderWithQuote.Data, val cryptoCurrency: CryptoCurrency) : OnrampChild() } \ No newline at end of file