Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-16 11:27:57 +05:00
parent 3d214007f4
commit fd4c93ce8d
7 changed files with 466 additions and 0 deletions

View file

@ -0,0 +1,51 @@
package com.tangem.features.walletconnect.connections.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import com.tangem.core.decompose.context.AppComponentContext
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.features.walletconnect.connections.ui.AlertsModalBottomSheet
internal class AlertsComponent(
appComponentContext: AppComponentContext,
private val params: Params,
) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent {
override fun dismiss() {
params.alertType.onDismiss()
}
@Composable
override fun BottomSheet() {
AlertsModalBottomSheet(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = params.alertType.onDismiss,
content = params.alertType,
),
)
}
internal data class Params(val alertType: AlertType)
@Immutable
internal sealed class AlertType : TangemBottomSheetConfigContent {
abstract val onDismiss: () -> Unit
data class VerifiedDomain(val appName: String, override val onDismiss: () -> Unit) : AlertType()
data class UnknownDomain(val onConnect: () -> Unit, override val onDismiss: () -> Unit) : AlertType()
data class UnsafeDomain(val onConnect: () -> Unit, override val onDismiss: () -> Unit) : AlertType()
data class UnsupportedNetworks(val appName: String, override val onDismiss: () -> Unit) : AlertType()
data class WcDisconnected(override val onDismiss: () -> Unit) : AlertType()
data class UnknownError(val errorCode: Int, override val onDismiss: () -> Unit) : AlertType()
data class WrongCardSelected(override val onDismiss: () -> Unit) : AlertType()
data class ConnectionTimeout(val onTryAgain: () -> Unit, override val onDismiss: () -> Unit) : AlertType()
data class MaliciousTransaction(
val descriptionMessage: String,
val onConnect: () -> Unit,
override val onDismiss: () -> Unit,
) : AlertType()
}
}

View file

@ -0,0 +1,355 @@
package com.tangem.features.walletconnect.connections.ui
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
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.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SecondaryButtonIconEnd
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.walletconnect.connections.components.AlertsComponent
@Composable
internal fun AlertsModalBottomSheet(config: TangemBottomSheetConfig) {
TangemModalBottomSheet<AlertsComponent.AlertType>(
config = config,
title = { alert ->
TangemModalBottomSheetTitle(
endIconRes = R.drawable.ic_close_24,
onEndClick = alert.onDismiss,
)
},
content = { alert -> Content(alert = alert) },
)
}
@Composable
private fun Content(alert: AlertsComponent.AlertType, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
ContentContainer(
modifier = Modifier
.heightIn(min = TangemTheme.dimens.size180)
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16)
.padding(bottom = 32.dp),
alert = alert,
)
ButtonsContainer(modifier = Modifier.fillMaxWidth(), alert = alert)
}
}
@Composable
private fun ContentContainer(alert: AlertsComponent.AlertType, modifier: Modifier = Modifier) {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
AlertIcon(alert = alert)
AlertContentTitle(
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing24),
alert = alert,
)
AlertContentDescription(
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing8),
alert = alert,
)
AlertAudit(alert, modifier = Modifier.padding(top = TangemTheme.dimens.spacing16))
}
}
@Suppress("LongMethod")
@Composable
private fun ButtonsContainer(alert: AlertsComponent.AlertType, modifier: Modifier = Modifier) {
Column(
modifier = modifier.padding(all = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
val buttonsModifier = Modifier.fillMaxWidth()
when (alert) {
is AlertsComponent.AlertType.UnsafeDomain -> {
PrimaryButton(
modifier = buttonsModifier,
text = stringResourceSafe(R.string.common_cancel),
onClick = alert.onDismiss,
)
SecondaryButton(
modifier = buttonsModifier,
onClick = alert.onConnect,
text = stringResourceSafe(R.string.wc_alert_connect_anyway),
)
}
is AlertsComponent.AlertType.MaliciousTransaction -> {
PrimaryButton(
modifier = buttonsModifier,
text = stringResourceSafe(R.string.common_cancel),
onClick = alert.onDismiss,
)
SecondaryButtonIconEnd(
modifier = buttonsModifier,
onClick = alert.onConnect,
iconResId = R.drawable.ic_tangem_24,
text = stringResourceSafe(R.string.wc_alert_connect_anyway),
)
}
is AlertsComponent.AlertType.UnknownDomain -> {
PrimaryButton(
modifier = buttonsModifier,
text = stringResourceSafe(R.string.common_cancel),
onClick = alert.onDismiss,
)
SecondaryButton(
modifier = buttonsModifier,
onClick = alert.onConnect,
text = stringResourceSafe(R.string.wc_alert_connect_anyway),
)
}
is AlertsComponent.AlertType.VerifiedDomain -> SecondaryButton(
modifier = buttonsModifier,
text = stringResourceSafe(R.string.common_done),
onClick = alert.onDismiss,
)
is AlertsComponent.AlertType.ConnectionTimeout -> {
PrimaryButton(
modifier = buttonsModifier,
onClick = alert.onTryAgain,
text = stringResourceSafe(R.string.alert_button_try_again),
)
SecondaryButton(
modifier = buttonsModifier,
onClick = alert.onDismiss,
text = stringResourceSafe(R.string.balance_hidden_got_it_button),
)
}
is AlertsComponent.AlertType.WrongCardSelected,
is AlertsComponent.AlertType.UnknownError,
-> SecondaryButton(
modifier = buttonsModifier,
onClick = alert.onDismiss,
text = stringResourceSafe(R.string.balance_hidden_got_it_button),
)
is AlertsComponent.AlertType.WcDisconnected,
is AlertsComponent.AlertType.UnsupportedNetworks,
-> PrimaryButton(
modifier = buttonsModifier,
onClick = alert.onDismiss,
text = stringResourceSafe(R.string.balance_hidden_got_it_button),
)
}
}
}
@Composable
private fun AlertIcon(alert: AlertsComponent.AlertType, modifier: Modifier = Modifier) {
val color = when (alert) {
is AlertsComponent.AlertType.VerifiedDomain -> TangemTheme.colors.icon.accent
is AlertsComponent.AlertType.WcDisconnected,
is AlertsComponent.AlertType.ConnectionTimeout,
is AlertsComponent.AlertType.UnsupportedNetworks,
-> TangemTheme.colors.icon.informative
is AlertsComponent.AlertType.WrongCardSelected,
is AlertsComponent.AlertType.UnknownDomain,
is AlertsComponent.AlertType.UnknownError,
-> TangemTheme.colors.icon.attention
is AlertsComponent.AlertType.MaliciousTransaction,
is AlertsComponent.AlertType.UnsafeDomain,
-> TangemTheme.colors.icon.warning
}
@DrawableRes val drawableId = when (alert) {
is AlertsComponent.AlertType.WcDisconnected,
is AlertsComponent.AlertType.ConnectionTimeout,
-> R.drawable.ic_wallet_connect_24
is AlertsComponent.AlertType.MaliciousTransaction -> R.drawable.img_knight_shield_32
is AlertsComponent.AlertType.UnknownDomain,
is AlertsComponent.AlertType.UnsafeDomain,
-> R.drawable.img_knight_shield_32
is AlertsComponent.AlertType.UnsupportedNetworks -> R.drawable.ic_network_new_24
is AlertsComponent.AlertType.WrongCardSelected,
is AlertsComponent.AlertType.UnknownError,
-> R.drawable.img_attention_20
is AlertsComponent.AlertType.VerifiedDomain -> R.drawable.img_approvale2_20
}
val iconTint = when (alert) {
is AlertsComponent.AlertType.UnknownDomain,
is AlertsComponent.AlertType.UnsafeDomain,
is AlertsComponent.AlertType.MaliciousTransaction,
is AlertsComponent.AlertType.UnsupportedNetworks,
is AlertsComponent.AlertType.ConnectionTimeout,
is AlertsComponent.AlertType.WcDisconnected,
-> color
is AlertsComponent.AlertType.VerifiedDomain,
is AlertsComponent.AlertType.UnknownError,
is AlertsComponent.AlertType.WrongCardSelected,
-> Color.Unspecified
}
Box(
modifier = modifier
.size(TangemTheme.dimens.size56)
.clip(CircleShape)
.background(color.copy(alpha = 0.1F)),
contentAlignment = Alignment.Center,
content = {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size32),
painter = painterResource(drawableId),
contentDescription = null,
tint = iconTint,
)
},
)
}
@Composable
private fun AlertContentTitle(alert: AlertsComponent.AlertType, modifier: Modifier = Modifier) {
@StringRes val titleRes: Int = when (alert) {
is AlertsComponent.AlertType.UnknownDomain,
is AlertsComponent.AlertType.UnsafeDomain,
is AlertsComponent.AlertType.MaliciousTransaction,
-> R.string.security_alert_title
is AlertsComponent.AlertType.ConnectionTimeout -> R.string.wc_alert_connection_timeout_title
is AlertsComponent.AlertType.UnknownError -> R.string.wc_alert_unknown_error_title
is AlertsComponent.AlertType.UnsupportedNetworks -> R.string.wc_alert_unsupported_networks_title
is AlertsComponent.AlertType.VerifiedDomain -> R.string.wc_alert_verified_domain_title
is AlertsComponent.AlertType.WcDisconnected -> R.string.wc_alert_session_disconnected_title
is AlertsComponent.AlertType.WrongCardSelected -> R.string.wc_alert_wrong_card_title
}
Text(
modifier = modifier,
text = stringResourceSafe(titleRes),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
}
@Composable
private fun AlertContentDescription(alert: AlertsComponent.AlertType, modifier: Modifier = Modifier) {
val message = when (alert) {
is AlertsComponent.AlertType.ConnectionTimeout -> stringResourceSafe(
R.string.wc_alert_connection_timeout_description,
)
is AlertsComponent.AlertType.MaliciousTransaction -> alert.descriptionMessage
is AlertsComponent.AlertType.UnknownDomain,
is AlertsComponent.AlertType.UnsafeDomain,
-> stringResourceSafe(R.string.wc_alert_domain_issues_description)
is AlertsComponent.AlertType.UnknownError -> stringResourceSafe(
R.string.wc_alert_unknown_error_description,
alert.errorCode,
)
is AlertsComponent.AlertType.UnsupportedNetworks -> stringResourceSafe(
R.string.wc_alert_unsupported_networks_description,
alert.appName,
)
is AlertsComponent.AlertType.VerifiedDomain -> stringResourceSafe(
R.string.wc_alert_verified_domain_description,
alert.appName,
)
is AlertsComponent.AlertType.WcDisconnected -> stringResourceSafe(
R.string.wc_alert_session_disconnected_description,
)
is AlertsComponent.AlertType.WrongCardSelected -> stringResourceSafe(R.string.wc_alert_wrong_card_description)
}
Text(
modifier = modifier,
text = message,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
}
@Composable
private fun AlertAudit(alert: AlertsComponent.AlertType, modifier: Modifier = Modifier) {
when (alert) {
is AlertsComponent.AlertType.UnknownDomain -> Text(
modifier = modifier
.background(
shape = RoundedCornerShape(TangemTheme.dimens.radius16),
color = TangemTheme.colors.text.primary1.copy(alpha = 0.1F),
)
.padding(vertical = TangemTheme.dimens.spacing4, horizontal = TangemTheme.dimens.spacing12),
text = stringResourceSafe(R.string.wc_alert_audit_unknown_domain),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.primary1,
)
is AlertsComponent.AlertType.UnsafeDomain -> Text(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing16)
.background(
shape = RoundedCornerShape(TangemTheme.dimens.radius16),
color = TangemTheme.colors.text.warning.copy(alpha = 0.1F),
)
.padding(vertical = TangemTheme.dimens.spacing4, horizontal = TangemTheme.dimens.spacing12),
text = stringResourceSafe(R.string.wc_alert_audit_unknown_domain),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.warning,
)
is AlertsComponent.AlertType.ConnectionTimeout,
is AlertsComponent.AlertType.MaliciousTransaction,
is AlertsComponent.AlertType.UnknownError,
is AlertsComponent.AlertType.UnsupportedNetworks,
is AlertsComponent.AlertType.VerifiedDomain,
is AlertsComponent.AlertType.WcDisconnected,
is AlertsComponent.AlertType.WrongCardSelected,
-> Unit
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AlertsModalBottomSheet_Preview(
@PreviewParameter(AlertTypesProvider::class) alert: AlertsComponent.AlertType,
) {
TangemThemePreview {
AlertsModalBottomSheet(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = alert,
),
)
}
}
private class AlertTypesProvider : CollectionPreviewParameterProvider<AlertsComponent.AlertType>(
collection = listOf(
AlertsComponent.AlertType.VerifiedDomain(appName = "React App", onDismiss = {}),
AlertsComponent.AlertType.UnknownDomain(onConnect = {}, onDismiss = {}),
AlertsComponent.AlertType.UnsafeDomain(onConnect = {}, onDismiss = {}),
AlertsComponent.AlertType.MaliciousTransaction(
descriptionMessage = "The transaction approves erc20 tokens to a known malicious address",
onConnect = {},
onDismiss = {},
),
AlertsComponent.AlertType.UnsupportedNetworks(appName = "React App", onDismiss = {}),
AlertsComponent.AlertType.WcDisconnected(onDismiss = {}),
AlertsComponent.AlertType.UnknownError(errorCode = 8005, onDismiss = {}),
AlertsComponent.AlertType.WrongCardSelected(onDismiss = {}),
AlertsComponent.AlertType.ConnectionTimeout(onTryAgain = {}, onDismiss = {}),
),
)