secant-android-wallet/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/view/ReceiveView.kt

342 lines
12 KiB
Kotlin

package co.electriccoin.zcash.ui.screen.receive.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BrightnessHigh
import androidx.compose.material.icons.filled.BrightnessLow
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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 cash.z.ecc.android.sdk.fixture.WalletAddressesFixture
import cash.z.ecc.android.sdk.model.WalletAddress
import cash.z.ecc.android.sdk.model.WalletAddresses
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.BrightenScreen
import co.electriccoin.zcash.ui.common.DisableScreenTimeout
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.test.CommonTag
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.Reference
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.component.SubHeader
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import co.electriccoin.zcash.ui.screen.receive.util.AndroidQrCodeImageGenerator
import co.electriccoin.zcash.ui.screen.receive.util.JvmQrCodeGenerator
import kotlinx.coroutines.runBlocking
import kotlin.math.roundToInt
@Preview("Receive")
@Composable
private fun ComposablePreview() {
ZcashTheme(forceDarkMode = false) {
GradientSurface {
Receive(
walletAddress = runBlocking { WalletAddressesFixture.new() },
snackbarHostState = SnackbarHostState(),
onSettings = {},
onAdjustBrightness = {},
onAddrCopyToClipboard = {},
onQrImageShare = {},
versionInfo = VersionInfoFixture.new()
)
}
}
}
@Suppress("LongParameterList")
@Composable
fun Receive(
walletAddress: WalletAddresses?,
snackbarHostState: SnackbarHostState,
onSettings: () -> Unit,
onAdjustBrightness: (Boolean) -> Unit,
onAddrCopyToClipboard: (String) -> Unit,
onQrImageShare: (ImageBitmap) -> Unit,
versionInfo: VersionInfo,
) {
val (brightness, setBrightness) = rememberSaveable { mutableStateOf(false) }
Scaffold(
topBar = {
ReceiveTopAppBar(
adjustBrightness = brightness,
onSettings = onSettings,
onBrightness = {
onAdjustBrightness(!brightness)
setBrightness(!brightness)
},
versionInfo = versionInfo,
)
},
snackbarHost = { SnackbarHost(snackbarHostState) },
) { paddingValues ->
if (null == walletAddress) {
CircularScreenProgressIndicator()
} else {
ReceiveContents(
walletAddress = walletAddress,
onAddressCopyToClipboard = onAddrCopyToClipboard,
onQrImageShare = onQrImageShare,
adjustBrightness = brightness,
versionInfo = versionInfo,
modifier =
Modifier.padding(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingDefault,
start = ZcashTheme.dimens.screenHorizontalSpacingRegular,
end = ZcashTheme.dimens.screenHorizontalSpacingRegular
)
)
}
}
}
@Composable
private fun ReceiveTopAppBar(
adjustBrightness: Boolean,
onSettings: () -> Unit,
onBrightness: () -> Unit,
versionInfo: VersionInfo
) {
SmallTopAppBar(
titleText = stringResource(id = R.string.receive_title),
regularActions = {
if (versionInfo.isDebuggable) {
IconButton(
onClick = onBrightness
) {
Icon(
imageVector =
if (adjustBrightness) {
Icons.Default.BrightnessLow
} else {
Icons.Default.BrightnessHigh
},
contentDescription = stringResource(R.string.receive_brightness_content_description)
)
}
}
},
hamburgerMenuActions = {
IconButton(
onClick = onSettings,
modifier = Modifier.testTag(CommonTag.SETTINGS_TOP_BAR_BUTTON)
) {
Icon(
painter = painterResource(id = co.electriccoin.zcash.ui.design.R.drawable.hamburger_menu_icon),
contentDescription = stringResource(id = R.string.settings_menu_content_description)
)
}
}
)
}
@Suppress("LongParameterList")
@Composable
private fun ReceiveContents(
walletAddress: WalletAddresses,
onAddressCopyToClipboard: (String) -> Unit,
onQrImageShare: (ImageBitmap) -> Unit,
adjustBrightness: Boolean,
versionInfo: VersionInfo,
modifier: Modifier = Modifier,
) {
Column(
modifier =
Modifier
.fillMaxHeight()
.verticalScroll(rememberScrollState())
.then(modifier),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (adjustBrightness) {
BrightenScreen()
DisableScreenTimeout()
}
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
Address(
walletAddress = walletAddress.unified,
onAddressCopyToClipboard = onAddressCopyToClipboard,
onQrImageShare = onQrImageShare,
)
if (versionInfo.isTestnet) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
Address(
walletAddress = walletAddress.sapling,
onAddressCopyToClipboard = onAddressCopyToClipboard,
onQrImageShare = onQrImageShare,
)
}
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
Address(
walletAddress = walletAddress.transparent,
onAddressCopyToClipboard = onAddressCopyToClipboard,
onQrImageShare = onQrImageShare,
)
}
}
private val DEFAULT_QR_CODE_SIZE = 320.dp
@Suppress("LongMethod")
@Composable
private fun Address(
walletAddress: WalletAddress,
onAddressCopyToClipboard: (String) -> Unit,
onQrImageShare: (ImageBitmap) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
SubHeader(
text =
stringResource(
id =
when (walletAddress) {
is WalletAddress.Unified -> R.string.receive_wallet_address_unified
is WalletAddress.Sapling -> R.string.receive_wallet_address_sapling
is WalletAddress.Transparent -> R.string.receive_wallet_address_transparent
}
),
modifier = Modifier.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
val sizePixels = with(LocalDensity.current) { DEFAULT_QR_CODE_SIZE.toPx() }.roundToInt()
val qrCodeImage =
remember {
qrCodeForAddress(
address = walletAddress.address,
size = sizePixels
)
}
QrCode(
qrCodeImage = qrCodeImage,
onQrImageBitmapShare = onQrImageShare,
contentDescription =
stringResource(
id =
when (walletAddress) {
is WalletAddress.Unified -> R.string.receive_unified_content_description
is WalletAddress.Sapling -> R.string.receive_sapling_content_description
is WalletAddress.Transparent -> R.string.receive_transparent_content_description
}
),
modifier = Modifier.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
// TODO [#163]: Ellipsize center of the string
// TODO [#163]: https://github.com/Electric-Coin-Company/zashi-android/issues/163
Text(
text = walletAddress.address,
style = ZcashTheme.typography.primary.bodyLarge,
color = ZcashTheme.colors.textDescription,
textAlign = TextAlign.Center,
modifier =
Modifier
.align(Alignment.CenterHorizontally)
.clickable { onAddressCopyToClipboard(walletAddress.address) }
.padding(horizontal = ZcashTheme.dimens.spacingLarge)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Reference(
text = stringResource(id = R.string.receive_copy),
onClick = { onAddressCopyToClipboard(walletAddress.address) },
textAlign = TextAlign.Center,
imageVector = ImageVector.vectorResource(R.drawable.copy),
imageContentDescription = null,
modifier = Modifier.wrapContentSize(),
)
Reference(
text = stringResource(id = R.string.receive_share),
onClick = { onQrImageShare(qrCodeImage) },
textAlign = TextAlign.Center,
imageVector = ImageVector.vectorResource(R.drawable.share),
imageContentDescription = null,
modifier = Modifier.wrapContentSize(),
)
}
}
}
private fun qrCodeForAddress(
address: String,
size: Int,
): ImageBitmap {
// In the future, use actual/expect to switch QR code generator implementations for multiplatform
// Note that our implementation has an extra array copy to BooleanArray, which is a cross-platform
// representation. This should have minimal performance impact since the QR code is relatively
// small and we only generate QR codes infrequently.
val qrCodePixelArray = JvmQrCodeGenerator.generate(address, size)
return AndroidQrCodeImageGenerator.generate(qrCodePixelArray, size)
}
@Composable
private fun QrCode(
contentDescription: String,
qrCodeImage: ImageBitmap,
onQrImageBitmapShare: (ImageBitmap) -> Unit,
modifier: Modifier = Modifier,
) {
Image(
bitmap = qrCodeImage,
contentDescription = contentDescription,
modifier =
Modifier
.clickable { onQrImageBitmapShare(qrCodeImage) }
.then(modifier)
)
}