diff --git a/common/src/main/java/org/dash/wallet/common/ui/components/EnterAmount.kt b/common/src/main/java/org/dash/wallet/common/ui/components/EnterAmount.kt index d4a3d8d979..812cdfd1d9 100644 --- a/common/src/main/java/org/dash/wallet/common/ui/components/EnterAmount.kt +++ b/common/src/main/java/org/dash/wallet/common/ui/components/EnterAmount.kt @@ -184,11 +184,19 @@ fun EnterAmount( // Wrap the picker to its content instead of letting its options' fillMaxWidth grab the // whole row: width = widest option label, height = the stacked options' natural height // (so it sits compact on the right rather than stretching across the amount area). + // Figma (node 38680:47341) shows these as plain stacked labels with no pill/background + // behind them, unlike the segmented-toggle style this component normally renders. SegmentedPicker( options = pickerIndices.map { SegmentedOption(currencyCodes[it]) }, showSelection = false, style = SegmentedPickerStyle( displayMode = PickerDisplayMode.Vertical, + backgroundColor = Color.Transparent, + cornerRadius = 0f, + shadowElevation = 0, + textStyle = MyTheme.Typography.LabelSmallMedium, + optionPaddingHorizontal = 6f, + optionPaddingVertical = 4f ), onOptionSelected = { option, index -> onCurrencyPickerSelect(option, pickerIndices[index]) diff --git a/common/src/main/java/org/dash/wallet/common/ui/components/MenuItem.kt b/common/src/main/java/org/dash/wallet/common/ui/components/MenuItem.kt index 9ed4f0f2c7..a06be666be 100644 --- a/common/src/main/java/org/dash/wallet/common/ui/components/MenuItem.kt +++ b/common/src/main/java/org/dash/wallet/common/ui/components/MenuItem.kt @@ -17,6 +17,7 @@ package org.dash.wallet.common.ui.components +import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -32,12 +33,15 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics -import android.content.res.Configuration +import androidx.compose.ui.text.TextMeasurer +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -49,6 +53,10 @@ fun MenuItem( helpTextAbove: String? = null, subtitle: String? = null, subtitleMaxLines: Int = Int.MAX_VALUE, + // Truncates `subtitle` from the middle to fit the available width (e.g. for addresses, + // where both the start and end need to stay checkable) instead of the standard end-ellipsis. + // Width-measured so it stays correct at any font scale, unlike a fixed character count. + subtitleMiddleEllipsis: Boolean = false, subtitle2: String? = null, icon: Int? = null, // Custom icon slot (e.g. a Coil AsyncImage for coin logos); used when `icon` is null @@ -168,14 +176,23 @@ fun MenuItem( // Subtitle subtitle?.let { - Text( - text = it, - style = MyTheme.Typography.BodyMedium, - color = colors.textSecondary, - maxLines = subtitleMaxLines, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.fillMaxWidth() - ) + if (subtitleMiddleEllipsis) { + MiddleEllipsisText( + text = it, + style = MyTheme.Typography.BodyMedium, + color = colors.textSecondary, + modifier = Modifier.fillMaxWidth() + ) + } else { + Text( + text = it, + style = MyTheme.Typography.BodyMedium, + color = colors.textSecondary, + maxLines = subtitleMaxLines, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth() + ) + } } // Second subtitle @@ -271,6 +288,44 @@ fun MenuItem( } } +/** + * Single-line text that keeps the start and end of [text] visible, truncating the middle + * with "…" only as much as needed to fit the measured width. Unlike a fixed character-count + * cut, this stays correct across screen widths, locales and font scales. + */ +@Composable +private fun MiddleEllipsisText( + text: String, + style: TextStyle, + color: Color, + modifier: Modifier = Modifier +) { + val measurer = rememberTextMeasurer() + val density = LocalDensity.current + BoxWithConstraints(modifier = modifier) { + val maxWidthPx = with(density) { maxWidth.toPx() } + val display = remember(text, maxWidthPx, style, density.fontScale) { + middleEllipsizeToFit(text, maxWidthPx, style, measurer) + } + Text(text = display, style = style, color = color, maxLines = 1, overflow = TextOverflow.Clip) + } +} + +private fun middleEllipsizeToFit(text: String, maxWidthPx: Float, style: TextStyle, measurer: TextMeasurer): String { + fun widthOf(s: String) = measurer.measure(text = s, style = style, softWrap = false).size.width + + if (maxWidthPx <= 0f || widthOf(text) <= maxWidthPx) return text + + var head = (text.length + 1) / 2 + var tail = text.length - head + while (head + tail > 0) { + val candidate = "${text.take(head)}…${text.takeLast(tail)}" + if (widthOf(candidate) <= maxWidthPx) return candidate + if (head >= tail) head-- else tail-- + } + return "…" +} + @Preview(name = "MenuItem Light", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "MenuItem Dark", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/common/src/main/java/org/dash/wallet/common/ui/segmented_picker/SegmentedPicker.kt b/common/src/main/java/org/dash/wallet/common/ui/segmented_picker/SegmentedPicker.kt index 7a6a561fd8..bad73700b0 100644 --- a/common/src/main/java/org/dash/wallet/common/ui/segmented_picker/SegmentedPicker.kt +++ b/common/src/main/java/org/dash/wallet/common/ui/segmented_picker/SegmentedPicker.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import org.dash.wallet.common.R @@ -67,7 +68,12 @@ data class SegmentedPickerStyle( val thumbColor: Color? = null, val cornerRadius: Float = 12f, val textStyle: TextStyle = MyTheme.CaptionMedium, - val shadowElevation: Int = 2 + val shadowElevation: Int = 2, + // Extra inset drawn around each option's text/icon, on top of the option's own weighted + // slot. Zero by default so existing fixed-height horizontal/vertical toggles are unaffected; + // set this for pickers whose options should have visible breathing room between them. + val optionPaddingHorizontal: Float = 0f, + val optionPaddingVertical: Float = 0f ) @Composable @@ -205,7 +211,9 @@ fun SegmentedPicker( internalSelectedIndex = index onOptionSelected(option, index) }, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), + paddingHorizontal = style.optionPaddingHorizontal.dp, + paddingVertical = style.optionPaddingVertical.dp ) } } @@ -225,7 +233,9 @@ fun SegmentedPicker( onOptionSelected(option, index) }, modifier = Modifier.weight(1f), - isHorizontal = false + isHorizontal = false, + paddingHorizontal = style.optionPaddingHorizontal.dp, + paddingVertical = style.optionPaddingVertical.dp ) } } @@ -240,7 +250,9 @@ private fun OptionContent( textStyle: TextStyle, onSelect: () -> Unit, modifier: Modifier = Modifier, - isHorizontal: Boolean = true + isHorizontal: Boolean = true, + paddingHorizontal: Dp = 0.dp, + paddingVertical: Dp = 0.dp ) { Box( modifier = modifier @@ -254,9 +266,9 @@ private fun OptionContent( Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, - modifier = Modifier.then( - if (isHorizontal) Modifier.fillMaxHeight() else Modifier.fillMaxWidth() - ) + modifier = Modifier + .then(if (isHorizontal) Modifier.fillMaxHeight() else Modifier.fillMaxWidth()) + .padding(horizontal = paddingHorizontal, vertical = paddingVertical) ) { val colors = LocalDashColors.current option.icon?.let { diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXReceiveScreen.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXReceiveScreen.kt index 169895b7a1..e9e78ba18f 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXReceiveScreen.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/DEXReceiveScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.CircularProgressIndicator @@ -48,6 +49,7 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -122,10 +124,9 @@ private fun DEXReceiveScreenContent( // height of the (overlaid) status + nav bar that NavBarBack already occupies, so // TopIntro's built-in padding (10dp `safe-area/top`, 20dp sides, 20dp below) covers // the remaining insets and the gap to the card (matches DEXRefundAddressScreen). - TopIntro( - heading = stringResource(R.string.dex_receive_heading, coinCode), - text = stringResource(R.string.dex_receive_description) - ) + // No subtitle here per Figma — the equivalent copy now lives in the feature row + // below the card instead (dex_receive_description). + TopIntro(heading = stringResource(R.string.dex_receive_heading, coinCode)) Column( modifier = Modifier @@ -133,7 +134,8 @@ private fun DEXReceiveScreenContent( .padding(start = 20.dp, end = 20.dp, bottom = 20.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { - // White card with QR + URI row. shadows/xs: #B8C1CC ~10% alpha, y=5, blur=20. + // White card: QR + URI row + expiry warning, each section supplying its own + // padding (matches Figma, which sets no gap on the card itself — see 35042:51782). Column( modifier = Modifier .fillMaxWidth() @@ -143,9 +145,7 @@ private fun DEXReceiveScreenContent( ambientColor = Color(0xFFB8C1CC), spotColor = Color(0xFFB8C1CC) ) - .background(LocalDashColors.current.backgroundSecondary, RoundedCornerShape(20.dp)) - .padding(top = 40.dp, bottom = 20.dp, start = 20.dp, end = 20.dp), - verticalArrangement = Arrangement.spacedBy(20.dp), + .background(LocalDashColors.current.backgroundSecondary, RoundedCornerShape(20.dp)), horizontalAlignment = Alignment.CenterHorizontally ) { if (errorMessageRes != null) { @@ -155,7 +155,9 @@ private fun DEXReceiveScreenContent( style = MyTheme.Body2Regular, color = LocalDashColors.current.red, textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 32.dp) ) } else { QrArea(content = qrContent, isLoading = isLoading || qrContent.isBlank()) @@ -179,14 +181,26 @@ private fun DEXReceiveScreenContent( text = stringResource(R.string.dex_receive_memo_warning), style = MyTheme.Body2Regular, color = LocalDashColors.current.red, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) ) } + + // Expiry warning, inside the card below the address (Figma 38669:7486). + Box( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp) + ) { + ExpiryWarning(coinCode = coinCode) + } } } - // Expiry warning card (Figma 36265:22210): yellow triangle + title + refund note. - ExpiryWarning(coinCode = coinCode) + // Feature row explaining the post-deposit flow (Figma 38669:27847) — the same + // copy that used to sit under the heading as a plain subtitle. + DepositInfoRow(text = stringResource(R.string.dex_receive_description)) } } @@ -218,15 +232,15 @@ private fun DEXReceiveScreenContent( } } -/** Gray card warning that the deposit address expires; refund vs. convert note. */ +/** Yellow "system message" card warning that the deposit address expires (Figma 38669:7448). */ @Composable private fun ExpiryWarning(coinCode: String) { Row( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(20.dp)) - .background(LocalDashColors.current.gray.copy(alpha = 0.10f)) - .padding(16.dp), + .background(LocalDashColors.current.warningYellow) + .padding(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.Top ) { @@ -243,7 +257,7 @@ private fun ExpiryWarning(coinCode: String) { Column( modifier = Modifier .weight(1f) - .padding(top = 2.dp, end = 20.dp), + .padding(vertical = 5.dp), verticalArrangement = Arrangement.spacedBy(1.dp) ) { Text( @@ -260,18 +274,56 @@ private fun ExpiryWarning(coinCode: String) { } } -/** White, rounded box holding the 200dp QR, or a centered spinner while the address is loading. */ +/** + * Icon + text row explaining what happens after the deposit is confirmed (Figma 38669:27832), + * sitting below the card. The extra end padding (on top of the screen's 20dp side inset) matches + * Figma's wider right margin so the paragraph doesn't run the full screen width. + */ +@Composable +private fun DepositInfoRow(text: String) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(end = 20.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Box( + modifier = Modifier + .size(40.dp) + .background(LocalDashColors.current.gray, CircleShape), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(CommonR.drawable.ic_arrow_downward_blue_24dp), + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(16.dp) + ) + } + Text( + text = text, + style = MyTheme.Typography.BodyMedium, + color = LocalDashColors.current.textPrimary, + modifier = Modifier + .weight(1f) + .padding(top = 10.dp) + ) + } +} + +/** 160dp QR centered in a 30dp vertical band (Figma "wrap.qr", 35042:51784), or a spinner while loading. */ @Composable private fun QrArea(content: String, isLoading: Boolean) { Box( modifier = Modifier + .fillMaxWidth() // Stays white in dark mode too: QR modules need a light background to scan reliably. - .background(Color.White, RoundedCornerShape(10.dp)) - .padding(10.dp), + .background(Color.White) + .padding(vertical = 30.dp), contentAlignment = Alignment.Center ) { Box( - modifier = Modifier.size(200.dp), + modifier = Modifier.size(160.dp), contentAlignment = Alignment.Center ) { val bitmap = remember(content, isLoading) { @@ -282,11 +334,11 @@ private fun QrArea(content: String, isLoading: Boolean) { Image( bitmap = bitmap.asImageBitmap(), contentDescription = null, - // The QR bitmap is the raw module grid (tiny); scaling it to 200dp with the + // The QR bitmap is the raw module grid (tiny); scaling it to 160dp with the // default (smoothing) filter blurs the modules. None = nearest-neighbour, so the // squares stay crisp (mirrors Qr.themeAwareDrawable's isFilterBitmap = false). filterQuality = FilterQuality.None, - modifier = Modifier.size(200.dp) + modifier = Modifier.size(160.dp) ) } else { CircularProgressIndicator(color = LocalDashColors.current.dashBlue) @@ -295,13 +347,13 @@ private fun QrArea(content: String, isLoading: Boolean) { } } -/** Full-width row: label + value on the left, a tinted-gray copy button on the right. */ +/** Full-width row: label + single-line value on the left, a tinted-gray copy button on the right. */ @Composable private fun LabeledCopyRow(label: String, value: String, onCopyClick: () -> Unit) { Row( modifier = Modifier .fillMaxWidth() - .padding(vertical = 6.dp), + .padding(horizontal = 20.dp, vertical = 6.dp), horizontalArrangement = Arrangement.spacedBy(40.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -319,7 +371,9 @@ private fun LabeledCopyRow(label: String, value: String, onCopyClick: () -> Unit Text( text = value, style = MyTheme.Typography.BodyMedium, - color = LocalDashColors.current.textPrimary + color = LocalDashColors.current.textPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) } @@ -376,6 +430,31 @@ private fun DEXReceiveScreenLoadedPreview() { ) } +// Mirrors a real Galaxy S22 (SM-S901U): 1080x2340px @ 480dpi (xxhdpi, scale 3.0) measures to +// exactly 360x780dp — confirmed via `adb shell wm size` / `wm density`. +@Preview( + name = "Galaxy S22 @ 1.25x font", + showBackground = true, + device = "spec:width=360dp,height=780dp,dpi=480", + fontScale = 1.25f, + showSystemUi = true +) +@Composable +private fun DEXReceiveScreenGalaxyS22Preview() { + DEXReceiveScreenContent( + coinCode = "BTC", + address = "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", + uri = "bitcoin:bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", + memo = "", + isLoading = false, + errorMessageRes = null, + isOnline = true, + onBackClick = {}, + onBackHomeClick = {}, + onCopyClick = {} + ) +} + @Preview(showBackground = true, widthDp = 393, heightDp = 760) @Composable private fun DEXReceiveScreenMemoPreview() { diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaAddressInputScreen.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaAddressInputScreen.kt index 365d8fa2c8..ee10d43a19 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaAddressInputScreen.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaAddressInputScreen.kt @@ -161,6 +161,7 @@ fun MayaAddressInputScreen( title = source.name, subtitle = source.address?.takeIf { it.isNotEmpty() }, subtitleMaxLines = 1, + subtitleMiddleEllipsis = true, icon = source.icon, trailingButtonText = if (connected) { null @@ -177,6 +178,7 @@ fun MayaAddressInputScreen( title = stringResource(R.string.maya_clipboard), subtitle = clipboardAddress, subtitleMaxLines = 1, + subtitleMiddleEllipsis = true, icon = R.drawable.ic_maya_clipboard, action = onClipboardClick ) diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConvertCryptoFragment.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConvertCryptoFragment.kt index 8f6e1bdc1c..dfd1a530bb 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConvertCryptoFragment.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConvertCryptoFragment.kt @@ -54,7 +54,6 @@ import org.dash.wallet.integrations.maya.model.AccountDataUIModel import org.dash.wallet.integrations.maya.model.Balance import org.dash.wallet.integrations.maya.model.CurrencyInputType import org.dash.wallet.integrations.maya.model.getCoinBaseExchangeRateConversion -import org.dash.wallet.integrations.maya.payments.MayaCurrencyList import org.dash.wallet.integrations.maya.ui.convert_currency.ConvertViewViewModel import org.dash.wallet.integrations.maya.ui.convert_currency.model.SwapRequest import org.dash.wallet.integrations.maya.ui.convert_currency.model.SwapValueErrorType @@ -170,14 +169,8 @@ class MayaConvertCryptoFragment : Fragment() { val dashPoolInfo = mayaViewModel.getPoolInfo(Constants.DASH_CURRENCY) val currencyMapper = MayaCurrencyMapper(requireContext()) - // Tokens are qualified with their host network ("USDT (Ethereum)"); native L1 coins - // (BTC.BTC, …) show just the code. - val network = MayaCurrencyList.networkName(args.asset) - val displayCode = if (network != null) "${args.currency} ($network)" else args.currency - uiState = uiState.copy( - // "Convert DASH to " per design, e.g. "Convert DASH to USDT (Ethereum)". - title = getString(R.string.maya_address_input_title, displayCode), + title = getString(R.string.maya_convert_enter_amount_title), toCurrencyName = currencyMapper.getCurrencyName(args.currency), toAddress = getArgAddress(), toIconUrls = GenericUtils.getCoinIconUrls(args.currency.lowercase(), args.asset), @@ -685,17 +678,17 @@ class MayaConvertCryptoFragment : Fragment() { convertViewModel.selectedLocalExchangeRate.value?.let { rate -> val currencyRate = ExchangeRate(Coin.COIN, rate.fiat) val fiatAmount = currencyRate.coinToFiat(dash).toFormattedString() - return "${getString(R.string.entered_amount_is_too_high)} $fiatAmount" + return "${getString(R.string.maya_max_amount_error)} $fiatAmount" } } } else { convertViewModel.selectedLocalExchangeRate.value?.let { rate -> selectedCoinBaseAccount?.getCoinBaseExchangeRateConversion(rate)?.first?.let { - return "${getString(R.string.entered_amount_is_too_high)} $it" + return "${getString(R.string.maya_max_amount_error)} $it" } } } - return getString(R.string.entered_amount_is_too_high) + return getString(R.string.maya_max_amount_error) } private fun minAmountErrorMessage(): String? { diff --git a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConvertCryptoScreen.kt b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConvertCryptoScreen.kt index c1a9777a88..d4badba104 100644 --- a/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConvertCryptoScreen.kt +++ b/integrations/maya/src/main/java/org/dash/wallet/integrations/maya/ui/MayaConvertCryptoScreen.kt @@ -28,8 +28,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -40,6 +40,7 @@ import androidx.compose.runtime.setValue 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.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -60,6 +61,7 @@ import org.dash.wallet.common.ui.components.ToastImageResource import org.dash.wallet.common.ui.enter_amount.NumericKeyboardCompose import org.dash.wallet.common.util.GenericUtils import org.dash.wallet.integrations.maya.R +import org.dash.wallet.integrations.maya.payments.MayaBitcoinCryptoCurrency import java.text.DecimalFormatSymbols import java.util.Locale @@ -248,9 +250,10 @@ fun MayaConvertCryptoScreen( } /** - * White card showing the conversion direction: the Dash wallet (with its balance) on top and - * the destination coin + address below, separated by a divider with an arrow-down badge. - * Built from the design-system [Menu]/[MenuItem] components; supplies its own horizontal inset. + * The conversion direction: the Dash wallet (with its balance) in its own white card, then the + * destination coin + address in a second card 5dp below it, with a direction (arrow-down) badge + * floating over the seam between the two. Matches Figma node 38680:47497 — two separate [Menu] + * cards, not one card with an internal divider. */ @Composable private fun ConvertDirectionCard( @@ -260,48 +263,52 @@ private fun ConvertDirectionCard( toAddress: String, toIconUrls: List ) { - Menu { - // From: Dash Wallet with its balance. - MenuItem( - title = stringResource(R.string.dash), - subtitle = stringResource(R.string.dash_wallet_name), - icon = R.drawable.ic_dash_blue_filled, - dashAmount = "${stringResource(R.string.balance)} $dashBalance", - dashIcon = R.drawable.ic_dash_d_black, - fiatAmount = fiatBalance - ) + Box(modifier = Modifier.fillMaxWidth()) { + Column(verticalArrangement = Arrangement.spacedBy(5.dp)) { + // From: Dash Wallet with its balance. + Menu { + MenuItem( + title = stringResource(R.string.dash), + subtitle = stringResource(R.string.dash_wallet_name), + icon = R.drawable.ic_dash_blue_filled, + dashAmount = dashBalance, + dashIcon = R.drawable.ic_dash_d_black, + fiatAmount = fiatBalance + ) + } + + // To: destination coin + address, truncated from the center so both ends stay + // checkable on one line (the common way of displaying a crypto address). + Menu { + MenuItem( + title = toCurrencyName, + subtitle = toAddress, + subtitleMaxLines = 1, + subtitleMiddleEllipsis = true, + customIcon = { CoinIcon(iconUrls = toIconUrls) } + ) + } + } - // Divider with the direction (arrow-down) badge in the middle. + // Direction badge: a white rounded-square button ringed by a screen-background-colored + // border, so it reads as inset into the gap between the two cards. Box( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 10.dp), + .align(Alignment.Center) + .size(30.dp) + .background(LocalDashColors.current.backgroundSecondary, RoundedCornerShape(10.dp)) + .border(5.dp, LocalDashColors.current.backgroundPrimary, RoundedCornerShape(10.dp)), contentAlignment = Alignment.Center ) { - HorizontalDivider(thickness = 1.dp, color = LocalDashColors.current.extraLightGray) - Box( - modifier = Modifier - .size(24.dp) - .background(LocalDashColors.current.backgroundSecondary, CircleShape) - .border(1.dp, LocalDashColors.current.extraLightGray, CircleShape), - contentAlignment = Alignment.Center - ) { - Icon( - painter = painterResource(R.drawable.ic_arrow_downward_blue_24dp), - contentDescription = null, - tint = LocalDashColors.current.textTertiary, - modifier = Modifier.size(12.dp) - ) - } + Icon( + painter = painterResource(R.drawable.ic_arrow_downward_blue_24dp), + contentDescription = null, + // The drawable's own path fill is already the design's blue; tinting it would + // override that with an unrelated color. + tint = Color.Unspecified, + modifier = Modifier.size(12.dp) + ) } - - // To: destination coin + address. - MenuItem( - title = toCurrencyName, - subtitle = toAddress, - subtitleMaxLines = 1, - customIcon = { CoinIcon(iconUrls = toIconUrls) } - ) } } @@ -334,14 +341,14 @@ private fun CoinIcon(iconUrls: List) { private fun MayaConvertCryptoScreenPreview() { MayaConvertCryptoScreen( state = MayaConvertCryptoUIState( - title = "Convert Dash to BTC", + title = "Convert", displayAmount = "0.06", currencyOptions = listOf("DASH", "USD", "BTC"), selectedCurrencyIndex = 2, - dashBalance = "0.00", - fiatBalance = "0.00 US$", + dashBalance = "4.00", + fiatBalance = "$140.00", toCurrencyName = "Bitcoin", - toAddress = "XbBzWvnvSyWFbYXFtjkWwuPApbfDD263uC", + toAddress = MayaBitcoinCryptoCurrency().exampleAddress, receiveAmount = "~ 0.0053 BTC", networkLabel = "using NEAR network", continueEnabled = true @@ -359,15 +366,15 @@ private fun MayaConvertCryptoScreenPreview() { private fun MayaConvertCryptoScreenErrorPreview() { MayaConvertCryptoScreen( state = MayaConvertCryptoUIState( - title = "Convert Dash to BTC", + title = "Convert", displayAmount = "0.5", currencyOptions = listOf("DASH", "USD", "BTC"), selectedCurrencyIndex = 0, dashBalance = "0.05", fiatBalance = "1.20 US$", toCurrencyName = "Bitcoin", - toAddress = "XbBzWvnvSyWFbYXFtjkWwuPApbfDD263uC", - errorMessage = "You don’t have enough balance", + toAddress = MayaBitcoinCryptoCurrency().exampleAddress, + errorMessage = "Max $1.20", continueEnabled = false ), onBackClick = {}, @@ -377,3 +384,36 @@ private fun MayaConvertCryptoScreenErrorPreview() { onContinueClick = {} ) } + +// Mirrors a real Galaxy S22 (SM-S901U): 1080x2340px @ 480dpi (xxhdpi, scale 3.0) measures to +// exactly 360x780dp — confirmed via `adb shell wm size` / `wm density`. +@Preview( + name = "Galaxy S22 @ 1.25x font", + showBackground = true, + device = "spec:width=360dp,height=780dp,dpi=480", + fontScale = 1.25f, + showSystemUi = true +) +@Composable +private fun MayaConvertCryptoScreenGalaxyS22Preview() { + MayaConvertCryptoScreen( + state = MayaConvertCryptoUIState( + title = "Convert", + displayAmount = "2", + currencyOptions = listOf("DASH", "USD", "BTC"), + selectedCurrencyIndex = 2, + dashBalance = "0.93999202", + fiatBalance = "$ 28.06", + toCurrencyName = "Bitcoin", + toAddress = "bc1qxhgnnp745xxxxxxxxxxxxxxxxxxxxgkkpkm35020js0", + receiveAmount = "~ 0.00095431 BTC", + networkLabel = "using Maya network", + continueEnabled = true + ), + onBackClick = {}, + onMaxClick = {}, + onCurrencySelected = {}, + onKeyInput = {}, + onContinueClick = {} + ) +} diff --git a/integrations/maya/src/main/res/values/strings-maya.xml b/integrations/maya/src/main/res/values/strings-maya.xml index dbf9bcb6de..6988e9760f 100644 --- a/integrations/maya/src/main/res/values/strings-maya.xml +++ b/integrations/maya/src/main/res/values/strings-maya.xml @@ -29,6 +29,8 @@ From Dash Wallet to any crypto %s Address Convert DASH to %s + + Convert Enter Address @@ -296,6 +298,8 @@ The minimum transaction amount is The maximum transaction amount is + + Max We didn’t find any assets on your Coinbase account. Something went wrong! You don\'t have any Dash in your Dash Wallet.