* QR dialog implementation

Closes #1731

* QR dialog refactor

Closes #1731

* Code cleanup

Closes #1731

* Code cleanup

Closes #1731

* Code cleanup

Closes #1731
This commit is contained in:
Milan 2025-02-06 07:37:24 +01:00 committed by GitHub
parent 69cf5ef7c3
commit a4090044ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 210 additions and 47 deletions

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.common.compose
package co.electriccoin.zcash.ui.design.component
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect

View File

@ -3,22 +3,36 @@ package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.graphics.ImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
import co.electriccoin.zcash.ui.design.util.AndroidQrCodeImageGenerator
@ -34,31 +48,101 @@ fun ZashiQr(
modifier: Modifier = Modifier,
qrSize: Dp = ZashiQrDefaults.width,
colors: QrCodeColors = QrCodeDefaults.colors(),
contentPadding: PaddingValues = QrCodeDefaults.contentPadding()
) {
var isFullscreenDialogVisible by remember { mutableStateOf(false) }
ZashiQrInternal(
state = state,
modifier =
modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {
isFullscreenDialogVisible = true
}
),
colors = colors,
contentPadding = contentPadding,
qrSize = qrSize,
enableBitmapReload = !isFullscreenDialogVisible,
centerImageResId = state.centerImageResId,
)
if (isFullscreenDialogVisible) {
Dialog(
onDismissRequest = { isFullscreenDialogVisible = false },
properties =
DialogProperties(
usePlatformDefaultWidth = false,
dismissOnClickOutside = true,
dismissOnBackPress = true
)
) {
val parent = LocalView.current.parent
BrightenScreen()
FullscreenDialogContent(
state = state,
onBack = { isFullscreenDialogVisible = false },
)
SideEffect {
(parent as? DialogWindowProvider)?.window?.setDimAmount(FULLSCREEN_DIM)
}
}
}
}
@Composable
private fun ZashiQrInternal(
state: QrState,
qrSize: Dp,
colors: QrCodeColors,
contentPadding: PaddingValues,
enableBitmapReload: Boolean,
centerImageResId: Int?,
modifier: Modifier = Modifier,
) {
val qrSizePx = with(LocalDensity.current) { qrSize.roundToPx() }
val bitmap = getQrCode(state.qrData, qrSizePx, colors)
var bitmap: ImageBitmap by remember {
mutableStateOf(getQrCode(state.qrData, qrSizePx, colors))
}
var reload by remember { mutableStateOf(false) }
LaunchedEffect(state.qrData, qrSizePx, colors) {
if (enableBitmapReload && reload) {
bitmap = getQrCode(state.qrData, qrSizePx, colors)
}
reload = true
}
Surface(
modifier = modifier,
shape = RoundedCornerShape(ZashiDimensions.Radius.radius4xl),
border = BorderStroke(width = 1.dp, color = ZashiColors.Surfaces.strokePrimary),
border = BorderStroke(width = 1.dp, color = colors.border).takeIf { colors.border.isSpecified },
color = colors.background,
) {
Box(
modifier = Modifier.padding(all = 16.dp)
modifier = Modifier.padding(contentPadding)
) {
Image(
modifier = Modifier,
bitmap = bitmap,
contentDescription = state.contentDescription?.getValue(),
Modifier.clickable { state.onClick() }
)
if (state.centerImageResId != null) {
if (centerImageResId != null) {
Image(
modifier =
Modifier
.size(64.dp)
.align(Alignment.Center),
imageVector = ImageVector.vectorResource(state.centerImageResId),
painter = painterResource(centerImageResId),
contentDescription = null,
)
}
@ -66,6 +150,42 @@ fun ZashiQr(
}
}
@Composable
private fun FullscreenDialogContent(
state: QrState,
onBack: () -> Unit
) {
Box(
modifier =
Modifier
.fillMaxSize()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onBack
)
.padding(start = 16.dp, end = 16.dp, bottom = 64.dp)
) {
ZashiQrInternal(
modifier =
Modifier
.fillMaxWidth()
.align(Alignment.Center),
state = state,
contentPadding = PaddingValues(6.dp),
colors =
QrCodeDefaults.colors(
background = Color.White,
foreground = Color.Black,
border = Color.Unspecified
),
qrSize = LocalConfiguration.current.screenWidthDp.dp - 44.dp,
enableBitmapReload = true,
centerImageResId = state.fullscreenCenterImageResId,
)
}
}
private fun getQrCode(
address: String,
size: Int,
@ -84,19 +204,25 @@ object ZashiQrDefaults {
private const val WIDTH_RATIO = 0.66
object QrCodeDefaults {
fun contentPadding() = PaddingValues(16.dp)
@Composable
fun colors(
background: Color = Color.White orDark ZashiColors.Surfaces.bgPrimary,
foreground: Color = Color.Black orDark Color.White
foreground: Color = Color.Black orDark Color.White,
border: Color = ZashiColors.Surfaces.strokePrimary
) = QrCodeColors(
background = background,
foreground = foreground
foreground = foreground,
border = border
)
}
data class QrState(
val qrData: String,
val contentDescription: StringResource? = null,
val onClick: () -> Unit = {},
val centerImageResId: Int? = null,
val fullscreenCenterImageResId: Int? = null,
)
private const val FULLSCREEN_DIM = .9f

View File

@ -30,5 +30,6 @@ private fun BooleanArray.toThemeColorArray(colors: QrCodeColors) =
data class QrCodeColors(
val background: Color,
val foreground: Color
val foreground: Color,
val border: Color,
)

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="61dp"
android:height="60dp"
android:viewportWidth="61"
android:viewportHeight="60">
<path
android:pathData="M0.5,30C0.5,13.432 13.932,0 30.5,0C47.069,0 60.5,13.432 60.5,30C60.5,46.569 47.069,60 30.5,60C13.932,60 0.5,46.569 0.5,30Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M15.5,13.5H33.5L21.5,39H12.5L15.5,13.5Z"
android:fillColor="#000000"/>
<path
android:pathData="M45.5,46.5H27.5L39.5,21H48.5L45.5,46.5Z"
android:fillColor="#1F5AFF"/>
</vector>

View File

@ -8,10 +8,10 @@ import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.compose.BrightenScreen
import co.electriccoin.zcash.ui.common.compose.LocalScreenBrightness
import co.electriccoin.zcash.ui.common.compose.ScreenBrightness
import co.electriccoin.zcash.ui.common.compose.ScreenBrightnessState
import co.electriccoin.zcash.ui.design.component.BrightenScreen
import co.electriccoin.zcash.ui.design.component.LocalScreenBrightness
import co.electriccoin.zcash.ui.design.component.ScreenBrightness
import co.electriccoin.zcash.ui.design.component.ScreenBrightnessState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update

View File

@ -10,6 +10,9 @@ import cash.z.ecc.android.sdk.ext.collectWith
import co.electriccoin.zcash.spackle.EmulatorWtfUtil
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.design.component.LocalScreenBrightness
import co.electriccoin.zcash.ui.design.component.ScreenBrightness
import co.electriccoin.zcash.ui.design.component.ScreenBrightnessState
import kotlinx.coroutines.flow.map
@Composable

View File

@ -20,12 +20,12 @@ import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.HomeTabNavigationRouter
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.common.compose.RestoreScreenBrightness
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.common.viewmodel.isSynced
import co.electriccoin.zcash.ui.design.component.RestoreScreenBrightness
import co.electriccoin.zcash.ui.screen.account.WrapAccount
import co.electriccoin.zcash.ui.screen.balances.WrapBalances
import co.electriccoin.zcash.ui.screen.home.model.TabItem

View File

@ -13,17 +13,17 @@ sealed class QrCodeState {
val walletAddress: WalletAddress,
val onAddressCopy: (String) -> Unit,
val onQrCodeShare: (ImageBitmap) -> Unit,
val onQrCodeClick: () -> Unit,
val onBack: () -> Unit,
) : QrCodeState() {
fun toQrState(
contentDescription: StringResource? = null,
centerImageResId: Int? = null
centerImageResId: Int? = null,
fullscreenCenterImageResId: Int? = null
) = QrState(
qrData = walletAddress.address,
onClick = onQrCodeClick,
contentDescription = contentDescription,
centerImageResId = centerImageResId
centerImageResId = centerImageResId,
fullscreenCenterImageResId = fullscreenCenterImageResId
)
}
}

View File

@ -86,7 +86,6 @@ private fun ZashiPreview() =
walletAddress = runBlocking { WalletAddressFixture.unified() },
onAddressCopy = {},
onQrCodeShare = {},
onQrCodeClick = {},
onBack = {},
),
snackbarHostState = SnackbarHostState(),
@ -105,7 +104,6 @@ private fun KeystonePreview() =
walletAddress = runBlocking { WalletAddressFixture.unified() },
onAddressCopy = {},
onQrCodeShare = {},
onQrCodeClick = {},
onBack = {},
),
snackbarHostState = SnackbarHostState(),
@ -438,6 +436,11 @@ private fun ColumnScope.QrCode(
when (preparedState.qrCodeType) {
QrCodeType.ZASHI -> R.drawable.logo_zec_fill_stroke
QrCodeType.KEYSTONE -> co.electriccoin.zcash.ui.design.R.drawable.ic_item_keystone_qr
},
fullscreenCenterImageResId =
when (preparedState.qrCodeType) {
QrCodeType.ZASHI -> R.drawable.logo_zec_fill_stroke_white
QrCodeType.KEYSTONE -> co.electriccoin.zcash.ui.design.R.drawable.ic_item_keystone_qr_white
}
),
modifier = modifier.align(CenterHorizontally),

View File

@ -57,10 +57,6 @@ class QrCodeViewModel(
walletAddress = walletAddress,
onAddressCopy = { address -> onAddressCopyClick(address) },
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,
qrCodeType =
when (account) {

View File

@ -36,9 +36,9 @@ internal sealed class RequestState {
data class QrCode(
val icon: Int,
val fullScreenIcon: Int,
val request: Request,
val walletAddress: WalletAddress,
val onQrCodeClick: () -> Unit,
val onQrCodeShare: (colors: QrCodeColors, pixels: Int, uri: String) -> Unit,
override val onBack: () -> Unit,
val onClose: () -> Unit,
@ -47,11 +47,12 @@ internal sealed class RequestState {
fun toQrState(
contentDescription: StringResource? = null,
centerImageResId: Int? = null,
fullscreenCenterImageResId: Int? = null
) = QrState(
qrData = request.qrCodeState.requestUri,
onClick = onQrCodeClick,
contentDescription = contentDescription,
centerImageResId = centerImageResId
centerImageResId = centerImageResId,
fullscreenCenterImageResId = fullscreenCenterImageResId
)
}
}

View File

@ -101,7 +101,8 @@ private fun ColumnScope.QrCode(
state =
requestState.toQrState(
contentDescription = stringRes(R.string.request_qr_code_content_description),
centerImageResId = requestState.icon
centerImageResId = requestState.icon,
fullscreenCenterImageResId = requestState.fullScreenIcon
),
modifier = modifier.align(CenterHorizontally),
)

View File

@ -138,12 +138,15 @@ class RequestViewModel(
.ic_item_keystone_qr
is ZashiAccount -> R.drawable.logo_zec_fill_stroke
},
fullScreenIcon =
when (account) {
is KeystoneAccount ->
co.electriccoin.zcash.ui.design.R.drawable
.ic_item_keystone_qr_white
is ZashiAccount -> R.drawable.logo_zec_fill_stroke_white
},
walletAddress = walletAddress,
request = request,
onQrCodeClick = {
// TODO [#1731]: Allow QR codes colors switching
// TODO [#1731]: https://github.com/Electric-Coin-Company/zashi-android/issues/1731
},
onQrCodeShare = { colors, pixels, uri ->
onShareQrCode(
colors = colors,

View File

@ -2,7 +2,7 @@ package co.electriccoin.zcash.ui.screen.settings.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import co.electriccoin.zcash.ui.common.compose.ScreenBrightness
import co.electriccoin.zcash.ui.design.component.ScreenBrightness
import kotlinx.coroutines.flow.MutableStateFlow
class ScreenBrightnessViewModel(application: Application) : AndroidViewModel(application) {

View File

@ -7,7 +7,6 @@ import co.electriccoin.zcash.ui.design.util.StringResource
data class SignKeystoneTransactionState(
val onBack: () -> Unit,
val onQrCodeClick: () -> Unit,
val accountInfo: ZashiAccountInfoListItemState,
val qrData: String?,
val generateNextQrCode: () -> Unit,
@ -22,7 +21,6 @@ data class SignKeystoneTransactionState(
requireNotNull(qrData) { "The QR code data needs to be set at this point" }
return QrState(
qrData = qrData,
onClick = onQrCodeClick,
contentDescription = contentDescription,
centerImageResId = centerImageResId
)

View File

@ -23,6 +23,7 @@ import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.QrCodeDefaults
import co.electriccoin.zcash.ui.design.component.ZashiBadge
import co.electriccoin.zcash.ui.design.component.ZashiBadgeDefaults
import co.electriccoin.zcash.ui.design.component.ZashiButton
@ -35,7 +36,6 @@ import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.QrCodeColors
import co.electriccoin.zcash.ui.design.util.getValue
import co.electriccoin.zcash.ui.design.util.scaffoldPadding
import co.electriccoin.zcash.ui.design.util.stringRes
@ -132,7 +132,7 @@ private fun ColumnScope.QrContent(ksState: SignKeystoneTransactionState) {
state = ksState.toQrState(),
modifier = Modifier.align(CenterHorizontally),
colors =
QrCodeColors(
QrCodeDefaults.colors(
background = Color.White,
foreground = Color.Black
)
@ -192,7 +192,6 @@ private fun Preview() =
positiveButton = ButtonState(stringRes("Get Signature")),
negativeButton = ButtonState(stringRes("Reject")),
onBack = {},
onQrCodeClick = {},
)
)
}
@ -216,7 +215,6 @@ private fun DebugPreview() =
positiveButton = ButtonState(stringRes("Get Signature")),
negativeButton = ButtonState(stringRes("Reject")),
onBack = {},
onQrCodeClick = {},
)
)
}

View File

@ -76,10 +76,6 @@ class SignKeystoneTransactionViewModel(
onClick = ::onSharePCZTClick
).takeIf { BuildConfig.DEBUG },
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)

View File

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="65dp"
android:height="64dp"
android:viewportWidth="65"
android:viewportHeight="64">
<group>
<clip-path
android:pathData="M32.5,8L32.5,8A24,24 0,0 1,56.5 32L56.5,32A24,24 0,0 1,32.5 56L32.5,56A24,24 0,0 1,8.5 32L8.5,32A24,24 0,0 1,32.5 8z"/>
<path
android:pathData="M32.5,8L32.5,8A24,24 0,0 1,56.5 32L56.5,32A24,24 0,0 1,32.5 56L32.5,56A24,24 0,0 1,8.5 32L8.5,32A24,24 0,0 1,32.5 8z"
android:fillColor="#ffffff"/>
<path
android:pathData="M8.5,32C8.5,18.764 19.264,8 32.5,8C45.736,8 56.5,18.764 56.5,32C56.5,45.236 45.736,56 32.5,56C19.264,56 8.5,45.236 8.5,32ZM41.061,20.862V24.515L30.903,38.292H41.061V43.137H34.512V47.151H30.488V43.137H23.939V39.484L34.087,25.707H23.939V20.862H30.488V16.837H34.512V20.862H41.061Z"
android:fillColor="#231F20"
android:fillType="evenOdd"/>
</group>
<path
android:pathData="M32.5,4L32.5,4A28,28 0,0 1,60.5 32L60.5,32A28,28 0,0 1,32.5 60L32.5,60A28,28 0,0 1,4.5 32L4.5,32A28,28 0,0 1,32.5 4z"
android:strokeWidth="8"
android:fillColor="#00000000"
android:strokeColor="#ffffff"/>
</vector>