[#1166] Add shielding funds feature
* [#1166] Add shielding funds feature - Changelog updated - Closes #1127 - Closes #1166 - Related #238
This commit is contained in:
parent
01e3741f22
commit
26a73f8e59
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -246,7 +246,7 @@ fun SmallTopAppBar(
|
|||
modifier =
|
||||
Modifier
|
||||
.wrapContentSize()
|
||||
.clip(RoundedCornerShape(ZcashTheme.dimens.topAppBarActionRippleCorner))
|
||||
.clip(RoundedCornerShape(ZcashTheme.dimens.regularRippleEffectCorner))
|
||||
.clickable { onBack?.run { onBack() } }
|
||||
) {
|
||||
Row(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
|
@ -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 < <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>
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue