From b19deae346aa44ca50f268c72836d6d08464db18 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 Dec 2023 17:28:03 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../models/response/ExchangeProvider.kt | 6 + core/res/src/main/res/values-ru/strings.xml | 6 + core/res/src/main/res/values/strings.xml | 4 + .../swap/converters/SwapPairInfoConverter.kt | 4 + .../domain/models/domain/SwapPairLeast.kt | 2 + features/swap/presentation/build.gradle.kts | 2 + .../feature/swap/models/SwapStateHolder.kt | 12 ++ .../tangem/feature/swap/models/UiActions.kt | 2 + .../models/states/WebViewBottomSheetConfig.kt | 5 + .../tangem/feature/swap/ui/StateBuilder.kt | 32 ++++++ .../com/tangem/feature/swap/ui/SwapScreen.kt | 7 +- .../feature/swap/ui/SwapScreenContent.kt | 104 ++++++++++++++++++ .../feature/swap/ui/WebViewBottomSheet.kt | 79 +++++++++++++ .../feature/swap/viewmodels/SwapViewModel.kt | 12 +- 14 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/WebViewBottomSheetConfig.kt create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt index 4e2bc6d69c..acbd56ec03 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt @@ -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 { diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 21bec9195d..9c3eb60b10 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -203,11 +203,17 @@ Провайдер запрашивает прохождение верификации Список токенов в вашем кошельке Получение наилучших курсов... + Плавающая ставка + К провайдеру + Пользуясь сервисом вы соглашаетесь с %s + Пользуясь сервисом вы соглашаетесь с %1$s и %2$s + Политикой конфиденциальности Провайдер Лучший курс Доступно с %s Недоступно для этой пары Требуется разрешение + Условиями использования Информация ниже не является обязательной. Вы можете стереть её, если хотите. Расскажите, каких функций вам не хватает, и мы постараемся вам помочь. Скажите, пожалуйста, какая у вас карта? diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 3088fc97ea..b81d198635 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -224,11 +224,15 @@ Fetching best rates... Floating rate Go to provider + By using swap functionality, you agree with provider’s %s + By using swap functionality, you agree with provider’s %1$s and %2$s + Privacy Policy Provider Best rate Available from %s Unavailable for this pair Permission Required + Terms of Use The following information is optional. You can erase it if you don\'t want to share it. Tell us what functions you are missing, and we will try to help you. Please tell us what card do you have diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index 4ea0faf9cd..5bb5079f95 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -43,6 +43,8 @@ class SwapPairInfoConverter : Converter 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 diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index 48a3d55f55..258039517e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -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, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/WebViewBottomSheetConfig.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/WebViewBottomSheetConfig.kt new file mode 100644 index 0000000000..ee2bce3961 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/WebViewBottomSheetConfig.kt @@ -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 \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index e66df2962d..89a2f5343e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -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) { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index c25ea64c0b..cc39867e7d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -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) + } } } } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 360cd4e016..ba5cc5a853 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -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 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 diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt new file mode 100644 index 0000000000..4a068cddbd --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt @@ -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/", + ), + ) + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index b2c3060ae4..399f3b6f79 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -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) + } + }, ) }