Exchange Rates implementation

Closes #532
Closes #578
This commit is contained in:
Milan Cerovsky 2024-07-26 09:45:00 +02:00
parent 28ce6b5a08
commit 095234a6cd
25 changed files with 697 additions and 177 deletions

View File

@ -6,6 +6,10 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
## [Unreleased]
### Added
- Balance now also displays USD value
- An option to enter USD amount in Send Transaction screen
## [1.1.4 (700)] - 2024-07-23
### Added

View File

@ -9,6 +9,10 @@ directly impact users rather than highlighting other key architectural updates.*
## [Unreleased]
### Added
- Balance now also displays USD value
- An option to enter USD amount in Send Transaction screen
## [1.1.4 (700)] - 2024-07-23
### Added

View File

@ -131,7 +131,7 @@ ZCASH_EMULATOR_WTF_API_KEY=
# Optional absolute path to a Zcash SDK checkout.
# When blank, it pulls the SDK from Maven.
# When set, it uses the path for a Gradle included build. Path can either be absolute or relative to the root of this app's Gradle project.
SDK_INCLUDED_BUILD_PATH=
SDK_INCLUDED_BUILD_PATH=/Users/milancerovsky/Developer/zcash/zcash-android-wallet-sdk
# When blank, it pulls the BIP-39 library from Maven.
# When set, it uses the path for a Gradle included build. Path can either be absolute or relative to the root of this app's Gradle project.

View File

@ -4,8 +4,6 @@ import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
@ -17,7 +15,6 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.model.MonetarySeparators
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.design.R
@ -26,7 +23,7 @@ import java.util.Locale
@Preview
@Composable
private fun StyledBalanceComposablePreview() {
private fun StyledBalancePreview() =
ZcashTheme(forceDarkMode = false) {
BlankSurface {
Column {
@ -35,9 +32,16 @@ private fun StyledBalanceComposablePreview() {
isHideBalances = false,
modifier = Modifier
)
}
}
}
Spacer(modifier = Modifier.height(24.dp))
@Preview
@Composable
private fun HiddenStyledBalancePreview() =
ZcashTheme(forceDarkMode = false) {
BlankSurface {
Column {
StyledBalance(
balanceParts = ZecAmountTriple(main = "1,234.56789012"),
isHideBalances = true,
@ -46,7 +50,6 @@ private fun StyledBalanceComposablePreview() {
}
}
}
}
/**
* This accepts string with balance and displays it in the UI component styled according to the design

View File

@ -123,7 +123,15 @@ class SendViewTestSetup(
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
},
setAmountState = {},
amountState = AmountState.new(context, monetarySeparators, "", false),
amountState =
AmountState.newFromZec(
context = context,
monetarySeparators = monetarySeparators,
value = "",
fiatValue = "",
isTransparentRecipient = false,
fiatCurrencyConversion = null
),
setMemoState = {},
memoState = MemoState.new(""),
topAppBarSubTitleState = TopAppBarSubTitleState.None,

View File

@ -0,0 +1,203 @@
package co.electriccoin.zcash.ui
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.tooling.preview.Preview
import cash.z.ecc.android.sdk.model.FiatCurrency
import cash.z.ecc.android.sdk.model.FiatCurrencyConversion
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState
import cash.z.ecc.android.sdk.model.FiatCurrencyResult
import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.toFiatCurrencyState
import co.electriccoin.zcash.ui.common.extension.toKotlinLocale
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.component.BlankSurface
import co.electriccoin.zcash.ui.design.component.CircularSmallProgressIndicator
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.util.StringResource
import co.electriccoin.zcash.ui.util.getValue
import co.electriccoin.zcash.ui.util.stringRes
import kotlinx.datetime.Clock
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun StyledExchangeBalance(
zatoshi: Zatoshi,
exchangeRate: FiatCurrencyResult,
modifier: Modifier = Modifier,
isHideBalances: Boolean = false,
hiddenBalancePlaceholder: StringResource = stringRes(R.string.hide_balance_placeholder),
textColor: Color = Color.Unspecified,
style: TextStyle = ZcashTheme.typography.primary.titleSmall
) {
val currencySymbol = exchangeRate.fiatCurrency.symbol
Row(
modifier =
modifier
.basicMarquee()
.animateContentSize(),
verticalAlignment = Alignment.CenterVertically
) {
when {
exchangeRate is FiatCurrencyResult.Error -> {
// empty view
}
exchangeRate is FiatCurrencyResult.Loading && !isHideBalances -> {
StyledExchangeText(
text = currencySymbol,
textColor = textColor,
style = style
)
CircularSmallProgressIndicator()
}
else -> {
StyledExchangeText(
text =
if (isHideBalances) {
"${currencySymbol}${hiddenBalancePlaceholder.getValue()}"
} else {
when (
val state =
zatoshi.toFiatCurrencyState(
fiatCurrencyResult = exchangeRate,
locale = Locale.current.toKotlinLocale(),
monetarySeparators = MonetarySeparators.current(java.util.Locale.getDefault())
)
) {
is FiatCurrencyConversionRateState.Current -> state.formattedFiatValue
is FiatCurrencyConversionRateState.Stale -> state.formattedFiatValue
FiatCurrencyConversionRateState.Unavailable ->
stringResource(co.electriccoin.zcash.ui.R.string.fiat_currency_conversion_rate_unavailable)
}
},
textColor = textColor,
style = style
)
}
}
}
}
@Composable
private fun StyledExchangeText(
text: String,
modifier: Modifier = Modifier,
textColor: Color,
style: TextStyle
) {
Text(
text = text,
color = textColor,
style = style,
maxLines = 1,
modifier = modifier
)
}
@Composable
private fun StyledExchangeBalancePreview() =
BlankSurface {
Column {
StyledExchangeBalance(
isHideBalances = false,
modifier = Modifier,
zatoshi = Zatoshi(1),
exchangeRate =
FiatCurrencyResult.Success(
FiatCurrencyConversion(
fiatCurrency = FiatCurrency.USD,
timestamp = Clock.System.now(),
priceOfZec = 25.0
)
)
)
}
}
@Composable
private fun HiddenStyledExchangeBalancePreview() =
BlankSurface {
Column {
StyledExchangeBalance(
isHideBalances = true,
modifier = Modifier,
zatoshi = Zatoshi(1),
exchangeRate =
FiatCurrencyResult.Success(
FiatCurrencyConversion(
fiatCurrency = FiatCurrency.USD,
timestamp = Clock.System.now(),
priceOfZec = 25.0
)
)
)
}
}
@Composable
private fun LoadingStyledExchangeBalancePreview() =
BlankSurface {
Column {
StyledExchangeBalance(
isHideBalances = true,
modifier = Modifier,
zatoshi = Zatoshi(1),
exchangeRate = FiatCurrencyResult.Loading()
)
}
}
@Preview
@Composable
private fun StyledExchangeBalancePreviewLight() =
ZcashTheme(forceDarkMode = false) {
StyledExchangeBalancePreview()
}
@Preview
@Composable
private fun HiddenStyledExchangeBalancePreviewLight() =
ZcashTheme(forceDarkMode = false) {
HiddenStyledExchangeBalancePreview()
}
@Preview
@Composable
private fun LoadingStyledExchangeBalancePreviewLight() =
ZcashTheme(forceDarkMode = false) {
LoadingStyledExchangeBalancePreview()
}
@Preview
@Composable
private fun StyledExchangeBalancePreviewDark() =
ZcashTheme(forceDarkMode = true) {
StyledExchangeBalancePreview()
}
@Preview
@Composable
private fun HiddenStyledExchangeBalancePreviewDark() =
ZcashTheme(forceDarkMode = true) {
HiddenStyledExchangeBalancePreview()
}
@Preview
@Composable
private fun LoadingStyledExchangeBalancePreviewDark() =
ZcashTheme(forceDarkMode = true) {
LoadingStyledExchangeBalancePreview()
}

View File

@ -17,9 +17,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import cash.z.ecc.android.sdk.model.FiatCurrencyResult
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.sdk.extension.toZecStringFull
import cash.z.ecc.sdk.type.ZcashCurrency
import co.electriccoin.zcash.ui.StyledExchangeBalance
import co.electriccoin.zcash.ui.common.extension.asZecAmountTriple
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.component.BlankSurface
@ -30,6 +32,7 @@ import co.electriccoin.zcash.ui.design.component.StyledBalance
import co.electriccoin.zcash.ui.design.component.StyledBalanceDefaults
import co.electriccoin.zcash.ui.design.component.ZecAmountTriple
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.FiatCurrencyResultFixture
@Preview(device = Devices.PIXEL_2)
@Composable
@ -44,12 +47,13 @@ private fun BalanceWidgetPreview() {
balanceState =
BalanceState.Available(
totalBalance = Zatoshi(1234567891234567L),
spendableBalance = Zatoshi(1234567891234567L)
spendableBalance = Zatoshi(1234567891234567L),
exchangeRate = FiatCurrencyResultFixture.new()
),
isHideBalances = false,
isReferenceToBalances = true,
onReferenceClick = {},
modifier = Modifier
modifier = Modifier,
)
)
}
@ -65,11 +69,15 @@ private fun BalanceWidgetNotAvailableYetPreview() {
) {
@Suppress("MagicNumber")
BalanceWidget(
balanceState = BalanceState.Loading(Zatoshi(0L)),
balanceState =
BalanceState.Loading(
totalBalance = Zatoshi(value = 0L),
exchangeRate = FiatCurrencyResultFixture.new()
),
isHideBalances = false,
isReferenceToBalances = true,
onReferenceClick = {},
modifier = Modifier
modifier = Modifier,
)
}
}
@ -84,22 +92,40 @@ private fun BalanceWidgetHiddenAmountPreview() {
) {
@Suppress("MagicNumber")
BalanceWidget(
balanceState = BalanceState.Loading(Zatoshi(0L)),
balanceState =
BalanceState.Loading(
totalBalance = Zatoshi(0L),
exchangeRate = FiatCurrencyResultFixture.new()
),
isHideBalances = true,
isReferenceToBalances = true,
onReferenceClick = {},
modifier = Modifier
modifier = Modifier,
)
}
}
}
sealed class BalanceState(open val totalBalance: Zatoshi) {
data object None : BalanceState(Zatoshi(0L))
sealed interface BalanceState {
val totalBalance: Zatoshi
val exchangeRate: FiatCurrencyResult
data class Loading(override val totalBalance: Zatoshi) : BalanceState(totalBalance)
data class None(
override val exchangeRate: FiatCurrencyResult
) : BalanceState {
override val totalBalance: Zatoshi = Zatoshi(0L)
}
data class Available(override val totalBalance: Zatoshi, val spendableBalance: Zatoshi) : BalanceState(totalBalance)
data class Loading(
override val totalBalance: Zatoshi,
override val exchangeRate: FiatCurrencyResult
) : BalanceState
data class Available(
override val totalBalance: Zatoshi,
override val exchangeRate: FiatCurrencyResult,
val spendableBalance: Zatoshi
) : BalanceState
}
@Composable
@ -123,6 +149,12 @@ fun BalanceWidget(
parts = balanceState.totalBalance.toZecStringFull().asZecAmountTriple()
)
StyledExchangeBalance(
zatoshi = balanceState.totalBalance,
exchangeRate = balanceState.exchangeRate,
isHideBalances = isHideBalances
)
Row(verticalAlignment = Alignment.CenterVertically) {
if (isReferenceToBalances) {
Reference(
@ -151,7 +183,7 @@ fun BalanceWidget(
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingTiny))
when (balanceState) {
BalanceState.None, is BalanceState.Loading -> {
is BalanceState.None, is BalanceState.Loading -> {
CircularSmallProgressIndicator(color = ZcashTheme.colors.circularProgressBarSmallDark)
}

View File

@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.common.model
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
import cash.z.ecc.android.sdk.model.FiatCurrencyResult
import cash.z.ecc.android.sdk.model.PercentDecimal
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
@ -15,6 +16,7 @@ data class WalletSnapshot(
val orchardBalance: WalletBalance,
val saplingBalance: WalletBalance,
val transparentBalance: Zatoshi,
val exchangeRateUsd: FiatCurrencyResult,
val progress: PercentDecimal,
val synchronizerError: SynchronizerError?
) {

View File

@ -14,7 +14,7 @@ import cash.z.ecc.android.sdk.WalletInitMode
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
import cash.z.ecc.android.sdk.model.Account
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.FiatCurrency
import cash.z.ecc.android.sdk.model.FiatCurrencyResult
import cash.z.ecc.android.sdk.model.PercentDecimal
import cash.z.ecc.android.sdk.model.PersistableWallet
import cash.z.ecc.android.sdk.model.TransactionOverview
@ -95,19 +95,6 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
null
)
/**
* A flow of the user's preferred fiat currency.
*/
val preferredFiatCurrency: StateFlow<FiatCurrency?> =
flow<FiatCurrency?> {
val preferenceProvider = StandardPreferenceSingleton.getInstance(application)
emitAll(StandardPreferenceKeys.PREFERRED_FIAT_CURRENCY.observe(preferenceProvider))
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
null
)
/**
* A flow of the wallet block synchronization state.
*/
@ -310,20 +297,22 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
(snapshot.hasChangePending() || snapshot.hasValuePending())
) -> {
BalanceState.Loading(
totalBalance = snapshot.totalBalance()
totalBalance = snapshot.totalBalance(),
exchangeRate = snapshot.exchangeRateUsd
)
}
else -> {
BalanceState.Available(
totalBalance = snapshot.totalBalance(),
spendableBalance = snapshot.spendableBalance()
spendableBalance = snapshot.spendableBalance(),
exchangeRate = snapshot.exchangeRateUsd
)
}
}
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
BalanceState.None
BalanceState.None(FiatCurrencyResult.Loading())
)
/**
@ -597,24 +586,29 @@ private fun Synchronizer.toWalletSnapshot() =
// 4
transparentBalance,
// 5
progress,
exchangeRateUsd,
// 6
progress,
// 7
toCommonError()
) { flows ->
val orchardBalance = flows[2] as WalletBalance?
val saplingBalance = flows[3] as WalletBalance?
val transparentBalance = flows[4] as Zatoshi?
val progressPercentDecimal = flows[5] as PercentDecimal
@Suppress("UNCHECKED_CAST")
val exchangeRateUsd = flows[5] as FiatCurrencyResult
val progressPercentDecimal = (flows[6] as PercentDecimal)
WalletSnapshot(
flows[0] as Synchronizer.Status,
flows[1] as CompactBlockProcessor.ProcessorInfo,
orchardBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0), Zatoshi(0)),
saplingBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0), Zatoshi(0)),
transparentBalance ?: Zatoshi(0),
progressPercentDecimal,
flows[6] as SynchronizerError?
status = flows[0] as Synchronizer.Status,
processorInfo = flows[1] as CompactBlockProcessor.ProcessorInfo,
orchardBalance = orchardBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0), Zatoshi(0)),
saplingBalance = saplingBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0), Zatoshi(0)),
transparentBalance = transparentBalance ?: Zatoshi(0),
exchangeRateUsd = exchangeRateUsd,
progress = progressPercentDecimal,
synchronizerError = flows[7] as SynchronizerError?
)
}

View File

@ -1,5 +1,6 @@
package co.electriccoin.zcash.ui.fixture
import cash.z.ecc.android.sdk.model.FiatCurrencyResult
import cash.z.ecc.android.sdk.model.Zatoshi
import co.electriccoin.zcash.ui.common.compose.BalanceState
@ -11,9 +12,11 @@ object BalanceStateFixture {
fun new(
totalBalance: Zatoshi = TOTAL_BALANCE,
spendableBalance: Zatoshi = SPENDABLE_BALANCE
spendableBalance: Zatoshi = SPENDABLE_BALANCE,
exchangeRate: FiatCurrencyResult = FiatCurrencyResultFixture.new()
) = BalanceState.Available(
totalBalance = totalBalance,
spendableBalance = spendableBalance
spendableBalance = spendableBalance,
exchangeRate = exchangeRate
)
}

View File

@ -0,0 +1,17 @@
package co.electriccoin.zcash.ui.fixture
import cash.z.ecc.android.sdk.model.FiatCurrency
import cash.z.ecc.android.sdk.model.FiatCurrencyConversion
import cash.z.ecc.android.sdk.model.FiatCurrencyResult
import kotlinx.datetime.Clock
object FiatCurrencyResultFixture {
fun new() =
FiatCurrencyResult.Success(
FiatCurrencyConversion(
fiatCurrency = FiatCurrency.USD,
timestamp = Clock.System.now(),
priceOfZec = 25.0
)
)
}

View File

@ -3,6 +3,7 @@ package co.electriccoin.zcash.ui.fixture
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
import cash.z.ecc.android.sdk.fixture.WalletBalanceFixture
import cash.z.ecc.android.sdk.model.FiatCurrencyResult
import cash.z.ecc.android.sdk.model.PercentDecimal
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
@ -34,12 +35,13 @@ object WalletSnapshotFixture {
progress: PercentDecimal = PROGRESS,
synchronizerError: SynchronizerError? = null
) = WalletSnapshot(
status,
processorInfo,
orchardBalance,
saplingBalance,
transparentBalance,
progress,
synchronizerError
status = status,
processorInfo = processorInfo,
orchardBalance = orchardBalance,
saplingBalance = saplingBalance,
transparentBalance = transparentBalance,
exchangeRateUsd = FiatCurrencyResult.Loading(),
progress = progress,
synchronizerError = synchronizerError
)
}

View File

@ -1,18 +0,0 @@
package co.electriccoin.zcash.ui.preference
import cash.z.ecc.android.sdk.model.FiatCurrency
import co.electriccoin.zcash.preference.api.PreferenceProvider
import co.electriccoin.zcash.preference.model.entry.PreferenceDefault
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
data class FiatCurrencyPreferenceDefault(
override val key: PreferenceKey
) : PreferenceDefault<FiatCurrency> {
override suspend fun getValue(preferenceProvider: PreferenceProvider) =
preferenceProvider.getString(key)?.let { FiatCurrency(it) } ?: FiatCurrency("USD")
override suspend fun putValue(
preferenceProvider: PreferenceProvider,
newValue: FiatCurrency
) = preferenceProvider.putString(key, newValue.code)
}

View File

@ -35,11 +35,6 @@ object StandardPreferenceKeys {
val IS_RESTORING_INITIAL_WARNING_SEEN =
BooleanPreferenceDefault(PreferenceKey("IS_RESTORING_INITIAL_WARNING_SEEN"), false)
/**
* The fiat currency that the user prefers.
*/
val PREFERRED_FIAT_CURRENCY = FiatCurrencyPreferenceDefault(PreferenceKey("preferred_fiat_currency_code"))
/**
* Screens or flows protected by required authentication
*/

View File

@ -18,6 +18,9 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import cash.z.ecc.android.sdk.model.FiatCurrency
import cash.z.ecc.android.sdk.model.FiatCurrencyConversion
import cash.z.ecc.android.sdk.model.FiatCurrencyResult
import cash.z.ecc.android.sdk.model.Zatoshi
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.compose.BalanceState
@ -37,6 +40,7 @@ import co.electriccoin.zcash.ui.screen.account.AccountTag
import co.electriccoin.zcash.ui.screen.account.fixture.TransactionsFixture
import co.electriccoin.zcash.ui.screen.account.model.TransactionUiState
import co.electriccoin.zcash.ui.screen.balances.model.StatusAction
import kotlinx.datetime.Clock
@Preview("Account No History")
@Composable
@ -69,8 +73,16 @@ private fun HistoryListComposablePreview() {
Account(
balanceState =
BalanceState.Available(
Zatoshi(123_000_000L),
Zatoshi(123_000_000L)
totalBalance = Zatoshi(value = 123_000_000L),
spendableBalance = Zatoshi(value = 123_000_000L),
exchangeRate =
FiatCurrencyResult.Success(
FiatCurrencyConversion(
fiatCurrency = FiatCurrency.USD,
timestamp = Clock.System.now(),
priceOfZec = 25.0
)
)
),
isHideBalances = false,
goBalances = {},
@ -216,7 +228,7 @@ private fun AccountMainContent(
isHideBalances = isHideBalances,
modifier =
Modifier
.padding(horizontal = ZcashTheme.dimens.screenHorizontalSpacingRegular)
.padding(horizontal = ZcashTheme.dimens.screenHorizontalSpacingRegular),
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
@ -252,7 +264,7 @@ private fun BalancesStatus(
balanceState = balanceState,
isHideBalances = isHideBalances,
isReferenceToBalances = true,
onReferenceClick = goBalances
onReferenceClick = goBalances,
)
}
}

View File

@ -34,12 +34,9 @@ data class WalletDisplayValues(
var statusText = ""
var statusAction: StatusAction = StatusAction.None
// TODO [#578]: Provide Zatoshi -> USD fiat currency formatting
// TODO [#578]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/578
// We'll ideally provide a "fresh" currencyConversion object here
val fiatCurrencyAmountState =
walletSnapshot.spendableBalance().toFiatCurrencyState(
null,
walletSnapshot.exchangeRateUsd,
Locale.current.toKotlinLocale(),
MonetarySeparators.current(java.util.Locale.getDefault())
)

View File

@ -13,6 +13,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.FiatCurrencyResult
import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
import cash.z.ecc.android.sdk.model.ZecSend
@ -138,24 +139,45 @@ internal fun WrapSend(
rememberSaveable(stateSaver = AmountState.Saver) {
// Default amount state
mutableStateOf(
AmountState.new(
AmountState.newFromZec(
context = context,
value = zecSend?.amount?.toZecString() ?: "",
monetarySeparators = monetarySeparators,
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false,
fiatValue = "",
fiatCurrencyConversion =
(walletSnapshot?.exchangeRateUsd as? FiatCurrencyResult.Success)
?.currencyConversion
)
)
}
// New amount state based on the recipient address type (e.g. shielded supports zero funds sending and
// transparent not)
LaunchedEffect(key1 = recipientAddressState) {
LaunchedEffect(recipientAddressState, walletSnapshot?.exchangeRateUsd) {
setAmountState(
AmountState.new(
context = context,
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false,
monetarySeparators = monetarySeparators,
value = amountState.value
)
if (amountState.value.isNotBlank() || amountState.fiatValue.isBlank()) {
AmountState.newFromZec(
context = context,
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false,
monetarySeparators = monetarySeparators,
value = amountState.value,
fiatValue = amountState.fiatValue,
fiatCurrencyConversion =
(walletSnapshot?.exchangeRateUsd as? FiatCurrencyResult.Success)
?.currencyConversion
)
} else {
AmountState.newFromFiat(
context = context,
isTransparentRecipient = recipientAddressState.type?.let { it == AddressType.Transparent } ?: false,
monetarySeparators = monetarySeparators,
value = amountState.value,
fiatValue = amountState.fiatValue,
fiatCurrencyConversion =
(walletSnapshot?.exchangeRateUsd as? FiatCurrencyResult.Success)
?.currencyConversion
)
}
)
}
@ -170,14 +192,28 @@ internal fun WrapSend(
setSendStage(SendStage.Form)
setZecSend(null)
setRecipientAddressState(RecipientAddressState.new("", null))
setAmountState(AmountState.new(context, monetarySeparators, "", false))
setAmountState(
AmountState.newFromZec(
context = context,
monetarySeparators = monetarySeparators,
value = "",
fiatValue = "",
isTransparentRecipient = false,
fiatCurrencyConversion =
(walletSnapshot?.exchangeRateUsd as? FiatCurrencyResult.Success)
?.currencyConversion
)
)
setMemoState(MemoState.new(""))
}
val onBackAction = {
when (sendStage) {
SendStage.Form -> goBack()
SendStage.Proposing -> { /* no action - wait until the sending is done */ }
SendStage.Proposing -> {
// no action - wait until the sending is done
}
is SendStage.SendFailure -> setSendStage(SendStage.Form)
}
}

View File

@ -2,51 +2,102 @@ package co.electriccoin.zcash.ui.screen.send.model
import android.content.Context
import androidx.compose.runtime.saveable.mapSaver
import androidx.compose.ui.text.intl.Locale
import cash.z.ecc.android.sdk.model.FiatCurrencyConversion
import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZecStringExt
import cash.z.ecc.android.sdk.model.fromZecString
import cash.z.ecc.android.sdk.model.toFiatString
import cash.z.ecc.android.sdk.model.toZatoshi
import cash.z.ecc.android.sdk.model.toZecString
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.extension.toKotlinLocale
sealed interface AmountState {
val value: String
val fiatValue: String
sealed class AmountState(
open val value: String,
) {
data class Valid(
override val value: String,
override val fiatValue: String,
val zatoshi: Zatoshi
) : AmountState(value)
) : AmountState
data class Invalid(
override val value: String,
) : AmountState(value)
data class Invalid(override val value: String, override val fiatValue: String) : AmountState
companion object {
fun new(
fun newFromZec(
context: Context,
monetarySeparators: MonetarySeparators,
value: String,
isTransparentRecipient: Boolean
fiatValue: String,
isTransparentRecipient: Boolean,
fiatCurrencyConversion: FiatCurrencyConversion?,
): AmountState {
// Validate raw input string
val validated =
runCatching {
ZecStringExt.filterContinuous(context, monetarySeparators, value)
}.onFailure {
Twig.error(it) { "Failed while filtering raw amount characters" }
}.getOrDefault(false)
val isValid = validate(context, monetarySeparators, value)
if (!validated) {
return Invalid(value)
if (!isValid) {
return Invalid(value, if (value.isBlank()) "" else fiatValue)
}
// Convert the input to Zatoshi type-safe amount representation
val zatoshi = (Zatoshi.fromZecString(context, value, monetarySeparators))
val zatoshi = Zatoshi.fromZecString(context, value, monetarySeparators)
// Note that the zero funds sending is supported for sending a memo-only shielded transaction
return when {
(zatoshi == null) -> Invalid(value)
(zatoshi.value == 0L && isTransparentRecipient) -> Invalid(value)
else -> Valid(value, zatoshi)
(zatoshi == null) -> Invalid(value, if (value.isBlank()) "" else fiatValue)
(zatoshi.value == 0L && isTransparentRecipient) -> Invalid(value, fiatValue)
else -> {
Valid(
value = value,
zatoshi = zatoshi,
fiatValue =
if (fiatCurrencyConversion == null) {
fiatValue
} else {
zatoshi.toFiatString(
currencyConversion = fiatCurrencyConversion,
locale = Locale.current.toKotlinLocale(),
monetarySeparators = MonetarySeparators.current(java.util.Locale.getDefault()),
includeSymbols = false
)
}
)
}
}
}
fun newFromFiat(
context: Context,
monetarySeparators: MonetarySeparators,
value: String,
fiatValue: String,
isTransparentRecipient: Boolean,
fiatCurrencyConversion: FiatCurrencyConversion?,
): AmountState {
val isValid = validate(context, monetarySeparators, fiatValue)
if (!isValid) {
return Invalid(value, fiatValue)
}
val zatoshi =
fiatCurrencyConversion?.toZatoshi(
context = context,
value = fiatValue,
monetarySeparators = MonetarySeparators.current(java.util.Locale.getDefault())
)
return when {
(zatoshi == null) -> Invalid(value, fiatValue)
(zatoshi.value == 0L && isTransparentRecipient) -> Invalid(value, fiatValue)
else -> {
Valid(
value = zatoshi.toZecString(),
zatoshi = zatoshi,
fiatValue = fiatValue
)
}
}
}
@ -54,22 +105,45 @@ sealed class AmountState(
private const val TYPE_INVALID = "invalid" // $NON-NLS
private const val KEY_TYPE = "type" // $NON-NLS
private const val KEY_VALUE = "value" // $NON-NLS
private const val KEY_FIAT_VALUE = "fiat_value" // $NON-NLS
private const val KEY_ZATOSHI = "zatoshi" // $NON-NLS
private fun validate(
context: Context,
monetarySeparators: MonetarySeparators,
value: String
) = runCatching {
ZecStringExt.filterContinuous(context, monetarySeparators, value)
}.onFailure {
Twig.error(it) { "Failed while filtering raw amount characters" }
}.getOrDefault(false)
internal val Saver
get() =
run {
mapSaver<AmountState>(
mapSaver(
save = { it.toSaverMap() },
restore = {
if (it.isEmpty()) {
null
} else {
val amountString = (it[KEY_VALUE] as String)
val fiatAmountString = (it[KEY_FIAT_VALUE] as String)
val type = (it[KEY_TYPE] as String)
when (type) {
TYPE_VALID -> Valid(amountString, Zatoshi(it[KEY_ZATOSHI] as Long))
TYPE_INVALID -> Invalid(amountString)
TYPE_VALID ->
Valid(
value = amountString,
fiatValue = fiatAmountString,
zatoshi = Zatoshi(it[KEY_ZATOSHI] as Long)
)
TYPE_INVALID ->
Invalid(
value = amountString,
fiatValue = fiatAmountString
)
else -> null
}
}
@ -84,9 +158,11 @@ sealed class AmountState(
saverMap[KEY_TYPE] = TYPE_VALID
saverMap[KEY_ZATOSHI] = this.zatoshi.value
}
is Invalid -> saverMap[KEY_TYPE] = TYPE_INVALID
}
saverMap[KEY_VALUE] = this.value
saverMap[KEY_FIAT_VALUE] = this.fiatValue
return saverMap
}

View File

@ -4,6 +4,7 @@ package co.electriccoin.zcash.ui.screen.send.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -12,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.relocation.BringIntoViewRequester
@ -32,6 +34,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInRoot
@ -47,6 +50,9 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import cash.z.ecc.android.sdk.model.FiatCurrencyResult
import cash.z.ecc.android.sdk.model.Memo
import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.ZecSend
@ -104,7 +110,12 @@ private fun PreviewSendForm() {
recipientAddressState = RecipientAddressState("invalid_address", AddressType.Invalid()),
onRecipientAddressChange = {},
setAmountState = {},
amountState = AmountState.Valid(ZatoshiFixture.ZATOSHI_LONG.toString(), ZatoshiFixture.new()),
amountState =
AmountState.Valid(
value = ZatoshiFixture.ZATOSHI_LONG.toString(),
fiatValue = "",
zatoshi = ZatoshiFixture.new()
),
setMemoState = {},
memoState = MemoState.new("Test message"),
topAppBarSubTitleState = TopAppBarSubTitleState.None,
@ -135,7 +146,12 @@ private fun SendFormTransparentAddressPreview() {
),
onRecipientAddressChange = {},
setAmountState = {},
amountState = AmountState.Valid(ZatoshiFixture.ZATOSHI_LONG.toString(), ZatoshiFixture.new()),
amountState =
AmountState.Valid(
value = ZatoshiFixture.ZATOSHI_LONG.toString(),
fiatValue = "",
zatoshi = ZatoshiFixture.new()
),
setMemoState = {},
memoState = MemoState.new("Test message"),
topAppBarSubTitleState = TopAppBarSubTitleState.None,
@ -363,7 +379,7 @@ private fun SendForm(
Spacer(Modifier.size(ZcashTheme.dimens.spacingDefault))
SendFormAmountTextField(
amountSate = amountState,
amountState = amountState,
imeAction =
if (recipientAddressState.type == AddressType.Transparent) {
ImeAction.Done
@ -583,7 +599,7 @@ fun SendFormAddressTextField(
@Suppress("LongParameterList", "LongMethod")
@Composable
fun SendFormAmountTextField(
amountSate: AmountState,
amountState: AmountState,
imeAction: ImeAction,
isTransparentRecipient: Boolean,
monetarySeparators: MonetarySeparators,
@ -597,9 +613,9 @@ fun SendFormAmountTextField(
val zcashCurrency = ZcashCurrency.getLocalizedName(context)
val amountError =
when (amountSate) {
when (amountState) {
is AmountState.Invalid -> {
if (amountSate.value.isEmpty()) {
if (amountState.value.isEmpty()) {
null
} else {
stringResource(id = R.string.send_amount_invalid)
@ -607,7 +623,7 @@ fun SendFormAmountTextField(
}
is AmountState.Valid -> {
if (walletSnapshot.spendableBalance() < amountSate.zatoshi) {
if (walletSnapshot.spendableBalance() < amountState.zatoshi) {
stringResource(id = R.string.send_amount_insufficient_balance)
} else {
null
@ -629,47 +645,123 @@ fun SendFormAmountTextField(
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
FormTextField(
value = amountSate.value,
onValueChange = { newValue ->
setAmountState(
AmountState.new(
context = context,
value = newValue,
monetarySeparators = monetarySeparators,
isTransparentRecipient = isTransparentRecipient
Row {
FormTextField(
textStyle = ZcashTheme.extendedTypography.textFieldValue.copy(fontSize = 14.sp),
value = amountState.value,
onValueChange = { newValue ->
setAmountState(
AmountState.newFromZec(
context = context,
value = newValue,
monetarySeparators = monetarySeparators,
isTransparentRecipient = isTransparentRecipient,
fiatValue = amountState.fiatValue,
fiatCurrencyConversion =
(walletSnapshot.exchangeRateUsd as? FiatCurrencyResult.Success)
?.currencyConversion
)
)
)
},
modifier = Modifier.fillMaxWidth(),
error = amountError,
placeholder = {
Text(
text =
stringResource(
id = R.string.send_amount_hint,
zcashCurrency
),
style = ZcashTheme.extendedTypography.textFieldHint,
color = ZcashTheme.colors.textFieldHint
)
},
keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = imeAction
),
keyboardActions =
KeyboardActions(
onDone = {
focusManager.clearFocus(true)
},
onNext = {
focusManager.moveFocus(FocusDirection.Down)
}
),
bringIntoViewRequester = bringIntoViewRequester,
)
},
modifier = Modifier.weight(1f),
error = amountError,
placeholder = {
Text(
text =
stringResource(
id = R.string.send_amount_hint,
zcashCurrency
),
style = ZcashTheme.extendedTypography.textFieldHint,
color = ZcashTheme.colors.textFieldHint
)
},
keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = imeAction
),
keyboardActions =
KeyboardActions(
onDone = {
focusManager.clearFocus(true)
},
onNext = {
focusManager.moveFocus(FocusDirection.Right)
}
),
bringIntoViewRequester = bringIntoViewRequester,
leadingIcon = {
Image(
modifier = Modifier.requiredSize(7.dp, 13.dp),
painter = painterResource(R.drawable.ic_send_zashi),
contentDescription = "",
colorFilter = ColorFilter.tint(color = ZcashTheme.colors.secondaryColor),
)
}
)
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingMin))
Image(
modifier = Modifier.padding(top = 24.dp),
painter = painterResource(id = R.drawable.ic_send_convert),
contentDescription = "",
colorFilter = ColorFilter.tint(color = ZcashTheme.colors.secondaryColor),
)
Spacer(modifier = Modifier.width(ZcashTheme.dimens.spacingMin))
FormTextField(
textStyle = ZcashTheme.extendedTypography.textFieldValue.copy(fontSize = 14.sp),
value = amountState.fiatValue,
onValueChange = { newValue -> // TODO
setAmountState(
AmountState.newFromFiat(
context = context,
value = amountState.value,
monetarySeparators = monetarySeparators,
isTransparentRecipient = isTransparentRecipient,
fiatValue = newValue,
fiatCurrencyConversion =
(walletSnapshot.exchangeRateUsd as? FiatCurrencyResult.Success)
?.currencyConversion
)
)
},
modifier = Modifier.weight(1f),
placeholder = {
Text(
text =
stringResource(
id = R.string.send_usd_amount_hint,
zcashCurrency
),
style = ZcashTheme.extendedTypography.textFieldHint,
color = ZcashTheme.colors.textFieldHint
)
},
keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = imeAction
),
keyboardActions =
KeyboardActions(
onDone = {
focusManager.clearFocus(true)
},
onNext = {
focusManager.moveFocus(FocusDirection.Down)
}
),
bringIntoViewRequester = bringIntoViewRequester,
leadingIcon = {
Image(
modifier = Modifier.requiredSize(7.dp, 13.dp),
painter = painterResource(R.drawable.ic_usd),
contentDescription = "",
colorFilter = ColorFilter.tint(color = ZcashTheme.colors.secondaryColor),
)
}
)
}
}
}

View File

@ -19,6 +19,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.FiatCurrencyResult
import cash.z.ecc.android.sdk.model.Proposal
import cash.z.ecc.android.sdk.model.TransactionSubmitResult
import cash.z.ecc.android.sdk.model.UnifiedSpendingKey
@ -27,6 +28,7 @@ import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationViewModel
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
@ -74,6 +76,8 @@ internal fun MainActivity.WrapSendConfirmation(
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
WrapSendConfirmation(
activity = this,
arguments = arguments,
@ -87,6 +91,7 @@ internal fun MainActivity.WrapSendConfirmation(
supportMessage = supportMessage,
synchronizer = synchronizer,
topAppBarSubTitleState = walletState,
walletSnapshot = walletSnapshot
)
}
@ -106,6 +111,7 @@ internal fun WrapSendConfirmation(
supportMessage: SupportInfo?,
synchronizer: Synchronizer?,
topAppBarSubTitleState: TopAppBarSubTitleState,
walletSnapshot: WalletSnapshot?
) {
val scope = rememberCoroutineScope()
@ -206,6 +212,7 @@ internal fun WrapSendConfirmation(
}
},
topAppBarSubTitleState = topAppBarSubTitleState,
exchangeRate = walletSnapshot?.exchangeRateUsd ?: FiatCurrencyResult.Loading()
)
if (sendFundsAuthentication.value) {

View File

@ -32,6 +32,7 @@ import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.model.FiatCurrencyResult
import cash.z.ecc.android.sdk.model.FirstClassByteArray
import cash.z.ecc.android.sdk.model.TransactionSubmitResult
import cash.z.ecc.android.sdk.model.ZecSend
@ -39,6 +40,7 @@ import cash.z.ecc.sdk.extension.toZecStringFull
import cash.z.ecc.sdk.fixture.MemoFixture
import cash.z.ecc.sdk.fixture.ZatoshiFixture
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.StyledExchangeBalance
import co.electriccoin.zcash.ui.common.compose.BalanceWidgetBigLineOnly
import co.electriccoin.zcash.ui.common.extension.asZecAmountTriple
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
@ -83,7 +85,8 @@ private fun SendConfirmationPreview() {
stage = SendConfirmationStage.Confirmation,
topAppBarSubTitleState = TopAppBarSubTitleState.None,
onContactSupport = {},
submissionResults = emptyList<TransactionSubmitResult>().toImmutableList()
submissionResults = emptyList<TransactionSubmitResult>().toImmutableList(),
exchangeRate = FiatCurrencyResult.Loading()
)
}
}
@ -106,7 +109,8 @@ private fun SendConfirmationDarkPreview() {
stage = SendConfirmationStage.Confirmation,
topAppBarSubTitleState = TopAppBarSubTitleState.None,
onContactSupport = {},
submissionResults = emptyList<TransactionSubmitResult>().toImmutableList()
submissionResults = emptyList<TransactionSubmitResult>().toImmutableList(),
exchangeRate = FiatCurrencyResult.Loading()
)
}
}
@ -129,7 +133,8 @@ private fun SendMultipleErrorPreview() {
stage = SendConfirmationStage.MultipleTrxFailure,
topAppBarSubTitleState = TopAppBarSubTitleState.None,
onContactSupport = {},
submissionResults = emptyList<TransactionSubmitResult>().toImmutableList()
submissionResults = emptyList<TransactionSubmitResult>().toImmutableList(),
exchangeRate = FiatCurrencyResult.Loading()
)
}
}
@ -152,7 +157,8 @@ private fun SendMultipleErrorDarkPreview() {
stage = SendConfirmationStage.MultipleTrxFailure,
topAppBarSubTitleState = TopAppBarSubTitleState.None,
onContactSupport = {},
submissionResults = emptyList<TransactionSubmitResult>().toImmutableList()
submissionResults = emptyList<TransactionSubmitResult>().toImmutableList(),
exchangeRate = FiatCurrencyResult.Loading()
)
}
}
@ -171,7 +177,8 @@ private fun PreviewSendConfirmation() {
),
onConfirmation = {},
onBack = {},
isSending = false
isSending = false,
exchangeRate = FiatCurrencyResult.Loading()
)
}
}
@ -244,6 +251,7 @@ fun SendConfirmation(
submissionResults: ImmutableList<TransactionSubmitResult>,
topAppBarSubTitleState: TopAppBarSubTitleState,
zecSend: ZecSend,
exchangeRate: FiatCurrencyResult
) {
BlankBgScaffold(
topBar = {
@ -269,7 +277,8 @@ fun SendConfirmation(
bottom = paddingValues.calculateBottomPadding(),
start = ZcashTheme.dimens.screenHorizontalSpacingRegular,
end = ZcashTheme.dimens.screenHorizontalSpacingRegular
)
),
exchangeRate = exchangeRate
)
}
}
@ -327,6 +336,7 @@ private fun SendConfirmationMainContent(
submissionResults: ImmutableList<TransactionSubmitResult>,
zecSend: ZecSend,
modifier: Modifier = Modifier,
exchangeRate: FiatCurrencyResult
) {
when (stage) {
SendConfirmationStage.Confirmation, SendConfirmationStage.Sending, is SendConfirmationStage.Failure -> {
@ -335,7 +345,8 @@ private fun SendConfirmationMainContent(
onBack = onBack,
onConfirmation = onConfirmation,
isSending = stage == SendConfirmationStage.Sending,
modifier = modifier
modifier = modifier,
exchangeRate = exchangeRate
)
if (stage is SendConfirmationStage.Failure) {
SendFailure(
@ -358,6 +369,7 @@ private fun SendConfirmationMainContent(
@Suppress("LongMethod")
private fun SendConfirmationContent(
zecSend: ZecSend,
exchangeRate: FiatCurrencyResult,
onConfirmation: () -> Unit,
onBack: () -> Unit,
isSending: Boolean,
@ -380,6 +392,12 @@ private fun SendConfirmationContent(
isHideBalances = false
)
StyledExchangeBalance(
zatoshi = zecSend.amount,
exchangeRate = exchangeRate,
isHideBalances = false
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault))
Small(stringResource(R.string.send_confirmation_address))

View File

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="11dp"
android:height="9dp"
android:viewportWidth="11"
android:viewportHeight="9">
<group>
<clip-path
android:pathData="M0.5,0.601h10v8h-10z"/>
<path
android:pathData="M3.289,8.601L0.5,6.047V5.181H10.5V6.162H2.064L3.926,7.868L3.289,8.601ZM7.71,0.601L10.499,3.155V4.053L0.499,4.022V3.04H8.935L7.073,1.334L7.71,0.601Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</group>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="7dp"
android:height="13dp"
android:viewportWidth="7"
android:viewportHeight="13">
<path
android:pathData="M7.009,3.942V2.534H4.48V0.98H2.926V2.534H0.397V4.405H4.321L0.397,9.728V11.138H2.926V12.683H4.48V11.138H7.009V9.267H3.085L7.009,3.942Z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="7dp"
android:height="12dp"
android:viewportWidth="7"
android:viewportHeight="12">
<path
android:pathData="M3.067,2.719V0.381H3.865V2.719H3.067ZM3.067,11.343V8.921H3.865V11.343H3.067ZM3.431,9.649C2.918,9.649 2.456,9.593 2.045,9.481C1.635,9.369 1.285,9.215 0.995,9.019C0.706,8.814 0.482,8.571 0.323,8.291C0.174,8.011 0.099,7.694 0.099,7.339C0.099,7.302 0.099,7.264 0.099,7.227C0.099,7.19 0.104,7.162 0.113,7.143H1.989C1.989,7.162 1.989,7.18 1.989,7.199C1.989,7.218 1.989,7.236 1.989,7.255C1.999,7.488 2.073,7.68 2.213,7.829C2.353,7.969 2.535,8.072 2.759,8.137C2.993,8.202 3.235,8.235 3.487,8.235C3.711,8.235 3.926,8.216 4.131,8.179C4.346,8.132 4.523,8.053 4.663,7.941C4.813,7.829 4.887,7.684 4.887,7.507C4.887,7.283 4.794,7.11 4.607,6.989C4.43,6.868 4.192,6.77 3.893,6.695C3.604,6.62 3.287,6.536 2.941,6.443C2.624,6.368 2.307,6.284 1.989,6.191C1.672,6.088 1.383,5.958 1.121,5.799C0.869,5.64 0.664,5.435 0.505,5.183C0.347,4.922 0.267,4.595 0.267,4.203C0.267,3.82 0.351,3.489 0.519,3.209C0.687,2.92 0.916,2.682 1.205,2.495C1.504,2.308 1.849,2.173 2.241,2.089C2.643,1.996 3.072,1.949 3.529,1.949C3.959,1.949 4.36,1.996 4.733,2.089C5.107,2.173 5.433,2.304 5.713,2.481C5.993,2.649 6.213,2.864 6.371,3.125C6.53,3.377 6.609,3.662 6.609,3.979C6.609,4.044 6.609,4.105 6.609,4.161C6.609,4.217 6.605,4.254 6.595,4.273H4.733V4.161C4.733,3.993 4.682,3.853 4.579,3.741C4.477,3.62 4.327,3.526 4.131,3.461C3.945,3.396 3.716,3.363 3.445,3.363C3.259,3.363 3.086,3.377 2.927,3.405C2.778,3.433 2.647,3.475 2.535,3.531C2.423,3.587 2.335,3.657 2.269,3.741C2.213,3.816 2.185,3.909 2.185,4.021C2.185,4.18 2.251,4.31 2.381,4.413C2.521,4.506 2.703,4.586 2.927,4.651C3.151,4.716 3.399,4.786 3.669,4.861C4.005,4.954 4.355,5.048 4.719,5.141C5.093,5.225 5.438,5.342 5.755,5.491C6.073,5.64 6.329,5.855 6.525,6.135C6.721,6.406 6.819,6.774 6.819,7.241C6.819,7.689 6.731,8.067 6.553,8.375C6.385,8.683 6.147,8.93 5.839,9.117C5.531,9.304 5.172,9.439 4.761,9.523C4.351,9.607 3.907,9.649 3.431,9.649Z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -6,6 +6,7 @@
<string name="send_address_invalid">Invalid address</string>
<string name="send_amount_label">Amount:</string>
<string name="send_amount_hint"><xliff:g id="currency" example="ZEC">%1$s</xliff:g> Amount</string>
<string name="send_usd_amount_hint"><xliff:g id="currency" example="USD">%1$s</xliff:g> Amount</string>
<string name="send_amount_insufficient_balance">Insufficient funds</string>
<string name="send_amount_invalid">Invalid amount</string>
<string name="send_memo_label">Message</string>