diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt new file mode 100644 index 0000000000..f97a4f5daf --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt @@ -0,0 +1,262 @@ +package com.tangem.core.ui.components.bottomsheets.modal + +import android.content.res.Configuration +import androidx.compose.foundation.background +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.* +import androidx.compose.material3.SheetValue.Expanded +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.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +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.SpacerH +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.internal.ModalBottomSheetWithBackHandling +import com.tangem.core.ui.components.bottomsheets.internal.collapse +import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.WindowInsetsZero + +const val MODAL_SHEET_MAX_HEIGHT = 0.8f + +/** + * Modal bottom sheet with [content] and optional [title]. + * + * Maximum height of sheet is 80% screen height + * + * [Show in Figma](https://www.figma.com/design/09KKG4ZVuFDZhj8WLv5rGJ/%F0%9F%9A%A7-App-experience?node-id=3254-61208&t=vixa7id6ggALcxfF-4) + */ +@Composable +inline fun TangemModalBottomSheet( + config: TangemBottomSheetConfig, + containerColor: Color = TangemTheme.colors.background.primary, + skipPartiallyExpanded: Boolean = true, + noinline onBack: (() -> Unit)? = null, + crossinline title: @Composable BoxScope.(T) -> Unit = {}, + crossinline content: @Composable ColumnScope.(T) -> Unit, +) { + val isAlwaysVisible = LocalBottomSheetAlwaysVisible.current + + if (isAlwaysVisible) { + PreviewModalBottomSheet( + config = config, + containerColor = containerColor, + title = title, + content = content, + skipPartiallyExpanded = skipPartiallyExpanded, + ) + } else { + DefaultModalBottomSheet( + config = config, + containerColor = containerColor, + title = title, + content = content, + onBack = onBack, + skipPartiallyExpanded = skipPartiallyExpanded, + ) + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +inline fun DefaultModalBottomSheet( + config: TangemBottomSheetConfig, + containerColor: Color, + skipPartiallyExpanded: Boolean = true, + noinline onBack: (() -> Unit)? = null, + crossinline title: @Composable (BoxScope.(T) -> Unit), + crossinline content: @Composable (ColumnScope.(T) -> Unit), +) { + var isVisible by remember { mutableStateOf(value = config.isShown) } + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded) + + if (isVisible && config.content is T) { + BasicModalBottomSheet( + config = config, + sheetState = sheetState, + containerColor = containerColor, + title = title, + onBack = onBack, + content = content, + ) + } + + LaunchedEffect(key1 = config.isShown) { + if (config.isShown) { + isVisible = true + } else { + sheetState.collapse { isVisible = false } + } + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +inline fun PreviewModalBottomSheet( + config: TangemBottomSheetConfig, + containerColor: Color, + skipPartiallyExpanded: Boolean = true, + crossinline title: @Composable (BoxScope.(T) -> Unit), + crossinline content: @Composable (ColumnScope.(T) -> Unit), +) { + BasicModalBottomSheet( + config = config, + sheetState = SheetState( + skipPartiallyExpanded = skipPartiallyExpanded, + initialValue = Expanded, + density = LocalDensity.current, + ), + onBack = null, + containerColor = containerColor, + title = title, + content = content, + ) +} + +@Suppress("LongParameterList") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +inline fun BasicModalBottomSheet( + config: TangemBottomSheetConfig, + sheetState: SheetState, + containerColor: Color, + noinline onBack: (() -> Unit)? = null, + crossinline title: @Composable (BoxScope.(T) -> Unit), + crossinline content: @Composable (ColumnScope.(T) -> Unit), + modifier: Modifier = Modifier, +) { + val model = config.content as? T ?: return + + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + val bsContent: @Composable ColumnScope.() -> Unit = { + val maxHeight = LocalConfiguration.current.screenHeightDp * MODAL_SHEET_MAX_HEIGHT + + Column( + modifier = Modifier + .systemBarsPadding() + .padding(horizontal = 8.dp, vertical = 8.dp) + .clip(TangemTheme.shapes.roundedCornersLarge) + .background(containerColor) + .heightIn(max = maxHeight.dp) + .padding(bottom = bottomBarHeight) + .fillMaxWidth(), + ) { + Box(modifier = Modifier.fillMaxWidth()) { + title(model) + } + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + ) { + content(model) + } + } + } + + if (onBack != null) { + ModalBottomSheetWithBackHandling( + modifier = modifier, + onDismissRequest = config.onDismissRequest, + sheetState = sheetState, + containerColor = Color.Transparent, + shape = TangemTheme.shapes.roundedCornersLarge, + contentWindowInsets = { WindowInsetsZero }, + onBack = onBack, + dragHandle = null, + content = bsContent, + ) + } else { + ModalBottomSheet( + modifier = modifier, + onDismissRequest = config.onDismissRequest, + sheetState = sheetState, + containerColor = Color.Transparent, + shape = TangemTheme.shapes.roundedCornersLarge, + contentWindowInsets = { WindowInsetsZero }, + dragHandle = null, + content = bsContent, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 800) +@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemModalBottomSheet_Preview() { + TangemThemePreview { + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = TangemBottomSheetConfigContentPreviewConfig(), + ), + title = { + TangemModalBottomSheetTitle( + endIconRes = R.drawable.ic_close_24, + onEndClick = {}, + ) + }, + content = { + Column( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(100)) + .background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f)) + .padding(12.dp), + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_alert_24), + ), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + SpacerH24() + Text( + text = "Unsuported networks", + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + SpacerH8() + Text( + text = "Tangem does not currently support a required network by React App.", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + SpacerH(48.dp) + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = "Go it", + onClick = {}, + ) + } + }, + ) + } +} + +private class TangemBottomSheetConfigContentPreviewConfig : TangemBottomSheetConfigContent +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetTitle.kt new file mode 100644 index 0000000000..7029f1c2e0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetTitle.kt @@ -0,0 +1,131 @@ +package com.tangem.core.ui.components.bottomsheets.modal + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.buttons.small.TangemIconButton +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Title component for [TangemModalBottomSheet] with [TangemIconButton] for buttons. + */ +@Composable +fun TangemModalBottomSheetTitle( + modifier: Modifier = Modifier, + title: TextReference? = null, + @DrawableRes startIconRes: Int? = null, + onStartClick: (() -> Unit)? = null, + @DrawableRes endIconRes: Int? = null, + onEndClick: (() -> Unit)? = null, +) { + Box( + modifier = modifier + .fillMaxWidth() + .height(56.dp), + ) { + if (startIconRes != null && onStartClick != null) { + TangemIconButton( + iconRes = startIconRes, + onClick = onStartClick, + modifier = Modifier + .padding(16.dp) + .align(Alignment.CenterStart), + ) + } + if (title != null) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.align(Alignment.Center), + ) + } + if (endIconRes != null && onEndClick != null) { + TangemIconButton( + iconRes = endIconRes, + onClick = onEndClick, + modifier = Modifier + .padding(16.dp) + .align(Alignment.CenterEnd), + ) + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_TangemModalBottomSheetTitle( + @PreviewParameter(TangemModalBottomSheetTitleProvider::class) params: TangemModalBottomSheetTitleData, +) { + TangemThemePreview { + TangemModalBottomSheetTitle( + title = params.title, + startIconRes = params.startIconRes, + onStartClick = params.onStartClick, + endIconRes = params.endIconRes, + onEndClick = params.onEndClick, + modifier = Modifier.background(TangemTheme.colors.background.secondary), + ) + } +} + +private data class TangemModalBottomSheetTitleData( + val title: TextReference?, + val startIconRes: Int?, + val onStartClick: (() -> Unit)?, + val endIconRes: Int?, + val onEndClick: (() -> Unit)?, +) + +private class TangemModalBottomSheetTitleProvider : PreviewParameterProvider { + override val values = sequenceOf( + TangemModalBottomSheetTitleData( + title = resourceReference(R.string.wallet_title), + startIconRes = R.drawable.ic_back_24, + onStartClick = {}, + endIconRes = null, + onEndClick = null, + ), + TangemModalBottomSheetTitleData( + title = resourceReference(R.string.wallet_title), + startIconRes = null, + onStartClick = null, + endIconRes = R.drawable.ic_close_24, + onEndClick = {}, + ), + TangemModalBottomSheetTitleData( + title = resourceReference(R.string.wallet_title), + startIconRes = R.drawable.ic_back_24, + onStartClick = {}, + endIconRes = R.drawable.ic_close_24, + onEndClick = {}, + ), + TangemModalBottomSheetTitleData( + title = null, + startIconRes = R.drawable.ic_back_24, + onStartClick = {}, + endIconRes = R.drawable.ic_close_24, + onEndClick = {}, + ), + ) +} +// endregion Preview \ No newline at end of file