diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 7a0bd7ff8e..0e158723f2 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -504,6 +504,7 @@ Pull this up or tap the search bar to add tokens directly from the market Add tokens NFC is not available on your device + About NFT %d item %d items @@ -1251,13 +1252,26 @@ Verified domain Wrong card or ring selected in Tangem App We\'ve got some kind of problem + Connect + Network + Networks + Wallet + View your wallet balance and activity + Sign transactions without your notice + Request approval for transactions + Will not be able to + Would like to + Connection request Connections Disconnect all Text about discnected all dApps Disconect All dApps + Add the %s network to your profile for this wallet + The wallet has no required networks New connection Connect your wallet to a different dApps No sessions + Wallet connect Discard You have an interrupted backup. Do you want to resume? Yes, resume diff --git a/core/ui/src/main/res/drawable/ic_connect_new_24.xml b/core/ui/src/main/res/drawable/ic_connect_new_24.xml new file mode 100644 index 0000000000..523f1a7759 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_connect_new_24.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_select_18_24.xml b/core/ui/src/main/res/drawable/ic_select_18_24.xml new file mode 100644 index 0000000000..5a131c0a6a --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_select_18_24.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_wallet_new_24.xml b/core/ui/src/main/res/drawable/ic_wallet_new_24.xml new file mode 100644 index 0000000000..5add836f95 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_wallet_new_24.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index e85bbfc291..841499063e 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(projects.core.decompose) implementation(projects.core.ui) implementation(projects.common.routing) + implementation(projects.common.ui) /** Domain models */ implementation(projects.domain.qrScanning.models) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoComponent.kt new file mode 100644 index 0000000000..bba2e61463 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoComponent.kt @@ -0,0 +1,31 @@ +package com.tangem.features.walletconnect.connections.components + +import androidx.compose.runtime.Composable +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.WcAppInfoModalBottomSheet + +internal class WcAppInfoComponent( + private val appComponentContext: AppComponentContext, + private val params: Params, +) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + WcAppInfoModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, // todo(wc) will be fixed in next PR's + ), + ) + } + + data class Params(val onDismiss: () -> Unit) +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt new file mode 100644 index 0000000000..a588d1ddd0 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt @@ -0,0 +1,25 @@ +package com.tangem.features.walletconnect.connections.entity + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal data class WcAppInfoUM( + val appName: String, + val appIcon: String, + val isVerified: Boolean, + val appSubtitle: String, + val notificationUM: NotificationUM?, + val walletName: String, + val networksInfo: WcNetworksInfo, + val onDismiss: () -> Unit, + val onConnect: () -> Unit, +) : TangemBottomSheetConfigContent + +@Immutable +internal sealed class WcNetworksInfo { + data class MissingRequiredNetworkInfo(val network: String) : WcNetworksInfo() + data class ContainsAllRequiredNetworks(val icons: ImmutableList) : WcNetworksInfo() +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt new file mode 100644 index 0000000000..ca17e5f44f --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt @@ -0,0 +1,69 @@ +package com.tangem.features.walletconnect.connections.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +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.unit.dp +import coil.compose.AsyncImage +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.walletconnect.impl.R + +@Composable +internal fun WcAppInfoItem( + iconUrl: String, + title: String, + subtitle: String, + isVerified: Boolean, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.padding(TangemTheme.dimens.spacing12), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { + AsyncImage( + modifier = Modifier + .size(TangemTheme.dimens.size48) + .clip(RoundedCornerShape(TangemTheme.dimens.radius8)) + .background(Color.Black), + model = iconUrl, + contentDescription = title, + ) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f, fill = false), + text = title, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h3, + ) + if (isVerified) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(R.drawable.img_approvale2_20), + contentDescription = null, + tint = Color.Unspecified, + ) + } + } + Text( + text = subtitle, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + ) + } + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt new file mode 100644 index 0000000000..5ce783ebb7 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoContent.kt @@ -0,0 +1,447 @@ +package com.tangem.features.walletconnect.connections.ui + +import android.content.res.Configuration +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.* +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.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices +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.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SecondaryButton +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.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM +import com.tangem.features.walletconnect.connections.entity.WcNetworksInfo +import com.tangem.features.walletconnect.impl.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun WcAppInfoModalBottomSheet(config: TangemBottomSheetConfig) { + TangemModalBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.tertiary, + title = { state -> + TangemModalBottomSheetTitle( + title = resourceReference(R.string.wc_wallet_connect), + endIconRes = R.drawable.ic_close_24, + onEndClick = state.onDismiss, + ) + }, + content = { state -> WcAppInfoModalBottomSheetContent(state) }, + ) +} + +@Composable +private fun WcAppInfoModalBottomSheetContent(state: WcAppInfoUM) { + Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) { + val blocksModifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .background(color = TangemTheme.colors.background.action) + .fillMaxWidth() + WcAppInfoFirstBlock(modifier = blocksModifier, state = state) + state.notificationUM?.config?.let { config -> + Notification( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing14), + config = config, + iconTint = TangemTheme.colors.icon.attention, + ) + } + WcAppInfoSecondBlock( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing14) + .then(blocksModifier), + state = state, + ) + WcAppInfoButtons( + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16), + onDismiss = state.onDismiss, + onConnect = state.onConnect, + ) + } +} + +@Composable +private fun WcAppInfoFirstBlock(state: WcAppInfoUM, modifier: Modifier = Modifier) { + var connectionRequestExpanded by remember { mutableStateOf(false) } + Column(modifier = modifier.animateContentSize()) { + WcAppInfoItem( + iconUrl = state.appIcon, + title = state.appName, + subtitle = state.appSubtitle, + isVerified = state.isVerified, + ) + HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) + ConnectionRequestBlock( + modifier = Modifier + .fillMaxWidth() + .clickable { connectionRequestExpanded = !connectionRequestExpanded } + .padding(TangemTheme.dimens.spacing12), + expanded = connectionRequestExpanded, + ) + if (connectionRequestExpanded) { + ConnectionRequestDescription( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing12) + .padding(top = 8.dp, bottom = 12.dp), + ) + } + } +} + +@Composable +private fun ConnectionRequestBlock(expanded: Boolean, modifier: Modifier = Modifier) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_connect_new_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + Text( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing4) + .weight(1f), + text = stringResourceSafe(R.string.wc_connection_request), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + Icon( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12) + .size(width = 18.dp, height = 24.dp), + painter = painterResource(if (expanded) R.drawable.ic_chevron_up_24 else R.drawable.ic_chevron_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } +} + +@Composable +private fun ConnectionRequestDescription(modifier: Modifier = Modifier) { + Column(modifier = modifier) { + Text( + text = stringResourceSafe(R.string.wc_connection_reqeust_would_like), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + ConnectionRequestDescriptionRow( + iconPainter = painterResource(R.drawable.ic_check_24), + tint = TangemTheme.colors.icon.accent, + text = stringResourceSafe(R.string.wc_connection_reqeust_can_view_balance), + ) + ConnectionRequestDescriptionRow( + iconPainter = painterResource(R.drawable.ic_check_24), + tint = TangemTheme.colors.icon.accent, + text = stringResourceSafe(R.string.wc_connection_reqeust_request_approval), + ) + HorizontalDivider( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + thickness = 0.5.dp, + color = TangemTheme.colors.stroke.primary, + ) + Text( + modifier = Modifier.padding(top = 12.dp), + text = stringResourceSafe(R.string.wc_connection_reqeust_will_not), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + ConnectionRequestDescriptionRow( + iconPainter = painterResource(R.drawable.ic_close_24), + tint = TangemTheme.colors.icon.warning, + text = stringResourceSafe(R.string.wc_connection_reqeust_cant_sign), + ) + } +} + +@Composable +private fun ConnectionRequestDescriptionRow( + iconPainter: Painter, + tint: Color, + text: String, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(top = TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = iconPainter, + contentDescription = null, + tint = tint, + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = text, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.primary1, + ) + } +} + +@Composable +private fun WcAppInfoSecondBlock(state: WcAppInfoUM, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + val itemsModifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12) + WalletRowItem(modifier = itemsModifier, walletName = state.walletName) + HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) + SelectNetworksBlock( + networksInfo = state.networksInfo, + modifier = itemsModifier, + ) + when (state.networksInfo) { + is WcNetworksInfo.ContainsAllRequiredNetworks -> Unit + is WcNetworksInfo.MissingRequiredNetworkInfo -> { + HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) + Notification( + config = NotificationConfig( + iconResId = R.drawable.ic_alert_circle_24, + title = resourceReference(R.string.wc_missing_required_network_title), + subtitle = resourceReference( + R.string.wc_missing_required_network_description, + wrappedList(state.networksInfo.network), + ), + ), + iconTint = TangemTheme.colors.icon.attention, + containerColor = TangemTheme.colors.background.action, + ) + } + } + } +} + +@Composable +private fun WalletRowItem(walletName: String, modifier: Modifier = Modifier) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_wallet_new_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + Text( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing4) + .weight(1f), + text = stringResourceSafe(R.string.manage_tokens_network_selector_wallet), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing16) + .weight(1f), + text = walletName, + textAlign = TextAlign.End, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + Icon( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12) + .size(width = 18.dp, height = 24.dp), + painter = painterResource(R.drawable.ic_select_18_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } +} + +@Composable +private fun SelectNetworksBlock(networksInfo: WcNetworksInfo, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_network_new_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + Text( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing4) + .weight(1f), + text = stringResourceSafe(R.string.wc_common_networks), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + when (networksInfo) { + is WcNetworksInfo.ContainsAllRequiredNetworks -> NetworkIcons(icons = networksInfo.icons) + is WcNetworksInfo.MissingRequiredNetworkInfo -> Unit + } + Icon( + modifier = Modifier.size(width = 18.dp, height = 24.dp), + painter = painterResource( + id = when (networksInfo) { + is WcNetworksInfo.ContainsAllRequiredNetworks -> R.drawable.ic_select_18_24 + is WcNetworksInfo.MissingRequiredNetworkInfo -> R.drawable.ic_chevron_right_24 + }, + ), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } +} + +@Suppress("MagicNumber") +@Composable +private fun NetworkIcons(icons: ImmutableList, modifier: Modifier = Modifier) { + val (iconsToShow, remainingCount) = remember(icons) { + when { + icons.size <= 4 -> icons to 0 + else -> icons.take(3) to icons.size - 3 + } + } + Box(modifier = modifier.wrapContentWidth()) { + iconsToShow.forEachIndexed { index, iconRes -> + Image( + painter = painterResource(id = iconRes), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing16.times(index)) + .border( + width = 2.dp, + color = TangemTheme.colors.background.action, + shape = CircleShape, + ) + .padding(2.dp) + .clip(CircleShape) + .size(20.dp), + ) + } + if (remainingCount > 0) { + Box( + modifier = modifier + .padding(start = 48.dp) + .border( + width = 2.dp, + color = TangemTheme.colors.background.action, + shape = CircleShape, + ) + .padding(2.dp) + .background(color = TangemTheme.colors.background.action) + .size(20.dp) + .clip(CircleShape) + .background(color = TangemTheme.colors.icon.primary1.copy(alpha = 0.1F)), + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = "+$remainingCount", + style = TangemTheme.typography.overline, + color = TangemTheme.colors.text.secondary, + ) + } + } + } +} + +@Composable +private fun WcAppInfoButtons(onDismiss: () -> Unit, onConnect: () -> Unit, modifier: Modifier = Modifier) { + Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + SecondaryButton( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + text = stringResourceSafe(R.string.common_cancel), + onClick = onDismiss, + ) + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + text = stringResourceSafe(R.string.wc_common_connect), + onClick = onConnect, + ) + } +} + +@Composable +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun WcAppInfoBottomSheetPreview(@PreviewParameter(WcAppInfoStateProvider::class) state: WcAppInfoUM) { + TangemThemePreview { + WcAppInfoModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = state, + ), + ) + } +} + +private class WcAppInfoStateProvider : CollectionPreviewParameterProvider( + collection = listOf( + WcAppInfoUM( + appName = "React App", + appIcon = "", + isVerified = true, + appSubtitle = "react-app.walletconnect.com", + notificationUM = null, + walletName = "Tangem 2.0", + networksInfo = WcNetworksInfo.ContainsAllRequiredNetworks( + icons = persistentListOf( + R.drawable.img_optimism_22, + R.drawable.img_bsc_22, + R.drawable.img_avalanche_22, + R.drawable.img_solana_22, + R.drawable.img_avalanche_22, + ), + ), + onDismiss = {}, + onConnect = {}, + ), + WcAppInfoUM( + appName = "React App", + appIcon = "", + isVerified = false, + appSubtitle = "react-app.walletconnect.com", + notificationUM = NotificationUM.Info( + title = resourceReference(R.string.wc_alert_audit_unknown_domain), + subtitle = resourceReference(R.string.wc_alert_domain_issues_description), + ), + walletName = "Tangem 2.0", + networksInfo = WcNetworksInfo.MissingRequiredNetworkInfo(network = "Solana"), + onDismiss = {}, + onConnect = {}, + ), + ), +) \ No newline at end of file