[#1166] Add shielding funds feature

* [#1166] Add shielding funds feature

- Changelog updated
- Closes #1127
- Closes #1166
- Related #238
This commit is contained in:
Honza Rychnovský 2024-02-13 09:30:51 +01:00 committed by GitHub
parent 01e3741f22
commit 26a73f8e59
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 415 additions and 57 deletions

View File

@ -14,8 +14,9 @@ directly impact users rather than highlighting other key architectural updates.*
### Added
- The Balances screen now provides details on current balances like Change pending and Pending transactions
- The screen also adds a new Block synchronization progress bar and status, which were initially part of the Account
screen and redesigned
- The Balances screen adds a new Block synchronization progress bar and status, which were initially part of the
Account screen and redesigned
- The Balances screen supports transparent funds shielding within its new shielding panel
### Fixed
- Fixed character replacement in Zcash addresses on the Receive screen caused by ligatures in the app's primary font

View File

@ -31,6 +31,7 @@ import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
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
@ -60,14 +61,15 @@ fun PrimaryButton(
onClick: () -> Unit,
text: String,
modifier: Modifier = Modifier,
buttonColor: Color = MaterialTheme.colorScheme.primary,
enabled: Boolean = true,
textColor: Color = MaterialTheme.colorScheme.onPrimary,
textStyle: TextStyle = ZcashTheme.extendedTypography.buttonText,
outerPaddingValues: PaddingValues =
PaddingValues(
horizontal = ZcashTheme.dimens.spacingNone,
vertical = ZcashTheme.dimens.spacingSmall
),
enabled: Boolean = true,
buttonColor: Color = MaterialTheme.colorScheme.primary,
textColor: Color = MaterialTheme.colorScheme.onPrimary,
)
) {
Button(
shape = RectangleShape,
@ -99,7 +101,7 @@ fun PrimaryButton(
onClick = onClick,
) {
Text(
style = ZcashTheme.extendedTypography.buttonText,
style = textStyle,
textAlign = TextAlign.Center,
text = text.uppercase(),
color = textColor

View File

@ -247,6 +247,7 @@ fun Reference(
modifier: Modifier = Modifier,
fontWeight: FontWeight = FontWeight.SemiBold,
textAlign: TextAlign = TextAlign.Center,
textStyle: TextStyle = ZcashTheme.typography.primary.bodyLarge,
imageVector: ImageVector? = null,
imageContentDescription: String? = null
) {
@ -254,7 +255,7 @@ fun Reference(
modifier =
Modifier
.wrapContentSize()
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
.clickable { onClick() }
.then(modifier),
verticalAlignment = Alignment.CenterVertically
@ -270,15 +271,14 @@ fun Reference(
text = text,
textAlign = TextAlign.Center,
style =
ZcashTheme.typography.primary.bodyLarge
.merge(
TextStyle(
color = ZcashTheme.colors.reference,
textAlign = textAlign,
textDecoration = TextDecoration.Underline,
fontWeight = fontWeight
)
textStyle.merge(
TextStyle(
color = ZcashTheme.colors.reference,
textAlign = textAlign,
textDecoration = TextDecoration.Underline,
fontWeight = fontWeight
)
)
)
}
}
@ -410,7 +410,7 @@ fun NavigationTabText(
color = ZcashTheme.colors.tabTextColor,
modifier =
Modifier
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
.clickable { onClick() }
.then(modifier)
)

View File

@ -246,7 +246,7 @@ fun SmallTopAppBar(
modifier =
Modifier
.wrapContentSize()
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
.clickable { onBack?.run { onBack() } }
) {
Row(

View File

@ -34,13 +34,14 @@ data class Dimens(
val linearProgressHeight: Dp,
// TopAppBar:
val topAppBarZcashLogoHeight: Dp,
val topAppBarActionRippleCorner: Dp,
// TextField:
val textFieldDefaultHeight: Dp,
val textFieldPanelDefaultHeight: Dp,
// Any Layout:
val divider: Dp,
val layoutStroke: Dp,
val regularRippleEffectCorner: Dp,
val smallRippleEffectCorner: Dp,
// Screen custom spacings:
val inScreenZcashLogoHeight: Dp,
val inScreenZcashLogoWidth: Dp,
@ -70,11 +71,12 @@ private val defaultDimens =
circularSmallProgressWidth = 14.dp,
linearProgressHeight = 14.dp,
topAppBarZcashLogoHeight = 24.dp,
topAppBarActionRippleCorner = 28.dp,
textFieldDefaultHeight = 64.dp,
textFieldPanelDefaultHeight = 215.dp,
layoutStroke = 1.dp,
divider = 1.dp,
regularRippleEffectCorner = 28.dp,
smallRippleEffectCorner = 10.dp,
inScreenZcashLogoHeight = 100.dp,
inScreenZcashLogoWidth = 60.dp,
inScreenZcashTextLogoHeight = 30.dp,

View File

@ -103,7 +103,7 @@ internal val SecondaryTypography =
TextStyle(
fontFamily = ArchivoFontFamily,
fontWeight = FontWeight.SemiBold,
fontSize = 20.sp,
fontSize = 18.sp,
textAlign = TextAlign.Center
),
bodyLarge =
@ -162,12 +162,14 @@ data class ExtendedTypography(
val addressStyle: TextStyle,
val aboutText: TextStyle,
val buttonText: TextStyle,
val buttonTextSmall: TextStyle,
val checkboxText: TextStyle,
val securityWarningText: TextStyle,
val textFieldHint: TextStyle,
val textFieldValue: TextStyle,
val textFieldBirthday: TextStyle,
val textNavTab: TextStyle,
val referenceSmall: TextStyle,
)
@Suppress("CompositionLocalAllowlist")
@ -236,9 +238,10 @@ val LocalExtendedTypography =
fontSize = 14.sp,
lineHeight = 20.sp
),
buttonText =
buttonText = PrimaryTypography.bodySmall,
buttonTextSmall =
PrimaryTypography.bodySmall.copy(
fontSize = 14.sp
fontSize = 11.sp
),
checkboxText =
PrimaryTypography.bodyMedium.copy(
@ -263,5 +266,9 @@ val LocalExtendedTypography =
SecondaryTypography.labelSmall.copy(
fontSize = 13.sp
),
referenceSmall =
PrimaryTypography.bodySmall.copy(
fontSize = 13.sp
)
)
}

View File

@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.balances.model.ShieldState
import co.electriccoin.zcash.ui.screen.balances.view.Balances
import java.util.concurrent.atomic.AtomicInteger
@ -28,13 +29,15 @@ class BalancesTestSetup(
@Suppress("TestFunctionName")
fun DefaultContent() {
Balances(
isFiatConversionEnabled = isShowFiatConversion,
isKeepScreenOnWhileSyncing = false,
isUpdateAvailable = false,
onSettings = {
onSettingsCount.incrementAndGet()
},
isFiatConversionEnabled = isShowFiatConversion,
isKeepScreenOnWhileSyncing = false,
isUpdateAvailable = false,
walletSnapshot = walletSnapshot,
onShielding = {},
shieldState = ShieldState.Available
)
}

View File

@ -20,12 +20,17 @@ data class WalletSnapshot(
val synchronizerError: SynchronizerError?
) {
// Note: the wallet is effectively empty if it cannot cover the miner's fee
val hasFunds =
val hasSaplingFunds =
saplingBalance.available.value >
(ZcashSdk.MINERS_FEE.value.toDouble() / Zatoshi.ZATOSHI_PER_ZEC) // 0.00001
val hasSaplingBalance = saplingBalance.total.value > 0
val isSendEnabled: Boolean get() = status == Synchronizer.Status.SYNCED && hasFunds
// Note: the wallet's transparent balance is effectively empty if it cannot cover the miner's fee
val hasTransparentFunds =
transparentBalance.value >
(ZcashSdk.MINERS_FEE.value.toDouble() / Zatoshi.ZATOSHI_PER_ZEC) // 0.00001
val isSendEnabled: Boolean get() = status == Synchronizer.Status.SYNCED && hasSaplingFunds
}
fun WalletSnapshot.totalBalance() = orchardBalance.total + saplingBalance.total + transparentBalance

View File

@ -5,46 +5,127 @@ package co.electriccoin.zcash.ui.screen.balances
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.balances.model.ShieldState
import co.electriccoin.zcash.ui.screen.balances.view.Balances
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jetbrains.annotations.VisibleForTesting
@Composable
internal fun WrapBalances(
activity: ComponentActivity,
goSettings: () -> Unit,
) {
// Show information about the app update, if available
val walletViewModel by activity.viewModels<WalletViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value
val checkUpdateViewModel by activity.viewModels<CheckUpdateViewModel> {
CheckUpdateViewModel.CheckUpdateViewModelFactory(
activity.application,
AppUpdateCheckerImp.new()
)
}
val settingsViewModel by activity.viewModels<SettingsViewModel>()
WrapBalances(
goSettings = goSettings,
checkUpdateViewModel = checkUpdateViewModel,
spendingKey = spendingKey,
settingsViewModel = settingsViewModel,
synchronizer = synchronizer,
walletSnapshot = walletSnapshot
)
}
@Composable
@VisibleForTesting
@Suppress("LongParameterList")
internal fun WrapBalances(
goSettings: () -> Unit,
checkUpdateViewModel: CheckUpdateViewModel,
settingsViewModel: SettingsViewModel,
spendingKey: UnifiedSpendingKey?,
synchronizer: Synchronizer?,
walletSnapshot: WalletSnapshot?,
) {
val scope = rememberCoroutineScope()
// To show information about the app update, if available
val isUpdateAvailable =
checkUpdateViewModel.updateInfo.collectAsStateWithLifecycle().value.let {
it?.appUpdateInfo != null && it.state == UpdateState.Prepared
}
val settingsViewModel by activity.viewModels<SettingsViewModel>()
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
val isFiatConversionEnabled = ConfigurationEntries.IS_FIAT_CONVERSION_ENABLED.getValue(RemoteConfig.current)
val walletViewModel by activity.viewModels<WalletViewModel>()
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
val (shieldState, setShieldState) =
rememberSaveable(stateSaver = ShieldState.Saver) {
mutableStateOf(
if (walletSnapshot?.hasTransparentFunds == true) {
ShieldState.Available
} else {
ShieldState.None
}
)
}
Balances(
isFiatConversionEnabled = isFiatConversionEnabled,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
isUpdateAvailable = isUpdateAvailable,
onSettings = goSettings,
walletSnapshot = walletSnapshot,
)
if (null == synchronizer || null == walletSnapshot || null == spendingKey) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
// TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available
// TODO [#1146]: https://github.com/Electric-Coin-Company/zashi-android/issues/1146
CircularScreenProgressIndicator()
} else {
Balances(
onSettings = goSettings,
isFiatConversionEnabled = isFiatConversionEnabled,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
isUpdateAvailable = isUpdateAvailable,
onShielding = {
scope.launch {
setShieldState(ShieldState.Running)
Twig.debug { "Shielding transparent funds" }
// Using empty string for memo to clear the default memo prefix value defined in the SDK
runCatching { synchronizer.shieldFunds(spendingKey, "") }
.onSuccess {
Twig.info { "Shielding transaction id:$it submitted successfully" }
setShieldState(ShieldState.None)
}
.onFailure {
Twig.info { "Shielding transaction submission failed with: $it" }
// Adding extra delay before notifying UI for a better UX
@Suppress("MagicNumber")
delay(1500)
setShieldState(ShieldState.Failed(it.localizedMessage ?: ""))
}
}
},
shieldState = shieldState,
walletSnapshot = walletSnapshot,
)
}
}

View File

@ -0,0 +1,60 @@
package co.electriccoin.zcash.ui.screen.balances.model
import androidx.compose.runtime.saveable.mapSaver
sealed class ShieldState {
data object None : ShieldState()
data object Available : ShieldState()
data object Running : ShieldState()
data class Failed(val error: String) : ShieldState()
companion object {
private const val TYPE_NONE = "none" // $NON-NLS
private const val TYPE_AVAILABLE = "available" // $NON-NLS
private const val TYPE_RUNNING = "running" // $NON-NLS
private const val TYPE_FAILED = "failed" // $NON-NLS
private const val KEY_TYPE = "type" // $NON-NLS
private const val KEY_ERROR = "error" // $NON-NLS
internal val Saver
get() =
run {
mapSaver(
save = { it.toSaverMap() },
restore = {
if (it.isEmpty()) {
null
} else {
val sendStageString = (it[KEY_TYPE] as String)
when (sendStageString) {
TYPE_NONE -> None
TYPE_AVAILABLE -> Available
TYPE_RUNNING -> Running
TYPE_FAILED -> Failed((it[KEY_ERROR] as String))
else -> null
}
}
}
)
}
private fun ShieldState.toSaverMap(): HashMap<String, String> {
val saverMap = HashMap<String, String>()
when (this) {
None -> saverMap[KEY_TYPE] = TYPE_NONE
Available -> saverMap[KEY_TYPE] = TYPE_AVAILABLE
Running -> saverMap[KEY_TYPE] = TYPE_RUNNING
is Failed -> {
saverMap[KEY_TYPE] = TYPE_FAILED
saverMap[KEY_ERROR] = this.error
}
}
return saverMap
}
}
}

View File

@ -1,25 +1,40 @@
@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.ui.screen.balances.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
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.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
@ -32,6 +47,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.toZecString
import cash.z.ecc.sdk.extension.toPercentageWithDecimal
import co.electriccoin.zcash.ui.R
@ -49,11 +65,14 @@ import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.design.component.CircularSmallProgressIndicator
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.LinearProgressIndicator
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.Reference
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.component.StyledBalance
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.balances.BalancesTag
import co.electriccoin.zcash.ui.screen.balances.model.ShieldState
import co.electriccoin.zcash.ui.screen.balances.model.WalletDisplayValues
@Preview("Balances")
@ -66,18 +85,23 @@ private fun ComposablePreview() {
isFiatConversionEnabled = false,
isKeepScreenOnWhileSyncing = false,
isUpdateAvailable = false,
onShielding = {},
walletSnapshot = WalletSnapshotFixture.new(),
shieldState = ShieldState.Available,
)
}
}
}
@Suppress("LongParameterList")
@Composable
fun Balances(
onSettings: () -> Unit,
isFiatConversionEnabled: Boolean,
isKeepScreenOnWhileSyncing: Boolean?,
isUpdateAvailable: Boolean,
onShielding: () -> Unit,
shieldState: ShieldState,
walletSnapshot: WalletSnapshot?,
) {
Scaffold(topBar = {
@ -90,14 +114,16 @@ fun Balances(
isFiatConversionEnabled = isFiatConversionEnabled,
isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing,
isUpdateAvailable = isUpdateAvailable,
onShielding = onShielding,
walletSnapshot = walletSnapshot,
shieldState = shieldState,
modifier =
Modifier.padding(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingHuge,
start = ZcashTheme.dimens.screenHorizontalSpacingRegular,
end = ZcashTheme.dimens.screenHorizontalSpacingRegular
)
),
)
}
}
@ -122,12 +148,15 @@ private fun BalancesTopAppBar(onSettings: () -> Unit) {
)
}
@Suppress("LongParameterList")
@Composable
private fun BalancesMainContent(
isFiatConversionEnabled: Boolean,
isKeepScreenOnWhileSyncing: Boolean?,
isUpdateAvailable: Boolean,
onShielding: () -> Unit,
walletSnapshot: WalletSnapshot,
shieldState: ShieldState,
modifier: Modifier = Modifier,
) {
Column(
@ -162,20 +191,11 @@ private fun BalancesMainContent(
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
Column(
modifier =
Modifier
.fillMaxWidth()
.height(166.dp)
.background(color = ZcashTheme.colors.panelBackgroundColor),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Body(
text = stringResource(id = R.string.balances_coming_soon),
textAlign = TextAlign.Center
)
}
TransparentBalancePanel(
onShielding = onShielding,
shieldState = shieldState,
walletSnapshot = walletSnapshot,
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
@ -192,6 +212,162 @@ private fun BalancesMainContent(
}
}
@Composable
fun TransparentBalancePanel(
onShielding: () -> Unit,
shieldState: ShieldState,
walletSnapshot: WalletSnapshot,
) {
var showHelpPanel by rememberSaveable { mutableStateOf(false) }
// TODO [#1242]: Create error popup UI
// TODO [#1242]: https://github.com/Electric-Coin-Company/zashi-android/issues/1242
// if (shieldState is ShieldState.Failed) {
// }
Box(
modifier =
Modifier
.background(color = ZcashTheme.colors.panelBackgroundColor)
.wrapContentSize()
.animateContentSize()
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
TransparentBalanceRow(
isProgressbarVisible = shieldState == ShieldState.Running,
onHelpClick = { showHelpPanel = !showHelpPanel },
walletSnapshot = walletSnapshot
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
PrimaryButton(
onClick = onShielding,
text = stringResource(R.string.balances_transparent_balance_shield),
textStyle = ZcashTheme.extendedTypography.buttonTextSmall,
enabled = shieldState == ShieldState.Available,
outerPaddingValues =
PaddingValues(
horizontal = 54.dp,
vertical = ZcashTheme.dimens.spacingSmall
)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
BodySmall(
text =
stringResource(
id = R.string.balances_transparent_balance_fee,
// TODO [#1047]: Representing Zatoshi amount
// TODO [#1047]: https://github.com/Electric-Coin-Company/zashi-android/issues/1047
@Suppress("MagicNumber")
Zatoshi(100_000L).toZecString()
),
textFontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingXlarge))
}
if (showHelpPanel) {
TransparentBalanceHelpPanel(
onHideHelpPanel = { showHelpPanel = !showHelpPanel }
)
}
}
}
@Composable
fun TransparentBalanceRow(
isProgressbarVisible: Boolean,
onHelpClick: () -> Unit,
walletSnapshot: WalletSnapshot,
) {
Row(
modifier =
Modifier
.fillMaxWidth()
.padding(
start = ZcashTheme.dimens.spacingDefault,
end = ZcashTheme.dimens.spacingSmall
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
// To keep both elements together in relatively sized row
Row(modifier = Modifier.fillMaxWidth(TEXT_PART_WIDTH_RATIO)) {
// Apply common click listener
Row(
modifier =
Modifier
.clip(RoundedCornerShape(ZcashTheme.dimens.smallRippleEffectCorner))
.clickable { onHelpClick() }
) {
BodySmall(text = stringResource(id = R.string.balances_transparent_balance).uppercase())
Image(
imageVector = ImageVector.vectorResource(R.drawable.ic_help_question_mark),
contentDescription = stringResource(id = R.string.balances_transparent_help_content_description),
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingXtiny)
)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
StyledBalance(
balanceString = walletSnapshot.transparentBalance.toZecString(),
textStyles =
Pair(
ZcashTheme.extendedTypography.balanceSingleStyles.first,
ZcashTheme.extendedTypography.balanceSingleStyles.second
),
textColor = ZcashTheme.colors.textPending
)
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingTiny))
Box(Modifier.width(ZcashTheme.dimens.circularSmallProgressWidth)) {
if (isProgressbarVisible) {
CircularSmallProgressIndicator()
}
}
}
}
}
@Composable
fun TransparentBalanceHelpPanel(onHideHelpPanel: () -> Unit) {
Column(
modifier =
Modifier
.padding(all = ZcashTheme.dimens.spacingDefault)
.background(color = Color.White)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
BodySmall(
text = stringResource(id = R.string.balances_transparent_balance_help),
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingDefault)
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
Reference(
text = stringResource(id = R.string.balances_transparent_balance_help_close).uppercase(),
onClick = onHideHelpPanel,
textStyle = ZcashTheme.extendedTypography.referenceSmall
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
}
}
@Composable
fun BalancesOverview(
walletSnapshot: WalletSnapshot,

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="11dp"
android:height="12dp"
android:viewportWidth="11"
android:viewportHeight="12">
<path
android:strokeWidth="1"
android:pathData="M5.5,5.5m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
android:fillColor="#000000"
android:strokeColor="#000000"/>
<path
android:pathData="M4.795,6.682V6.545C4.795,6.233 4.82,5.984 4.869,5.798C4.919,5.613 4.991,5.464 5.088,5.352C5.185,5.239 5.303,5.136 5.443,5.045C5.564,4.966 5.672,4.889 5.767,4.815C5.864,4.741 5.939,4.663 5.994,4.58C6.051,4.496 6.08,4.402 6.08,4.295C6.08,4.201 6.057,4.117 6.011,4.045C5.966,3.973 5.904,3.918 5.827,3.878C5.749,3.838 5.663,3.818 5.568,3.818C5.466,3.818 5.371,3.842 5.284,3.889C5.199,3.937 5.13,4.002 5.077,4.085C5.026,4.169 5,4.265 5,4.375H3.545C3.549,3.958 3.644,3.62 3.83,3.361C4.015,3.099 4.261,2.908 4.568,2.787C4.875,2.664 5.212,2.602 5.58,2.602C5.985,2.602 6.347,2.662 6.665,2.781C6.983,2.899 7.234,3.077 7.418,3.315C7.601,3.552 7.693,3.848 7.693,4.205C7.693,4.434 7.653,4.635 7.574,4.81C7.496,4.982 7.387,5.134 7.247,5.267C7.109,5.398 6.947,5.517 6.761,5.625C6.625,5.705 6.51,5.787 6.418,5.872C6.325,5.955 6.255,6.051 6.207,6.159C6.16,6.265 6.136,6.394 6.136,6.545V6.682H4.795ZM5.489,8.591C5.269,8.591 5.08,8.514 4.923,8.361C4.768,8.205 4.691,8.017 4.693,7.795C4.691,7.58 4.768,7.395 4.923,7.241C5.08,7.088 5.269,7.011 5.489,7.011C5.697,7.011 5.881,7.088 6.04,7.241C6.201,7.395 6.282,7.58 6.284,7.795C6.282,7.943 6.243,8.078 6.168,8.199C6.094,8.318 5.997,8.414 5.878,8.486C5.759,8.556 5.629,8.591 5.489,8.591Z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -3,7 +3,14 @@
<string name="balances_shielded_spendable">Shielded zec (spendable)</string>
<string name="balances_change_pending">Change pending</string>
<string name="balances_pending_transactions">Pending transactions</string>
<string name="balances_coming_soon">Transparent funds shielding\n(coming soon)</string>
<string name="balances_transparent_balance">Transparent balance</string>
<string name="balances_transparent_balance_help">Zashi uses the latest network upgrade and does not support
sending transparent (unshielded) ZEC. Converting your funds will move them to your available balance so you
can send or spend them.</string>
<string name="balances_transparent_balance_help_close">I got it!</string>
<string name="balances_transparent_help_content_description">Show help</string>
<string name="balances_transparent_balance_shield">Shield and consolidate funds</string>
<string name="balances_transparent_balance_fee">(Fee &lt; <xliff:g id="fee_amount" example="0.001">%1$s</xliff:g>)</string>
<string name="balances_status_syncing" formatted="true">Syncing…</string>
<string name="balances_status_syncing_amount_suffix" formatted="true"><xliff:g id="amount_prefix" example="123$">%1$s</xliff:g> so far</string>

View File

@ -398,7 +398,7 @@ private fun balancesScreenshots(
// TODO [#1127]: Implement Balances screen
// TODO [#1127]: https://github.com/Electric-Coin-Company/zashi-android/issues/1127
composeTestRule.onNodeWithText(resContext.getString(R.string.balances_coming_soon)).also {
composeTestRule.onNodeWithText(resContext.getString(R.string.balances_title)).also {
it.assertExists()
ScreenshotTest.takeScreenshot(tag, "Balances 1")
}