#1763 Keystone confirmation flow design update (#1773)

* #1763 Keystone confirmation flow design update

Closes #1763

* #1763 Documentation update

Closes #1763

* #1763 Documentation update

Closes #1763

* #1763 Back handling

* #1763 Strings update

* #1763 Code cleanup
This commit is contained in:
Milan 2025-02-12 22:10:02 +01:00 committed by GitHub
parent ed8dad3c54
commit 1ed5088953
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 322 additions and 9 deletions

View File

@ -6,8 +6,12 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
## [Unreleased]
### Added
- Confirm the rejection of a Keystone transaction dialog added.
### Changed
- `Flexa` version has been bumped to 1.0.11
- Keystone flows swapped the buttons for the better UX, the main CTA is the closes button for a thumb.
## [1.3.3 (839)] - 2025-01-23

View File

@ -201,6 +201,22 @@ object ZashiButtonDefaults {
borderColor = borderColor,
disabledBorderColor = Color.Unspecified
)
@Composable
fun destructive2Colors(
containerColor: Color = ZashiColors.Btns.Destructive2.btnDestroy2Bg,
contentColor: Color = ZashiColors.Btns.Destructive2.btnDestroy2Fg,
borderColor: Color = Color.Unspecified,
disabledContainerColor: Color = ZashiColors.Btns.Destructive2.btnDestroy2BgDisabled,
disabledContentColor: Color = ZashiColors.Btns.Destructive2.btnDestroy2FgDisabled,
) = ZashiButtonColors(
containerColor = containerColor,
contentColor = contentColor,
disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor,
borderColor = borderColor,
disabledBorderColor = Color.Unspecified
)
}
@Immutable

View File

@ -0,0 +1,58 @@
package co.electriccoin.zcash.ui.design.component
import androidx.activity.compose.BackHandler
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheetProperties
import androidx.compose.material3.SheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T : ModalBottomSheetState> ZashiInScreenModalBottomSheet(
state: T?,
modifier: Modifier = Modifier,
sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
content: @Composable (T) -> Unit = {},
) {
var normalizedState: T? by remember { mutableStateOf(null) }
normalizedState?.let {
ZashiModalBottomSheet(
onDismissRequest = {
it.onBack()
},
modifier = modifier,
sheetState = sheetState,
properties =
ModalBottomSheetProperties(
shouldDismissOnBackPress = false
)
) {
BackHandler {
it.onBack()
}
content(it)
}
}
LaunchedEffect(state) {
if (state != null) {
normalizedState = state
sheetState.show()
} else {
sheetState.hide()
normalizedState = null
}
}
}
interface ModalBottomSheetState {
val onBack: () -> Unit
}

View File

@ -12,6 +12,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.ModalBottomSheetDefaults
import androidx.compose.material3.ModalBottomSheetProperties
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue
import androidx.compose.material3.SheetValue.Expanded
@ -36,6 +38,7 @@ fun ZashiModalBottomSheet(
modifier: Modifier = Modifier,
scrimColor: Color = BottomSheetDefaults.ScrimColor,
sheetState: SheetState = rememberModalBottomSheetState(),
properties: ModalBottomSheetProperties = ModalBottomSheetDefaults.properties,
content: @Composable ColumnScope.() -> Unit,
) {
ModalBottomSheet(
@ -46,6 +49,7 @@ fun ZashiModalBottomSheet(
shape = ZashiModalBottomSheetDefaults.SheetShape,
containerColor = ZashiModalBottomSheetDefaults.ContainerColor,
dragHandle = { ZashiModalBottomSheetDragHandle() },
properties = properties,
content = content,
)
}

View File

@ -358,15 +358,15 @@ private fun AmountWidget(state: AmountState) {
private fun BottomBar(state: ReviewTransactionState) {
ZashiBottomBar {
ZashiButton(
state = state.primaryButton,
state = state.negativeButton,
colors = ZashiButtonDefaults.secondaryColors(),
modifier =
Modifier
.padding(horizontal = 24.dp)
.fillMaxWidth()
)
ZashiButton(
state = state.negativeButton,
colors = ZashiButtonDefaults.secondaryColors(),
state = state.primaryButton,
modifier =
Modifier
.padding(horizontal = 24.dp)

View File

@ -1,17 +1,21 @@
package co.electriccoin.zcash.ui.screen.signkeystonetransaction
import androidx.activity.compose.BackHandler
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.ui.screen.signkeystonetransaction.view.SignKeystoneTransactionBottomSheet
import co.electriccoin.zcash.ui.screen.signkeystonetransaction.view.SignKeystoneTransactionView
import co.electriccoin.zcash.ui.screen.signkeystonetransaction.viewmodel.SignKeystoneTransactionViewModel
import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AndroidSignKeystoneTransaction() {
val viewModel = koinViewModel<SignKeystoneTransactionViewModel>()
val state by viewModel.state.collectAsStateWithLifecycle()
val bottomSheetState by viewModel.bottomSheetState.collectAsStateWithLifecycle()
BackHandler {
state?.onBack?.invoke()
@ -20,4 +24,8 @@ fun AndroidSignKeystoneTransaction() {
state?.let {
SignKeystoneTransactionView(it)
}
SignKeystoneTransactionBottomSheet(
state = bottomSheetState
)
}

View File

@ -0,0 +1,110 @@
package co.electriccoin.zcash.ui.screen.signkeystonetransaction.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.ModalBottomSheetState
import co.electriccoin.zcash.ui.design.component.ZashiButton
import co.electriccoin.zcash.ui.design.component.ZashiButtonDefaults
import co.electriccoin.zcash.ui.design.component.ZashiInScreenModalBottomSheet
import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState
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.stringRes
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SignKeystoneTransactionBottomSheet(
state: SignKeystoneTransactionBottomSheetState?,
modifier: Modifier = Modifier,
sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
) {
ZashiInScreenModalBottomSheet(
state = state,
sheetState = sheetState,
modifier = modifier
) {
Column(
modifier = Modifier.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(R.drawable.ic_keystone_sign_reject),
contentDescription = null
)
Spacer(Modifier.height(8.dp))
Text(
text = stringResource(R.string.sign_keystone_transaction_bottom_sheet_title),
style = ZashiTypography.header6,
color = ZashiColors.Text.textPrimary,
fontWeight = FontWeight.SemiBold
)
Spacer(Modifier.height(8.dp))
Text(
text = stringResource(R.string.sign_keystone_transaction_bottom_sheet_subtitle),
style = ZashiTypography.textSm,
color = ZashiColors.Text.textTertiary,
)
Spacer(Modifier.height(32.dp))
ZashiButton(
modifier = Modifier.fillMaxWidth(),
state = it.positiveButton
)
Spacer(Modifier.height(8.dp))
ZashiButton(
modifier = Modifier.fillMaxWidth(),
state = it.negativeButton,
colors = ZashiButtonDefaults.destructive2Colors()
)
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
}
}
}
data class SignKeystoneTransactionBottomSheetState(
override val onBack: () -> Unit,
val positiveButton: ButtonState,
val negativeButton: ButtonState,
) : ModalBottomSheetState
@OptIn(ExperimentalMaterial3Api::class)
@PreviewScreens
@Composable
private fun Preview() =
ZcashTheme {
SignKeystoneTransactionBottomSheet(
sheetState =
rememberModalBottomSheetState(
skipPartiallyExpanded = true,
skipHiddenState = true,
initialValue = SheetValue.Expanded,
),
state =
SignKeystoneTransactionBottomSheetState(
onBack = {},
positiveButton = ButtonState(stringRes("Get Signature")),
negativeButton = ButtonState(stringRes("Reject")),
)
)
}

View File

@ -163,12 +163,12 @@ private fun BottomSection(
}
ZashiButton(
modifier = Modifier.fillMaxWidth(),
state = state.positiveButton
state = state.negativeButton,
colors = ZashiButtonDefaults.destructive1Colors()
)
ZashiButton(
modifier = Modifier.fillMaxWidth(),
state = state.negativeButton,
colors = ZashiButtonDefaults.secondaryColors()
state = state.positiveButton
)
}
}

View File

@ -18,15 +18,19 @@ import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.ADDRESS_MAX_LENGTH
import co.electriccoin.zcash.ui.screen.scankeystone.ScanKeystonePCZTRequest
import co.electriccoin.zcash.ui.screen.signkeystonetransaction.state.SignKeystoneTransactionState
import co.electriccoin.zcash.ui.screen.signkeystonetransaction.state.ZashiAccountInfoListItemState
import co.electriccoin.zcash.ui.screen.signkeystonetransaction.view.SignKeystoneTransactionBottomSheetState
import com.sparrowwallet.hummingbird.UREncoder
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds
class SignKeystoneTransactionViewModel(
observeSelectedWalletAccount: ObserveSelectedWalletAccountUseCase,
@ -38,8 +42,37 @@ class SignKeystoneTransactionViewModel(
) : ViewModel() {
private var encoder: UREncoder? = null
private val isBottomSheetVisible = MutableStateFlow(false)
private val currentQrPart = MutableStateFlow<String?>(null)
val bottomSheetState =
isBottomSheetVisible
.map { isVisible ->
if (isVisible) {
SignKeystoneTransactionBottomSheetState(
onBack = ::onCloseBottomSheetClick,
positiveButton =
ButtonState(
text = stringRes(R.string.sign_keystone_transaction_bottom_sheet_go_back),
onClick = ::onCloseBottomSheetClick
),
negativeButton =
ButtonState(
text = stringRes(R.string.sign_keystone_transaction_bottom_sheet_reject),
onClick = ::onRejectBottomSheetClick
),
)
} else {
null
}
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = null
)
val state: StateFlow<SignKeystoneTransactionState?> =
combine(
observeProposalUseCase(),
@ -81,7 +114,11 @@ class SignKeystoneTransactionViewModel(
// TODO [#1731]: https://github.com/Electric-Coin-Company/zashi-android/issues/1731
},
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = null
)
init {
viewModelScope.launch {
@ -94,17 +131,29 @@ class SignKeystoneTransactionViewModel(
}
}
private fun onRejectBottomSheetClick() {
viewModelScope.launch {
isBottomSheetVisible.update { false }
delay(350.milliseconds)
cancelKeystoneProposalFlow()
}
}
private fun onCloseBottomSheetClick() {
isBottomSheetVisible.update { false }
}
private fun onSharePCZTClick() =
viewModelScope.launch {
sharePCZT()
}
private fun onBack() {
cancelKeystoneProposalFlow()
isBottomSheetVisible.update { !it }
}
private fun onRejectClick() {
cancelKeystoneProposalFlow()
isBottomSheetVisible.update { true }
}
private fun onSignTransactionClick() {

View File

@ -0,0 +1,28 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="45dp"
android:height="44dp"
android:viewportWidth="45"
android:viewportHeight="44">
<path
android:pathData="M0.5,22C0.5,9.85 10.35,0 22.5,0C34.65,0 44.5,9.85 44.5,22C44.5,34.15 34.65,44 22.5,44C10.35,44 0.5,34.15 0.5,22Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="22.5"
android:startY="0"
android:endX="22.5"
android:endY="44"
android:type="linear">
<item android:offset="0" android:color="#FF55160C"/>
<item android:offset="1" android:color="#FF7A271A"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M18.333,29.219C15.842,27.778 14.167,25.085 14.167,22C14.167,17.398 17.897,13.667 22.5,13.667C27.102,13.667 30.833,17.398 30.833,22C30.833,25.085 29.157,27.778 26.667,29.219M25.833,22L22.5,18.667M22.5,18.667L19.167,22M22.5,18.667V30.333"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#F04438"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,28 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="45dp"
android:height="44dp"
android:viewportWidth="45"
android:viewportHeight="44">
<path
android:pathData="M0.5,22C0.5,9.85 10.35,0 22.5,0C34.65,0 44.5,9.85 44.5,22C44.5,34.15 34.65,44 22.5,44C10.35,44 0.5,34.15 0.5,22Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="22.5"
android:startY="0"
android:endX="22.5"
android:endY="44"
android:type="linear">
<item android:offset="0" android:color="#FFFEF3F2"/>
<item android:offset="1" android:color="#FFFEE4E2"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M18.333,29.219C15.842,27.778 14.167,25.085 14.167,22C14.167,17.398 17.898,13.667 22.5,13.667C27.102,13.667 30.833,17.398 30.833,22C30.833,25.085 29.157,27.778 26.667,29.219M25.833,22L22.5,18.667M22.5,18.667L19.167,22M22.5,18.667V30.333"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#F04438"
android:strokeLineCap="round"/>
</vector>

View File

@ -6,4 +6,8 @@
<string name="sign_keystone_transaction_badge">Hardware</string>
<string name="sign_keystone_transaction_positive">Obtener Firma</string>
<string name="sign_keystone_transaction_negative">Rechazar</string>
<string name="sign_keystone_transaction_bottom_sheet_title">¿Estás seguro?</string>
<string name="sign_keystone_transaction_bottom_sheet_subtitle">Rechazar la firma cancelará la transacción y tendrás que empezar de nuevo si deseas continuar. Esta acción no se puede deshacer.</string>
<string name="sign_keystone_transaction_bottom_sheet_go_back">Volver</string>
<string name="sign_keystone_transaction_bottom_sheet_reject">Rechazar firma</string>
</resources>

View File

@ -6,4 +6,8 @@
<string name="sign_keystone_transaction_badge">Hardware</string>
<string name="sign_keystone_transaction_positive">Get Signature</string>
<string name="sign_keystone_transaction_negative">Reject</string>
<string name="sign_keystone_transaction_bottom_sheet_title">Are you sure?</string>
<string name="sign_keystone_transaction_bottom_sheet_subtitle">Rejecting the signature will cancel the transaction, and youll need to start over if you want to proceed. This action cannot be undone.</string>
<string name="sign_keystone_transaction_bottom_sheet_go_back">Go back</string>
<string name="sign_keystone_transaction_bottom_sheet_reject">Reject Signature</string>
</resources>