Updated on 2026-08-14
This commit is contained in:
parent
38276b01ea
commit
9378e4fa31
63 changed files with 1434 additions and 75 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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<PaymentMethodDTO>?
|
||||
|
||||
fun get(key: String): Flow<List<PaymentMethodDTO>>
|
||||
|
||||
suspend fun store(key: String, value: List<PaymentMethodDTO>)
|
||||
|
||||
suspend fun contains(key: String): Boolean
|
||||
|
|
|
|||
|
|
@ -296,7 +296,7 @@
|
|||
<string name="express_provider_recommended">Empfohlen</string>
|
||||
<string name="express_status_bought">Gekauft %s</string>
|
||||
<string name="express_status_buying">Kauf %s</string>
|
||||
<string name="express_status_buying_active">Kauf %s...</string>
|
||||
<string name="express_status_buying_active">Kauf %s…</string>
|
||||
<string name="express_token_list_empty_search">Keine Token gefunden. Bitte versuche eine andere Anfrage</string>
|
||||
<string name="express_transaction_id">ID: %s</string>
|
||||
<string name="express_transaction_id_copied">Transaktions-ID kopiert</string>
|
||||
|
|
@ -590,8 +590,11 @@
|
|||
<string name="qr_scanner_camera_denied_settings_button">Einstellungen</string>
|
||||
<string name="qr_scanner_camera_denied_text">Du hast keinen Zugriff auf deine Kamera gewährt</string>
|
||||
<string name="qr_scanner_camera_denied_title">Kamerazugriff verweigert</string>
|
||||
<string name="receive_bottom_sheet_no_memo_required_message">Kein Memo erforderlich</string>
|
||||
<string name="receive_bottom_sheet_warning_message">%1$s ( %2$s ) im %3$s Netzwerk</string>
|
||||
<string name="receive_bottom_sheet_warning_message_description">Das Senden einer anderen Währung führt zu deren irreversiblem Verlust.</string>
|
||||
<string name="receive_bottom_sheet_warning_message_full">Sende nur %s an diese Adresse. Der Versand einer anderen Währung führt zu ihrem unwiderruflichen Verlust.</string>
|
||||
<string name="receive_bottom_sheet_warning_message_title">Sende nur %s an diese Adresse.</string>
|
||||
<string name="receive_token_description">Überweise Geld von einem anderen Wallet oder einer anderen Börse</string>
|
||||
<string name="referral_button_participate">Teilnehmen</string>
|
||||
<string name="referral_error_failed_to_load_info">Die Informationen zum Empfehlungsprogramm konnten nicht geladen werden. Bitte versuche es später noch einmal.</string>
|
||||
|
|
|
|||
|
|
@ -432,7 +432,6 @@
|
|||
<string name="markets_token_details_fully_diluted_valuation_description">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.</string>
|
||||
<string name="markets_token_details_fully_diluted_valuation_full">Valoración totalmente diluida</string>
|
||||
<string name="markets_token_details_genesis_date">Dato de Genesis</string>
|
||||
<string name="markets_token_details_genesis_date_description">Vacío</string>
|
||||
<string name="markets_token_details_high">Alto</string>
|
||||
<string name="markets_token_details_holders">Titulares</string>
|
||||
<string name="markets_token_details_holders_description">El cambio en el número de poseedores de tokens dentro de un período de tiempo específico</string>
|
||||
|
|
@ -443,7 +442,6 @@
|
|||
<string name="markets_token_details_liquidity_description">El cambio en la cantidad de liquidez disponible para el token durante el período de tiempo especificado</string>
|
||||
<string name="markets_token_details_liquidity_full">Liquidez</string>
|
||||
<string name="markets_token_details_liquidity_index">Índice de liquidez</string>
|
||||
<string name="markets_token_details_liquidity_index_description">Vacío</string>
|
||||
<string name="markets_token_details_listed_on">Listado en</string>
|
||||
<string name="markets_token_details_low">Bajo</string>
|
||||
<string name="markets_token_details_market_capitalization">Capital. de mercado</string>
|
||||
|
|
@ -517,12 +515,12 @@
|
|||
<string name="onboarding_navbar_title_creating_backup">Creando un backup</string>
|
||||
<string name="onboarding_seed_button_read_more">Leer más sobre seed phrase</string>
|
||||
<plurals name="onboarding_seed_generate_message_words_count">
|
||||
<item quantity="one">Vacío</item>
|
||||
<item quantity="one"></item>
|
||||
<item quantity="other">Escriba estas %d palabras en el orden que se indica a continuación y guárdelas en un lugar seguro y secreto.</item>
|
||||
</plurals>
|
||||
<string name="onboarding_seed_generate_title">Su seed phrase</string>
|
||||
<plurals name="onboarding_seed_generate_words_count">
|
||||
<item quantity="one">Vacío</item>
|
||||
<item quantity="one"></item>
|
||||
<item quantity="other">%d palabras</item>
|
||||
</plurals>
|
||||
<string name="onboarding_seed_import_message">Para importar su billetera, ingrese su seed phrase en el campo de abajo</string>
|
||||
|
|
|
|||
|
|
@ -370,7 +370,7 @@
|
|||
<string name="manage_tokens_wallet_selector_title">Choisissez un portefeuille</string>
|
||||
<string name="manage_tokens_wallet_support_only_one_network_title">Le portefeuille ne prend pas en charge plus d\'un réseau</string>
|
||||
<string name="markets_add_to_my_portfolio_description">Pour acheter, échanger ou recevoir cet actif, ajoutez-le à votre portefeuille</string>
|
||||
<string name="markets_add_to_my_portfolio_unavailable_description">Cet actif n\'est plus pris en charge dans le portefeuille</string>
|
||||
<string name="markets_add_to_my_portfolio_unavailable_description">Cet actif n\'est pas pris en charge dans le portefeuille</string>
|
||||
<string name="markets_add_to_my_portfolio_unavailable_for_wallet_description">Cet actif n\'est pas disponible pour ce portefeuille</string>
|
||||
<string name="markets_add_to_portfolio_button">Ajouter au portfolio</string>
|
||||
<string name="markets_add_token">Ajoutez un jeton</string>
|
||||
|
|
@ -432,7 +432,6 @@
|
|||
<string name="markets_token_details_fully_diluted_valuation_description">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</string>
|
||||
<string name="markets_token_details_fully_diluted_valuation_full">Valorisation entièrement diluée</string>
|
||||
<string name="markets_token_details_genesis_date">Date de la Genesis</string>
|
||||
<string name="markets_token_details_genesis_date_description">Vide</string>
|
||||
<string name="markets_token_details_high">Haut</string>
|
||||
<string name="markets_token_details_holders">Détenteurs</string>
|
||||
<string name="markets_token_details_holders_description">L\'évolution du nombre de détenteurs de jetons au cours d\'une période donnée</string>
|
||||
|
|
@ -443,7 +442,6 @@
|
|||
<string name="markets_token_details_liquidity_description">Le changement dans la quantité de liquidité disponible pour le jeton pendant la période spécifiée</string>
|
||||
<string name="markets_token_details_liquidity_full">Liquidité</string>
|
||||
<string name="markets_token_details_liquidity_index">Indice de liquidité</string>
|
||||
<string name="markets_token_details_liquidity_index_description">Vide</string>
|
||||
<string name="markets_token_details_listed_on">Listé sur</string>
|
||||
<string name="markets_token_details_low">Faible</string>
|
||||
<string name="markets_token_details_market_capitalization">Cap. boursière</string>
|
||||
|
|
@ -517,12 +515,12 @@
|
|||
<string name="onboarding_navbar_title_creating_backup">Sauvegarde en cours</string>
|
||||
<string name="onboarding_seed_button_read_more">En savoir plus sur les seed phrases</string>
|
||||
<plurals name="onboarding_seed_generate_message_words_count">
|
||||
<item quantity="one">Vide</item>
|
||||
<item quantity="one"></item>
|
||||
<item quantity="other">Écrivez ces %d mots dans l\'ordre indiqué ci-dessous et conservez-les dans un endroit sûr et secret.</item>
|
||||
</plurals>
|
||||
<string name="onboarding_seed_generate_title">Votre seed phrase</string>
|
||||
<plurals name="onboarding_seed_generate_words_count">
|
||||
<item quantity="one">Vide</item>
|
||||
<item quantity="one"></item>
|
||||
<item quantity="other">%d mots</item>
|
||||
</plurals>
|
||||
<string name="onboarding_seed_import_message">Pour importer votre portefeuille, entrez votre seed phrase dans le champ ci-dessous</string>
|
||||
|
|
@ -607,9 +605,9 @@
|
|||
<item quantity="one">pour le portefeuille %d</item>
|
||||
<item quantity="other">pour les portefeuilles %d </item>
|
||||
</plurals>
|
||||
<string name="referral_point_currencies_description">Obtiendras ^^%1$s^^ pour chaque portefeuille acheté par votre ami sur votre adresse de réseau %2$s %3$s ^^30 jours après^^</string>
|
||||
<string name="referral_point_currencies_description">Obtiendrez ^^%1$s^^ pour chaque portefeuille acheté par votre ami sur votre adresse de réseau %2$s %3$s ^^30 jours après^^</string>
|
||||
<string name="referral_point_currencies_title">Vous</string>
|
||||
<string name="referral_point_discount_description_prefix">Obtiendrez</string>
|
||||
<string name="referral_point_discount_description_prefix">Obtiendra</string>
|
||||
<string name="referral_point_discount_description_suffix">lors de l\'achat d\'un portefeuille sur tangem.com</string>
|
||||
<string name="referral_point_discount_description_value">%s réduction</string>
|
||||
<string name="referral_point_discount_title">Votre ami</string>
|
||||
|
|
|
|||
|
|
@ -582,8 +582,11 @@
|
|||
<string name="qr_scanner_camera_denied_settings_button">設定</string>
|
||||
<string name="qr_scanner_camera_denied_text">カメラへのアクセスを許可していません。</string>
|
||||
<string name="qr_scanner_camera_denied_title">カメラへのアクセスが拒否されました</string>
|
||||
<string name="receive_bottom_sheet_no_memo_required_message">メモ不要</string>
|
||||
<string name="receive_bottom_sheet_warning_message">%3$sネットワーク上の%1$s ( %2$s )</string>
|
||||
<string name="receive_bottom_sheet_warning_message_description">他の暗号資産を送信すると、取り返しのつかない損失が発生します。</string>
|
||||
<string name="receive_bottom_sheet_warning_message_full">このアドレスには%sのみを送金してください。他のトークンを送信すると、取り返しのつかない損失が発生します。</string>
|
||||
<string name="receive_bottom_sheet_warning_message_title">このアドレスには%sのみを送信してください。</string>
|
||||
<string name="receive_token_description">ウォレットや取引所から資金を送金する</string>
|
||||
<string name="referral_button_participate">参加する</string>
|
||||
<string name="referral_error_failed_to_load_info">紹介プログラムに関する情報を読み込めませんでした。しばらくしてからもう一度お試しください。</string>
|
||||
|
|
|
|||
|
|
@ -297,6 +297,8 @@
|
|||
<string name="express_provider_not_available">Недоступно для этой пары</string>
|
||||
<string name="express_provider_permission_needed">Требуется разрешение</string>
|
||||
<string name="express_provider_recommended">Рекомендовано</string>
|
||||
<string name="express_status_buying">Покупка %s</string>
|
||||
<string name="express_status_buying_active">Покупка %s...</string>
|
||||
<string name="express_token_list_empty_search">Токены не найдены. Пожалуйста, попробуйте другой запрос</string>
|
||||
<string name="express_transaction_id">ID: %s</string>
|
||||
<string name="express_transaction_id_copied">ID транзакции скопирован</string>
|
||||
|
|
@ -889,6 +891,7 @@
|
|||
<string name="token_swap_promotion_button">Обменять</string>
|
||||
<string name="tokens_list_unavailable_to_purchase_header">Недоступно для покупки</string>
|
||||
<string name="tokens_list_unavailable_to_sell_header">Недоступно для продажи</string>
|
||||
<string name="tokens_list_unavailable_to_swap_header">Недоступно для обмена с %s</string>
|
||||
<string name="transaction_history_contract_address">контракт: %s</string>
|
||||
<string name="transaction_history_empty_transactions">У вас еще нет транзакций</string>
|
||||
<string name="transaction_history_error_failed_to_load">Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию.</string>
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@
|
|||
<string name="express_provider_permission_needed">Потрібен дозвіл</string>
|
||||
<string name="express_provider_recommended">Рекомендовано</string>
|
||||
<string name="express_status_bought">Купив %s</string>
|
||||
<string name="express_status_buying">Купівля %s...</string>
|
||||
<string name="express_status_buying">Купівля %s</string>
|
||||
<string name="express_status_buying_active">Купівля %s...</string>
|
||||
<string name="express_token_list_empty_search">Токенів не знайдено. Будь ласка, спробуйте інший запит</string>
|
||||
<string name="express_transaction_id">ID: %s</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="action_buttons_buy_empty_search_message">Token not found in your portfolio? Check the Markets to find and add it for purchase</string>
|
||||
<string name="action_buttons_sell_empty_search_message">Token not found in your portfolio? Check the Markets to find and add it for sell</string>
|
||||
<string name="action_buttons_sell_empty_search_message">Token not found in your portfolio? Check the Markets to find and add it for selling</string>
|
||||
<string name="action_buttons_swap_choose_token">Choose the Token</string>
|
||||
<string name="action_buttons_swap_empty_search_message">Token not found in your portfolio? Check the Markets to find and add it for swap</string>
|
||||
<string name="action_buttons_swap_no_available_pair_notification_message">There are no available tokens to swap with the selected token. Please choose another one.</string>
|
||||
|
|
@ -565,6 +565,7 @@
|
|||
<string name="onboarding_wallet_info_title_fourth">Access code restore</string>
|
||||
<string name="onboarding_wallet_info_title_second">Identical cards</string>
|
||||
<string name="onboarding_wallet_info_title_third">Access code</string>
|
||||
<string name="onramp_choose_provider_title_hint">Providers facilitate transactions, ensuring smooth and efficient token exchanges</string>
|
||||
<string name="onramp_country_search">Search by country</string>
|
||||
<string name="onramp_country_unavailable">Unavailable</string>
|
||||
<string name="onramp_currency_search">Search by currency</string>
|
||||
|
|
@ -590,8 +591,11 @@
|
|||
<string name="qr_scanner_camera_denied_settings_button">Settings</string>
|
||||
<string name="qr_scanner_camera_denied_text">You have not given access to your camera</string>
|
||||
<string name="qr_scanner_camera_denied_title">Camera access denied</string>
|
||||
<string name="receive_bottom_sheet_no_memo_required_message">No memo required</string>
|
||||
<string name="receive_bottom_sheet_warning_message">%1$s (%2$s) on %3$s network</string>
|
||||
<string name="receive_bottom_sheet_warning_message_description">Sending any other currency will result in its irreversible loss.</string>
|
||||
<string name="receive_bottom_sheet_warning_message_full">Send only %s to this address. Sending any other currency will result in its irreversible loss.</string>
|
||||
<string name="receive_bottom_sheet_warning_message_title">Send only %s to this address.</string>
|
||||
<string name="receive_token_description">Transfer funds from any wallet or exchange</string>
|
||||
<string name="referral_button_participate">Participate</string>
|
||||
<string name="referral_error_failed_to_load_info">Failed to load the information about the referral program. Please try again later.</string>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ dependencies {
|
|||
|
||||
/** Domain modules */
|
||||
implementation(projects.domain.onramp)
|
||||
implementation(projects.domain.legacy)
|
||||
|
||||
// region DI
|
||||
|
||||
|
|
|
|||
|
|
@ -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<OnrampCurrency> = 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<OnrampQuote>? {
|
||||
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<OnrampPaymentMethod> {
|
||||
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<OnrampPaymentMethod> {
|
||||
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<PaymentMethodDTO>.removeApplePay(): List<PaymentMethodDTO> = 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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<PaymentMethodDTO, OnrampPaymentMethod> {
|
||||
internal class PaymentMethodConverter : TwoWayConverter<PaymentMethodDTO, OnrampPaymentMethod> {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ internal class TransactionConverter : TwoWayConverter<OnrampTransactionDTO, Onra
|
|||
providerName = value.providerName,
|
||||
providerImageUrl = value.providerImageUrl,
|
||||
providerType = value.providerType,
|
||||
|
||||
redirectUrl = "", // not necessary field
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ package com.tangem.data.onramp.converters.error
|
|||
import com.squareup.moshi.JsonAdapter
|
||||
import com.tangem.datasource.api.express.models.response.ExpressError
|
||||
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
|
||||
import com.tangem.domain.onramp.model.OnrampAmount
|
||||
import com.tangem.domain.onramp.model.OnrampQuote
|
||||
import com.tangem.domain.tokens.model.Amount
|
||||
import com.tangem.domain.tokens.model.AmountType
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
internal class OnrampQuotesErrorConverter(
|
||||
|
|
@ -40,7 +39,6 @@ internal class OnrampQuotesErrorConverter(
|
|||
amountWithOffset = minAmount,
|
||||
decimals = decimals,
|
||||
symbol = input.amount.currencySymbol,
|
||||
type = input.amount.type,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -59,24 +57,17 @@ internal class OnrampQuotesErrorConverter(
|
|||
amountWithOffset = maxAmount,
|
||||
decimals = decimals,
|
||||
symbol = input.amount.currencySymbol,
|
||||
type = input.amount.type,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFromAmountWithOffset(
|
||||
amountWithOffset: String,
|
||||
decimals: Int,
|
||||
symbol: String,
|
||||
type: AmountType,
|
||||
): Amount {
|
||||
return Amount(
|
||||
private fun createFromAmountWithOffset(amountWithOffset: String, decimals: Int, symbol: String): OnrampAmount {
|
||||
return OnrampAmount(
|
||||
value = requireNotNull(
|
||||
amountWithOffset.toBigDecimalOrNull()?.movePointLeft(decimals),
|
||||
) { "wrong amount format, use only digits" },
|
||||
decimals = decimals,
|
||||
currencySymbol = symbol,
|
||||
type = type,
|
||||
symbol = symbol,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import com.tangem.data.onramp.converters.error.OnrampQuotesErrorConverter
|
|||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
|
||||
import com.tangem.datasource.api.onramp.OnrampApi
|
||||
import com.tangem.datasource.crypto.DataSignatureVerifier
|
||||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore
|
||||
import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore
|
||||
|
|
@ -16,6 +17,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
|
|||
import com.tangem.domain.onramp.repositories.OnrampErrorResolver
|
||||
import com.tangem.domain.onramp.repositories.OnrampRepository
|
||||
import com.tangem.domain.onramp.repositories.OnrampTransactionRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -38,6 +40,9 @@ internal object OnrampDataModule {
|
|||
pairsStore: OnrampPairsStore,
|
||||
quotesStore: OnrampQuotesStore,
|
||||
quotesErrorConverter: OnrampQuotesErrorConverter,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
dataSignatureVerifier: DataSignatureVerifier,
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
): OnrampRepository {
|
||||
return DefaultOnrampRepository(
|
||||
onrampApi = onrampApi,
|
||||
|
|
@ -48,6 +53,9 @@ internal object OnrampDataModule {
|
|||
pairsStore = pairsStore,
|
||||
quotesStore = quotesStore,
|
||||
quotesErrorConverter = quotesErrorConverter,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dataSignatureVerifier = dataSignatureVerifier,
|
||||
moshi = moshi,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.domain.onramp.model
|
||||
|
||||
import com.tangem.domain.core.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class OnrampAmount(
|
||||
val symbol: String,
|
||||
val value: SerializedBigDecimal,
|
||||
val decimals: Int,
|
||||
)
|
||||
|
|
@ -1,9 +1,13 @@
|
|||
package com.tangem.domain.onramp.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class OnrampProvider(
|
||||
val id: String,
|
||||
val info: OnrampProviderInfo,
|
||||
val paymentMethods: List<OnrampPaymentMethod>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class OnrampProviderInfo(val name: String, val imageLarge: String)
|
||||
|
|
@ -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<OnrampPaymentMethod>,
|
||||
) : Unavailable
|
||||
|
||||
data class AvailableFrom(override val provider: OnrampProvider, val amount: OnrampAmount) : Unavailable
|
||||
data class AvailableUpTo(override val provider: OnrampProvider, val amount: OnrampAmount) : Unavailable
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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<Throwable, List<OnrampProviderWithQuote>> {
|
||||
return Either.catch {
|
||||
val quotes: List<OnrampQuote> = requireNotNull(repository.getQuotesSync()) { "Quotes must not be null" }
|
||||
quotes
|
||||
.groupBy { it.provider }
|
||||
.mapNotNull { (provider, quotes) ->
|
||||
quotes.quoteWithProvider(provider = provider, selectedPaymentMethod = paymentMethod)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<OnrampQuote>.quoteWithProvider(
|
||||
provider: OnrampProvider,
|
||||
selectedPaymentMethod: OnrampPaymentMethod,
|
||||
): OnrampProviderWithQuote? {
|
||||
val quoteData = this.filterIsInstance<OnrampQuote.Data>()
|
||||
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<OnrampQuote.Error>()
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Throwable, String> {
|
||||
return Either.catch {
|
||||
val transaction = repository.getOnrampData(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
quote = quote,
|
||||
)
|
||||
transactionRepository.storeTransaction(transaction)
|
||||
transaction.redirectUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Throwable, OnrampPaymentMethod> {
|
||||
return repository.getSelectedPaymentMethod()
|
||||
.map<OnrampPaymentMethod, Either<Throwable, OnrampPaymentMethod>> { it.right() }
|
||||
.catch { emit(it.left()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Throwable, Unit> {
|
||||
return Either.catch { repository.saveSelectedPaymentMethod(paymentMethod) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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<OnrampCurrency>
|
||||
|
|
@ -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<OnrampCountry?>
|
||||
suspend fun getPaymentMethods(): List<OnrampPaymentMethod>
|
||||
suspend fun saveSelectedPaymentMethod(paymentMethod: OnrampPaymentMethod)
|
||||
fun getSelectedPaymentMethod(): Flow<OnrampPaymentMethod>
|
||||
fun getQuotes(): Flow<List<OnrampQuote>>
|
||||
suspend fun getQuotesSync(): List<OnrampQuote>?
|
||||
suspend fun clearCache()
|
||||
}
|
||||
|
|
@ -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<Params, OnrampComponent>
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Params, OnrampMainComponent>
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ internal class OnrampStateFactory(
|
|||
currency = currency,
|
||||
onClose = onClose,
|
||||
openSettings = onrampIntents::openSettings,
|
||||
onBuyClick = onrampIntents::onBuyClick,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Params, SelectProviderComponent>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<PaymentMethodUM>,
|
||||
) : ProviderListBottomSheetConfig
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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<ProviderListItemUM>,
|
||||
)
|
||||
|
|
@ -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<ProviderListUM> get() = _state.asStateFlow()
|
||||
val bottomSheetNavigation: SlotNavigation<ProviderListBottomSheetConfig> = 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<ProviderListItemUM>().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<OnrampPaymentMethod>.toPaymentUMList(): List<PaymentMethodUM> = 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<OnrampProviderWithQuote>.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()
|
||||
}
|
||||
|
|
@ -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<TangemBottomSheetConfigContent.Empty>(
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Params, OnrampRedirectComponent>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<OnrampChild>()
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue