QR code image logic refactoring (#1732)

* QR code image logic refactor

The QR code image logic of the `QrCode` and `Request` screens has been refactored to work upon the newer `ZashiQr` component

* Refactor values into state

* Changelog update
This commit is contained in:
Honza Rychnovský 2025-01-21 16:32:34 -05:00 committed by GitHub
parent db983c692f
commit 603178fd67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 159 additions and 236 deletions

View File

@ -6,6 +6,10 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
## [Unreleased] ## [Unreleased]
### Changed
- The QR code image logic of the `QrCode`, `Request`, and `SignTransaction` screens has been refactored to work
with the newer `ZashiQr` component
### Fixed ### Fixed
- The Disconnected popup trigger when the app is backgrounded has been fixed - The Disconnected popup trigger when the app is backgrounded has been fixed

View File

@ -2,16 +2,21 @@ package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
@ -19,31 +24,44 @@ import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
import co.electriccoin.zcash.ui.design.util.AndroidQrCodeImageGenerator import co.electriccoin.zcash.ui.design.util.AndroidQrCodeImageGenerator
import co.electriccoin.zcash.ui.design.util.JvmQrCodeGenerator import co.electriccoin.zcash.ui.design.util.JvmQrCodeGenerator
import co.electriccoin.zcash.ui.design.util.QrCodeColors import co.electriccoin.zcash.ui.design.util.QrCodeColors
import co.electriccoin.zcash.ui.design.util.StringResource
import co.electriccoin.zcash.ui.design.util.getValue
import co.electriccoin.zcash.ui.design.util.orDark import co.electriccoin.zcash.ui.design.util.orDark
@Composable @Composable
fun ZashiQr( fun ZashiQr(
qrData: String, state: QrState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
qrSize: Dp = ZashiQrDefaults.width, qrSize: Dp = ZashiQrDefaults.width,
colors: QrCodeColors = QrCodeDefaults.colors() colors: QrCodeColors = QrCodeDefaults.colors(),
) { ) {
val qrSizePx = with(LocalDensity.current) { qrSize.roundToPx() } val qrSizePx = with(LocalDensity.current) { qrSize.roundToPx() }
val bitmap = getQrCode(qrData, qrSizePx, colors) val bitmap = getQrCode(state.qrData, qrSizePx, colors)
Surface( Surface(
modifier = modifier, modifier = modifier,
shape = RoundedCornerShape(ZashiDimensions.Radius.radius4xl), shape = RoundedCornerShape(ZashiDimensions.Radius.radius4xl),
border = BorderStroke(width = 1.dp, color = ZashiColors.Surfaces.strokePrimary), border = BorderStroke(width = 1.dp, color = ZashiColors.Surfaces.strokePrimary),
color = ZashiColors.Surfaces.bgPrimary color = ZashiColors.Surfaces.bgPrimary,
) { ) {
Box( Box(
modifier = Modifier.padding(all = 12.dp) modifier = Modifier.padding(all = 12.dp)
) { ) {
Image( Image(
bitmap = bitmap, bitmap = bitmap,
contentDescription = null, contentDescription = state.contentDescription?.getValue(),
Modifier.clickable { state.onClick() }
) )
if (state.centerImageResId != null) {
Image(
modifier =
Modifier
.size(64.dp)
.align(Alignment.Center),
imageVector = ImageVector.vectorResource(state.centerImageResId),
contentDescription = null,
)
}
} }
} }
} }
@ -75,3 +93,10 @@ object QrCodeDefaults {
foreground = foreground foreground = foreground
) )
} }
data class QrState(
val qrData: String,
val contentDescription: StringResource? = null,
val onClick: () -> Unit = {},
val centerImageResId: Int? = null,
)

View File

@ -541,20 +541,7 @@ fun NavHostController.popBackStackJustOnce(currentRouteToBePopped: String) {
object NavigationArguments { object NavigationArguments {
const val SEND_SCAN_RECIPIENT_ADDRESS = "send_scan_recipient_address" const val SEND_SCAN_RECIPIENT_ADDRESS = "send_scan_recipient_address"
const val SEND_SCAN_ZIP_321_URI = "send_scan_zip_321_uri" const val SEND_SCAN_ZIP_321_URI = "send_scan_zip_321_uri"
const val SEND_CONFIRM_RECIPIENT_ADDRESS = "send_confirm_recipient_address"
const val SEND_CONFIRM_AMOUNT = "send_confirm_amount"
const val SEND_CONFIRM_MEMO = "send_confirm_memo"
const val SEND_CONFIRM_PROPOSAL = "send_confirm_proposal"
const val SEND_CONFIRM_INITIAL_STAGE = "send_confirm_initial_stage"
const val MULTIPLE_SUBMISSION_CLEAR_FORM = "multiple_submission_clear_form" const val MULTIPLE_SUBMISSION_CLEAR_FORM = "multiple_submission_clear_form"
const val PAYMENT_REQUEST_ADDRESS = "payment_request_address"
const val PAYMENT_REQUEST_AMOUNT = "payment_request_amount"
const val PAYMENT_REQUEST_MEMO = "payment_request_memo"
const val PAYMENT_REQUEST_PROPOSAL = "payment_request_proposal"
const val PAYMENT_REQUEST_URI = "payment_request_uri"
} }
object NavigationTargets { object NavigationTargets {

View File

@ -5,7 +5,6 @@ package co.electriccoin.zcash.ui.screen.exchangerate.widget
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -93,7 +92,6 @@ fun StyledExchangeBalance(
} }
@Suppress("LongParameterList", "LongMethod") @Suppress("LongParameterList", "LongMethod")
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun ExchangeAvailableRateLabelInternal( private fun ExchangeAvailableRateLabelInternal(
style: TextStyle, style: TextStyle,

View File

@ -2,8 +2,10 @@ package co.electriccoin.zcash.ui.screen.qrcode.model
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import cash.z.ecc.android.sdk.model.WalletAddress import cash.z.ecc.android.sdk.model.WalletAddress
import co.electriccoin.zcash.ui.design.component.QrState
import co.electriccoin.zcash.ui.design.util.StringResource
internal sealed class QrCodeState { sealed class QrCodeState {
data object Loading : QrCodeState() data object Loading : QrCodeState()
data class Prepared( data class Prepared(
@ -11,8 +13,19 @@ internal sealed class QrCodeState {
val walletAddress: WalletAddress, val walletAddress: WalletAddress,
val onAddressCopy: (String) -> Unit, val onAddressCopy: (String) -> Unit,
val onQrCodeShare: (ImageBitmap) -> Unit, val onQrCodeShare: (ImageBitmap) -> Unit,
val onQrCodeClick: () -> Unit,
val onBack: () -> Unit, val onBack: () -> Unit,
) : QrCodeState() ) : QrCodeState() {
fun toQrState(
contentDescription: StringResource? = null,
centerImageResId: Int? = null
) = QrState(
qrData = walletAddress.address,
onClick = onQrCodeClick,
contentDescription = contentDescription,
centerImageResId = centerImageResId
)
}
} }
enum class QrCodeType { enum class QrCodeType {

View File

@ -3,15 +3,10 @@
package co.electriccoin.zcash.ui.screen.qrcode.view package co.electriccoin.zcash.ui.screen.qrcode.view
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -21,7 +16,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
@ -33,7 +27,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
@ -55,15 +49,16 @@ import co.electriccoin.zcash.ui.design.component.ZashiBadgeColors
import co.electriccoin.zcash.ui.design.component.ZashiBottomBar import co.electriccoin.zcash.ui.design.component.ZashiBottomBar
import co.electriccoin.zcash.ui.design.component.ZashiButton import co.electriccoin.zcash.ui.design.component.ZashiButton
import co.electriccoin.zcash.ui.design.component.ZashiButtonDefaults import co.electriccoin.zcash.ui.design.component.ZashiButtonDefaults
import co.electriccoin.zcash.ui.design.component.ZashiQr
import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.AndroidQrCodeImageGenerator import co.electriccoin.zcash.ui.design.util.AndroidQrCodeImageGenerator
import co.electriccoin.zcash.ui.design.util.JvmQrCodeGenerator import co.electriccoin.zcash.ui.design.util.JvmQrCodeGenerator
import co.electriccoin.zcash.ui.design.util.QrCodeColors import co.electriccoin.zcash.ui.design.util.QrCodeColors
import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.qrcode.model.QrCodeState import co.electriccoin.zcash.ui.screen.qrcode.model.QrCodeState
import co.electriccoin.zcash.ui.screen.qrcode.model.QrCodeType import co.electriccoin.zcash.ui.screen.qrcode.model.QrCodeType
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -91,6 +86,7 @@ private fun ZashiPreview() =
walletAddress = runBlocking { WalletAddressFixture.unified() }, walletAddress = runBlocking { WalletAddressFixture.unified() },
onAddressCopy = {}, onAddressCopy = {},
onQrCodeShare = {}, onQrCodeShare = {},
onQrCodeClick = {},
onBack = {}, onBack = {},
), ),
snackbarHostState = SnackbarHostState(), snackbarHostState = SnackbarHostState(),
@ -109,6 +105,7 @@ private fun KeystonePreview() =
walletAddress = runBlocking { WalletAddressFixture.unified() }, walletAddress = runBlocking { WalletAddressFixture.unified() },
onAddressCopy = {}, onAddressCopy = {},
onQrCodeShare = {}, onQrCodeShare = {},
onQrCodeClick = {},
onBack = {}, onBack = {},
), ),
snackbarHostState = SnackbarHostState(), snackbarHostState = SnackbarHostState(),
@ -154,10 +151,7 @@ internal fun QrCodeView(
} }
) { paddingValues -> ) { paddingValues ->
QrCodeContents( QrCodeContents(
qrCodeType = state.qrCodeType, state = state,
walletAddress = state.walletAddress,
onAddressCopy = state.onAddressCopy,
onQrCodeShare = state.onQrCodeShare,
modifier = modifier =
Modifier.padding( Modifier.padding(
top = paddingValues.calculateTopPadding(), top = paddingValues.calculateTopPadding(),
@ -239,10 +233,7 @@ private fun QrCodeBottomBar(
@Composable @Composable
private fun QrCodeContents( private fun QrCodeContents(
qrCodeType: QrCodeType, state: QrCodeState.Prepared,
walletAddress: WalletAddress,
onAddressCopy: (String) -> Unit,
onQrCodeShare: (ImageBitmap) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(
@ -254,16 +245,16 @@ private fun QrCodeContents(
) { ) {
Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault)) Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault))
when (walletAddress) { when (state.walletAddress) {
// We use the same design for the Sapling address for the Testnet app variant // We use the same design for the Sapling address for the Testnet app variant
is WalletAddress.Unified, is WalletAddress.Sapling -> { is WalletAddress.Unified, is WalletAddress.Sapling -> {
UnifiedQrCodePanel(qrCodeType, walletAddress, onAddressCopy, onQrCodeShare) UnifiedQrCodePanel(state)
} }
is WalletAddress.Transparent -> { is WalletAddress.Transparent -> {
TransparentQrCodePanel(qrCodeType, walletAddress, onAddressCopy, onQrCodeShare) TransparentQrCodePanel(state)
} }
else -> { else -> {
error("Unsupported address type: $walletAddress") error("Unsupported address type: ${state.walletAddress}")
} }
} }
} }
@ -272,10 +263,7 @@ private fun QrCodeContents(
@Composable @Composable
@Suppress("LongMethod") @Suppress("LongMethod")
fun UnifiedQrCodePanel( fun UnifiedQrCodePanel(
qrCodeType: QrCodeType, state: QrCodeState.Prepared,
walletAddress: WalletAddress,
onAddressCopy: (String) -> Unit,
onQrCodeShare: (ImageBitmap) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var expandedAddress by rememberSaveable { mutableStateOf(false) } var expandedAddress by rememberSaveable { mutableStateOf(false) }
@ -284,15 +272,13 @@ fun UnifiedQrCodePanel(
modifier = modifier =
modifier modifier
.padding(vertical = ZcashTheme.dimens.spacingDefault), .padding(vertical = ZcashTheme.dimens.spacingDefault),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = CenterHorizontally
) { ) {
QrCode( QrCode(
walletAddress = walletAddress, preparedState = state,
onQrImageShare = onQrCodeShare,
modifier = modifier =
Modifier Modifier
.padding(horizontal = 24.dp), .padding(horizontal = 24.dp),
qrCodeType = qrCodeType,
) )
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
@ -312,18 +298,18 @@ fun UnifiedQrCodePanel(
Text( Text(
text = text =
when (walletAddress) { when (state.walletAddress) {
is WalletAddress.Unified -> is WalletAddress.Unified ->
when (qrCodeType) { when (state.qrCodeType) {
QrCodeType.ZASHI -> stringResource(R.string.qr_code_wallet_address_shielded) QrCodeType.ZASHI -> stringResource(R.string.qr_code_wallet_address_shielded)
QrCodeType.KEYSTONE -> stringResource(R.string.qr_code_wallet_address_shielded_keystone) QrCodeType.KEYSTONE -> stringResource(R.string.qr_code_wallet_address_shielded_keystone)
} }
is WalletAddress.Sapling -> is WalletAddress.Sapling ->
when (qrCodeType) { when (state.qrCodeType) {
QrCodeType.ZASHI -> stringResource(id = R.string.qr_code_wallet_address_sapling) QrCodeType.ZASHI -> stringResource(id = R.string.qr_code_wallet_address_sapling)
QrCodeType.KEYSTONE -> stringResource(id = R.string.qr_code_wallet_address_sapling_keystone) QrCodeType.KEYSTONE -> stringResource(id = R.string.qr_code_wallet_address_sapling_keystone)
} }
else -> error("Unsupported address type: $walletAddress") else -> error("Unsupported address type: ${state.walletAddress}")
}, },
color = ZashiColors.Text.textPrimary, color = ZashiColors.Text.textPrimary,
style = ZashiTypography.textXl, style = ZashiTypography.textXl,
@ -335,7 +321,7 @@ fun UnifiedQrCodePanel(
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
Text( Text(
text = walletAddress.address, text = state.walletAddress.address,
color = ZashiColors.Text.textTertiary, color = ZashiColors.Text.textTertiary,
style = ZashiTypography.textSm, style = ZashiTypography.textSm,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -353,7 +339,7 @@ fun UnifiedQrCodePanel(
indication = null, indication = null,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
onClick = { expandedAddress = !expandedAddress }, onClick = { expandedAddress = !expandedAddress },
onLongClick = { onAddressCopy(walletAddress.address) } onLongClick = { state.onAddressCopy(state.walletAddress.address) }
) )
) )
} }
@ -362,10 +348,7 @@ fun UnifiedQrCodePanel(
@Composable @Composable
@Suppress("LongMethod") @Suppress("LongMethod")
fun TransparentQrCodePanel( fun TransparentQrCodePanel(
qrCodeType: QrCodeType, state: QrCodeState.Prepared,
walletAddress: WalletAddress,
onAddressCopy: (String) -> Unit,
onQrCodeShare: (ImageBitmap) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var expandedAddress by rememberSaveable { mutableStateOf(false) } var expandedAddress by rememberSaveable { mutableStateOf(false) }
@ -374,15 +357,13 @@ fun TransparentQrCodePanel(
modifier = modifier =
modifier modifier
.padding(vertical = ZcashTheme.dimens.spacingDefault), .padding(vertical = ZcashTheme.dimens.spacingDefault),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = CenterHorizontally
) { ) {
QrCode( QrCode(
walletAddress = walletAddress, preparedState = state,
onQrImageShare = onQrCodeShare,
modifier = modifier =
Modifier Modifier
.padding(horizontal = 24.dp), .padding(horizontal = 24.dp),
qrCodeType = qrCodeType,
) )
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge))
@ -412,7 +393,7 @@ fun TransparentQrCodePanel(
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
Text( Text(
text = walletAddress.address, text = state.walletAddress.address,
color = ZashiColors.Text.textTertiary, color = ZashiColors.Text.textTertiary,
style = ZashiTypography.textSm, style = ZashiTypography.textSm,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@ -430,7 +411,7 @@ fun TransparentQrCodePanel(
indication = null, indication = null,
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
onClick = { expandedAddress = !expandedAddress }, onClick = { expandedAddress = !expandedAddress },
onLongClick = { onAddressCopy(walletAddress.address) } onLongClick = { state.onAddressCopy(state.walletAddress.address) }
) )
) )
} }
@ -438,51 +419,28 @@ fun TransparentQrCodePanel(
@Composable @Composable
private fun ColumnScope.QrCode( private fun ColumnScope.QrCode(
qrCodeType: QrCodeType, preparedState: QrCodeState.Prepared,
walletAddress: WalletAddress,
onQrImageShare: (ImageBitmap) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val sizePixels = with(LocalDensity.current) { DEFAULT_QR_CODE_SIZE.toPx() }.roundToInt() ZashiQr(
val colors = QrCodeDefaults.colors() state =
val qrCodeImage = preparedState.toQrState(
remember { contentDescription =
qrCodeForAddress( stringRes(
address = walletAddress.address, when (preparedState.walletAddress) {
size = sizePixels, is WalletAddress.Unified -> R.string.qr_code_unified_content_description
colors = colors is WalletAddress.Sapling -> R.string.qr_code_sapling_content_description
) is WalletAddress.Transparent -> R.string.qr_code_transparent_content_description
} else -> error("Unsupported address type: ${preparedState.walletAddress}")
}
QrCode( ),
qrCodeImage = qrCodeImage, centerImageResId =
onQrImageBitmapShare = onQrImageShare, when (preparedState.qrCodeType) {
contentDescription = QrCodeType.ZASHI -> R.drawable.logo_zec_fill_stroke
stringResource( QrCodeType.KEYSTONE -> co.electriccoin.zcash.ui.design.R.drawable.ic_item_keystone_qr
when (walletAddress) { }
is WalletAddress.Unified -> R.string.qr_code_unified_content_description
is WalletAddress.Sapling -> R.string.qr_code_sapling_content_description
is WalletAddress.Transparent -> R.string.qr_code_transparent_content_description
else -> error("Unsupported address type: $walletAddress")
}
), ),
modifier = modifier = modifier.align(CenterHorizontally),
modifier
.align(Alignment.CenterHorizontally)
.border(
border =
BorderStroke(
width = 1.dp,
color = ZashiColors.Surfaces.strokePrimary
),
shape = RoundedCornerShape(ZashiDimensions.Radius.radius4xl)
)
.background(
color = ZashiColors.Surfaces.bgPrimary,
shape = RoundedCornerShape(ZashiDimensions.Radius.radius4xl)
)
.padding(all = 12.dp),
qrCodeType = qrCodeType
) )
} }
@ -502,45 +460,4 @@ private fun qrCodeForAddress(
return AndroidQrCodeImageGenerator.generate(qrCodePixelArray, size, colors) return AndroidQrCodeImageGenerator.generate(qrCodePixelArray, size, colors)
} }
@Composable
private fun QrCode(
qrCodeType: QrCodeType,
contentDescription: String,
qrCodeImage: ImageBitmap,
onQrImageBitmapShare: (ImageBitmap) -> Unit,
modifier: Modifier = Modifier,
) {
Box(
contentAlignment = Alignment.Center,
modifier =
Modifier
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
onClick = { onQrImageBitmapShare(qrCodeImage) },
)
.then(modifier)
) {
Image(
bitmap = qrCodeImage,
contentDescription = contentDescription,
)
Image(
modifier = Modifier.size(64.dp),
painter =
when (qrCodeType) {
QrCodeType.ZASHI -> painterResource(id = R.drawable.logo_zec_fill_stroke)
QrCodeType.KEYSTONE ->
painterResource(
id =
co.electriccoin.zcash.ui.design.R.drawable
.ic_item_keystone_qr
)
},
contentDescription = contentDescription,
)
}
}
private val DEFAULT_QR_CODE_SIZE = 320.dp private val DEFAULT_QR_CODE_SIZE = 320.dp

View File

@ -57,6 +57,10 @@ class QrCodeViewModel(
walletAddress = walletAddress, walletAddress = walletAddress,
onAddressCopy = { address -> onAddressCopyClick(address) }, onAddressCopy = { address -> onAddressCopyClick(address) },
onQrCodeShare = { onQrCodeShareClick(it, versionInfo) }, onQrCodeShare = { onQrCodeShareClick(it, versionInfo) },
onQrCodeClick = {
// TODO [#1731]: Allow QR codes colors switching
// TODO [#1731]: https://github.com/Electric-Coin-Company/zashi-android/issues/1731
},
onBack = ::onBack, onBack = ::onBack,
qrCodeType = qrCodeType =
when (account) { when (account) {

View File

@ -5,7 +5,9 @@ import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.WalletAddress import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.sdk.type.ZcashCurrency import cash.z.ecc.sdk.type.ZcashCurrency
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.design.component.QrState
import co.electriccoin.zcash.ui.design.util.QrCodeColors import co.electriccoin.zcash.ui.design.util.QrCodeColors
import co.electriccoin.zcash.ui.design.util.StringResource
internal sealed class RequestState { internal sealed class RequestState {
data object Loading : RequestState() data object Loading : RequestState()
@ -37,10 +39,21 @@ internal sealed class RequestState {
val icon: Int, val icon: Int,
val request: Request, val request: Request,
val walletAddress: WalletAddress, val walletAddress: WalletAddress,
val onQrCodeClick: () -> Unit,
val onQrCodeShare: (ImageBitmap) -> Unit, val onQrCodeShare: (ImageBitmap) -> Unit,
val onQrCodeGenerate: (pixels: Int, colors: QrCodeColors) -> Unit, val onQrCodeGenerate: (pixels: Int, colors: QrCodeColors) -> Unit,
override val onBack: () -> Unit, override val onBack: () -> Unit,
val onClose: () -> Unit, val onClose: () -> Unit,
val zcashCurrency: ZcashCurrency, val zcashCurrency: ZcashCurrency,
) : Prepared(onBack) ) : Prepared(onBack) {
fun toQrState(
contentDescription: StringResource? = null,
centerImageResId: Int? = null,
) = QrState(
qrData = walletAddress.address,
onClick = onQrCodeClick,
contentDescription = contentDescription,
centerImageResId = centerImageResId
)
}
} }

View File

@ -1,28 +1,18 @@
package co.electriccoin.zcash.ui.screen.request.view package co.electriccoin.zcash.ui.screen.request.view
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
@ -32,16 +22,14 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.model.WalletAddress import cash.z.ecc.android.sdk.model.WalletAddress
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.design.component.QrCodeDefaults
import co.electriccoin.zcash.ui.design.component.ZashiBadge import co.electriccoin.zcash.ui.design.component.ZashiBadge
import co.electriccoin.zcash.ui.design.component.ZashiBadgeColors import co.electriccoin.zcash.ui.design.component.ZashiBadgeColors
import co.electriccoin.zcash.ui.design.component.ZashiQr
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.request.model.RequestState import co.electriccoin.zcash.ui.screen.request.model.RequestState
import kotlin.math.roundToInt
@Composable @Composable
internal fun RequestQrCodeView( internal fun RequestQrCodeView(
@ -96,7 +84,7 @@ internal fun RequestQrCodeView(
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingBig)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingBig))
QrCode( QrCode(
state = state, requestState = state,
modifier = Modifier.padding(horizontal = 24.dp), modifier = Modifier.padding(horizontal = 24.dp),
) )
@ -106,71 +94,19 @@ internal fun RequestQrCodeView(
@Composable @Composable
private fun ColumnScope.QrCode( private fun ColumnScope.QrCode(
state: RequestState.QrCode, requestState: RequestState.QrCode,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val sizePixels = with(LocalDensity.current) { DEFAULT_QR_CODE_SIZE.toPx() }.roundToInt() ZashiQr(
state =
if (state.request.qrCodeState.bitmap == null) { requestState.toQrState(
val colors = QrCodeDefaults.colors() contentDescription = stringRes(R.string.request_qr_code_content_description),
state.onQrCodeGenerate(sizePixels, colors) centerImageResId = requestState.icon
} ),
modifier = modifier.align(CenterHorizontally),
QrCode(
state = state,
contentDescription = stringResource(id = R.string.request_qr_code_content_description),
modifier =
modifier
.align(Alignment.CenterHorizontally)
.border(
border =
BorderStroke(
width = 1.dp,
color = ZashiColors.Surfaces.strokePrimary
),
shape = RoundedCornerShape(ZashiDimensions.Radius.radius4xl)
)
.background(
color = ZashiColors.Surfaces.bgPrimary,
shape = RoundedCornerShape(ZashiDimensions.Radius.radius4xl)
)
.padding(all = 12.dp)
) )
} }
@Composable
private fun QrCode(
state: RequestState.QrCode,
contentDescription: String,
modifier: Modifier = Modifier,
) {
Box(
contentAlignment = Alignment.Center,
modifier =
Modifier
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
onClick = { state.request.qrCodeState.bitmap?.let { state.onQrCodeShare(it) } },
)
.then(modifier)
) {
if (state.request.qrCodeState.bitmap == null) {
CircularScreenProgressIndicator()
} else {
Image(
bitmap = state.request.qrCodeState.bitmap,
contentDescription = contentDescription,
)
Image(
modifier = Modifier.size(64.dp),
painter = painterResource(id = state.icon),
contentDescription = contentDescription,
)
}
}
}
@Composable @Composable
private fun RequestQrCodeZecAmountView( private fun RequestQrCodeZecAmountView(
state: RequestState.QrCode, state: RequestState.QrCode,
@ -196,5 +132,3 @@ private fun RequestQrCodeZecAmountView(
modifier = modifier modifier = modifier
) )
} }
private val DEFAULT_QR_CODE_SIZE = 320.dp

View File

@ -144,6 +144,10 @@ class RequestViewModel(
colors = colors, colors = colors,
) )
}, },
onQrCodeClick = {
// TODO [#1731]: Allow QR codes colors switching
// TODO [#1731]: https://github.com/Electric-Coin-Company/zashi-android/issues/1731
},
onQrCodeShare = { onRequestQrCodeShare(it, shareImageBitmap) }, onQrCodeShare = { onRequestQrCodeShare(it, shareImageBitmap) },
onBack = ::onBack, onBack = ::onBack,
onClose = ::onClose, onClose = ::onClose,

View File

@ -2,17 +2,32 @@ package co.electriccoin.zcash.ui.screen.signkeystonetransaction.state
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import co.electriccoin.zcash.ui.design.component.ButtonState import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.QrState
import co.electriccoin.zcash.ui.design.util.StringResource import co.electriccoin.zcash.ui.design.util.StringResource
data class SignKeystoneTransactionState( data class SignKeystoneTransactionState(
val onBack: () -> Unit, val onBack: () -> Unit,
val onQrCodeClick: () -> Unit,
val accountInfo: ZashiAccountInfoListItemState, val accountInfo: ZashiAccountInfoListItemState,
val qrData: String?, val qrData: String?,
val generateNextQrCode: () -> Unit, val generateNextQrCode: () -> Unit,
val shareButton: ButtonState?, val shareButton: ButtonState?,
val positiveButton: ButtonState, val positiveButton: ButtonState,
val negativeButton: ButtonState, val negativeButton: ButtonState,
) ) {
fun toQrState(
contentDescription: StringResource? = null,
centerImageResId: Int? = null,
): QrState {
requireNotNull(qrData) { "The QR code data needs to be set at this point" }
return QrState(
qrData = qrData,
onClick = onQrCodeClick,
contentDescription = contentDescription,
centerImageResId = centerImageResId
)
}
}
data class ZashiAccountInfoListItemState( data class ZashiAccountInfoListItemState(
@DrawableRes val icon: Int, @DrawableRes val icon: Int,

View File

@ -124,14 +124,17 @@ private fun ZashiAccountInfoListItem(
} }
@Composable @Composable
private fun ColumnScope.QrContent(state: SignKeystoneTransactionState) { private fun ColumnScope.QrContent(ksState: SignKeystoneTransactionState) {
state.qrData?.let { ksState.qrData?.let {
ZashiQr(qrData = it, modifier = Modifier.align(CenterHorizontally)) ZashiQr(
state = ksState.toQrState(),
modifier = Modifier.align(CenterHorizontally)
)
} }
LaunchedEffect(state.qrData) { LaunchedEffect(ksState.qrData) {
if (state.qrData != null) { if (ksState.qrData != null) {
delay(100.milliseconds) delay(100.milliseconds)
state.generateNextQrCode() ksState.generateNextQrCode()
} }
} }
} }
@ -182,6 +185,7 @@ private fun Preview() =
positiveButton = ButtonState(stringRes("Get Signature")), positiveButton = ButtonState(stringRes("Get Signature")),
negativeButton = ButtonState(stringRes("Reject")), negativeButton = ButtonState(stringRes("Reject")),
onBack = {}, onBack = {},
onQrCodeClick = {},
) )
) )
} }
@ -205,6 +209,7 @@ private fun DebugPreview() =
positiveButton = ButtonState(stringRes("Get Signature")), positiveButton = ButtonState(stringRes("Get Signature")),
negativeButton = ButtonState(stringRes("Reject")), negativeButton = ButtonState(stringRes("Reject")),
onBack = {}, onBack = {},
onQrCodeClick = {},
) )
) )
} }

View File

@ -75,7 +75,11 @@ class SignKeystoneTransactionViewModel(
text = stringRes("Share PCZT"), text = stringRes("Share PCZT"),
onClick = ::onSharePCZTClick onClick = ::onSharePCZTClick
).takeIf { BuildConfig.DEBUG }, ).takeIf { BuildConfig.DEBUG },
onBack = ::onBack onBack = ::onBack,
onQrCodeClick = {
// TODO [#1731]: Allow QR codes colors switching
// TODO [#1731]: https://github.com/Electric-Coin-Company/zashi-android/issues/1731
},
) )
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null)