Updated on 2026-08-14
This commit is contained in:
parent
2d34e8cb1f
commit
b19deae346
14 changed files with 274 additions and 3 deletions
|
|
@ -17,6 +17,12 @@ data class ExchangeProvider(
|
|||
|
||||
@Json(name = "imageSmall")
|
||||
val imageSmallUrl: String,
|
||||
|
||||
@Json(name = "termsOfUse")
|
||||
val termsOfUse: String?,
|
||||
|
||||
@Json(name = "privacyPolicy")
|
||||
val privacyPolicy: String?,
|
||||
)
|
||||
|
||||
enum class ExchangeProviderType {
|
||||
|
|
|
|||
|
|
@ -203,11 +203,17 @@
|
|||
<string name="express_exchange_notification_verification_title">Провайдер запрашивает прохождение верификации</string>
|
||||
<string name="express_exchange_token_list_subtitle">Список токенов в вашем кошельке</string>
|
||||
<string name="express_fetch_best_rates">Получение наилучших курсов...</string>
|
||||
<string name="express_floating_rate">Плавающая ставка</string>
|
||||
<string name="express_go_to_provider">К провайдеру</string>
|
||||
<string name="express_legal_one_placeholder">Пользуясь сервисом вы соглашаетесь с %s</string>
|
||||
<string name="express_legal_two_placeholders">Пользуясь сервисом вы соглашаетесь с %1$s и %2$s</string>
|
||||
<string name="express_privacy_policy">Политикой конфиденциальности</string>
|
||||
<string name="express_provider">Провайдер</string>
|
||||
<string name="express_provider_best_rate">Лучший курс</string>
|
||||
<string name="express_provider_min_amount">Доступно с %s</string>
|
||||
<string name="express_provider_not_available">Недоступно для этой пары</string>
|
||||
<string name="express_provider_permission_needed">Требуется разрешение</string>
|
||||
<string name="express_terms_of_use">Условиями использования</string>
|
||||
<string name="feedback_data_collection_message">Информация ниже не является обязательной. Вы можете стереть её, если хотите.</string>
|
||||
<string name="feedback_preface_rate_negative">Расскажите, каких функций вам не хватает, и мы постараемся вам помочь.</string>
|
||||
<string name="feedback_preface_scan_failed">Скажите, пожалуйста, какая у вас карта?</string>
|
||||
|
|
|
|||
|
|
@ -224,11 +224,15 @@
|
|||
<string name="express_fetch_best_rates">Fetching best rates...</string>
|
||||
<string name="express_floating_rate">Floating rate</string>
|
||||
<string name="express_go_to_provider">Go to provider</string>
|
||||
<string name="express_legal_one_placeholder">By using swap functionality, you agree with provider’s %s</string>
|
||||
<string name="express_legal_two_placeholders">By using swap functionality, you agree with provider’s %1$s and %2$s</string>
|
||||
<string name="express_privacy_policy">Privacy Policy</string>
|
||||
<string name="express_provider">Provider</string>
|
||||
<string name="express_provider_best_rate">Best rate</string>
|
||||
<string name="express_provider_min_amount">Available from %s</string>
|
||||
<string name="express_provider_not_available">Unavailable for this pair</string>
|
||||
<string name="express_provider_permission_needed">Permission Required</string>
|
||||
<string name="express_terms_of_use">Terms of Use</string>
|
||||
<string name="feedback_data_collection_message">The following information is optional. You can erase it if you don\'t want to share it.</string>
|
||||
<string name="feedback_preface_rate_negative">Tell us what functions you are missing, and we will try to help you.</string>
|
||||
<string name="feedback_preface_scan_failed">Please tell us what card do you have</string>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
|
|||
name = exchangeProvider.name,
|
||||
type = convertExchangeType(exchangeProvider.type),
|
||||
imageLarge = exchangeProvider.imageLargeUrl,
|
||||
termsOfUse = exchangeProvider.termsOfUse,
|
||||
privacyPolicy = exchangeProvider.privacyPolicy,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -57,6 +59,8 @@ class SwapPairInfoConverter : Converter<SwapPairsWithProviders, PairsWithProvide
|
|||
name = additionalProvider.name,
|
||||
type = convertExchangeType(additionalProvider.type),
|
||||
imageLarge = additionalProvider.imageLargeUrl,
|
||||
termsOfUse = additionalProvider.termsOfUse,
|
||||
privacyPolicy = additionalProvider.privacyPolicy,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ data class SwapProvider(
|
|||
val name: String,
|
||||
val type: ExchangeProviderType,
|
||||
val imageLarge: String,
|
||||
val termsOfUse: String?,
|
||||
val privacyPolicy: String?,
|
||||
)
|
||||
|
||||
enum class ExchangeProviderType {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ dependencies {
|
|||
|
||||
/** Other libraries */
|
||||
implementation(deps.compose.shimmer)
|
||||
implementation(deps.compose.accompanist.webView)
|
||||
implementation(deps.compose.accompanist.systemUiController)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.timber)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ data class SwapStateHolder(
|
|||
val bottomSheetConfig: TangemBottomSheetConfig? = null,
|
||||
|
||||
val swapButton: SwapButton,
|
||||
val tosState: TosState? = null,
|
||||
|
||||
val onRefresh: () -> Unit,
|
||||
val onBackClicked: () -> Unit,
|
||||
|
|
@ -106,6 +107,17 @@ sealed interface TransactionCardType {
|
|||
) : TransactionCardType
|
||||
}
|
||||
|
||||
data class TosState(
|
||||
val tosLink: LegalState?,
|
||||
val policyLink: LegalState?,
|
||||
)
|
||||
|
||||
data class LegalState(
|
||||
val title: TextReference,
|
||||
val link: String,
|
||||
val onClick: (String) -> Unit,
|
||||
)
|
||||
|
||||
sealed interface SwapWarning {
|
||||
data class PermissionNeeded(val notificationConfig: NotificationConfig) : SwapWarning
|
||||
object InsufficientFunds : SwapWarning
|
||||
|
|
|
|||
|
|
@ -21,4 +21,6 @@ data class UiActions(
|
|||
val onProviderClick: (String) -> Unit,
|
||||
val onProviderSelect: (String) -> Unit,
|
||||
val onBuyClick: () -> Unit,
|
||||
val onPolicyClick: (String) -> Unit,
|
||||
val onTosClick: (String) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.feature.swap.models.states
|
||||
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
|
||||
internal data class WebViewBottomSheetConfig(val url: String) : TangemBottomSheetConfigContent
|
||||
|
|
@ -271,6 +271,26 @@ internal class StateBuilder(
|
|||
selectionType = ProviderState.SelectionType.CLICK,
|
||||
onProviderClick = actions.onProviderClick,
|
||||
),
|
||||
tosState = createTosState(swapProvider),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createTosState(swapProvider: SwapProvider): TosState {
|
||||
return TosState(
|
||||
tosLink = swapProvider.termsOfUse?.let {
|
||||
LegalState(
|
||||
title = resourceReference(R.string.express_terms_of_use),
|
||||
link = it,
|
||||
onClick = actions.onTosClick,
|
||||
)
|
||||
},
|
||||
policyLink = swapProvider.privacyPolicy?.let {
|
||||
LegalState(
|
||||
title = resourceReference(R.string.express_privacy_policy),
|
||||
link = it,
|
||||
onClick = actions.onPolicyClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -422,6 +442,7 @@ internal class StateBuilder(
|
|||
ChangeCardsButtonState.DISABLED
|
||||
},
|
||||
providerState = providerState,
|
||||
tosState = createTosState(swapProvider),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -804,6 +825,17 @@ internal class StateBuilder(
|
|||
}
|
||||
}
|
||||
|
||||
fun showWebViewBottomSheet(uiState: SwapStateHolder, url: String, onDismiss: () -> Unit): SwapStateHolder {
|
||||
val config = WebViewBottomSheetConfig(url = url)
|
||||
return uiState.copy(
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShow = true,
|
||||
onDismissRequest = onDismiss,
|
||||
content = config,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun showPermissionBottomSheet(uiState: SwapStateHolder, onDismiss: () -> Unit): SwapStateHolder {
|
||||
val permissionState = uiState.permissionState
|
||||
if (permissionState is SwapPermissionState.ReadyForRequest) {
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ package com.tangem.feature.swap.ui
|
|||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -14,6 +13,7 @@ import com.tangem.feature.swap.models.SwapStateHolder
|
|||
import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig
|
||||
import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig
|
||||
import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig
|
||||
import com.tangem.feature.swap.models.states.WebViewBottomSheetConfig
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
|
||||
@Composable
|
||||
|
|
@ -48,6 +48,9 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) {
|
|||
is ChooseFeeBottomSheetConfig -> {
|
||||
ChooseFeeBottomSheet(config = config)
|
||||
}
|
||||
is WebViewBottomSheetConfig -> {
|
||||
WebViewBottomSheet(config = config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedVisibility
|
|||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -11,8 +12,12 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import com.tangem.common.Strings.STARS
|
||||
|
|
@ -72,6 +77,15 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi
|
|||
}
|
||||
}
|
||||
|
||||
state.tosState?.let {
|
||||
ProviderTos(
|
||||
tosState = it,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = keyboard is Keyboard.Opened,
|
||||
modifier = Modifier
|
||||
|
|
@ -185,6 +199,84 @@ private fun TransactionCardData(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProviderTos(tosState: TosState, modifier: Modifier = Modifier) {
|
||||
val tos = tosState.tosLink
|
||||
val policy = tosState.policyLink
|
||||
if (tos == null && policy == null) return
|
||||
|
||||
val (annotatedString, click) = getAnnotatedStringForLegalsWithClick(tos, policy)
|
||||
|
||||
ClickableText(
|
||||
text = annotatedString,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing54),
|
||||
style = TangemTheme.typography.caption2.copy(textAlign = TextAlign.Center),
|
||||
onClick = click,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getAnnotatedStringForLegalsWithClick(
|
||||
tos: LegalState?,
|
||||
policy: LegalState?,
|
||||
): Pair<AnnotatedString, (Int) -> Unit> {
|
||||
return if (tos != null && policy != null) {
|
||||
val tosTitle = tos.title.resolveReference()
|
||||
val policyTitle = policy.title.resolveReference()
|
||||
val fullString = stringResource(id = R.string.express_legal_two_placeholders, tosTitle, policyTitle)
|
||||
val tosIndex = fullString.indexOf(tosTitle)
|
||||
val policyIndex = fullString.indexOf(policyTitle)
|
||||
val string = buildAnnotatedString {
|
||||
withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) {
|
||||
append(fullString.substring(0, tosIndex))
|
||||
}
|
||||
withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) {
|
||||
append(fullString.substring(tosIndex, tosIndex + tosTitle.length))
|
||||
}
|
||||
withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) {
|
||||
append(fullString.substring(tosIndex + tosTitle.length, policyIndex))
|
||||
}
|
||||
withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) {
|
||||
append(fullString.substring(policyIndex, policyIndex + policyTitle.length))
|
||||
}
|
||||
}
|
||||
val click = { i: Int ->
|
||||
val tosStyle = requireNotNull(string.spanStyles.getOrNull(1))
|
||||
if (i in tosStyle.start..tosStyle.end) {
|
||||
tos.onClick(tos.link)
|
||||
}
|
||||
val policyStyle = requireNotNull(string.spanStyles.lastOrNull())
|
||||
if (i in policyStyle.start..policyStyle.end) {
|
||||
policy.onClick(policy.link)
|
||||
}
|
||||
}
|
||||
string to click
|
||||
} else {
|
||||
val legal = requireNotNull(tos ?: policy) { "tos or policy must not be null" }
|
||||
val legalTitle = legal.title
|
||||
.resolveReference()
|
||||
val fullString = stringResource(id = R.string.express_legal_one_placeholder, legal)
|
||||
val legalIndex = fullString.indexOf(legalTitle)
|
||||
val string = buildAnnotatedString {
|
||||
withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) {
|
||||
append(fullString.substring(0, legalIndex))
|
||||
}
|
||||
withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) {
|
||||
append(fullString.substring(legalIndex, legalIndex + legalTitle.length))
|
||||
}
|
||||
}
|
||||
val click = { i: Int ->
|
||||
val legalStyle = requireNotNull(string.spanStyles.lastOrNull())
|
||||
if (i in legalStyle.start..legalStyle.end) {
|
||||
legal.onClick(legal.link)
|
||||
}
|
||||
}
|
||||
string to click
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) {
|
||||
|
|
@ -407,6 +499,18 @@ private val state = SwapStateHolder(
|
|||
permissionState = SwapPermissionState.InProgress,
|
||||
blockchainId = "POLYGON",
|
||||
providerState = ProviderState.Loading(),
|
||||
tosState = TosState(
|
||||
tosLink = LegalState(
|
||||
title = stringReference("Tangem"),
|
||||
link = "https://tangem.com",
|
||||
onClick = {},
|
||||
),
|
||||
policyLink = LegalState(
|
||||
title = stringReference("Tangem"),
|
||||
link = "https://tangem.com",
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@Preview
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.feature.swap.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.google.accompanist.web.WebView
|
||||
import com.google.accompanist.web.rememberWebViewState
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.swap.models.states.WebViewBottomSheetConfig
|
||||
|
||||
@Composable
|
||||
fun WebViewBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet(
|
||||
config = config,
|
||||
contentColor = TangemTheme.colors.background.tertiary,
|
||||
) { content: WebViewBottomSheetConfig ->
|
||||
WebViewBottomSheetContent(
|
||||
WebViewBottomSheetConfig(url = content.url),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WebViewBottomSheetContent(content: WebViewBottomSheetConfig) {
|
||||
val state = rememberWebViewState(content.url)
|
||||
val isInPreviewMode = LocalInspectionMode.current
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
WebView(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.secondary),
|
||||
captureBackPresses = false,
|
||||
onCreated = {
|
||||
if (!isInPreviewMode) {
|
||||
it.settings.apply {
|
||||
javaScriptEnabled = false
|
||||
allowFileAccess = false
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_WebViewBottomSheetContent_InLightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
WebViewBottomSheetContent(
|
||||
content = WebViewBottomSheetConfig(
|
||||
url = "https://tangem.com/en/",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_WebViewBottomSheetContent_InDarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
WebViewBottomSheetContent(
|
||||
content = WebViewBottomSheetConfig(
|
||||
url = "https://tangem.com/en/",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,8 +22,8 @@ import com.tangem.feature.swap.domain.models.ExpressException
|
|||
import com.tangem.feature.swap.domain.models.domain.PermissionOptions
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
import com.tangem.feature.swap.models.*
|
||||
import com.tangem.feature.swap.presentation.SwapFragment
|
||||
import com.tangem.feature.swap.router.SwapNavScreen
|
||||
|
|
@ -845,6 +845,16 @@ internal class SwapViewModel @Inject constructor(
|
|||
onRetryClick = {
|
||||
startLoadingQuotesFromLastState()
|
||||
},
|
||||
onPolicyClick = {
|
||||
uiState = stateBuilder.showWebViewBottomSheet(uiState, it) {
|
||||
uiState = stateBuilder.dismissBottomSheet(uiState)
|
||||
}
|
||||
},
|
||||
onTosClick = {
|
||||
uiState = stateBuilder.showWebViewBottomSheet(uiState, it) {
|
||||
uiState = stateBuilder.dismissBottomSheet(uiState)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue