Balance actions bussiness logic

This commit is contained in:
Milan Cerovsky 2025-04-15 16:51:19 +02:00
parent 2b9635502e
commit 64af1a3d50
30 changed files with 397 additions and 181 deletions

View File

@ -34,11 +34,6 @@ fun Zatoshi.toZecStringAbbreviated(suffix: String): ZecAmountPair {
} }
} }
private const val DEFAULT_LESS_THAN_FEE = 100_000L
val DEFAULT_FEE: String
get() = Zatoshi(DEFAULT_LESS_THAN_FEE).toZecStringFull()
data class ZecAmountPair( data class ZecAmountPair(
val main: String, val main: String,
val suffix: String val suffix: String

View File

@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
@ -66,6 +67,7 @@ fun StyledBalance(
balanceParts: ZecAmountTriple, balanceParts: ZecAmountTriple,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isHideBalances: Boolean = false, isHideBalances: Boolean = false,
showDust: Boolean = true,
hiddenBalancePlaceholder: String = stringResource(id = R.string.hide_balance_placeholder), hiddenBalancePlaceholder: String = stringResource(id = R.string.hide_balance_placeholder),
textColor: Color = Color.Unspecified, textColor: Color = Color.Unspecified,
textStyle: BalanceTextStyle = StyledBalanceDefaults.textStyles(), textStyle: BalanceTextStyle = StyledBalanceDefaults.textStyles(),
@ -88,6 +90,7 @@ fun StyledBalance(
) { ) {
append(balanceSplit.first) append(balanceSplit.first)
} }
if (showDust) {
withStyle( withStyle(
style = textStyle.leastSignificantPart.toSpanStyle() style = textStyle.leastSignificantPart.toSpanStyle()
) { ) {
@ -95,12 +98,14 @@ fun StyledBalance(
} }
} }
} }
}
val resultModifier = val resultModifier =
Modifier Modifier
.basicMarquee() .basicMarquee()
.then(modifier) .then(modifier)
SelectionContainer {
Text( Text(
text = content, text = content,
color = textColor, color = textColor,
@ -108,6 +113,7 @@ fun StyledBalance(
modifier = resultModifier modifier = resultModifier
) )
} }
}
private const val CUT_POSITION_OFFSET = 4 private const val CUT_POSITION_OFFSET = 4

View File

@ -1,6 +1,7 @@
package co.electriccoin.zcash.ui.design.component package co.electriccoin.zcash.ui.design.component
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -13,10 +14,17 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.pointer.changedToDown
import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -36,6 +44,9 @@ fun ZashiBigIconButton(
state: BigIconButtonState, state: BigIconButtonState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
var isPressed by remember { mutableStateOf(false) }
val shadowElevation by animateDpAsState(if (isPressed) 0.dp else (2.dp orDark 4.dp))
val darkBgGradient = val darkBgGradient =
Brush.verticalGradient( Brush.verticalGradient(
0f to ZashiColors.Surfaces.strokeSecondary, 0f to ZashiColors.Surfaces.strokeSecondary,
@ -54,14 +65,29 @@ fun ZashiBigIconButton(
Modifier.background(darkBgGradient) Modifier.background(darkBgGradient)
Surface( Surface(
modifier = modifier, modifier = modifier
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
event.changes.forEach { change ->
if (change.changedToDown()) {
isPressed = true
}
if (change.changedToUp()) {
isPressed = false
}
}
}
}
},
onClick = state.onClick, onClick = state.onClick,
color = ZashiColors.Surfaces.bgPrimary, color = ZashiColors.Surfaces.bgPrimary,
shape = RoundedCornerShape(22.dp), shape = RoundedCornerShape(22.dp),
border = border =
BorderStroke(.5.dp, ZashiColors.Utility.Gray.utilityGray100) orDark BorderStroke(.5.dp, ZashiColors.Utility.Gray.utilityGray100) orDark
BorderStroke(.5.dp, darkBorderGradient), BorderStroke(.5.dp, darkBorderGradient),
shadowElevation = 2.dp orDark 4.dp shadowElevation = shadowElevation
) { ) {
Column( Column(
modifier = backgroundModifier.padding(16.dp), modifier = backgroundModifier.padding(16.dp),

View File

@ -57,8 +57,15 @@ sealed interface StringResource {
val address: String, val address: String,
val abbreviated: Boolean val abbreviated: Boolean
) : StringResource ) : StringResource
operator fun plus(other: StringResource): StringResource = CompositeStringResource(listOf(this, other))
operator fun plus(other: String): StringResource = CompositeStringResource(listOf(this, stringRes(other)))
} }
@Immutable
private data class CompositeStringResource(val resources: List<StringResource>): StringResource
@Stable @Stable
fun stringRes( fun stringRes(
@StringRes resource: Int, @StringRes resource: Int,
@ -109,18 +116,14 @@ fun StringResource.getValue(
convertYearMonth: (YearMonth) -> String = StringResourceDefaults::convertYearMonth, convertYearMonth: (YearMonth) -> String = StringResourceDefaults::convertYearMonth,
convertAddress: (StringResource.ByAddress) -> String = StringResourceDefaults::convertAddress, convertAddress: (StringResource.ByAddress) -> String = StringResourceDefaults::convertAddress,
convertTransactionId: (StringResource.ByTransactionId) -> String = StringResourceDefaults::convertTransactionId convertTransactionId: (StringResource.ByTransactionId) -> String = StringResourceDefaults::convertTransactionId
) = when (this) { ): String = getString(
is StringResource.ByResource -> { context = LocalContext.current,
val context = LocalContext.current convertZatoshi = convertZatoshi,
context.getString(resource, *args.normalize(context).toTypedArray()) convertDateTime = convertDateTime,
} convertYearMonth = convertYearMonth,
is StringResource.ByString -> value convertAddress = convertAddress,
is StringResource.ByZatoshi -> convertZatoshi(zatoshi) convertTransactionId = convertTransactionId
is StringResource.ByDateTime -> convertDateTime(this) )
is StringResource.ByYearMonth -> convertYearMonth(yearMonth)
is StringResource.ByAddress -> convertAddress(this)
is StringResource.ByTransactionId -> convertTransactionId(this)
}
@Suppress("SpreadOperator") @Suppress("SpreadOperator")
fun StringResource.getString( fun StringResource.getString(
@ -130,7 +133,7 @@ fun StringResource.getString(
convertYearMonth: (YearMonth) -> String = StringResourceDefaults::convertYearMonth, convertYearMonth: (YearMonth) -> String = StringResourceDefaults::convertYearMonth,
convertAddress: (StringResource.ByAddress) -> String = StringResourceDefaults::convertAddress, convertAddress: (StringResource.ByAddress) -> String = StringResourceDefaults::convertAddress,
convertTransactionId: (StringResource.ByTransactionId) -> String = StringResourceDefaults::convertTransactionId convertTransactionId: (StringResource.ByTransactionId) -> String = StringResourceDefaults::convertTransactionId
) = when (this) { ): String = when (this) {
is StringResource.ByResource -> context.getString(resource, *args.normalize(context).toTypedArray()) is StringResource.ByResource -> context.getString(resource, *args.normalize(context).toTypedArray())
is StringResource.ByString -> value is StringResource.ByString -> value
is StringResource.ByZatoshi -> convertZatoshi(zatoshi) is StringResource.ByZatoshi -> convertZatoshi(zatoshi)
@ -138,6 +141,16 @@ fun StringResource.getString(
is StringResource.ByYearMonth -> convertYearMonth(yearMonth) is StringResource.ByYearMonth -> convertYearMonth(yearMonth)
is StringResource.ByAddress -> convertAddress(this) is StringResource.ByAddress -> convertAddress(this)
is StringResource.ByTransactionId -> convertTransactionId(this) is StringResource.ByTransactionId -> convertTransactionId(this)
is CompositeStringResource -> this.resources.joinToString(separator = "") {
it.getString(
context = context,
convertZatoshi = convertZatoshi,
convertDateTime = convertDateTime,
convertYearMonth = convertYearMonth,
convertAddress = convertAddress,
convertTransactionId = convertTransactionId,
)
}
} }
private fun List<Any>.normalize(context: Context): List<Any> = private fun List<Any>.normalize(context: Context): List<Any> =

View File

@ -104,7 +104,7 @@ class SendViewTestSetup(
// TODO [#1260]: Cover Send.Form screen UI with tests // TODO [#1260]: Cover Send.Form screen UI with tests
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260 // TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
Send( Send(
balanceState = BalanceStateFixture.new(), balanceWidgetState = BalanceStateFixture.new(),
sendStage = sendStage, sendStage = sendStage,
onCreateZecSend = setZecSend, onCreateZecSend = setZecSend,
onBack = onBackAction, onBack = onBackAction,

View File

@ -8,7 +8,7 @@ import co.electriccoin.zcash.ui.screen.accountlist.viewmodel.AccountListViewMode
import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.AddressBookViewModel import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.AddressBookViewModel
import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.SelectRecipientViewModel import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.SelectRecipientViewModel
import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsViewModel import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsViewModel
import co.electriccoin.zcash.ui.screen.balances.BalanceViewModel import co.electriccoin.zcash.ui.screen.balances.BalanceWidgetViewModel
import co.electriccoin.zcash.ui.screen.balances.action.BalanceActionViewModel import co.electriccoin.zcash.ui.screen.balances.action.BalanceActionViewModel
import co.electriccoin.zcash.ui.screen.chooseserver.ChooseServerViewModel import co.electriccoin.zcash.ui.screen.chooseserver.ChooseServerViewModel
import co.electriccoin.zcash.ui.screen.contact.viewmodel.AddContactViewModel import co.electriccoin.zcash.ui.screen.contact.viewmodel.AddContactViewModel
@ -143,7 +143,7 @@ val viewModelModule =
) )
} }
viewModelOf(::TaxExportViewModel) viewModelOf(::TaxExportViewModel)
viewModelOf(::BalanceViewModel) viewModelOf(::BalanceWidgetViewModel)
viewModelOf(::HomeViewModel) viewModelOf(::HomeViewModel)
viewModelOf(::RestoreBDHeightViewModel) viewModelOf(::RestoreBDHeightViewModel)
viewModelOf(::RestoreBDDateViewModel) viewModelOf(::RestoreBDDateViewModel)

View File

@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import java.time.Instant import java.time.Instant
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
interface ShieldFundsDataSource { interface ShieldFundsDataSource {
@ -87,7 +88,7 @@ sealed interface ShieldFundsAvailability {
} }
enum class ShieldFundsLockoutDuration(val duration: Duration, @StringRes val res: Int) { enum class ShieldFundsLockoutDuration(val duration: Duration, @StringRes val res: Int) {
TWO_DAYS(10.seconds, R.string.general_remind_me_in_two_days), TWO_DAYS(2.days, R.string.general_remind_me_in_two_days),
TWO_WEEKS(20.seconds, R.string.general_remind_me_in_two_weeks), TWO_WEEKS(2.days, R.string.general_remind_me_in_two_weeks),
ONE_MONTH(30.seconds, R.string.general_remind_me_in_two_months) ONE_MONTH(30.days, R.string.general_remind_me_in_two_months)
} }

View File

@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import java.time.Instant import java.time.Instant
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
interface WalletBackupDataSource { interface WalletBackupDataSource {
@ -96,7 +97,7 @@ sealed interface WalletBackupAvailability {
} }
enum class WalletBackupLockoutDuration(val duration: Duration, @StringRes val res: Int) { enum class WalletBackupLockoutDuration(val duration: Duration, @StringRes val res: Int) {
TWO_DAYS(10.seconds, R.string.general_remind_me_in_two_days), TWO_DAYS(2.days, R.string.general_remind_me_in_two_days),
TWO_WEEKS(20.seconds, R.string.general_remind_me_in_two_weeks), TWO_WEEKS(14.days, R.string.general_remind_me_in_two_weeks),
ONE_MONTH(30.seconds, R.string.general_remind_me_in_two_months), ONE_MONTH(30.days, R.string.general_remind_me_in_two_months),
} }

View File

@ -29,11 +29,25 @@ sealed interface WalletAccount : Comparable<WalletAccount> {
val totalShieldedBalance: Zatoshi val totalShieldedBalance: Zatoshi
val spendableBalance: Zatoshi val spendableBalance: Zatoshi
val changePendingBalance: Zatoshi val changePendingBalance: Zatoshi
val hasChangePending: Boolean
val valuePendingBalance: Zatoshi val valuePendingBalance: Zatoshi
val pendingBalance: Zatoshi
get() = changePendingBalance + valuePendingBalance
val hasChangePending: Boolean
val hasValuePending: Boolean val hasValuePending: Boolean
val isPending: Boolean
get() = pendingBalance > Zatoshi(0)
fun canSpend(amount: Zatoshi): Boolean = spendableBalance >= amount fun canSpend(amount: Zatoshi): Boolean = spendableBalance >= amount
fun isProcessingZeroAvailableBalance(): Boolean {
if (totalShieldedBalance == Zatoshi(0) && transparent.balance > Zatoshi(0)) {
return false
}
return totalBalance != totalShieldedBalance && totalShieldedBalance == Zatoshi(0)
}
} }
data class ZashiAccount( data class ZashiAccount(
@ -55,10 +69,10 @@ data class ZashiAccount(
get() = unified.balance.available + sapling.balance.available get() = unified.balance.available + sapling.balance.available
override val changePendingBalance: Zatoshi override val changePendingBalance: Zatoshi
get() = unified.balance.changePending + sapling.balance.changePending get() = unified.balance.changePending + sapling.balance.changePending
override val hasChangePending: Boolean
get() = changePendingBalance.value > 0L
override val valuePendingBalance: Zatoshi override val valuePendingBalance: Zatoshi
get() = unified.balance.valuePending + sapling.balance.valuePending get() = unified.balance.valuePending + sapling.balance.valuePending
override val hasChangePending: Boolean
get() = changePendingBalance.value > 0L
override val hasValuePending: Boolean override val hasValuePending: Boolean
get() = valuePendingBalance.value > 0L get() = valuePendingBalance.value > 0L
@ -88,10 +102,10 @@ data class KeystoneAccount(
get() = unified.balance.available get() = unified.balance.available
override val changePendingBalance: Zatoshi override val changePendingBalance: Zatoshi
get() = unified.balance.changePending get() = unified.balance.changePending
override val hasChangePending: Boolean
get() = changePendingBalance.value > 0L
override val valuePendingBalance: Zatoshi override val valuePendingBalance: Zatoshi
get() = unified.balance.valuePending get() = unified.balance.valuePending
override val hasChangePending: Boolean
get() = changePendingBalance.value > 0L
override val hasValuePending: Boolean override val hasValuePending: Boolean
get() = valuePendingBalance.value > 0L get() = valuePendingBalance.value > 0L
@ -110,7 +124,10 @@ data class UnifiedInfo(
data class TransparentInfo( data class TransparentInfo(
val address: WalletAddress.Transparent, val address: WalletAddress.Transparent,
val balance: Zatoshi val balance: Zatoshi
) ) {
val isShieldingAvailable: Boolean
get() = balance > Zatoshi(100000L)
}
data class SaplingInfo( data class SaplingInfo(
val address: WalletAddress.Sapling, val address: WalletAddress.Sapling,

View File

@ -29,7 +29,7 @@ class ShieldFundsRepositoryImpl(
account == null -> account == null ->
flowOf(ShieldFundsData.Unavailable) flowOf(ShieldFundsData.Unavailable)
account.transparent.balance >= Zatoshi(DEFAULT_SHIELDING_THRESHOLD) -> account.transparent.isShieldingAvailable ->
shieldFundsDataSource.observe(account.sdkAccount.accountUuid).map { shieldFundsDataSource.observe(account.sdkAccount.accountUuid).map {
when (it) { when (it) {
is ShieldFundsAvailability.Available -> ShieldFundsData.Available( is ShieldFundsAvailability.Available -> ShieldFundsData.Available(
@ -60,6 +60,3 @@ sealed interface ShieldFundsData {
data object Unavailable : ShieldFundsData data object Unavailable : ShieldFundsData
} }
private const val DEFAULT_SHIELDING_THRESHOLD = 100000L

View File

@ -25,7 +25,7 @@ class ShieldFundsUseCase(
) { ) {
private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())
operator fun invoke(navigateBackAfterSuccess: Boolean) { operator fun invoke(closeCurrentScreen: Boolean) {
scope.launch { scope.launch {
when (accountDataSource.getSelectedAccount()) { when (accountDataSource.getSelectedAccount()) {
is KeystoneAccount -> { is KeystoneAccount -> {
@ -33,7 +33,7 @@ class ShieldFundsUseCase(
} }
is ZashiAccount -> { is ZashiAccount -> {
if (navigateBackAfterSuccess) { if (closeCurrentScreen) {
navigationRouter.back() navigationRouter.back()
} }
shieldZashiFunds() shieldZashiFunds()

View File

@ -2,7 +2,7 @@ package co.electriccoin.zcash.ui.fixture
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.screen.balances.BalanceState import co.electriccoin.zcash.ui.screen.balances.BalanceWidgetState
object BalanceStateFixture { object BalanceStateFixture {
private const val BALANCE_VALUE = 0L private const val BALANCE_VALUE = 0L
@ -12,9 +12,10 @@ object BalanceStateFixture {
fun new( fun new(
totalBalance: Zatoshi = TOTAL_BALANCE, totalBalance: Zatoshi = TOTAL_BALANCE,
exchangeRate: ExchangeRateState = ObserveFiatCurrencyResultFixture.new() exchangeRate: ExchangeRateState = ObserveFiatCurrencyResultFixture.new()
) = BalanceState( ) = BalanceWidgetState(
totalBalance = totalBalance, totalBalance = totalBalance,
exchangeRate = exchangeRate, exchangeRate = exchangeRate,
button = null button = null,
showDust = true
) )
} }

View File

@ -1,44 +0,0 @@
package co.electriccoin.zcash.ui.screen.balances
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
import co.electriccoin.zcash.ui.common.model.WalletAccount
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.stateIn
class BalanceViewModel(
accountDataSource: AccountDataSource,
exchangeRateRepository: ExchangeRateRepository,
) : ViewModel() {
val state: StateFlow<BalanceState> =
combine(
accountDataSource.selectedAccount.filterNotNull(),
exchangeRateRepository.state,
) { account, exchangeRateUsd ->
createState(account, exchangeRateUsd)
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
createState(
account = accountDataSource.allAccounts.value?.firstOrNull { it.isSelected },
exchangeRateUsd = exchangeRateRepository.state.value
)
)
private fun createState(account: WalletAccount?, exchangeRateUsd: ExchangeRateState): BalanceState {
return BalanceState(
totalBalance = account?.totalBalance ?: Zatoshi(0),
exchangeRate = exchangeRateUsd,
button = null
)
}
}

View File

@ -34,7 +34,7 @@ import co.electriccoin.zcash.ui.screen.balances.BalanceTag.BALANCE_VIEWS
import co.electriccoin.zcash.ui.screen.exchangerate.widget.StyledExchangeBalance import co.electriccoin.zcash.ui.screen.exchangerate.widget.StyledExchangeBalance
@Composable @Composable
fun BalanceWidget(state: BalanceState, modifier: Modifier = Modifier) { fun BalanceWidget(state: BalanceWidgetState, modifier: Modifier = Modifier) {
Column( Column(
modifier = modifier =
Modifier Modifier
@ -44,12 +44,13 @@ fun BalanceWidget(state: BalanceState, modifier: Modifier = Modifier) {
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
BalanceWidgetHeader( BalanceWidgetHeader(
parts = state.totalBalance.toZecStringFull().asZecAmountTriple() parts = state.totalBalance.toZecStringFull().asZecAmountTriple(),
showDust = state.showDust
) )
state.button?.let { state.button?.let {
Spacer(12.dp) Spacer(12.dp)
BalanceButton(it) BalanceWidgetButton(it)
} }
state.exchangeRate?.let { state.exchangeRate?.let {
@ -57,9 +58,6 @@ fun BalanceWidget(state: BalanceState, modifier: Modifier = Modifier) {
Spacer(12.dp) Spacer(12.dp)
} }
StyledExchangeBalance(state = it, zatoshi = state.totalBalance) StyledExchangeBalance(state = it, zatoshi = state.totalBalance)
if (state.exchangeRate is ExchangeRateState.Data) {
Spacer(12.dp)
}
} }
} }
} }
@ -69,6 +67,7 @@ fun BalanceWidgetHeader(
parts: ZecAmountTriple, parts: ZecAmountTriple,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isHideBalances: Boolean = LocalBalancesAvailable.current.not(), isHideBalances: Boolean = LocalBalancesAvailable.current.not(),
showDust: Boolean = true,
) { ) {
Row( Row(
modifier = modifier, modifier = modifier,
@ -81,6 +80,7 @@ fun BalanceWidgetHeader(
) )
Spacer(6.dp) Spacer(6.dp)
StyledBalance( StyledBalance(
showDust = showDust,
balanceParts = parts, balanceParts = parts,
isHideBalances = isHideBalances, isHideBalances = isHideBalances,
textStyle = textStyle =
@ -101,7 +101,7 @@ private fun BalanceWidgetPreview() {
) { ) {
BalanceWidget( BalanceWidget(
state = state =
BalanceState( BalanceWidgetState(
totalBalance = Zatoshi(1234567891234567L), totalBalance = Zatoshi(1234567891234567L),
button = BalanceButtonState( button = BalanceButtonState(
icon = R.drawable.ic_help, icon = R.drawable.ic_help,
@ -109,7 +109,8 @@ private fun BalanceWidgetPreview() {
amount = Zatoshi(1000), amount = Zatoshi(1000),
onClick = {} onClick = {}
), ),
exchangeRate = ObserveFiatCurrencyResultFixture.new() exchangeRate = ObserveFiatCurrencyResultFixture.new(),
showDust = true
), ),
modifier = Modifier, modifier = Modifier,
) )

View File

@ -1,5 +1,6 @@
package co.electriccoin.zcash.ui.screen.balances package co.electriccoin.zcash.ui.screen.balances
import androidx.annotation.DrawableRes
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@ -11,6 +12,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -30,12 +32,12 @@ import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.StringResource
import co.electriccoin.zcash.ui.design.util.getValue import co.electriccoin.zcash.ui.design.util.getValue
import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.design.util.stringRes
@Suppress("LongParameterList")
@Composable @Composable
fun BalanceButton( internal fun BalanceWidgetButton(
state: BalanceButtonState, state: BalanceButtonState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -52,7 +54,7 @@ fun BalanceButton(
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 10.dp), contentPadding = PaddingValues(horizontal = 12.dp, vertical = 10.dp),
colors = colors.toButtonColors(), colors = colors.toButtonColors(),
elevation = ButtonDefaults.buttonElevation( elevation = ButtonDefaults.buttonElevation(
defaultElevation = 2.dp, defaultElevation = 1.dp,
pressedElevation = 0.dp pressedElevation = 0.dp
), ),
border = borderColor.takeIf { it != Color.Unspecified }?.let { BorderStroke(1.dp, it) }, border = borderColor.takeIf { it != Color.Unspecified }?.let { BorderStroke(1.dp, it) },
@ -98,11 +100,19 @@ fun BalanceButton(
) )
} }
@Immutable
data class BalanceButtonState(
@DrawableRes val icon: Int,
val text: StringResource,
val amount: Zatoshi?,
val onClick: () -> Unit
)
@PreviewScreens @PreviewScreens
@Composable @Composable
private fun Preview() = ZcashTheme { private fun Preview() = ZcashTheme {
BlankSurface { BlankSurface {
BalanceButton( BalanceWidgetButton(
state = BalanceButtonState( state = BalanceButtonState(
icon = R.drawable.ic_help, icon = R.drawable.ic_help,
text = stringRes("text"), text = stringRes("text"),

View File

@ -1,22 +1,13 @@
package co.electriccoin.zcash.ui.screen.balances package co.electriccoin.zcash.ui.screen.balances
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.design.util.StringResource
@Immutable @Immutable
data class BalanceState( data class BalanceWidgetState(
val showDust: Boolean,
val totalBalance: Zatoshi, val totalBalance: Zatoshi,
val button: BalanceButtonState?, val button: BalanceButtonState?,
val exchangeRate: ExchangeRateState?, val exchangeRate: ExchangeRateState?,
) )
@Immutable
data class BalanceButtonState(
@DrawableRes val icon: Int,
val text: StringResource,
val amount: Zatoshi?,
val onClick: () -> Unit
)

View File

@ -0,0 +1,81 @@
package co.electriccoin.zcash.ui.screen.balances
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.NavigationRouter
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
import co.electriccoin.zcash.ui.common.model.WalletAccount
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.balances.action.BalanceAction
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.stateIn
class BalanceWidgetViewModel(
private val args: BalanceWidgetArgs,
accountDataSource: AccountDataSource,
exchangeRateRepository: ExchangeRateRepository,
private val navigationRouter: NavigationRouter,
) : ViewModel() {
val state: StateFlow<BalanceWidgetState> =
combine(
accountDataSource.selectedAccount.filterNotNull(),
exchangeRateRepository.state,
) { account, exchangeRateUsd ->
createState(account, exchangeRateUsd)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = createState(
account = accountDataSource.allAccounts.value?.firstOrNull { it.isSelected },
exchangeRateUsd = exchangeRateRepository.state.value
)
)
private fun createState(account: WalletAccount?, exchangeRateUsd: ExchangeRateState): BalanceWidgetState {
return BalanceWidgetState(
totalBalance = account?.totalBalance ?: Zatoshi(0),
exchangeRate = if (args.isExchangeRateButtonEnabled) exchangeRateUsd else null,
button = when {
!args.isBalanceButtonEnabled -> null
account == null -> null
account.totalBalance == account.spendableBalance -> null
account.isProcessingZeroAvailableBalance() && !account.isPending ->
BalanceButtonState(
icon = R.drawable.ic_balances_expand,
text = stringRes(R.string.widget_balances_button_spendable),
amount = null,
onClick = ::onBalanceButtonClick
)
account.totalBalance > account.spendableBalance -> BalanceButtonState(
icon = R.drawable.ic_balances_expand,
text = stringRes(R.string.widget_balances_button_spendable),
amount = account.spendableBalance,
onClick = ::onBalanceButtonClick
)
else -> null
},
showDust = args.showDust
)
}
private fun onBalanceButtonClick() = navigationRouter.forward(BalanceAction)
}
data class BalanceWidgetArgs(
val showDust: Boolean,
val isBalanceButtonEnabled: Boolean,
val isExchangeRateButtonEnabled: Boolean,
)

View File

@ -1,39 +1,108 @@
package co.electriccoin.zcash.ui.screen.balances.action package co.electriccoin.zcash.ui.screen.balances.action
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import cash.z.ecc.android.sdk.model.Zatoshi import androidx.lifecycle.viewModelScope
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.NavigationRouter
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
import co.electriccoin.zcash.ui.common.model.WalletAccount
import co.electriccoin.zcash.ui.common.usecase.ShieldFundsUseCase
import co.electriccoin.zcash.ui.design.component.ButtonState import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.util.StringResource
import co.electriccoin.zcash.ui.design.util.imageRes import co.electriccoin.zcash.ui.design.util.imageRes
import co.electriccoin.zcash.ui.design.util.loadingImageRes import co.electriccoin.zcash.ui.design.util.loadingImageRes
import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.design.util.stringRes
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
class BalanceActionViewModel : ViewModel() { class BalanceActionViewModel(
val state = MutableStateFlow( accountDataSource: AccountDataSource,
BalanceActionState( private val navigationRouter: NavigationRouter,
title = stringRes("Error"), private val shieldFunds: ShieldFundsUseCase,
message = stringRes("Something went wrong"), ) : ViewModel() {
positive = ButtonState( val state = accountDataSource.selectedAccount
text = stringRes("Positive") .mapNotNull {
), createState(it)
onBack = {}, }
rows = listOf( .stateIn(
BalanceActionRowState( scope = viewModelScope,
title = stringRes("Row"), started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
icon = loadingImageRes(), initialValue = createState(accountDataSource.allAccounts.value.orEmpty().firstOrNull { it.isSelected })
value = stringRes("Value")
),
BalanceActionRowState(
title = stringRes("Row"),
icon = imageRes(R.drawable.ic_home_buy),
value = stringRes("Value")
)
),
shieldButton = BalanceShieldButtonState(
amount = Zatoshi(10000),
onShieldClick = {}
)
) )
private fun createState(account: WalletAccount?): BalanceActionState? {
if (account == null) return null
return BalanceActionState(
title = stringRes("Spendable Balance"),
message = createMessage(account),
positive = createPositiveButton(account),
onBack = ::onBack,
rows = createInfoRows(account),
shieldButton = createShieldButtonState(account)
) )
} }
private fun createMessage(account: WalletAccount): StringResource {
val pending = when {
account.totalBalance == account.spendableBalance && !account.isPending ->
stringRes("All your funds are shielded and spendable.")
account.isPending || account.isProcessingZeroAvailableBalance() ->
stringRes("Pending transactions are getting mined and confirmed.")
else -> null
}
val shielding =
stringRes("Shield your transparent ZEC to make it spendable and private. Shielding transparent funds will create a shielding in-wallet transaction, consolidating your transparent and shielded funds. (Typical fee: .001 ZEC)")
.takeIf { account.transparent.isShieldingAvailable }
return if (pending != null && shielding != null) {
pending + stringRes("\n\n") + shielding
} else {
listOfNotNull(pending, shielding).reduceOrNull { acc, stringResource -> acc + stringResource } ?: stringRes(
""
)
}
}
private fun createPositiveButton(account: WalletAccount) = ButtonState(
text = if (account.transparent.isShieldingAvailable) stringRes("Dismiss") else stringRes("Ok"),
onClick = ::onBack
)
private fun createInfoRows(account: WalletAccount) = listOfNotNull(
BalanceActionRowState(
title = stringRes("Shielded ZEC (Spendable)"),
icon = imageRes(R.drawable.ic_balance_shield),
value = stringRes(R.string.general_zec, stringRes(account.spendableBalance))
),
if (!account.isProcessingZeroAvailableBalance()) {
BalanceActionRowState(
title = stringRes("Pending"),
icon = loadingImageRes(),
value = stringRes(R.string.general_zec, stringRes(account.totalBalance))
)
} else {
BalanceActionRowState(
title = stringRes("Pending"),
icon = loadingImageRes(),
value = stringRes(R.string.general_zec, stringRes(account.pendingBalance))
).takeIf { account.isPending }
},
)
private fun createShieldButtonState(account: WalletAccount): BalanceShieldButtonState? {
return BalanceShieldButtonState(
amount = account.transparent.balance,
onShieldClick = ::onShieldClick
).takeIf { account.transparent.isShieldingAvailable }
}
private fun onBack() = navigationRouter.back()
private fun onShieldClick() = shieldFunds(closeCurrentScreen = true)
}

View File

@ -28,7 +28,7 @@ class ExchangeRateSettingsViewModel(
private fun createState(it: ExchangeRateState) = private fun createState(it: ExchangeRateState) =
ExchangeRateSettingsState( ExchangeRateSettingsState(
isOptedIn = it is ExchangeRateState.OptIn, isOptedIn = it is ExchangeRateState.Data,
onSaveClick = ::onOptInExchangeRateUsdClick, onSaveClick = ::onOptInExchangeRateUsdClick,
onDismiss = ::onBack onDismiss = ::onBack
) )

View File

@ -7,28 +7,38 @@ import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppBarViewModel import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppBarViewModel
import co.electriccoin.zcash.ui.screen.balances.BalanceViewModel import co.electriccoin.zcash.ui.screen.balances.BalanceWidgetArgs
import co.electriccoin.zcash.ui.screen.balances.BalanceWidgetViewModel
import co.electriccoin.zcash.ui.screen.restoresuccess.WrapRestoreSuccess import co.electriccoin.zcash.ui.screen.restoresuccess.WrapRestoreSuccess
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetViewModel import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetViewModel
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
@Composable @Composable
internal fun AndroidHome() { internal fun AndroidHome() {
val topAppBarViewModel = koinActivityViewModel<ZashiTopAppBarViewModel>() val topAppBarViewModel = koinActivityViewModel<ZashiTopAppBarViewModel>()
val balanceViewModel = koinViewModel<BalanceViewModel>() val balanceWidgetViewModel = koinViewModel<BalanceWidgetViewModel> {
parametersOf(
BalanceWidgetArgs(
isBalanceButtonEnabled = false,
isExchangeRateButtonEnabled = true,
showDust = false,
)
)
}
val homeViewModel = koinViewModel<HomeViewModel>() val homeViewModel = koinViewModel<HomeViewModel>()
val transactionHistoryWidgetViewModel = koinViewModel<TransactionHistoryWidgetViewModel>() val transactionHistoryWidgetViewModel = koinViewModel<TransactionHistoryWidgetViewModel>()
val restoreDialogState by homeViewModel.restoreDialogState.collectAsStateWithLifecycle() val restoreDialogState by homeViewModel.restoreDialogState.collectAsStateWithLifecycle()
val appBarState by topAppBarViewModel.state.collectAsStateWithLifecycle() val appBarState by topAppBarViewModel.state.collectAsStateWithLifecycle()
val balanceState by balanceViewModel.state.collectAsStateWithLifecycle() val balanceState by balanceWidgetViewModel.state.collectAsStateWithLifecycle()
val state by homeViewModel.state.collectAsStateWithLifecycle() val state by homeViewModel.state.collectAsStateWithLifecycle()
val transactionWidgetState by transactionHistoryWidgetViewModel.state.collectAsStateWithLifecycle() val transactionWidgetState by transactionHistoryWidgetViewModel.state.collectAsStateWithLifecycle()
state?.let { state?.let {
HomeView( HomeView(
appBarState = appBarState, appBarState = appBarState,
balanceState = balanceState, balanceWidgetState = balanceState,
state = it, state = it,
transactionWidgetState = transactionWidgetState transactionWidgetState = transactionWidgetState
) )

View File

@ -23,14 +23,16 @@ import co.electriccoin.zcash.ui.common.appbar.ZashiMainTopAppBarState
import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppBarWithAccountSelection import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppBarWithAccountSelection
import co.electriccoin.zcash.ui.design.component.BigIconButtonState import co.electriccoin.zcash.ui.design.component.BigIconButtonState
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
import co.electriccoin.zcash.ui.design.component.Spacer
import co.electriccoin.zcash.ui.design.component.ZashiBigIconButton import co.electriccoin.zcash.ui.design.component.ZashiBigIconButton
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
import co.electriccoin.zcash.ui.design.util.scaffoldPadding import co.electriccoin.zcash.ui.design.util.scaffoldPadding
import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.fixture.BalanceStateFixture import co.electriccoin.zcash.ui.fixture.BalanceStateFixture
import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture
import co.electriccoin.zcash.ui.screen.balances.BalanceState import co.electriccoin.zcash.ui.screen.balances.BalanceWidgetState
import co.electriccoin.zcash.ui.screen.balances.BalanceWidget import co.electriccoin.zcash.ui.screen.balances.BalanceWidget
import co.electriccoin.zcash.ui.screen.home.error.WalletErrorMessageState import co.electriccoin.zcash.ui.screen.home.error.WalletErrorMessageState
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetState import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetState
@ -40,7 +42,7 @@ import co.electriccoin.zcash.ui.screen.transactionhistory.widget.createTransacti
@Composable @Composable
internal fun HomeView( internal fun HomeView(
appBarState: ZashiMainTopAppBarState?, appBarState: ZashiMainTopAppBarState?,
balanceState: BalanceState, balanceWidgetState: BalanceWidgetState,
transactionWidgetState: TransactionHistoryWidgetState, transactionWidgetState: TransactionHistoryWidgetState,
state: HomeState state: HomeState
) { ) {
@ -48,10 +50,10 @@ internal fun HomeView(
topBar = { ZashiTopAppBarWithAccountSelection(appBarState) } topBar = { ZashiTopAppBarWithAccountSelection(appBarState) }
) { paddingValues -> ) { paddingValues ->
Content( Content(
modifier = Modifier.padding(top = paddingValues.calculateTopPadding() + 24.dp), modifier = Modifier.padding(top = paddingValues.calculateTopPadding() + ZashiDimensions.Spacing.spacingLg),
paddingValues = paddingValues, paddingValues = paddingValues,
transactionHistoryWidgetState = transactionWidgetState, transactionHistoryWidgetState = transactionWidgetState,
balanceState = balanceState, balanceWidgetState = balanceWidgetState,
state = state state = state
) )
} }
@ -61,7 +63,7 @@ internal fun HomeView(
private fun Content( private fun Content(
transactionHistoryWidgetState: TransactionHistoryWidgetState, transactionHistoryWidgetState: TransactionHistoryWidgetState,
paddingValues: PaddingValues, paddingValues: PaddingValues,
balanceState: BalanceState, balanceWidgetState: BalanceWidgetState,
state: HomeState, state: HomeState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -71,7 +73,7 @@ private fun Content(
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall)) Spacer(8.dp)
BalanceWidget( BalanceWidget(
modifier = modifier =
Modifier Modifier
@ -79,8 +81,9 @@ private fun Content(
start = ZcashTheme.dimens.screenHorizontalSpacingRegular, start = ZcashTheme.dimens.screenHorizontalSpacingRegular,
end = ZcashTheme.dimens.screenHorizontalSpacingRegular, end = ZcashTheme.dimens.screenHorizontalSpacingRegular,
), ),
state = balanceState, state = balanceWidgetState,
) )
Spacer(16.dp)
NavButtons( NavButtons(
modifier = modifier =
Modifier Modifier
@ -160,7 +163,7 @@ private fun Preview() {
ZcashTheme { ZcashTheme {
HomeView( HomeView(
appBarState = ZashiMainTopAppBarStateFixture.new(), appBarState = ZashiMainTopAppBarStateFixture.new(),
balanceState = BalanceStateFixture.new(), balanceWidgetState = BalanceStateFixture.new(),
transactionWidgetState = TransactionHistoryWidgetStateFixture.new(), transactionWidgetState = TransactionHistoryWidgetStateFixture.new(),
state = state =
HomeState( HomeState(

View File

@ -237,7 +237,7 @@ class HomeViewModel(
private fun onShieldFundsMessageClick() = navigationRouter.forward(ShieldFundsInfo) private fun onShieldFundsMessageClick() = navigationRouter.forward(ShieldFundsInfo)
private fun onShieldFundsMessageButtonClick() = shieldFunds(navigateBackAfterSuccess = false) private fun onShieldFundsMessageButtonClick() = shieldFunds(closeCurrentScreen = false)
private fun onWalletErrorMessageClick(homeMessageData: HomeMessageData.Error) = private fun onWalletErrorMessageClick(homeMessageData: HomeMessageData.Error) =
navigateToError(ErrorArgs.SyncError(homeMessageData.synchronizerError)) navigateToError(ErrorArgs.SyncError(homeMessageData.synchronizerError))

View File

@ -74,6 +74,6 @@ class ShieldFundsInfoViewModel(
private fun onBack() = navigationRouter.back() private fun onBack() = navigationRouter.back()
private fun onShieldClick() = shieldFunds(navigateBackAfterSuccess = true) private fun onShieldClick() = shieldFunds(closeCurrentScreen = true)
} }

View File

@ -32,8 +32,9 @@ import co.electriccoin.zcash.ui.common.usecase.PrefillSendUseCase
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.balances.BalanceState import co.electriccoin.zcash.ui.screen.balances.BalanceWidgetArgs
import co.electriccoin.zcash.ui.screen.balances.BalanceViewModel import co.electriccoin.zcash.ui.screen.balances.BalanceWidgetState
import co.electriccoin.zcash.ui.screen.balances.BalanceWidgetViewModel
import co.electriccoin.zcash.ui.screen.scan.Scan import co.electriccoin.zcash.ui.screen.scan.Scan
import co.electriccoin.zcash.ui.screen.scan.ScanFlow import co.electriccoin.zcash.ui.screen.scan.ScanFlow
import co.electriccoin.zcash.ui.screen.send.ext.Saver import co.electriccoin.zcash.ui.screen.send.ext.Saver
@ -45,6 +46,7 @@ import co.electriccoin.zcash.ui.screen.send.view.Send
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject import org.koin.compose.koinInject
import org.koin.core.parameter.parametersOf
import java.util.Locale import java.util.Locale
@Composable @Composable
@ -55,7 +57,16 @@ internal fun WrapSend(args: Send) {
val walletViewModel = koinActivityViewModel<WalletViewModel>() val walletViewModel = koinActivityViewModel<WalletViewModel>()
val balanceViewModel = koinViewModel<BalanceViewModel>() val balanceWidgetViewModel =
koinViewModel<BalanceWidgetViewModel> {
parametersOf(
BalanceWidgetArgs(
isBalanceButtonEnabled = true,
isExchangeRateButtonEnabled = false,
showDust = true
)
)
}
val accountDataSource = koinInject<AccountDataSource>() val accountDataSource = koinInject<AccountDataSource>()
@ -69,12 +80,12 @@ internal fun WrapSend(args: Send) {
val monetarySeparators = MonetarySeparators.current(Locale.getDefault()) val monetarySeparators = MonetarySeparators.current(Locale.getDefault())
val balanceState = balanceViewModel.state.collectAsStateWithLifecycle().value val balanceState = balanceWidgetViewModel.state.collectAsStateWithLifecycle().value
val exchangeRateState = exchangeRateRepository.state.collectAsStateWithLifecycle().value val exchangeRateState = exchangeRateRepository.state.collectAsStateWithLifecycle().value
WrapSend( WrapSend(
balanceState = balanceState, balanceWidgetState = balanceState,
exchangeRateState = exchangeRateState, exchangeRateState = exchangeRateState,
goToQrScanner = { goToQrScanner = {
navigationRouter.forward( navigationRouter.forward(
@ -97,7 +108,7 @@ internal fun WrapSend(args: Send) {
@VisibleForTesting @VisibleForTesting
@Composable @Composable
internal fun WrapSend( internal fun WrapSend(
balanceState: BalanceState, balanceWidgetState: BalanceWidgetState,
exchangeRateState: ExchangeRateState, exchangeRateState: ExchangeRateState,
goToQrScanner: () -> Unit, goToQrScanner: () -> Unit,
goBack: () -> Unit, goBack: () -> Unit,
@ -286,7 +297,7 @@ internal fun WrapSend(
CircularScreenProgressIndicator() CircularScreenProgressIndicator()
} else { } else {
Send( Send(
balanceState = balanceState, balanceWidgetState = balanceWidgetState,
sendStage = sendStage, sendStage = sendStage,
onCreateZecSend = { newZecSend -> onCreateZecSend = { newZecSend ->
viewModel.onCreateZecSendClick( viewModel.onCreateZecSendClick(

View File

@ -64,6 +64,7 @@ import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
import co.electriccoin.zcash.ui.design.component.AppAlertDialog import co.electriccoin.zcash.ui.design.component.AppAlertDialog
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
import co.electriccoin.zcash.ui.design.component.BlankSurface import co.electriccoin.zcash.ui.design.component.BlankSurface
import co.electriccoin.zcash.ui.design.component.Spacer
import co.electriccoin.zcash.ui.design.component.ZashiButton import co.electriccoin.zcash.ui.design.component.ZashiButton
import co.electriccoin.zcash.ui.design.component.ZashiTextField import co.electriccoin.zcash.ui.design.component.ZashiTextField
import co.electriccoin.zcash.ui.design.component.ZashiTextFieldDefaults import co.electriccoin.zcash.ui.design.component.ZashiTextFieldDefaults
@ -74,7 +75,7 @@ import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.scaffoldPadding import co.electriccoin.zcash.ui.design.util.scaffoldPadding
import co.electriccoin.zcash.ui.fixture.BalanceStateFixture import co.electriccoin.zcash.ui.fixture.BalanceStateFixture
import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture
import co.electriccoin.zcash.ui.screen.balances.BalanceState import co.electriccoin.zcash.ui.screen.balances.BalanceWidgetState
import co.electriccoin.zcash.ui.screen.balances.BalanceWidget import co.electriccoin.zcash.ui.screen.balances.BalanceWidget
import co.electriccoin.zcash.ui.screen.send.SendTag import co.electriccoin.zcash.ui.screen.send.SendTag
import co.electriccoin.zcash.ui.screen.send.model.AmountState import co.electriccoin.zcash.ui.screen.send.model.AmountState
@ -90,7 +91,7 @@ import java.util.Locale
private fun PreviewSendForm() { private fun PreviewSendForm() {
ZcashTheme(forceDarkMode = false) { ZcashTheme(forceDarkMode = false) {
Send( Send(
balanceState = BalanceStateFixture.new(), balanceWidgetState = BalanceStateFixture.new(),
sendStage = SendStage.Form, sendStage = SendStage.Form,
onCreateZecSend = {}, onCreateZecSend = {},
onBack = {}, onBack = {},
@ -125,7 +126,7 @@ private fun PreviewSendForm() {
private fun SendFormTransparentAddressPreview() { private fun SendFormTransparentAddressPreview() {
ZcashTheme(forceDarkMode = false) { ZcashTheme(forceDarkMode = false) {
Send( Send(
balanceState = BalanceStateFixture.new(), balanceWidgetState = BalanceStateFixture.new(),
sendStage = SendStage.Form, sendStage = SendStage.Form,
onCreateZecSend = {}, onCreateZecSend = {},
onBack = {}, onBack = {},
@ -164,7 +165,7 @@ private fun SendFormTransparentAddressPreview() {
@Suppress("LongParameterList") @Suppress("LongParameterList")
@Composable @Composable
fun Send( fun Send(
balanceState: BalanceState, balanceWidgetState: BalanceWidgetState,
sendStage: SendStage, sendStage: SendStage,
onCreateZecSend: (ZecSend) -> Unit, onCreateZecSend: (ZecSend) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
@ -193,7 +194,7 @@ fun Send(
) )
}) { paddingValues -> }) { paddingValues ->
SendMainContent( SendMainContent(
balanceState = balanceState, balanceWidgetState = balanceWidgetState,
selectedAccount = selectedAccount, selectedAccount = selectedAccount,
exchangeRateState = exchangeRateState, exchangeRateState = exchangeRateState,
onBack = onBack, onBack = onBack,
@ -216,7 +217,7 @@ fun Send(
@Suppress("LongParameterList") @Suppress("LongParameterList")
@Composable @Composable
private fun SendMainContent( private fun SendMainContent(
balanceState: BalanceState, balanceWidgetState: BalanceWidgetState,
selectedAccount: WalletAccount, selectedAccount: WalletAccount,
exchangeRateState: ExchangeRateState, exchangeRateState: ExchangeRateState,
onBack: () -> Unit, onBack: () -> Unit,
@ -237,7 +238,7 @@ private fun SendMainContent(
// loader if calling the Proposal API takes longer than expected // loader if calling the Proposal API takes longer than expected
SendForm( SendForm(
balanceState = balanceState, balanceWidgetState = balanceWidgetState,
selectedAccount = selectedAccount, selectedAccount = selectedAccount,
recipientAddressState = recipientAddressState, recipientAddressState = recipientAddressState,
exchangeRateState = exchangeRateState, exchangeRateState = exchangeRateState,
@ -270,7 +271,7 @@ private fun SendMainContent(
@Suppress("LongParameterList", "LongMethod") @Suppress("LongParameterList", "LongMethod")
@Composable @Composable
private fun SendForm( private fun SendForm(
balanceState: BalanceState, balanceWidgetState: BalanceWidgetState,
selectedAccount: WalletAccount, selectedAccount: WalletAccount,
recipientAddressState: RecipientAddressState, recipientAddressState: RecipientAddressState,
exchangeRateState: ExchangeRateState, exchangeRateState: ExchangeRateState,
@ -295,13 +296,13 @@ private fun SendForm(
.then(modifier), .then(modifier),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall)) Spacer(8.dp)
BalanceWidget( BalanceWidget(
state = balanceState state = balanceWidgetState
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(24.dp)
// TODO [#1256]: Consider Send.Form TextFields scrolling // TODO [#1256]: Consider Send.Form TextFields scrolling
// TODO [#1256]: https://github.com/Electric-Coin-Company/zashi-android/issues/1256 // TODO [#1256]: https://github.com/Electric-Coin-Company/zashi-android/issues/1256

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M10.239,0.908C10.08,0.885 9.92,0.885 9.762,0.908C9.58,0.934 9.41,0.998 9.276,1.049L9.239,1.063L4.662,2.779C4.148,2.971 3.695,3.14 3.347,3.447C3.043,3.715 2.808,4.054 2.664,4.433C2.499,4.867 2.499,5.35 2.5,5.899L2.5,10.001C2.5,12.356 3.781,14.32 5.166,15.763C6.56,17.215 8.155,18.241 8.999,18.733L9.033,18.753C9.187,18.843 9.386,18.961 9.651,19.017C9.867,19.064 10.134,19.064 10.35,19.017C10.614,18.961 10.814,18.843 10.968,18.753L11.002,18.733C11.845,18.241 13.44,17.215 14.834,15.763C16.219,14.32 17.5,12.356 17.5,10.001L17.5,5.899C17.501,5.35 17.502,4.867 17.337,4.433C17.192,4.054 16.958,3.715 16.653,3.447C16.305,3.14 15.853,2.971 15.338,2.779L10.761,1.063L10.725,1.049C10.59,0.998 10.421,0.934 10.239,0.908Z"
android:fillColor="#231F20"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="17dp"
android:height="16dp"
android:viewportWidth="17"
android:viewportHeight="16">
<path
android:pathData="M9.833,6.667L14.5,2M14.5,2H10.5M14.5,2V6M7.167,9.333L2.5,14M2.5,14H6.5M2.5,14L2.5,10"
android:strokeLineJoin="round"
android:strokeWidth="1.33333"
android:fillColor="#00000000"
android:strokeColor="#716C5D"
android:strokeLineCap="round"/>
</vector>

View File

@ -2,4 +2,5 @@
<resources> <resources>
<string name="balance_action_shield">Shield</string> <string name="balance_action_shield">Shield</string>
<string name="balance_action_shield_button_header">Transparent</string> <string name="balance_action_shield_button_header">Transparent</string>
<string name="widget_balances_button_spendable">Spendable</string>
</resources> </resources>

View File

@ -2,4 +2,5 @@
<resources> <resources>
<string name="balance_action_shield">Shield</string> <string name="balance_action_shield">Shield</string>
<string name="balance_action_shield_button_header">Transparent</string> <string name="balance_action_shield_button_header">Transparent</string>
<string name="widget_balances_button_spendable">Spendable</string>
</resources> </resources>

View File

@ -35,4 +35,6 @@
<string name="general_remind_me_in_two_days">two days</string> <string name="general_remind_me_in_two_days">two days</string>
<string name="general_remind_me_in_two_weeks">two weeks</string> <string name="general_remind_me_in_two_weeks">two weeks</string>
<string name="general_remind_me_in_two_months">two months</string> <string name="general_remind_me_in_two_months">two months</string>
<string name="general_zec">%S ZEC</string>
</resources> </resources>