Code cleanup

This commit is contained in:
Milan Cerovsky 2025-04-22 16:45:58 +02:00
parent d488daeafd
commit 0db65c410a
81 changed files with 1081 additions and 939 deletions

View File

@ -3,7 +3,9 @@ package co.electriccoin.zcash.preference.model.entry
import co.electriccoin.zcash.preference.api.PreferenceProvider import co.electriccoin.zcash.preference.api.PreferenceProvider
import java.time.Instant import java.time.Instant
class TimestampPreferenceDefault(override val key: PreferenceKey): PreferenceDefault<Instant?> { class TimestampPreferenceDefault(
override val key: PreferenceKey
) : PreferenceDefault<Instant?> {
override suspend fun getValue(preferenceProvider: PreferenceProvider) = override suspend fun getValue(preferenceProvider: PreferenceProvider) =
preferenceProvider.getLong(key)?.let { Instant.ofEpochMilli(it) } preferenceProvider.getLong(key)?.let { Instant.ofEpochMilli(it) }
@ -11,4 +13,4 @@ class TimestampPreferenceDefault(override val key: PreferenceKey): PreferenceDef
preferenceProvider: PreferenceProvider, preferenceProvider: PreferenceProvider,
newValue: Instant? newValue: Instant?
) = preferenceProvider.putLong(key, newValue?.toEpochMilli()) ) = preferenceProvider.putLong(key, newValue?.toEpochMilli())
} }

View File

@ -65,22 +65,23 @@ fun ZashiBigIconButton(
Modifier.background(darkBgGradient) Modifier.background(darkBgGradient)
Surface( Surface(
modifier = modifier modifier =
.pointerInput(Unit) { modifier
awaitPointerEventScope { .pointerInput(Unit) {
while (true) { awaitPointerEventScope {
val event = awaitPointerEvent() while (true) {
event.changes.forEach { change -> val event = awaitPointerEvent()
if (change.changedToDown()) { event.changes.forEach { change ->
isPressed = true if (change.changedToDown()) {
} isPressed = true
if (change.changedToUp()) { }
isPressed = false 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),

View File

@ -40,4 +40,4 @@ fun ZashiInfoRow(
) )
} }
} }
} }

View File

@ -41,12 +41,12 @@ fun ZashiScreenDialog(
@Composable @Composable
private fun Dialog( private fun Dialog(
modifier: Modifier = Modifier,
positive: ButtonState, positive: ButtonState,
negative: ButtonState, negative: ButtonState,
onDismissRequest: (() -> Unit),
title: StringResource, title: StringResource,
message: StringResource, message: StringResource,
onDismissRequest: (() -> Unit),
modifier: Modifier = Modifier,
properties: DialogProperties = DialogProperties() properties: DialogProperties = DialogProperties()
) { ) {
AlertDialog( AlertDialog(

View File

@ -1,15 +1,15 @@
@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.ui.design.component package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
@ -77,26 +77,28 @@ fun ZashiYearMonthWheelDatePicker(
} }
Box(modifier = modifier) { Box(modifier = modifier) {
Row ( Row(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.align(Alignment.Center), .align(Alignment.Center),
) { ) {
Box( Box(
modifier = Modifier modifier =
.weight(1f) Modifier
.height(34.dp) .weight(1f)
.padding(top = 1.dp) .height(34.dp)
.background(ZashiColors.Surfaces.bgSecondary, RoundedCornerShape(6.dp)) .padding(top = 1.dp)
.background(ZashiColors.Surfaces.bgSecondary, RoundedCornerShape(6.dp))
) )
Spacer(36.dp) Spacer(36.dp)
Box( Box(
modifier = Modifier modifier =
.weight(1f) Modifier
.height(34.dp) .weight(1f)
.padding(top = 1.dp) .height(34.dp)
.background(ZashiColors.Surfaces.bgSecondary, RoundedCornerShape(6.dp)) .padding(top = 1.dp)
.background(ZashiColors.Surfaces.bgSecondary, RoundedCornerShape(6.dp))
) )
} }
Row( Row(
@ -109,9 +111,10 @@ fun ZashiYearMonthWheelDatePicker(
itemVerticalOffset = verticallyVisibleItems, itemVerticalOffset = verticallyVisibleItems,
isInfiniteScroll = false, isInfiniteScroll = false,
onFocusItem = { onFocusItem = {
state = state.copy( state =
selectedDate = state.selectedDate.withMonth(state.months[it].value) state.copy(
) selectedDate = state.selectedDate.withMonth(state.months[it].value)
)
}, },
itemContent = { itemContent = {
Text( Text(
@ -133,18 +136,20 @@ fun ZashiYearMonthWheelDatePicker(
isInfiniteScroll = false, isInfiniteScroll = false,
onFocusItem = { onFocusItem = {
val year = state.years[it] val year = state.years[it]
val normalizedSelectedMonth = getSelectedMonthForYear( val normalizedSelectedMonth =
year = year, getSelectedMonthForYear(
selectedMonth = state.selectedDate.month, year = year,
startYearMonth = startInclusive, selectedMonth = state.selectedDate.month,
endYearMonth = endInclusive startYearMonth = startInclusive,
) endYearMonth = endInclusive
)
val months = getMonthsForYear(year, startInclusive, endInclusive) val months = getMonthsForYear(year, startInclusive, endInclusive)
val selectedDate = state.selectedDate.withYear(year.value).withMonth(normalizedSelectedMonth.value) val selectedDate = state.selectedDate.withYear(year.value).withMonth(normalizedSelectedMonth.value)
state = state.copy( state =
selectedDate = selectedDate, state.copy(
months = months selectedDate = selectedDate,
) months = months
)
}, },
itemContent = { itemContent = {
Text( Text(
@ -161,8 +166,8 @@ fun ZashiYearMonthWheelDatePicker(
} }
} }
private fun getMonthsForYear(year: Year, startYearMonth: YearMonth, endYearMonth: YearMonth): List<Month> { private fun getMonthsForYear(year: Year, startYearMonth: YearMonth, endYearMonth: YearMonth): List<Month> =
return when (year.value) { when (year.value) {
startYearMonth.year -> { startYearMonth.year -> {
(startYearMonth.month.value..Month.DECEMBER.value).map { index -> (startYearMonth.month.value..Month.DECEMBER.value).map { index ->
Month.entries.first { it.value == index } Month.entries.first { it.value == index }
@ -192,32 +197,32 @@ private fun getMonthsForYear(year: Year, startYearMonth: YearMonth, endYearMonth
) )
} }
} }
}
private fun getSelectedMonthForYear( private fun getSelectedMonthForYear(
year: Year, year: Year,
selectedMonth: Month, selectedMonth: Month,
startYearMonth: YearMonth, startYearMonth: YearMonth,
endYearMonth: YearMonth endYearMonth: YearMonth
): Month { ): Month =
return when (year.value) { when (year.value) {
startYearMonth.year -> { startYearMonth.year -> {
val months = (startYearMonth.month.value..Month.DECEMBER.value).map { index -> val months =
Month.entries.first { it.value == index } (startYearMonth.month.value..Month.DECEMBER.value).map { index ->
} Month.entries.first { it.value == index }
}
if (selectedMonth in months) selectedMonth else months.findClosest(selectedMonth) if (selectedMonth in months) selectedMonth else months.findClosest(selectedMonth)
} }
endYearMonth.year -> { endYearMonth.year -> {
val months = (Month.JANUARY.value..endYearMonth.month.value).map { index -> val months =
Month.entries.first { it.value == index } (Month.JANUARY.value..endYearMonth.month.value).map { index ->
} Month.entries.first { it.value == index }
}
if (selectedMonth in months) selectedMonth else months.findClosest(selectedMonth) if (selectedMonth in months) selectedMonth else months.findClosest(selectedMonth)
} }
else -> selectedMonth else -> selectedMonth
} }
}
private fun List<Month>.findClosest(target: Month): Month { private fun List<Month>.findClosest(target: Month): Month {
var closestNumber = this[0] // Initialize with the first element var closestNumber = this[0] // Initialize with the first element
@ -400,11 +405,12 @@ private data class InternalState(
@PreviewScreens @PreviewScreens
@Composable @Composable
private fun Preview() = ZcashTheme { private fun Preview() =
BlankSurface { ZcashTheme {
ZashiYearMonthWheelDatePicker( BlankSurface {
selection = YearMonth.now(), ZashiYearMonthWheelDatePicker(
onSelectionChange = {} selection = YearMonth.now(),
) onSelectionChange = {}
)
}
} }
}

View File

@ -8,14 +8,18 @@ import androidx.compose.runtime.Stable
sealed interface ImageResource { sealed interface ImageResource {
@Immutable @Immutable
@JvmInline @JvmInline
value class ByDrawable(@DrawableRes val resource: Int) : ImageResource value class ByDrawable(
@DrawableRes val resource: Int
) : ImageResource
@JvmInline @JvmInline
@Immutable @Immutable
value class DisplayString(val value: String) : ImageResource value class DisplayString(
val value: String
) : ImageResource
@Immutable @Immutable
data object Loading: ImageResource data object Loading : ImageResource
} }
@Stable @Stable

View File

@ -64,7 +64,9 @@ sealed interface StringResource {
} }
@Immutable @Immutable
private data class CompositeStringResource(val resources: List<StringResource>): StringResource private data class CompositeStringResource(
val resources: List<StringResource>
) : StringResource
@Stable @Stable
fun stringRes( fun stringRes(
@ -116,14 +118,15 @@ 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
): String = getString( ): String =
context = LocalContext.current, getString(
convertZatoshi = convertZatoshi, context = LocalContext.current,
convertDateTime = convertDateTime, convertZatoshi = convertZatoshi,
convertYearMonth = convertYearMonth, convertDateTime = convertDateTime,
convertAddress = convertAddress, convertYearMonth = convertYearMonth,
convertTransactionId = convertTransactionId convertAddress = convertAddress,
) convertTransactionId = convertTransactionId
)
@Suppress("SpreadOperator") @Suppress("SpreadOperator")
fun StringResource.getString( fun StringResource.getString(
@ -133,25 +136,27 @@ 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
): String = when (this) { ): String =
is StringResource.ByResource -> context.getString(resource, *args.normalize(context).toTypedArray()) when (this) {
is StringResource.ByString -> value is StringResource.ByResource -> context.getString(resource, *args.normalize(context).toTypedArray())
is StringResource.ByZatoshi -> convertZatoshi(zatoshi) is StringResource.ByString -> value
is StringResource.ByDateTime -> convertDateTime(this) is StringResource.ByZatoshi -> convertZatoshi(zatoshi)
is StringResource.ByYearMonth -> convertYearMonth(yearMonth) is StringResource.ByDateTime -> convertDateTime(this)
is StringResource.ByAddress -> convertAddress(this) is StringResource.ByYearMonth -> convertYearMonth(yearMonth)
is StringResource.ByTransactionId -> convertTransactionId(this) is StringResource.ByAddress -> convertAddress(this)
is CompositeStringResource -> this.resources.joinToString(separator = "") { is StringResource.ByTransactionId -> convertTransactionId(this)
it.getString( is CompositeStringResource ->
context = context, this.resources.joinToString(separator = "") {
convertZatoshi = convertZatoshi, it.getString(
convertDateTime = convertDateTime, context = context,
convertYearMonth = convertYearMonth, convertZatoshi = convertZatoshi,
convertAddress = convertAddress, convertDateTime = convertDateTime,
convertTransactionId = convertTransactionId, convertYearMonth = convertYearMonth,
) convertAddress = convertAddress,
convertTransactionId = convertTransactionId,
)
}
} }
}
private fun List<Any>.normalize(context: Context): List<Any> = private fun List<Any>.normalize(context: Context): List<Any> =
this.map { this.map {

View File

@ -37,7 +37,6 @@ import co.electriccoin.zcash.ui.screen.scan.Scan
import co.electriccoin.zcash.ui.screen.scan.viewmodel.ScanViewModel import co.electriccoin.zcash.ui.screen.scan.viewmodel.ScanViewModel
import co.electriccoin.zcash.ui.screen.scankeystone.viewmodel.ScanKeystonePCZTViewModel import co.electriccoin.zcash.ui.screen.scankeystone.viewmodel.ScanKeystonePCZTViewModel
import co.electriccoin.zcash.ui.screen.scankeystone.viewmodel.ScanKeystoneSignInRequestViewModel import co.electriccoin.zcash.ui.screen.scankeystone.viewmodel.ScanKeystoneSignInRequestViewModel
import co.electriccoin.zcash.ui.screen.walletbackup.WalletBackupViewModel
import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.SelectKeystoneAccount import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.SelectKeystoneAccount
import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.viewmodel.SelectKeystoneAccountViewModel import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.viewmodel.SelectKeystoneAccountViewModel
import co.electriccoin.zcash.ui.screen.send.SendViewModel import co.electriccoin.zcash.ui.screen.send.SendViewModel
@ -54,6 +53,7 @@ import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHist
import co.electriccoin.zcash.ui.screen.transactionnote.TransactionNote import co.electriccoin.zcash.ui.screen.transactionnote.TransactionNote
import co.electriccoin.zcash.ui.screen.transactionnote.viewmodel.TransactionNoteViewModel import co.electriccoin.zcash.ui.screen.transactionnote.viewmodel.TransactionNoteViewModel
import co.electriccoin.zcash.ui.screen.transactionprogress.TransactionProgressViewModel import co.electriccoin.zcash.ui.screen.transactionprogress.TransactionProgressViewModel
import co.electriccoin.zcash.ui.screen.walletbackup.WalletBackupViewModel
import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel
import co.electriccoin.zcash.ui.screen.whatsnew.viewmodel.WhatsNewViewModel import co.electriccoin.zcash.ui.screen.whatsnew.viewmodel.WhatsNewViewModel
import org.koin.core.module.dsl.viewModel import org.koin.core.module.dsl.viewModel

View File

@ -154,7 +154,7 @@ class NavigatorImpl(
} }
} }
if (command.routes.lastOrNull() in listOf(ExternalUrl, co.electriccoin.zcash.ui.screen.flexa.Flexa) ) { if (command.routes.lastOrNull() in listOf(ExternalUrl, co.electriccoin.zcash.ui.screen.flexa.Flexa)) {
messageAvailabilityDataSource.onThirdPartyUiShown() messageAvailabilityDataSource.onThirdPartyUiShown()
} }
} }

View File

@ -16,25 +16,28 @@ import kotlinx.coroutines.flow.update
interface MessageAvailabilityDataSource { interface MessageAvailabilityDataSource {
val canShowMessage: Flow<Boolean> val canShowMessage: Flow<Boolean>
val canShowShieldMessage: Flow<Boolean> val canShowShieldMessage: Flow<Boolean>
fun onMessageShown() fun onMessageShown()
fun onThirdPartyUiShown() fun onThirdPartyUiShown()
fun onShieldingInitiated() fun onShieldingInitiated()
} }
class MessageAvailabilityDataSourceImpl( class MessageAvailabilityDataSourceImpl(
applicationStateProvider: ApplicationStateProvider applicationStateProvider: ApplicationStateProvider
) : MessageAvailabilityDataSource { ) : MessageAvailabilityDataSource {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private val state = MutableStateFlow( private val state =
MessageAvailabilityData( MutableStateFlow(
isAppInForeground = true, MessageAvailabilityData(
isThirdPartyUiShown = false, isAppInForeground = true,
hasMessageBeenShown = false, isThirdPartyUiShown = false,
canShowShieldMessage = true hasMessageBeenShown = false,
canShowShieldMessage = true
)
) )
)
override val canShowMessage: Flow<Boolean> = state.map { it.canShowMessage }.distinctUntilChanged() override val canShowMessage: Flow<Boolean> = state.map { it.canShowMessage }.distinctUntilChanged()
override val canShowShieldMessage: Flow<Boolean> = state.map { it.canShowShieldMessage }.distinctUntilChanged() override val canShowShieldMessage: Flow<Boolean> = state.map { it.canShowShieldMessage }.distinctUntilChanged()
@ -58,8 +61,7 @@ class MessageAvailabilityDataSourceImpl(
) )
} }
} }
} }.launchIn(scope)
.launchIn(scope)
} }
override fun onMessageShown() { override fun onMessageShown() {

View File

@ -15,7 +15,6 @@ 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.days
import kotlin.time.Duration.Companion.seconds
interface ShieldFundsDataSource { interface ShieldFundsDataSource {
suspend fun observe(forAccount: AccountUuid): Flow<ShieldFundsAvailability> suspend fun observe(forAccount: AccountUuid): Flow<ShieldFundsAvailability>
@ -26,34 +25,37 @@ interface ShieldFundsDataSource {
class ShieldFundsDataSourceImpl( class ShieldFundsDataSourceImpl(
private val shieldFundsRemindMeCountStorageProvider: ShieldFundsRemindMeCountStorageProvider, private val shieldFundsRemindMeCountStorageProvider: ShieldFundsRemindMeCountStorageProvider,
private val shieldFundsRemindMeTimestampStorageProvider: ShieldFundsRemindMeTimestampStorageProvider private val shieldFundsRemindMeTimestampStorageProvider: ShieldFundsRemindMeTimestampStorageProvider
): ShieldFundsDataSource { ) : ShieldFundsDataSource {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override suspend fun observe(forAccount: AccountUuid): Flow<ShieldFundsAvailability> = combine( override suspend fun observe(forAccount: AccountUuid): Flow<ShieldFundsAvailability> =
shieldFundsRemindMeCountStorageProvider.observe(forAccount), combine(
shieldFundsRemindMeTimestampStorageProvider.observe(forAccount) shieldFundsRemindMeCountStorageProvider.observe(forAccount),
) { count, timestamp -> shieldFundsRemindMeTimestampStorageProvider.observe(forAccount)
count to timestamp ) { count, timestamp ->
}.flatMapLatest {(count, timestamp) -> count to timestamp
when { }.flatMapLatest { (count, timestamp) ->
timestamp == null -> flowOf(ShieldFundsAvailability.Available(ShieldFundsLockoutDuration.TWO_DAYS)) when {
count == 1 -> calculateNext( timestamp == null -> flowOf(ShieldFundsAvailability.Available(ShieldFundsLockoutDuration.TWO_DAYS))
lastTimestamp = timestamp, count == 1 ->
lastLockoutDuration = ShieldFundsLockoutDuration.TWO_DAYS, calculateNext(
nextLockoutDuration = ShieldFundsLockoutDuration.TWO_WEEKS lastTimestamp = timestamp,
) lastLockoutDuration = ShieldFundsLockoutDuration.TWO_DAYS,
nextLockoutDuration = ShieldFundsLockoutDuration.TWO_WEEKS
)
else -> calculateNext( else ->
lastTimestamp = timestamp, calculateNext(
lastLockoutDuration = if (count == 2) { lastTimestamp = timestamp,
ShieldFundsLockoutDuration.TWO_WEEKS lastLockoutDuration =
} else { if (count == 2) {
ShieldFundsLockoutDuration.ONE_MONTH ShieldFundsLockoutDuration.TWO_WEEKS
}, } else {
nextLockoutDuration = ShieldFundsLockoutDuration.ONE_MONTH ShieldFundsLockoutDuration.ONE_MONTH
) },
nextLockoutDuration = ShieldFundsLockoutDuration.ONE_MONTH
)
}
} }
}
override suspend fun remindMeLater(forAccount: AccountUuid) { override suspend fun remindMeLater(forAccount: AccountUuid) {
val count = shieldFundsRemindMeCountStorageProvider.get(forAccount) val count = shieldFundsRemindMeCountStorageProvider.get(forAccount)
@ -83,11 +85,17 @@ class ShieldFundsDataSourceImpl(
} }
sealed interface ShieldFundsAvailability { sealed interface ShieldFundsAvailability {
data class Available(val lockoutDuration: ShieldFundsLockoutDuration) : ShieldFundsAvailability data class Available(
val lockoutDuration: ShieldFundsLockoutDuration
) : ShieldFundsAvailability
data object Unavailable : ShieldFundsAvailability data object Unavailable : ShieldFundsAvailability
} }
enum class ShieldFundsLockoutDuration(val duration: Duration, @StringRes val res: Int) { enum class ShieldFundsLockoutDuration(
val duration: Duration,
@StringRes val res: Int
) {
TWO_DAYS(2.days, R.string.general_remind_me_in_two_days), TWO_DAYS(2.days, R.string.general_remind_me_in_two_days),
TWO_WEEKS(2.days, R.string.general_remind_me_in_two_weeks), TWO_WEEKS(2.days, R.string.general_remind_me_in_two_weeks),
ONE_MONTH(30.days, R.string.general_remind_me_in_two_months) ONE_MONTH(30.days, R.string.general_remind_me_in_two_months)

View File

@ -15,7 +15,6 @@ 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.days
import kotlin.time.Duration.Companion.seconds
interface WalletBackupDataSource { interface WalletBackupDataSource {
fun observe(): Flow<WalletBackupAvailability> fun observe(): Flow<WalletBackupAvailability>
@ -30,35 +29,38 @@ class WalletBackupDataSourceImpl(
private val walletBackupRemindMeCountStorageProvider: WalletBackupRemindMeCountStorageProvider, private val walletBackupRemindMeCountStorageProvider: WalletBackupRemindMeCountStorageProvider,
private val walletBackupRemindMeTimestampStorageProvider: WalletBackupRemindMeTimestampStorageProvider private val walletBackupRemindMeTimestampStorageProvider: WalletBackupRemindMeTimestampStorageProvider
) : WalletBackupDataSource { ) : WalletBackupDataSource {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override fun observe(): Flow<WalletBackupAvailability> = combine( override fun observe(): Flow<WalletBackupAvailability> =
walletBackupFlagStorageProvider.observe(), combine(
walletBackupRemindMeCountStorageProvider.observe(), walletBackupFlagStorageProvider.observe(),
walletBackupRemindMeTimestampStorageProvider.observe() walletBackupRemindMeCountStorageProvider.observe(),
) { isBackedUp, count, timestamp -> walletBackupRemindMeTimestampStorageProvider.observe()
Triple(isBackedUp, count, timestamp) ) { isBackedUp, count, timestamp ->
}.flatMapLatest { (isBackedUp, count, timestamp) -> Triple(isBackedUp, count, timestamp)
when { }.flatMapLatest { (isBackedUp, count, timestamp) ->
isBackedUp -> flowOf(WalletBackupAvailability.Unavailable) when {
timestamp == null -> flowOf(WalletBackupAvailability.Available(WalletBackupLockoutDuration.TWO_DAYS)) isBackedUp -> flowOf(WalletBackupAvailability.Unavailable)
count == 1 -> calculateNext( timestamp == null -> flowOf(WalletBackupAvailability.Available(WalletBackupLockoutDuration.TWO_DAYS))
lastTimestamp = timestamp, count == 1 ->
lastLockoutDuration = WalletBackupLockoutDuration.TWO_DAYS, calculateNext(
nextLockoutDuration = WalletBackupLockoutDuration.TWO_WEEKS lastTimestamp = timestamp,
) lastLockoutDuration = WalletBackupLockoutDuration.TWO_DAYS,
nextLockoutDuration = WalletBackupLockoutDuration.TWO_WEEKS
)
else -> calculateNext( else ->
lastTimestamp = timestamp, calculateNext(
lastLockoutDuration = if (count == 2) { lastTimestamp = timestamp,
WalletBackupLockoutDuration.TWO_WEEKS lastLockoutDuration =
} else { if (count == 2) {
WalletBackupLockoutDuration.ONE_MONTH WalletBackupLockoutDuration.TWO_WEEKS
}, } else {
nextLockoutDuration = WalletBackupLockoutDuration.ONE_MONTH WalletBackupLockoutDuration.ONE_MONTH
) },
nextLockoutDuration = WalletBackupLockoutDuration.ONE_MONTH
)
}
} }
}
override suspend fun onUserSavedWalletBackup() { override suspend fun onUserSavedWalletBackup() {
walletBackupFlagStorageProvider.store(true) walletBackupFlagStorageProvider.store(true)
@ -92,11 +94,17 @@ class WalletBackupDataSourceImpl(
} }
sealed interface WalletBackupAvailability { sealed interface WalletBackupAvailability {
data class Available(val lockoutDuration: WalletBackupLockoutDuration) : WalletBackupAvailability data class Available(
val lockoutDuration: WalletBackupLockoutDuration
) : WalletBackupAvailability
data object Unavailable : WalletBackupAvailability data object Unavailable : WalletBackupAvailability
} }
enum class WalletBackupLockoutDuration(val duration: Duration, @StringRes val res: Int) { enum class WalletBackupLockoutDuration(
val duration: Duration,
@StringRes val res: Int
) {
TWO_DAYS(2.days, R.string.general_remind_me_in_two_days), TWO_DAYS(2.days, R.string.general_remind_me_in_two_days),
TWO_WEEKS(14.days, R.string.general_remind_me_in_two_weeks), TWO_WEEKS(14.days, R.string.general_remind_me_in_two_weeks),
ONE_MONTH(30.days, R.string.general_remind_me_in_two_months), ONE_MONTH(30.days, R.string.general_remind_me_in_two_months),

View File

@ -30,38 +30,37 @@ class WalletSnapshotDataSourceImpl(
synchronizerProvider: SynchronizerProvider, synchronizerProvider: SynchronizerProvider,
walletRestoringStateProvider: WalletRestoringStateProvider, walletRestoringStateProvider: WalletRestoringStateProvider,
) : WalletSnapshotDataSource { ) : WalletSnapshotDataSource {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
val flow = synchronizerProvider val flow =
.synchronizer synchronizerProvider
.flatMapLatest { synchronizer -> .synchronizer
if (synchronizer == null) { .flatMapLatest { synchronizer ->
flowOf(null) if (synchronizer == null) {
} else { flowOf(null)
combine( } else {
synchronizer.status, combine(
synchronizer.progress, synchronizer.status,
synchronizer.toCommonError(), synchronizer.progress,
synchronizer.areFundsSpendable, synchronizer.toCommonError(),
walletRestoringStateProvider.observe() synchronizer.areFundsSpendable,
) { status, progress, error, isSpendable, restoringState -> walletRestoringStateProvider.observe()
WalletSnapshot( ) { status, progress, error, isSpendable, restoringState ->
status = status, WalletSnapshot(
progress = progress, status = status,
synchronizerError = error, progress = progress,
isSpendable = isSpendable, synchronizerError = error,
restoringState = restoringState, isSpendable = isSpendable,
) restoringState = restoringState,
)
}
} }
} }.stateIn(
} scope = scope,
.stateIn( started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
scope = scope, initialValue = null
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), )
initialValue = null
)
override fun observe(): StateFlow<WalletSnapshot?> = flow override fun observe(): StateFlow<WalletSnapshot?> = flow

View File

@ -53,17 +53,10 @@ sealed interface WalletAccount : Comparable<WalletAccount> {
val isShieldedPending: Boolean val isShieldedPending: Boolean
get() = pendingShieldedBalance > Zatoshi(0) get() = pendingShieldedBalance > Zatoshi(0)
@Suppress("MagicNumber")
val isShieldingAvailable: Boolean val isShieldingAvailable: Boolean
get() = totalTransparentBalance > Zatoshi(100000L) get() = totalTransparentBalance > Zatoshi(100000L)
val isProcessingZeroSpendableBalance: Boolean
get() {
if (totalShieldedBalance == Zatoshi(0) && totalTransparentBalance > Zatoshi(0)) {
return false
}
return totalBalance > Zatoshi(0) && totalShieldedBalance == Zatoshi(0)
}
fun canSpend(amount: Zatoshi): Boolean = spendableShieldedBalance >= amount fun canSpend(amount: Zatoshi): Boolean = spendableShieldedBalance >= amount
} }

View File

@ -1,10 +1,7 @@
package co.electriccoin.zcash.ui.common.model package co.electriccoin.zcash.ui.common.model
import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
import cash.z.ecc.android.sdk.model.PercentDecimal import cash.z.ecc.android.sdk.model.PercentDecimal
import cash.z.ecc.android.sdk.model.WalletBalance
import cash.z.ecc.android.sdk.model.Zatoshi
import co.electriccoin.zcash.ui.common.viewmodel.SynchronizerError import co.electriccoin.zcash.ui.common.viewmodel.SynchronizerError
// TODO [#292]: Should be moved to SDK-EXT-UI module. // TODO [#292]: Should be moved to SDK-EXT-UI module.

View File

@ -5,6 +5,9 @@ import co.electriccoin.zcash.preference.model.entry.PreferenceKey
interface BooleanStorageProvider : StorageProvider<Boolean> interface BooleanStorageProvider : StorageProvider<Boolean>
abstract class BaseBooleanStorageProvider(key: PreferenceKey) : BaseStorageProvider<Boolean>(), BooleanStorageProvider { abstract class BaseBooleanStorageProvider(
key: PreferenceKey
) : BaseStorageProvider<Boolean>(),
BooleanStorageProvider {
override val default = BooleanPreferenceDefault(key = key, defaultValue = false) override val default = BooleanPreferenceDefault(key = key, defaultValue = false)
} }

View File

@ -5,6 +5,9 @@ import co.electriccoin.zcash.preference.model.entry.PreferenceKey
interface IntStorageProvider : StorageProvider<Int> interface IntStorageProvider : StorageProvider<Int>
abstract class BaseIntStorageProvider(key: PreferenceKey) : IntStorageProvider, BaseStorageProvider<Int>() { abstract class BaseIntStorageProvider(
key: PreferenceKey
) : BaseStorageProvider<Int>(),
IntStorageProvider {
override val default = IntegerPreferenceDefault(key = key, defaultValue = 0) override val default = IntegerPreferenceDefault(key = key, defaultValue = 0)
} }

View File

@ -7,4 +7,5 @@ interface RestoreTimestampStorageProvider : TimestampStorageProvider
class RestoreTimestampStorageProviderImpl( class RestoreTimestampStorageProviderImpl(
override val preferenceHolder: EncryptedPreferenceProvider override val preferenceHolder: EncryptedPreferenceProvider
) : BaseTimestampStorageProvider(PreferenceKey("restore_timestamp")), RestoreTimestampStorageProvider ) : BaseTimestampStorageProvider(PreferenceKey("restore_timestamp")),
RestoreTimestampStorageProvider

View File

@ -2,7 +2,6 @@ package co.electriccoin.zcash.ui.common.provider
import cash.z.ecc.android.sdk.model.AccountUuid import cash.z.ecc.android.sdk.model.AccountUuid
import co.electriccoin.zcash.preference.EncryptedPreferenceProvider import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
import co.electriccoin.zcash.preference.api.PreferenceProvider
import co.electriccoin.zcash.preference.model.entry.IntegerPreferenceDefault import co.electriccoin.zcash.preference.model.entry.IntegerPreferenceDefault
import co.electriccoin.zcash.preference.model.entry.PreferenceKey import co.electriccoin.zcash.preference.model.entry.PreferenceKey
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -18,22 +17,21 @@ interface ShieldFundsRemindMeCountStorageProvider {
class ShieldFundsRemindMeCountStorageProviderImpl( class ShieldFundsRemindMeCountStorageProviderImpl(
private val encryptedPreferenceProvider: EncryptedPreferenceProvider private val encryptedPreferenceProvider: EncryptedPreferenceProvider
) : ShieldFundsRemindMeCountStorageProvider { ) : ShieldFundsRemindMeCountStorageProvider {
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
private fun getDefault(forAccount: AccountUuid): IntegerPreferenceDefault { private fun getDefault(forAccount: AccountUuid): IntegerPreferenceDefault {
val key = PreferenceKey("shield_funds_remind_me_count_${forAccount.value.toHexString()}") val key = PreferenceKey("shield_funds_remind_me_count_${forAccount.value.toHexString()}")
return IntegerPreferenceDefault(key = key, defaultValue = 0) return IntegerPreferenceDefault(key = key, defaultValue = 0)
} }
override suspend fun get(forAccount: AccountUuid): Int { override suspend fun get(forAccount: AccountUuid): Int =
return getDefault(forAccount).getValue(encryptedPreferenceProvider()) getDefault(forAccount)
} .getValue(encryptedPreferenceProvider())
override suspend fun store(forAccount: AccountUuid, amount: Int) { override suspend fun store(forAccount: AccountUuid, amount: Int) {
getDefault(forAccount).putValue(encryptedPreferenceProvider(), amount) getDefault(forAccount).putValue(encryptedPreferenceProvider(), amount)
} }
override suspend fun observe(forAccount: AccountUuid): Flow<Int> { override suspend fun observe(forAccount: AccountUuid): Flow<Int> =
return getDefault(forAccount).observe(encryptedPreferenceProvider()) getDefault(forAccount)
} .observe(encryptedPreferenceProvider())
} }

View File

@ -2,8 +2,8 @@ package co.electriccoin.zcash.ui.common.provider
import cash.z.ecc.android.sdk.model.AccountUuid import cash.z.ecc.android.sdk.model.AccountUuid
import co.electriccoin.zcash.preference.EncryptedPreferenceProvider import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
import co.electriccoin.zcash.preference.model.entry.TimestampPreferenceDefault
import co.electriccoin.zcash.preference.model.entry.PreferenceKey import co.electriccoin.zcash.preference.model.entry.PreferenceKey
import co.electriccoin.zcash.preference.model.entry.TimestampPreferenceDefault
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import java.time.Instant import java.time.Instant
@ -17,22 +17,20 @@ interface ShieldFundsRemindMeTimestampStorageProvider {
class ShieldFundsRemindMeTimestampStorageProviderImpl( class ShieldFundsRemindMeTimestampStorageProviderImpl(
private val encryptedPreferenceProvider: EncryptedPreferenceProvider private val encryptedPreferenceProvider: EncryptedPreferenceProvider
) : ShieldFundsRemindMeTimestampStorageProvider { ) : ShieldFundsRemindMeTimestampStorageProvider {
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
private fun getDefault(forAccount: AccountUuid): TimestampPreferenceDefault { private fun getDefault(forAccount: AccountUuid): TimestampPreferenceDefault {
val key = PreferenceKey("shield_funds_remind_me_timestamp_${forAccount.value.toHexString()}") val key = PreferenceKey("shield_funds_remind_me_timestamp_${forAccount.value.toHexString()}")
return TimestampPreferenceDefault(key = key) return TimestampPreferenceDefault(key = key)
} }
override suspend fun get(forAccount: AccountUuid): Instant? { override suspend fun get(forAccount: AccountUuid) = getDefault(forAccount).getValue(encryptedPreferenceProvider())
return getDefault(forAccount).getValue(encryptedPreferenceProvider())
}
override suspend fun store(forAccount: AccountUuid, timestamp: Instant) { override suspend fun store(forAccount: AccountUuid, timestamp: Instant) {
getDefault(forAccount).putValue(encryptedPreferenceProvider(), timestamp) getDefault(forAccount).putValue(encryptedPreferenceProvider(), timestamp)
} }
override suspend fun observe(forAccount: AccountUuid): Flow<Instant?> { override suspend fun observe(forAccount: AccountUuid): Flow<Instant?> =
return getDefault(forAccount).observe(encryptedPreferenceProvider()) getDefault(forAccount)
} .observe(encryptedPreferenceProvider())
} }

View File

@ -58,4 +58,3 @@ abstract class BaseNullableStorageProvider<T : Any> : NullableStorageProvider<T>
private suspend fun getPreferenceProvider(): PreferenceProvider = preferenceHolder() private suspend fun getPreferenceProvider(): PreferenceProvider = preferenceHolder()
} }

View File

@ -10,7 +10,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
@ -18,7 +17,6 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.time.Duration
interface SynchronizerProvider { interface SynchronizerProvider {
val synchronizer: StateFlow<Synchronizer?> val synchronizer: StateFlow<Synchronizer?>

View File

@ -8,6 +8,7 @@ interface TimestampStorageProvider : NullableStorageProvider<Instant>
abstract class BaseTimestampStorageProvider( abstract class BaseTimestampStorageProvider(
key: PreferenceKey key: PreferenceKey
) : BaseNullableStorageProvider<Instant>(), TimestampStorageProvider { ) : BaseNullableStorageProvider<Instant>(),
TimestampStorageProvider {
override val default = TimestampPreferenceDefault(key) override val default = TimestampPreferenceDefault(key)
} }

View File

@ -7,4 +7,5 @@ interface WalletBackupConsentStorageProvider : BooleanStorageProvider
class WalletBackupConsentStorageProviderImpl( class WalletBackupConsentStorageProviderImpl(
override val preferenceHolder: EncryptedPreferenceProvider override val preferenceHolder: EncryptedPreferenceProvider
) : BaseBooleanStorageProvider(key = PreferenceKey("wallet_backup_consent")), WalletBackupConsentStorageProvider ) : BaseBooleanStorageProvider(key = PreferenceKey("wallet_backup_consent")),
WalletBackupConsentStorageProvider

View File

@ -7,4 +7,5 @@ interface WalletBackupFlagStorageProvider : BooleanStorageProvider
class WalletBackupFlagStorageProviderImpl( class WalletBackupFlagStorageProviderImpl(
override val preferenceHolder: EncryptedPreferenceProvider override val preferenceHolder: EncryptedPreferenceProvider
) : BaseBooleanStorageProvider(key = PreferenceKey("wallet_backup_flag")), WalletBackupFlagStorageProvider ) : BaseBooleanStorageProvider(key = PreferenceKey("wallet_backup_flag")),
WalletBackupFlagStorageProvider

View File

@ -11,12 +11,12 @@ interface WalletRestoringStateProvider : StorageProvider<WalletRestoringState>
class WalletRestoringStateProviderImpl( class WalletRestoringStateProviderImpl(
override val preferenceHolder: StandardPreferenceProvider, override val preferenceHolder: StandardPreferenceProvider,
) : BaseStorageProvider<WalletRestoringState>(), WalletRestoringStateProvider { ) : BaseStorageProvider<WalletRestoringState>(),
WalletRestoringStateProvider {
override val default: PreferenceDefault<WalletRestoringState> = WalletRestoringStatePreferenceDefault() override val default: PreferenceDefault<WalletRestoringState> = WalletRestoringStatePreferenceDefault()
} }
private class WalletRestoringStatePreferenceDefault : PreferenceDefault<WalletRestoringState> { private class WalletRestoringStatePreferenceDefault : PreferenceDefault<WalletRestoringState> {
private val internal = StandardPreferenceKeys.WALLET_RESTORING_STATE private val internal = StandardPreferenceKeys.WALLET_RESTORING_STATE
override val key: PreferenceKey = internal.key override val key: PreferenceKey = internal.key

View File

@ -44,7 +44,6 @@ class ExchangeRateRepositoryImpl(
private val synchronizerProvider: SynchronizerProvider, private val synchronizerProvider: SynchronizerProvider,
private val standardPreferenceProvider: StandardPreferenceProvider, private val standardPreferenceProvider: StandardPreferenceProvider,
) : ExchangeRateRepository { ) : ExchangeRateRepository {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val isExchangeRateUsdOptedIn = nullableBooleanStateFlow(StandardPreferenceKeys.EXCHANGE_RATE_OPTED_IN) private val isExchangeRateUsdOptedIn = nullableBooleanStateFlow(StandardPreferenceKeys.EXCHANGE_RATE_OPTED_IN)

View File

@ -28,7 +28,6 @@ interface HomeMessageCacheRepository {
class HomeMessageCacheRepositoryImpl( class HomeMessageCacheRepositoryImpl(
private val messageAvailabilityDataSource: MessageAvailabilityDataSource private val messageAvailabilityDataSource: MessageAvailabilityDataSource
) : HomeMessageCacheRepository { ) : HomeMessageCacheRepository {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
override var lastShownMessage: HomeMessageData? = null override var lastShownMessage: HomeMessageData? = null
@ -42,8 +41,7 @@ class HomeMessageCacheRepositoryImpl(
lastShownMessage = null lastShownMessage = null
lastMessage = null lastMessage = null
} }
} }.launchIn(scope)
.launchIn(scope)
} }
override fun reset() { override fun reset() {
@ -52,21 +50,33 @@ class HomeMessageCacheRepositoryImpl(
} }
} }
@Suppress("MagicNumber")
sealed interface HomeMessageData { sealed interface HomeMessageData {
val priority: Int val priority: Int
data class Error(val synchronizerError: SynchronizerError) : RuntimeMessage() data class Error(
val synchronizerError: SynchronizerError
) : RuntimeMessage()
data object Disconnected : RuntimeMessage() data object Disconnected : RuntimeMessage()
data class Restoring(val progress: Float) : RuntimeMessage()
data class Syncing(val progress: Float) : RuntimeMessage() data class Restoring(
val progress: Float
) : RuntimeMessage()
data class Syncing(
val progress: Float
) : RuntimeMessage()
data object Updating : RuntimeMessage() data object Updating : RuntimeMessage()
data object Backup : Prioritized { data object Backup : Prioritized {
override val priority: Int = 4 override val priority: Int = 4
} }
data class ShieldFunds(val zatoshi: Zatoshi) : Prioritized { data class ShieldFunds(
val zatoshi: Zatoshi
) : Prioritized {
override val priority: Int = 3 override val priority: Int = 3
} }
@ -82,6 +92,7 @@ sealed interface HomeMessageData {
/** /**
* Message which always is shown. * Message which always is shown.
*/ */
@Suppress("MagicNumber")
sealed class RuntimeMessage : HomeMessageData { sealed class RuntimeMessage : HomeMessageData {
override val priority: Int = 5 override val priority: Int = 5
} }
@ -89,4 +100,4 @@ sealed class RuntimeMessage : HomeMessageData {
/** /**
* Message which always is displayed only if previous message was lower priority. * Message which always is displayed only if previous message was lower priority.
*/ */
sealed interface Prioritized : HomeMessageData sealed interface Prioritized : HomeMessageData

View File

@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
interface ShieldFundsRepository { interface ShieldFundsRepository {
val availability: Flow<ShieldFundsData> val availability: Flow<ShieldFundsData>
@ -25,31 +24,33 @@ class ShieldFundsRepositoryImpl(
private val messageAvailabilityDataSource: MessageAvailabilityDataSource, private val messageAvailabilityDataSource: MessageAvailabilityDataSource,
) : ShieldFundsRepository { ) : ShieldFundsRepository {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override val availability: Flow<ShieldFundsData> = accountDataSource override val availability: Flow<ShieldFundsData> =
.selectedAccount accountDataSource
.flatMapLatest { account -> .selectedAccount
when { .flatMapLatest { account ->
account == null -> flowOf(ShieldFundsData.Unavailable) when {
account == null -> flowOf(ShieldFundsData.Unavailable)
account.isShieldingAvailable -> account.isShieldingAvailable ->
combine( combine(
messageAvailabilityDataSource.canShowShieldMessage, messageAvailabilityDataSource.canShowShieldMessage,
shieldFundsDataSource.observe(account.sdkAccount.accountUuid) shieldFundsDataSource.observe(account.sdkAccount.accountUuid)
) { canShowShieldMessage, availability -> ) { canShowShieldMessage, availability ->
when { when {
!canShowShieldMessage -> ShieldFundsData.Unavailable !canShowShieldMessage -> ShieldFundsData.Unavailable
availability is ShieldFundsAvailability.Available -> ShieldFundsData.Available( availability is ShieldFundsAvailability.Available ->
lockoutDuration = availability.lockoutDuration, ShieldFundsData.Available(
amount = account.transparent.balance lockoutDuration = availability.lockoutDuration,
) amount = account.transparent.balance
)
else -> ShieldFundsData.Unavailable else -> ShieldFundsData.Unavailable
}
} }
}
else -> flowOf(ShieldFundsData.Unavailable) else -> flowOf(ShieldFundsData.Unavailable)
}
} }
}
override suspend fun remindMeLater() { override suspend fun remindMeLater() {
shieldFundsDataSource.remindMeLater( shieldFundsDataSource.remindMeLater(

View File

@ -73,23 +73,25 @@ class TransactionRepositoryImpl(
) )
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override val currentTransactions: Flow<List<Transaction>?> = accountDataSource.selectedAccount override val currentTransactions: Flow<List<Transaction>?> =
.distinctUntilChangedBy { it?.sdkAccount?.accountUuid } accountDataSource.selectedAccount
.flatMapLatest { selected -> .distinctUntilChangedBy { it?.sdkAccount?.accountUuid }
if (selected is ZashiAccount) { .flatMapLatest { selected ->
zashiTransactions if (selected is ZashiAccount) {
} else { zashiTransactions
observeTransactions( } else {
accountFlow = accountDataSource.selectedAccount.map { it?.sdkAccount?.accountUuid } observeTransactions(
.distinctUntilChanged() accountFlow =
) accountDataSource.selectedAccount
} .map { it?.sdkAccount?.accountUuid }
} .distinctUntilChanged()
.stateIn( )
scope = scope, }
started = SharingStarted.WhileSubscribed(5.seconds, Duration.ZERO), }.stateIn(
initialValue = null scope = scope,
) started = SharingStarted.WhileSubscribed(5.seconds, Duration.ZERO),
initialValue = null
)
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private fun TransactionRepositoryImpl.observeTransactions(accountFlow: Flow<AccountUuid?>) = private fun TransactionRepositoryImpl.observeTransactions(accountFlow: Flow<AccountUuid?>) =
@ -123,6 +125,7 @@ class TransactionRepositoryImpl(
} }
} }
@Suppress("CyclomaticComplexMethod")
private suspend fun createTransactions( private suspend fun createTransactions(
transactions: List<TransactionOverview>, transactions: List<TransactionOverview>,
synchronizer: Synchronizer synchronizer: Synchronizer

View File

@ -6,7 +6,6 @@ import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.WalletInitMode import cash.z.ecc.android.sdk.WalletInitMode
import cash.z.ecc.android.sdk.model.BlockHeight import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.FastestServersResult import cash.z.ecc.android.sdk.model.FastestServersResult
import cash.z.ecc.android.sdk.model.PercentDecimal
import cash.z.ecc.android.sdk.model.PersistableWallet import cash.z.ecc.android.sdk.model.PersistableWallet
import cash.z.ecc.android.sdk.model.SeedPhrase import cash.z.ecc.android.sdk.model.SeedPhrase
import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.android.sdk.model.ZcashNetwork
@ -14,7 +13,6 @@ import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.lightwallet.client.model.LightWalletEndpoint import co.electriccoin.lightwallet.client.model.LightWalletEndpoint
import co.electriccoin.zcash.preference.EncryptedPreferenceProvider import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
import co.electriccoin.zcash.preference.StandardPreferenceProvider import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
import co.electriccoin.zcash.ui.common.datasource.RestoreTimestampDataSource import co.electriccoin.zcash.ui.common.datasource.RestoreTimestampDataSource
import co.electriccoin.zcash.ui.common.datasource.WalletSnapshotDataSource import co.electriccoin.zcash.ui.common.datasource.WalletSnapshotDataSource
@ -28,7 +26,6 @@ import co.electriccoin.zcash.ui.common.provider.PersistableWalletProvider
import co.electriccoin.zcash.ui.common.provider.SynchronizerProvider import co.electriccoin.zcash.ui.common.provider.SynchronizerProvider
import co.electriccoin.zcash.ui.common.provider.WalletRestoringStateProvider import co.electriccoin.zcash.ui.common.provider.WalletRestoringStateProvider
import co.electriccoin.zcash.ui.common.viewmodel.SecretState import co.electriccoin.zcash.ui.common.viewmodel.SecretState
import co.electriccoin.zcash.ui.common.viewmodel.SynchronizerError
import co.electriccoin.zcash.ui.preference.PersistableWalletPreferenceDefault import co.electriccoin.zcash.ui.preference.PersistableWalletPreferenceDefault
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.screen.chooseserver.AvailableServerProvider import co.electriccoin.zcash.ui.screen.chooseserver.AvailableServerProvider
@ -36,13 +33,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll
@ -51,7 +46,6 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -196,13 +190,14 @@ class WalletRepositoryImpl(
/** /**
* A flow of the wallet block synchronization state. * A flow of the wallet block synchronization state.
*/ */
override val walletRestoringState: StateFlow<WalletRestoringState> = walletRestoringStateProvider override val walletRestoringState: StateFlow<WalletRestoringState> =
.observe() walletRestoringStateProvider
.stateIn( .observe()
scope = scope, .stateIn(
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), scope = scope,
initialValue = WalletRestoringState.NONE started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
) initialValue = WalletRestoringState.NONE
)
/** /**
* Persists a wallet asynchronously. Clients observe [secretState] to see the side effects. * Persists a wallet asynchronously. Clients observe [secretState] to see the side effects.

View File

@ -23,7 +23,6 @@ class WalletSnapshotRepositoryImpl(
private val synchronizerProvider: SynchronizerProvider, private val synchronizerProvider: SynchronizerProvider,
private val walletRestoringStateProvider: WalletRestoringStateProvider private val walletRestoringStateProvider: WalletRestoringStateProvider
) : WalletSnapshotRepository { ) : WalletSnapshotRepository {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@ -38,8 +37,7 @@ class WalletSnapshotRepositoryImpl(
status to restoringState status to restoringState
} }
} }
} }.collect { (status, restoringState) ->
.collect { (status, restoringState) ->
// Once the wallet is fully synced and still in restoring state, persist the new state // Once the wallet is fully synced and still in restoring state, persist the new state
if (status == Synchronizer.Status.SYNCED && restoringState in listOf(RESTORING, NONE)) { if (status == Synchronizer.Status.SYNCED && restoringState in listOf(RESTORING, NONE)) {
walletRestoringStateProvider.store(WalletRestoringState.SYNCING) walletRestoringStateProvider.store(WalletRestoringState.SYNCING)

View File

@ -43,10 +43,11 @@ class CreateFlexaTransactionUseCase(
) )
}.onSuccess { proposal -> }.onSuccess { proposal ->
Twig.debug { "Transaction proposal successful: ${proposal.toPrettyString()}" } Twig.debug { "Transaction proposal successful: ${proposal.toPrettyString()}" }
val result = submitTransactions( val result =
proposal = proposal, submitTransactions(
spendingKey = zashiSpendingKeyDataSource.getZashiSpendingKey() proposal = proposal,
) spendingKey = zashiSpendingKeyDataSource.getZashiSpendingKey()
)
when (val output = result.first) { when (val output = result.first) {
is SubmitResult.Success -> { is SubmitResult.Success -> {
Twig.debug { "Transaction successful $result" } Twig.debug { "Transaction successful $result" }

View File

@ -38,80 +38,89 @@ class GetHomeMessageUseCase(
private val messageAvailabilityDataSource: MessageAvailabilityDataSource, private val messageAvailabilityDataSource: MessageAvailabilityDataSource,
private val cache: HomeMessageCacheRepository, private val cache: HomeMessageCacheRepository,
) { ) {
private val backupFlow = combine( private val backupFlow =
transactionRepository.zashiTransactions, combine(
walletBackupDataSource.observe() transactionRepository.zashiTransactions,
) { transactions, backup -> walletBackupDataSource.observe()
if (backup is WalletBackupAvailability.Available && transactions.orEmpty().any { it is ReceiveTransaction }) { ) { transactions, backup ->
backup if (backup is WalletBackupAvailability.Available &&
} else { transactions.orEmpty().any { it is ReceiveTransaction }
WalletBackupAvailability.Unavailable ) {
} backup
}.distinctUntilChanged() } else {
WalletBackupAvailability.Unavailable
}
}.distinctUntilChanged()
private val runtimeMessage = channelFlow { @Suppress("MagicNumber")
var firstSyncing: WalletSnapshot? = null private val runtimeMessage =
launch { channelFlow {
walletSnapshotDataSource var firstSyncing: WalletSnapshot? = null
.observe() launch {
.filterNotNull() walletSnapshotDataSource
.collect { walletSnapshot -> .observe()
val result = when { .filterNotNull()
walletSnapshot.synchronizerError != null -> .collect { walletSnapshot ->
HomeMessageData.Error(walletSnapshot.synchronizerError) val result =
when {
walletSnapshot.synchronizerError != null ->
HomeMessageData.Error(walletSnapshot.synchronizerError)
walletSnapshot.status == Synchronizer.Status.DISCONNECTED -> walletSnapshot.status == Synchronizer.Status.DISCONNECTED ->
HomeMessageData.Disconnected HomeMessageData.Disconnected
walletSnapshot.status in listOf( walletSnapshot.status in
Synchronizer.Status.INITIALIZING, listOf(
Synchronizer.Status.SYNCING, Synchronizer.Status.INITIALIZING,
Synchronizer.Status.STOPPED Synchronizer.Status.SYNCING,
) -> { Synchronizer.Status.STOPPED
val progress = walletSnapshot.progress.decimal * 100f )
if (walletSnapshot.restoringState == WalletRestoringState.RESTORING) { -> {
HomeMessageData.Restoring(progress = progress) val progress = walletSnapshot.progress.decimal * 100f
} else { if (walletSnapshot.restoringState == WalletRestoringState.RESTORING) {
HomeMessageData.Syncing(progress = progress) HomeMessageData.Restoring(progress = progress)
} else {
HomeMessageData.Syncing(progress = progress)
}
}
else -> null
} }
}
else -> null if (result is HomeMessageData.Syncing) {
} if (firstSyncing == null) {
firstSyncing = walletSnapshot
}
if (result is HomeMessageData.Syncing) { if ((firstSyncing?.progress?.decimal ?: 0f) >= .95f) {
if (firstSyncing == null) { send(null)
firstSyncing = walletSnapshot } else {
} send(result)
}
if ((firstSyncing?.progress?.decimal ?: 0f) >= .95f) {
send(null)
} else { } else {
firstSyncing = null
send(result) send(result)
} }
} else {
firstSyncing = null
send(result)
} }
} }
}
awaitClose { awaitClose {
// do nothing // do nothing
}
} }
}
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
private val flow = combine( private val flow =
runtimeMessage, combine(
backupFlow, runtimeMessage,
exchangeRateRepository.state.map { it == ExchangeRateState.OptIn }.distinctUntilChanged(), backupFlow,
shieldFundsRepository.availability exchangeRateRepository.state.map { it == ExchangeRateState.OptIn }.distinctUntilChanged(),
) { runtimeMessage, backup, isCCAvailable, shieldFunds -> shieldFundsRepository.availability
createMessage(runtimeMessage, backup, shieldFunds, isCCAvailable) ) { runtimeMessage, backup, isCCAvailable, shieldFunds ->
}.distinctUntilChanged() createMessage(runtimeMessage, backup, shieldFunds, isCCAvailable)
.debounce(.5.seconds) }.distinctUntilChanged()
.map { message -> prioritizeMessage(message) } .debounce(.5.seconds)
.map { message -> prioritizeMessage(message) }
fun observe(): Flow<HomeMessageData?> = flow fun observe(): Flow<HomeMessageData?> = flow
@ -133,18 +142,20 @@ class GetHomeMessageUseCase(
val someMessageBeenShown = cache.lastShownMessage != null // has any message been shown while app in fg val someMessageBeenShown = cache.lastShownMessage != null // has any message been shown while app in fg
val hasNoMessageBeenShownLately = cache.lastMessage == null // has no message been shown val hasNoMessageBeenShownLately = cache.lastMessage == null // has no message been shown
val isHigherPriorityMessage = (message?.priority ?: 0) > (cache.lastShownMessage?.priority ?: 0) val isHigherPriorityMessage = (message?.priority ?: 0) > (cache.lastShownMessage?.priority ?: 0)
val result = when { val result =
message == null -> null when {
message is RuntimeMessage -> message message == null -> null
isSameMessageUpdate -> message message is RuntimeMessage -> message
isHigherPriorityMessage -> if (hasNoMessageBeenShownLately) { isSameMessageUpdate -> message
if (someMessageBeenShown) null else message isHigherPriorityMessage ->
} else { if (hasNoMessageBeenShownLately) {
message if (someMessageBeenShown) null else message
} } else {
message
}
else -> null else -> null
} }
if (result != null) { if (result != null) {
messageAvailabilityDataSource.onMessageShown() messageAvailabilityDataSource.onMessageShown()

View File

@ -29,8 +29,19 @@ class NavigateToErrorUseCase(
} }
sealed interface ErrorArgs { sealed interface ErrorArgs {
data class SyncError(val synchronizerError: SynchronizerError) : ErrorArgs data class SyncError(
data class ShieldingError(val error: SubmitResult.Failure): ErrorArgs val synchronizerError: SynchronizerError
data class ShieldingGeneralError(val exception: Exception): ErrorArgs ) : ErrorArgs
data class General(val exception: Exception): ErrorArgs
data class ShieldingError(
val error: SubmitResult.Failure
) : ErrorArgs
data class ShieldingGeneralError(
val exception: Exception
) : ErrorArgs
data class General(
val exception: Exception
) : ErrorArgs
} }

View File

@ -11,4 +11,4 @@ class OnUserSavedWalletBackupUseCase(
walletBackupDataSource.onUserSavedWalletBackup() walletBackupDataSource.onUserSavedWalletBackup()
navigationRouter.backToRoot() navigationRouter.backToRoot()
} }
} }

View File

@ -1,7 +1,6 @@
package co.electriccoin.zcash.ui.common.usecase package co.electriccoin.zcash.ui.common.usecase
import co.electriccoin.zcash.ui.NavigationRouter import co.electriccoin.zcash.ui.NavigationRouter
import co.electriccoin.zcash.ui.common.datasource.WalletBackupDataSource
import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepository import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepository
class RemindShieldFundsLaterUseCase( class RemindShieldFundsLaterUseCase(
@ -12,4 +11,4 @@ class RemindShieldFundsLaterUseCase(
shieldFundsRepository.remindMeLater() shieldFundsRepository.remindMeLater()
navigationRouter.backToRoot() navigationRouter.backToRoot()
} }
} }

View File

@ -16,4 +16,4 @@ class RemindWalletBackupLaterUseCase(
walletBackupDataSource.remindMeLater() walletBackupDataSource.remindMeLater()
navigationRouter.backToRoot() navigationRouter.backToRoot()
} }
} }

View File

@ -50,7 +50,6 @@ class SendEmailUseCase(
runCatching { context.startActivity(mailIntent) } runCatching { context.startActivity(mailIntent) }
} }
suspend operator fun invoke(synchronizerError: SynchronizerError) { suspend operator fun invoke(synchronizerError: SynchronizerError) {
val fullMessage = val fullMessage =
EmailUtil.formatMessage( EmailUtil.formatMessage(

View File

@ -46,14 +46,16 @@ class ShieldFundsUseCase(
} }
} }
@Suppress("TooGenericExceptionCaught")
private suspend fun shieldZashiFunds() { private suspend fun shieldZashiFunds() {
try { try {
zashiProposalRepository.createShieldProposal() zashiProposalRepository.createShieldProposal()
zashiProposalRepository.submitTransaction() zashiProposalRepository.submitTransaction()
val result = zashiProposalRepository.submitState val result =
.filterIsInstance<SubmitProposalState.Result>() zashiProposalRepository.submitState
.first() .filterIsInstance<SubmitProposalState.Result>()
.submitResult .first()
.submitResult
if (result is SubmitResult.Failure) { if (result is SubmitResult.Failure) {
navigateToError(ErrorArgs.ShieldingError(result)) navigateToError(ErrorArgs.ShieldingError(result))
@ -65,6 +67,7 @@ class ShieldFundsUseCase(
} }
} }
@Suppress("TooGenericExceptionCaught")
private suspend fun createKeystoneShieldProposal() { private suspend fun createKeystoneShieldProposal() {
try { try {
keystoneProposalRepository.createShieldProposal() keystoneProposalRepository.createShieldProposal()
@ -75,4 +78,4 @@ class ShieldFundsUseCase(
navigateToError(ErrorArgs.ShieldingGeneralError(e)) navigateToError(ErrorArgs.ShieldingGeneralError(e))
} }
} }
} }

View File

@ -8,8 +8,8 @@ import co.electriccoin.zcash.ui.NavigationTargets
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.WalletRestoringState import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToWalletBackupUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToTaxExportUseCase import co.electriccoin.zcash.ui.common.usecase.NavigateToTaxExportUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToWalletBackupUseCase
import co.electriccoin.zcash.ui.design.component.ButtonState import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.design.util.stringRes

View File

@ -103,12 +103,13 @@ private fun BalanceWidgetPreview() {
state = state =
BalanceWidgetState( BalanceWidgetState(
totalBalance = Zatoshi(1234567891234567L), totalBalance = Zatoshi(1234567891234567L),
button = BalanceButtonState( button =
icon = R.drawable.ic_help, BalanceButtonState(
text = stringRes("text"), icon = R.drawable.ic_help,
amount = Zatoshi(1000), text = stringRes("text"),
onClick = {} amount = Zatoshi(1000),
), onClick = {}
),
exchangeRate = ObserveFiatCurrencyResultFixture.new(), exchangeRate = ObserveFiatCurrencyResultFixture.new(),
showDust = true showDust = true
), ),

View File

@ -41,10 +41,11 @@ internal fun BalanceWidgetButton(
state: BalanceButtonState, state: BalanceButtonState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val colors = ZashiButtonDefaults.secondaryColors( val colors =
containerColor = ZashiColors.Surfaces.bgPrimary, ZashiButtonDefaults.secondaryColors(
borderColor = ZashiColors.Utility.Gray.utilityGray100 containerColor = ZashiColors.Surfaces.bgPrimary,
) borderColor = ZashiColors.Utility.Gray.utilityGray100
)
val borderColor = colors.borderColor val borderColor = colors.borderColor
Button( Button(
@ -53,10 +54,11 @@ internal fun BalanceWidgetButton(
shape = RoundedCornerShape(ZashiDimensions.Radius.radiusIg), shape = RoundedCornerShape(ZashiDimensions.Radius.radiusIg),
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 =
defaultElevation = 1.dp, ButtonDefaults.buttonElevation(
pressedElevation = 0.dp defaultElevation = 1.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) },
content = { content = {
Row( Row(
@ -110,15 +112,17 @@ data class BalanceButtonState(
@PreviewScreens @PreviewScreens
@Composable @Composable
private fun Preview() = ZcashTheme { private fun Preview() =
BlankSurface { ZcashTheme {
BalanceWidgetButton( BlankSurface {
state = BalanceButtonState( BalanceWidgetButton(
icon = R.drawable.ic_help, state =
text = stringRes("text"), BalanceButtonState(
amount = Zatoshi(1000), icon = R.drawable.ic_help,
onClick = {} text = stringRes("text"),
amount = Zatoshi(1000),
onClick = {}
)
) )
) }
} }
}

View File

@ -34,54 +34,57 @@ class BalanceWidgetViewModel(
}.stateIn( }.stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = createState( initialValue =
account = accountDataSource.allAccounts.value?.firstOrNull { it.isSelected }, createState(
exchangeRateUsd = exchangeRateRepository.state.value account = accountDataSource.allAccounts.value?.firstOrNull { it.isSelected },
) exchangeRateUsd = exchangeRateRepository.state.value
)
) )
@Suppress("CyclomaticComplexMethod")
private fun createState(account: WalletAccount?, exchangeRateUsd: ExchangeRateState) = private fun createState(account: WalletAccount?, exchangeRateUsd: ExchangeRateState) =
BalanceWidgetState( BalanceWidgetState(
totalBalance = account?.totalBalance ?: Zatoshi(0), totalBalance = account?.totalBalance ?: Zatoshi(0),
exchangeRate = if (args.isExchangeRateButtonEnabled) exchangeRateUsd else null, exchangeRate = if (args.isExchangeRateButtonEnabled) exchangeRateUsd else null,
button = when { button =
!args.isBalanceButtonEnabled -> null when {
account == null -> null !args.isBalanceButtonEnabled -> null
account.totalBalance == account.spendableShieldedBalance -> null account == null -> null
account.totalBalance > account.spendableShieldedBalance && account.totalBalance == account.spendableShieldedBalance -> null
!account.isShieldedPending && account.totalBalance > account.spendableShieldedBalance &&
account.totalShieldedBalance > Zatoshi(0) && !account.isShieldedPending &&
account.spendableShieldedBalance == Zatoshi(0) && account.totalShieldedBalance > Zatoshi(0) &&
account.totalTransparentBalance > Zatoshi(0) -> account.spendableShieldedBalance == Zatoshi(0) &&
BalanceButtonState( account.totalTransparentBalance > Zatoshi(0) ->
icon = R.drawable.ic_balances_expand, BalanceButtonState(
text = stringRes(R.string.widget_balances_button_spendable), icon = R.drawable.ic_balances_expand,
amount = null, text = stringRes(R.string.widget_balances_button_spendable),
onClick = ::onBalanceButtonClick amount = null,
) onClick = ::onBalanceButtonClick
)
account.totalBalance > account.spendableShieldedBalance && account.totalBalance > account.spendableShieldedBalance &&
account.isShieldedPending && account.isShieldedPending &&
account.totalShieldedBalance > Zatoshi(0) && account.totalShieldedBalance > Zatoshi(0) &&
account.spendableShieldedBalance == Zatoshi(0) && account.spendableShieldedBalance == Zatoshi(0) &&
account.totalTransparentBalance == Zatoshi(0) -> account.totalTransparentBalance == Zatoshi(0) ->
BalanceButtonState( BalanceButtonState(
icon = R.drawable.ic_balances_expand, icon = R.drawable.ic_balances_expand,
text = stringRes(R.string.widget_balances_button_spendable), text = stringRes(R.string.widget_balances_button_spendable),
amount = null, amount = null,
onClick = ::onBalanceButtonClick onClick = ::onBalanceButtonClick
) )
account.totalBalance > account.spendableShieldedBalance ->
BalanceButtonState(
icon = R.drawable.ic_balances_expand,
text = stringRes(R.string.widget_balances_button_spendable),
amount = account.spendableShieldedBalance,
onClick = ::onBalanceButtonClick
)
account.totalBalance > account.spendableShieldedBalance -> BalanceButtonState( else -> null
icon = R.drawable.ic_balances_expand, },
text = stringRes(R.string.widget_balances_button_spendable),
amount = account.spendableShieldedBalance,
onClick = ::onBalanceButtonClick
)
else -> null
},
showDust = args.showDust showDust = args.showDust
) )

View File

@ -15,7 +15,7 @@ data class BalanceActionState(
val shieldButton: BalanceShieldButtonState?, val shieldButton: BalanceShieldButtonState?,
val positive: ButtonState, val positive: ButtonState,
override val onBack: () -> Unit, override val onBack: () -> Unit,
): ModalBottomSheetState ) : ModalBottomSheetState
@Immutable @Immutable
data class BalanceActionRowState( data class BalanceActionRowState(

View File

@ -108,11 +108,12 @@ private fun BalanceActionRow(state: BalanceActionRowState) {
) )
Spacer(1f) Spacer(1f)
when (state.icon) { when (state.icon) {
is ImageResource.ByDrawable -> Image( is ImageResource.ByDrawable ->
modifier = Modifier.size(20.dp), Image(
painter = painterResource(state.icon.resource), modifier = Modifier.size(20.dp),
contentDescription = null painter = painterResource(state.icon.resource),
) contentDescription = null
)
ImageResource.Loading -> LottieProgress(modifier = Modifier.size(20.dp)) ImageResource.Loading -> LottieProgress(modifier = Modifier.size(20.dp))
is ImageResource.DisplayString -> { is ImageResource.DisplayString -> {
// do nothing // do nothing
@ -122,11 +123,12 @@ private fun BalanceActionRow(state: BalanceActionRowState) {
SelectionContainer { SelectionContainer {
Text( Text(
text = state.value.getValue(), text = state.value.getValue(),
color = if (state.icon is ImageResource.Loading) { color =
ZashiColors.Text.textTertiary if (state.icon is ImageResource.Loading) {
} else { ZashiColors.Text.textTertiary
ZashiColors.Text.textPrimary } else {
}, ZashiColors.Text.textPrimary
},
style = ZashiTypography.textSm, style = ZashiTypography.textSm,
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium
) )
@ -176,9 +178,11 @@ private fun BalanceShieldButton(state: BalanceShieldButtonState) {
} }
Spacer(1f) Spacer(1f)
ZashiButton( ZashiButton(
state = ButtonState( state =
text = stringRes(R.string.balance_action_shield), ButtonState(
onClick = state.onShieldClick) text = stringRes(R.string.balance_action_shield),
onClick = state.onShieldClick
)
) )
} }
} }
@ -187,31 +191,36 @@ private fun BalanceShieldButton(state: BalanceShieldButtonState) {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@PreviewScreens @PreviewScreens
@Composable @Composable
private fun Preview() = ZcashTheme { private fun Preview() =
BalanceActionView( ZcashTheme {
state = BalanceActionState( BalanceActionView(
title = stringRes("Title"), state =
message = stringRes("Subtitle"), BalanceActionState(
positive = ButtonState( title = stringRes("Title"),
text = stringRes("Positive") message = stringRes("Subtitle"),
), positive =
onBack = {}, ButtonState(
rows = listOf( text = stringRes("Positive")
BalanceActionRowState( ),
title = stringRes("Row"), onBack = {},
icon = loadingImageRes(), rows =
value = stringRes("Value") listOf(
), BalanceActionRowState(
BalanceActionRowState( title = stringRes("Row"),
title = stringRes("Row"), icon = loadingImageRes(),
icon = imageRes(R.drawable.ic_balance_shield), value = stringRes("Value")
value = stringRes("Value") ),
BalanceActionRowState(
title = stringRes("Row"),
icon = imageRes(R.drawable.ic_balance_shield),
value = stringRes("Value")
)
),
shieldButton =
BalanceShieldButtonState(
amount = Zatoshi(10000),
onShieldClick = {}
)
) )
),
shieldButton = BalanceShieldButtonState(
amount = Zatoshi(10000),
onShieldClick = {}
)
) )
) }
}

View File

@ -23,15 +23,20 @@ class BalanceActionViewModel(
private val navigationRouter: NavigationRouter, private val navigationRouter: NavigationRouter,
private val shieldFunds: ShieldFundsUseCase, private val shieldFunds: ShieldFundsUseCase,
) : ViewModel() { ) : ViewModel() {
val state = accountDataSource.selectedAccount val state =
.mapNotNull { accountDataSource.selectedAccount
createState(it) .mapNotNull {
} createState(it)
.stateIn( }.stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = createState(accountDataSource.allAccounts.value.orEmpty().firstOrNull { it.isSelected }) initialValue =
) createState(
accountDataSource.allAccounts.value
.orEmpty()
.firstOrNull { it.isSelected }
)
)
private fun createState(account: WalletAccount?): BalanceActionState? { private fun createState(account: WalletAccount?): BalanceActionState? {
if (account == null) return null if (account == null) return null
@ -47,15 +52,16 @@ class BalanceActionViewModel(
} }
private fun createMessage(account: WalletAccount): StringResource { private fun createMessage(account: WalletAccount): StringResource {
val pending = when { val pending =
account.totalShieldedBalance == account.spendableShieldedBalance -> when {
stringRes(R.string.balance_action_all_shielded) account.totalShieldedBalance == account.spendableShieldedBalance ->
stringRes(R.string.balance_action_all_shielded)
account.totalShieldedBalance > account.spendableShieldedBalance -> account.totalShieldedBalance > account.spendableShieldedBalance ->
stringRes(R.string.balance_action_pending) stringRes(R.string.balance_action_pending)
else -> null else -> null
} }
val shielding = stringRes(R.string.balance_action_shield_message).takeIf { account.isShieldingAvailable } val shielding = stringRes(R.string.balance_action_shield_message).takeIf { account.isShieldingAvailable }
@ -66,51 +72,54 @@ class BalanceActionViewModel(
} }
} }
private fun createPositiveButton(account: WalletAccount) = ButtonState( private fun createPositiveButton(account: WalletAccount) =
text = if (account.isShieldingAvailable) { ButtonState(
stringRes(R.string.general_dismiss) text =
} else { if (account.isShieldingAvailable) {
stringRes(R.string.general_ok) stringRes(R.string.general_dismiss)
}, } else {
onClick = ::onBack stringRes(R.string.general_ok)
) },
onClick = ::onBack
)
private fun createInfoRows(account: WalletAccount) = listOfNotNull( private fun createInfoRows(account: WalletAccount) =
BalanceActionRowState( listOfNotNull(
title = stringRes(R.string.balance_action_info_shielded), BalanceActionRowState(
icon = imageRes(R.drawable.ic_balance_shield), title = stringRes(R.string.balance_action_info_shielded),
value = stringRes(R.string.general_zec, stringRes(account.spendableShieldedBalance)) icon = imageRes(R.drawable.ic_balance_shield),
), value = stringRes(R.string.general_zec, stringRes(account.spendableShieldedBalance))
when { ),
account.totalShieldedBalance > account.spendableShieldedBalance && account.isShieldedPending -> when {
BalanceActionRowState( account.totalShieldedBalance > account.spendableShieldedBalance && account.isShieldedPending ->
title = stringRes(R.string.balance_action_info_pending), BalanceActionRowState(
icon = loadingImageRes(), title = stringRes(R.string.balance_action_info_pending),
value = stringRes(R.string.general_zec, stringRes(account.pendingShieldedBalance)) icon = loadingImageRes(),
) value = stringRes(R.string.general_zec, stringRes(account.pendingShieldedBalance))
account.totalShieldedBalance > account.spendableShieldedBalance ->
BalanceActionRowState(
title = stringRes(R.string.balance_action_info_pending),
icon = loadingImageRes(),
value = stringRes(
R.string.general_zec,
stringRes(account.totalShieldedBalance - account.spendableShieldedBalance)
) )
)
else -> null account.totalShieldedBalance > account.spendableShieldedBalance ->
}, BalanceActionRowState(
) title = stringRes(R.string.balance_action_info_pending),
icon = loadingImageRes(),
value =
stringRes(
R.string.general_zec,
stringRes(account.totalShieldedBalance - account.spendableShieldedBalance)
)
)
private fun createShieldButtonState(account: WalletAccount): BalanceShieldButtonState? { else -> null
return BalanceShieldButtonState( },
)
private fun createShieldButtonState(account: WalletAccount): BalanceShieldButtonState? =
BalanceShieldButtonState(
amount = account.transparent.balance, amount = account.transparent.balance,
onShieldClick = ::onShieldClick onShieldClick = ::onShieldClick
).takeIf { account.isShieldingAvailable } ).takeIf { account.isShieldingAvailable }
}
private fun onBack() = navigationRouter.back() private fun onBack() = navigationRouter.back()
private fun onShieldClick() = shieldFunds(closeCurrentScreen = true) private fun onShieldClick() = shieldFunds(closeCurrentScreen = true)
} }

View File

@ -7,7 +7,6 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R

View File

@ -85,18 +85,22 @@ fun BottomSheetContent(state: ErrorState, modifier: Modifier = Modifier) {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@PreviewScreens @PreviewScreens
@Composable @Composable
private fun Preview() = ZcashTheme { private fun Preview() =
BottomSheetErrorView( ZcashTheme {
state = ErrorState( BottomSheetErrorView(
title = stringRes("Error"), state =
message = stringRes("Something went wrong"), ErrorState(
positive = ButtonState( title = stringRes("Error"),
text = stringRes("Positive") message = stringRes("Something went wrong"),
), positive =
negative = ButtonState( ButtonState(
text = stringRes("Negative") text = stringRes("Positive")
), ),
onBack = {} negative =
ButtonState(
text = stringRes("Negative")
),
onBack = {}
)
) )
) }
}

View File

@ -11,32 +11,37 @@ import co.electriccoin.zcash.ui.design.util.stringRes
@Composable @Composable
fun DialogView(state: ErrorState?) { fun DialogView(state: ErrorState?) {
ZashiScreenDialog( ZashiScreenDialog(
state = state?.let { state =
DialogState( state?.let {
title = it.title, DialogState(
message = it.message, title = it.title,
positive = it.positive, message = it.message,
negative = it.negative, positive = it.positive,
onDismissRequest = it.onBack negative = it.negative,
) onDismissRequest = it.onBack
}, )
},
) )
} }
@PreviewScreens @PreviewScreens
@Composable @Composable
private fun Preview() = ZcashTheme { private fun Preview() =
DialogView( ZcashTheme {
state = ErrorState( DialogView(
title = stringRes("Error"), state =
message = stringRes("Something went wrong"), ErrorState(
positive = ButtonState( title = stringRes("Error"),
text = stringRes("Positive") message = stringRes("Something went wrong"),
), positive =
negative = ButtonState( ButtonState(
text = stringRes("Negative") text = stringRes("Positive")
), ),
onBack = {} negative =
ButtonState(
text = stringRes("Negative")
),
onBack = {}
)
) )
) }
}

View File

@ -10,4 +10,4 @@ data class ErrorState(
val positive: ButtonState, val positive: ButtonState,
val negative: ButtonState, val negative: ButtonState,
override val onBack: () -> Unit override val onBack: () -> Unit
): ModalBottomSheetState ) : ModalBottomSheetState

View File

@ -33,96 +33,115 @@ class ErrorViewModel(
private fun onBack() = navigationRouter.back() private fun onBack() = navigationRouter.back()
private fun createState(args: ErrorArgs): ErrorState = when (args) { private fun createState(args: ErrorArgs): ErrorState =
is ErrorArgs.SyncError -> createSyncErrorState(args) when (args) {
is ErrorArgs.ShieldingError -> createShieldingErrorState(args) is ErrorArgs.SyncError -> createSyncErrorState(args)
is ErrorArgs.General -> createGeneralErrorState(args) is ErrorArgs.ShieldingError -> createShieldingErrorState(args)
is ErrorArgs.ShieldingGeneralError -> createGeneralShieldingErrorState(args) is ErrorArgs.General -> createGeneralErrorState(args)
} is ErrorArgs.ShieldingGeneralError -> createGeneralShieldingErrorState(args)
private fun createSyncErrorState(args: ErrorArgs.SyncError) = ErrorState(
title = stringRes(R.string.error_sync_title),
message = stringRes(args.synchronizerError.getStackTrace(STACKTRACE_LIMIT).orEmpty()),
positive = ButtonState(
text = stringRes(R.string.general_ok),
onClick = { navigationRouter.back() }
),
negative = ButtonState(
text = stringRes(R.string.general_report),
onClick = { sendReportClick(args) }
),
onBack = ::onBack,
)
private fun createShieldingErrorState(args: ErrorArgs.ShieldingError) = ErrorState(
title = stringRes(R.string.error_shielding_title),
message = when (args.error) {
is SubmitResult.MultipleTrxFailure -> stringRes(R.string.error_shielding_message_grpc)
is SubmitResult.SimpleTrxFailure -> stringRes(
R.string.error_shielding_message,
stringRes(args.error.toErrorStacktrace())
)
},
positive = ButtonState(
text = stringRes(R.string.general_ok),
onClick = { navigationRouter.back() }
),
negative = ButtonState(
text = stringRes(R.string.general_report),
onClick = { sendReportClick(args) }
),
onBack = ::onBack,
)
private fun createGeneralErrorState(args: ErrorArgs.General) = ErrorState(
title = stringRes(R.string.error_general_title),
message = stringRes(R.string.error_general_message),
positive = ButtonState(
text = stringRes(R.string.general_ok),
onClick = { navigationRouter.back() }
),
negative = ButtonState(
text = stringRes(R.string.general_report),
onClick = { sendReportClick(args.exception) }
),
onBack = ::onBack,
)
private fun createGeneralShieldingErrorState(args: ErrorArgs.ShieldingGeneralError) = ErrorState(
title = stringRes(R.string.error_shielding_title),
message = stringRes(
R.string.error_shielding_message,
stringRes(args.exception.stackTraceToString().take(STACKTRACE_LIMIT))
),
positive = ButtonState(
text = stringRes(R.string.general_ok),
onClick = { navigationRouter.back() }
),
negative = ButtonState(
text = stringRes(R.string.general_report),
onClick = { sendReportClick(args.exception) }
),
onBack = ::onBack,
)
private fun sendReportClick(args: ErrorArgs.ShieldingError) = viewModelScope.launch {
withContext(NonCancellable) {
navigationRouter.back()
sendEmailUseCase(args.error)
} }
}
private fun sendReportClick(args: ErrorArgs.SyncError) = viewModelScope.launch { private fun createSyncErrorState(args: ErrorArgs.SyncError) =
withContext(NonCancellable) { ErrorState(
navigationRouter.back() title = stringRes(R.string.error_sync_title),
sendEmailUseCase(args.synchronizerError) message = stringRes(args.synchronizerError.getStackTrace(STACKTRACE_LIMIT).orEmpty()),
} positive =
} ButtonState(
text = stringRes(R.string.general_ok),
onClick = { navigationRouter.back() }
),
negative =
ButtonState(
text = stringRes(R.string.general_report),
onClick = { sendReportClick(args) }
),
onBack = ::onBack,
)
private fun sendReportClick(exception: Exception) = viewModelScope.launch { private fun createShieldingErrorState(args: ErrorArgs.ShieldingError) =
withContext(NonCancellable) { ErrorState(
navigationRouter.back() title = stringRes(R.string.error_shielding_title),
sendEmailUseCase(exception) message =
when (args.error) {
is SubmitResult.MultipleTrxFailure -> stringRes(R.string.error_shielding_message_grpc)
is SubmitResult.SimpleTrxFailure ->
stringRes(
R.string.error_shielding_message,
stringRes(args.error.toErrorStacktrace())
)
},
positive =
ButtonState(
text = stringRes(R.string.general_ok),
onClick = { navigationRouter.back() }
),
negative =
ButtonState(
text = stringRes(R.string.general_report),
onClick = { sendReportClick(args) }
),
onBack = ::onBack,
)
private fun createGeneralErrorState(args: ErrorArgs.General) =
ErrorState(
title = stringRes(R.string.error_general_title),
message = stringRes(R.string.error_general_message),
positive =
ButtonState(
text = stringRes(R.string.general_ok),
onClick = { navigationRouter.back() }
),
negative =
ButtonState(
text = stringRes(R.string.general_report),
onClick = { sendReportClick(args.exception) }
),
onBack = ::onBack,
)
private fun createGeneralShieldingErrorState(args: ErrorArgs.ShieldingGeneralError) =
ErrorState(
title = stringRes(R.string.error_shielding_title),
message =
stringRes(
R.string.error_shielding_message,
stringRes(args.exception.stackTraceToString().take(STACKTRACE_LIMIT))
),
positive =
ButtonState(
text = stringRes(R.string.general_ok),
onClick = { navigationRouter.back() }
),
negative =
ButtonState(
text = stringRes(R.string.general_report),
onClick = { sendReportClick(args.exception) }
),
onBack = ::onBack,
)
private fun sendReportClick(args: ErrorArgs.ShieldingError) =
viewModelScope.launch {
withContext(NonCancellable) {
navigationRouter.back()
sendEmailUseCase(args.error)
}
}
private fun sendReportClick(args: ErrorArgs.SyncError) =
viewModelScope.launch {
withContext(NonCancellable) {
navigationRouter.back()
sendEmailUseCase(args.synchronizerError)
}
}
private fun sendReportClick(exception: Exception) =
viewModelScope.launch {
withContext(NonCancellable) {
navigationRouter.back()
sendEmailUseCase(exception)
}
} }
}
} }

View File

@ -4,4 +4,4 @@ data class ExchangeRateOptInState(
val onEnableClick: () -> Unit, val onEnableClick: () -> Unit,
val onBack: () -> Unit, val onBack: () -> Unit,
val onSkipClick: () -> Unit, val onSkipClick: () -> Unit,
) )

View File

@ -77,11 +77,12 @@ private fun CurrencyConversionOptInPreview() =
ZcashTheme { ZcashTheme {
BlankSurface { BlankSurface {
ExchangeRateOptInView( ExchangeRateOptInView(
state = ExchangeRateOptInState( state =
onEnableClick = {}, ExchangeRateOptInState(
onBack = {}, onEnableClick = {},
onSkipClick = {}, onBack = {},
) onSkipClick = {},
)
) )
} }
} }

View File

@ -9,14 +9,15 @@ import kotlinx.coroutines.flow.StateFlow
class ExchangeRateOptInViewModel( class ExchangeRateOptInViewModel(
private val exchangeRateRepository: ExchangeRateRepository, private val exchangeRateRepository: ExchangeRateRepository,
private val navigationRouter: NavigationRouter private val navigationRouter: NavigationRouter
): ViewModel() { ) : ViewModel() {
val state: StateFlow<ExchangeRateOptInState> = MutableStateFlow( val state: StateFlow<ExchangeRateOptInState> =
ExchangeRateOptInState( MutableStateFlow(
onBack = ::dismissOptInExchangeRateUsd, ExchangeRateOptInState(
onEnableClick = ::optInExchangeRateUsd, onBack = ::dismissOptInExchangeRateUsd,
onSkipClick = ::onSkipClick onEnableClick = ::optInExchangeRateUsd,
onSkipClick = ::onSkipClick
)
) )
)
private fun onSkipClick() { private fun onSkipClick() {
exchangeRateRepository.optInExchangeRateUsd(false) exchangeRateRepository.optInExchangeRateUsd(false)
@ -31,4 +32,4 @@ class ExchangeRateOptInViewModel(
private fun dismissOptInExchangeRateUsd() { private fun dismissOptInExchangeRateUsd() {
navigationRouter.back() navigationRouter.back()
} }
} }

View File

@ -7,4 +7,4 @@ data class ExchangeRateSettingsState(
val isOptedIn: Boolean, val isOptedIn: Boolean,
val onSaveClick: (optIn: Boolean) -> Unit, val onSaveClick: (optIn: Boolean) -> Unit,
val onDismiss: () -> Unit, val onDismiss: () -> Unit,
) )

View File

@ -187,11 +187,12 @@ private fun SettingsExchangeRateOptInPreview() =
ZcashTheme { ZcashTheme {
BlankSurface { BlankSurface {
ExchangeRateSettingsView( ExchangeRateSettingsView(
state = ExchangeRateSettingsState( state =
isOptedIn = true, ExchangeRateSettingsState(
onSaveClick = {}, isOptedIn = true,
onDismiss = {} onSaveClick = {},
) onDismiss = {}
)
) )
} }
} }

View File

@ -15,16 +15,15 @@ class ExchangeRateSettingsViewModel(
private val exchangeRateRepository: ExchangeRateRepository, private val exchangeRateRepository: ExchangeRateRepository,
private val navigationRouter: NavigationRouter, private val navigationRouter: NavigationRouter,
) : ViewModel() { ) : ViewModel() {
val state =
val state = exchangeRateRepository.state exchangeRateRepository.state
.map { .map {
createState(it) createState(it)
} }.stateIn(
.stateIn( scope = viewModelScope,
scope = viewModelScope, started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), initialValue = createState(exchangeRateRepository.state.value)
initialValue = createState(exchangeRateRepository.state.value) )
)
private fun createState(it: ExchangeRateState) = private fun createState(it: ExchangeRateState) =
ExchangeRateSettingsState( ExchangeRateSettingsState(
@ -41,4 +40,4 @@ class ExchangeRateSettingsViewModel(
exchangeRateRepository.optInExchangeRateUsd(optIn = optInt) exchangeRateRepository.optInExchangeRateUsd(optIn = optInt)
navigationRouter.back() navigationRouter.back()
} }
} }

View File

@ -18,15 +18,16 @@ import org.koin.core.parameter.parametersOf
@Composable @Composable
internal fun AndroidHome() { internal fun AndroidHome() {
val topAppBarViewModel = koinActivityViewModel<ZashiTopAppBarViewModel>() val topAppBarViewModel = koinActivityViewModel<ZashiTopAppBarViewModel>()
val balanceWidgetViewModel = koinViewModel<BalanceWidgetViewModel> { val balanceWidgetViewModel =
parametersOf( koinViewModel<BalanceWidgetViewModel> {
BalanceWidgetArgs( parametersOf(
isBalanceButtonEnabled = false, BalanceWidgetArgs(
isExchangeRateButtonEnabled = true, isBalanceButtonEnabled = false,
showDust = 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()

View File

@ -47,10 +47,10 @@ import co.electriccoin.zcash.ui.screen.home.reporting.CrashReportMessage
import co.electriccoin.zcash.ui.screen.home.reporting.CrashReportMessageState import co.electriccoin.zcash.ui.screen.home.reporting.CrashReportMessageState
import co.electriccoin.zcash.ui.screen.home.restoring.WalletRestoringMessage import co.electriccoin.zcash.ui.screen.home.restoring.WalletRestoringMessage
import co.electriccoin.zcash.ui.screen.home.restoring.WalletRestoringMessageState import co.electriccoin.zcash.ui.screen.home.restoring.WalletRestoringMessageState
import co.electriccoin.zcash.ui.screen.home.syncing.WalletSyncingMessage
import co.electriccoin.zcash.ui.screen.home.syncing.WalletSyncingMessageState
import co.electriccoin.zcash.ui.screen.home.shieldfunds.ShieldFundsMessage import co.electriccoin.zcash.ui.screen.home.shieldfunds.ShieldFundsMessage
import co.electriccoin.zcash.ui.screen.home.shieldfunds.ShieldFundsMessageState import co.electriccoin.zcash.ui.screen.home.shieldfunds.ShieldFundsMessageState
import co.electriccoin.zcash.ui.screen.home.syncing.WalletSyncingMessage
import co.electriccoin.zcash.ui.screen.home.syncing.WalletSyncingMessageState
import co.electriccoin.zcash.ui.screen.home.updating.WalletUpdatingMessage import co.electriccoin.zcash.ui.screen.home.updating.WalletUpdatingMessage
import co.electriccoin.zcash.ui.screen.home.updating.WalletUpdatingMessageState import co.electriccoin.zcash.ui.screen.home.updating.WalletUpdatingMessageState
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -243,6 +243,7 @@ private fun <T> animationSpec(delay: Duration? = null): TweenSpec<T> {
) )
} }
@Suppress("MagicNumber")
private fun <T> elevationAnimationSpec(delay: Duration? = null): TweenSpec<T> { private fun <T> elevationAnimationSpec(delay: Duration? = null): TweenSpec<T> {
val delayMs = delay?.inWholeMilliseconds?.toInt() ?: 0 val delayMs = delay?.inWholeMilliseconds?.toInt() ?: 0
return tween( return tween(

View File

@ -107,8 +107,7 @@ private fun Container(
0f to ZashiLightColors.Utility.Purple.utilityPurple500, 0f to ZashiLightColors.Utility.Purple.utilityPurple500,
1f to ZashiLightColors.Utility.Purple.utilityPurple900, 1f to ZashiLightColors.Utility.Purple.utilityPurple900,
) )
) ).clickable(onClick = onClick)
.clickable(onClick = onClick)
.padding(contentPadding), .padding(contentPadding),
) { ) {
Row( Row(

View File

@ -32,8 +32,8 @@ 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.BalanceWidgetState
import co.electriccoin.zcash.ui.screen.balances.BalanceWidget import co.electriccoin.zcash.ui.screen.balances.BalanceWidget
import co.electriccoin.zcash.ui.screen.balances.BalanceWidgetState
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
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetStateFixture import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetStateFixture

View File

@ -28,8 +28,8 @@ import co.electriccoin.zcash.ui.screen.home.currency.EnableCurrencyConversionMes
import co.electriccoin.zcash.ui.screen.home.disconnected.WalletDisconnectedInfo import co.electriccoin.zcash.ui.screen.home.disconnected.WalletDisconnectedInfo
import co.electriccoin.zcash.ui.screen.home.disconnected.WalletDisconnectedMessageState import co.electriccoin.zcash.ui.screen.home.disconnected.WalletDisconnectedMessageState
import co.electriccoin.zcash.ui.screen.home.error.WalletErrorMessageState import co.electriccoin.zcash.ui.screen.home.error.WalletErrorMessageState
import co.electriccoin.zcash.ui.screen.home.reporting.CrashReportOptIn
import co.electriccoin.zcash.ui.screen.home.reporting.CrashReportMessageState import co.electriccoin.zcash.ui.screen.home.reporting.CrashReportMessageState
import co.electriccoin.zcash.ui.screen.home.reporting.CrashReportOptIn
import co.electriccoin.zcash.ui.screen.home.restoring.WalletRestoringInfo import co.electriccoin.zcash.ui.screen.home.restoring.WalletRestoringInfo
import co.electriccoin.zcash.ui.screen.home.restoring.WalletRestoringMessageState import co.electriccoin.zcash.ui.screen.home.restoring.WalletRestoringMessageState
import co.electriccoin.zcash.ui.screen.home.shieldfunds.ShieldFundsInfo import co.electriccoin.zcash.ui.screen.home.shieldfunds.ShieldFundsInfo
@ -53,6 +53,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Suppress("TooManyFunctions")
class HomeViewModel( class HomeViewModel(
getHomeMessage: GetHomeMessageUseCase, getHomeMessage: GetHomeMessageUseCase,
getVersionInfoProvider: GetVersionInfoProvider, getVersionInfoProvider: GetVersionInfoProvider,
@ -63,15 +64,15 @@ class HomeViewModel(
private val shieldFunds: ShieldFundsUseCase, private val shieldFunds: ShieldFundsUseCase,
private val navigateToError: NavigateToErrorUseCase private val navigateToError: NavigateToErrorUseCase
) : ViewModel() { ) : ViewModel() {
private val messageState =
private val messageState = getHomeMessage getHomeMessage
.observe() .observe()
.map { createMessageState(it) } .map { createMessageState(it) }
.stateIn( .stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(), started = SharingStarted.WhileSubscribed(),
initialValue = null initialValue = null
) )
private val isRestoreDialogVisible: Flow<Boolean?> = private val isRestoreDialogVisible: Flow<Boolean?> =
isRestoreSuccessDialogVisible isRestoreSuccessDialogVisible
@ -155,54 +156,65 @@ class HomeViewModel(
message = messageState message = messageState
) )
private fun createMessageState(it: HomeMessageData?) = when (it) { private fun createMessageState(it: HomeMessageData?) =
is HomeMessageData.Backup -> WalletBackupMessageState( when (it) {
onClick = ::onWalletBackupMessageClick, is HomeMessageData.Backup ->
onButtonClick = ::onWalletBackupMessageButtonClick, WalletBackupMessageState(
) onClick = ::onWalletBackupMessageClick,
onButtonClick = ::onWalletBackupMessageButtonClick,
)
HomeMessageData.Disconnected -> WalletDisconnectedMessageState( HomeMessageData.Disconnected ->
onClick = ::onWalletDisconnectedMessageClick WalletDisconnectedMessageState(
) onClick = ::onWalletDisconnectedMessageClick
)
HomeMessageData.EnableCurrencyConversion -> EnableCurrencyConversionMessageState( HomeMessageData.EnableCurrencyConversion ->
onClick = ::onEnableCurrencyConversionClick, EnableCurrencyConversionMessageState(
onButtonClick = ::onEnableCurrencyConversionClick onClick = ::onEnableCurrencyConversionClick,
) onButtonClick = ::onEnableCurrencyConversionClick
)
is HomeMessageData.Error -> WalletErrorMessageState( is HomeMessageData.Error ->
onClick = { onWalletErrorMessageClick(it) } WalletErrorMessageState(
) onClick = { onWalletErrorMessageClick(it) }
)
is HomeMessageData.Restoring -> WalletRestoringMessageState( is HomeMessageData.Restoring ->
progress = it.progress, WalletRestoringMessageState(
onClick = ::onWalletRestoringMessageClick progress = it.progress,
) onClick = ::onWalletRestoringMessageClick
)
is HomeMessageData.Syncing -> WalletSyncingMessageState( is HomeMessageData.Syncing ->
progress = it.progress, WalletSyncingMessageState(
onClick = ::onWalletSyncingMessageClick progress = it.progress,
) onClick = ::onWalletSyncingMessageClick
)
is HomeMessageData.ShieldFunds -> ShieldFundsMessageState( is HomeMessageData.ShieldFunds ->
subtitle = stringRes( ShieldFundsMessageState(
R.string.home_message_transparent_balance_subtitle, subtitle =
stringRes(it.zatoshi) stringRes(
), R.string.home_message_transparent_balance_subtitle,
onClick = ::onShieldFundsMessageClick, stringRes(it.zatoshi)
onButtonClick = ::onShieldFundsMessageButtonClick, ),
) onClick = ::onShieldFundsMessageClick,
onButtonClick = ::onShieldFundsMessageButtonClick,
)
HomeMessageData.Updating -> WalletUpdatingMessageState( HomeMessageData.Updating ->
onClick = ::onWalletUpdatingMessageClick WalletUpdatingMessageState(
) onClick = ::onWalletUpdatingMessageClick
)
HomeMessageData.CrashReport -> CrashReportMessageState( HomeMessageData.CrashReport ->
onClick = ::onCrashReportMessageClick, CrashReportMessageState(
onButtonClick = ::onCrashReportMessageClick onClick = ::onCrashReportMessageClick,
) onButtonClick = ::onCrashReportMessageClick
null -> null )
} null -> null
}
private fun onCrashReportMessageClick() = navigationRouter.forward(CrashReportOptIn) private fun onCrashReportMessageClick() = navigationRouter.forward(CrashReportOptIn)

View File

@ -20,10 +20,8 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.ButtonState import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.HorizontalSpacer
import co.electriccoin.zcash.ui.design.component.IconButtonState import co.electriccoin.zcash.ui.design.component.IconButtonState
import co.electriccoin.zcash.ui.design.component.Spacer import co.electriccoin.zcash.ui.design.component.Spacer
import co.electriccoin.zcash.ui.design.component.VerticalSpacer
import co.electriccoin.zcash.ui.design.component.ZashiButton import co.electriccoin.zcash.ui.design.component.ZashiButton
import co.electriccoin.zcash.ui.design.component.ZashiIconButton import co.electriccoin.zcash.ui.design.component.ZashiIconButton
import co.electriccoin.zcash.ui.design.component.ZashiInfoRow import co.electriccoin.zcash.ui.design.component.ZashiInfoRow

View File

@ -14,13 +14,14 @@ class WalletBackupDetailViewModel(
private val navigationRouter: NavigationRouter, private val navigationRouter: NavigationRouter,
private val navigateToWalletBackup: NavigateToWalletBackupUseCase private val navigateToWalletBackup: NavigateToWalletBackupUseCase
) : ViewModel() { ) : ViewModel() {
val state = MutableStateFlow( val state =
WalletBackupDetailState( MutableStateFlow(
onBack = ::onBack, WalletBackupDetailState(
onNextClick = ::onNextClick, onBack = ::onBack,
onInfoClick = ::onInfoClick onNextClick = ::onNextClick,
) onInfoClick = ::onInfoClick
).asStateFlow() )
).asStateFlow()
private fun onNextClick() = private fun onNextClick() =
viewModelScope.launch { viewModelScope.launch {
@ -39,5 +40,3 @@ class WalletBackupDetailViewModel(
} }
} }
} }

View File

@ -42,11 +42,11 @@ fun WalletBackupInfoView(
state = state state = state
) { ) {
Column( Column(
modifier = Modifier modifier =
.weight(1f, false) Modifier
.verticalScroll(rememberScrollState()) .weight(1f, false)
.padding(horizontal = 24.dp) .verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp)
) { ) {
Image( Image(
painter = painterResource(R.drawable.ic_info_backup), painter = painterResource(R.drawable.ic_info_backup),
@ -118,20 +118,23 @@ private fun Preview() =
WalletBackupInfoView( WalletBackupInfoView(
WalletBackupInfoState( WalletBackupInfoState(
onBack = {}, onBack = {},
secondaryButton = ButtonState( secondaryButton =
text = stringRes(R.string.general_remind_me_later), ButtonState(
onClick = {}, text = stringRes(R.string.general_remind_me_later),
isEnabled = false onClick = {},
), isEnabled = false
primaryButton = ButtonState( ),
text = stringRes(R.string.general_ok), primaryButton =
onClick = {} ButtonState(
), text = stringRes(R.string.general_ok),
checkboxState = CheckboxState( onClick = {}
isChecked = false, ),
onClick = {}, checkboxState =
text = stringRes(R.string.home_info_backup_checkbox) CheckboxState(
) isChecked = false,
onClick = {},
text = stringRes(R.string.home_info_backup_checkbox)
)
) )
) )
} }

View File

@ -31,44 +31,47 @@ class WalletBackupInfoViewModel(
private val navigationRouter: NavigationRouter, private val navigationRouter: NavigationRouter,
private val remindWalletBackupLater: RemindWalletBackupLaterUseCase, private val remindWalletBackupLater: RemindWalletBackupLaterUseCase,
) : ViewModel() { ) : ViewModel() {
private val isConsentChecked = MutableStateFlow(false) private val isConsentChecked = MutableStateFlow(false)
private val lockoutDuration = walletBackupDataSource private val lockoutDuration =
.observe() walletBackupDataSource
.filterIsInstance<WalletBackupAvailability.Available>() .observe()
.take(1) .filterIsInstance<WalletBackupAvailability.Available>()
.map { it.lockoutDuration } .take(1)
.stateIn( .map { it.lockoutDuration }
scope = viewModelScope, .stateIn(
started = SharingStarted.WhileSubscribed(), scope = viewModelScope,
initialValue = null started = SharingStarted.WhileSubscribed(),
) initialValue = null
)
val state: StateFlow<WalletBackupInfoState?> = combine( val state: StateFlow<WalletBackupInfoState?> =
lockoutDuration.filterNotNull(), combine(
isConsentChecked, lockoutDuration.filterNotNull(),
walletBackupConsentStorageProvider.observe().take(1) isConsentChecked,
) { lockout, isConsentChecked, isConsentSaved -> walletBackupConsentStorageProvider.observe().take(1)
) { lockout, isConsentChecked, isConsentSaved ->
WalletBackupInfoState( WalletBackupInfoState(
onBack = ::onBack, onBack = ::onBack,
secondaryButton = ButtonState( secondaryButton =
text = stringRes(R.string.general_remind_me_in, stringRes(lockout.res)), ButtonState(
onClick = ::onRemindMeLaterClick, text = stringRes(R.string.general_remind_me_in, stringRes(lockout.res)),
isEnabled = isConsentChecked || isConsentSaved onClick = ::onRemindMeLaterClick,
), isEnabled = isConsentChecked || isConsentSaved
checkboxState = CheckboxState( ),
isChecked = isConsentChecked, checkboxState =
onClick = ::onConsentClick, CheckboxState(
text = stringRes(R.string.home_info_backup_checkbox) isChecked = isConsentChecked,
).takeIf { !isConsentSaved }, onClick = ::onConsentClick,
primaryButton = ButtonState( text = stringRes(R.string.home_info_backup_checkbox)
text = stringRes(R.string.general_ok), ).takeIf { !isConsentSaved },
onClick = ::onPrimaryClick primaryButton =
) ButtonState(
text = stringRes(R.string.general_ok),
onClick = ::onPrimaryClick
)
) )
} }.stateIn(
.stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = null initialValue = null

View File

@ -62,7 +62,10 @@ fun CrashReportMessage(
) )
} }
class CrashReportMessageState(val onClick: () -> Unit, val onButtonClick: () -> Unit) : HomeMessageState class CrashReportMessageState(
val onClick: () -> Unit,
val onButtonClick: () -> Unit
) : HomeMessageState
@PreviewScreens @PreviewScreens
@Composable @Composable

View File

@ -12,6 +12,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.BlankSurface import co.electriccoin.zcash.ui.design.component.BlankSurface
import co.electriccoin.zcash.ui.design.component.ZashiBaseSettingsOptIn
import co.electriccoin.zcash.ui.design.component.ZashiButton import co.electriccoin.zcash.ui.design.component.ZashiButton
import co.electriccoin.zcash.ui.design.component.ZashiButtonDefaults import co.electriccoin.zcash.ui.design.component.ZashiButtonDefaults
import co.electriccoin.zcash.ui.design.component.ZashiInfoRow import co.electriccoin.zcash.ui.design.component.ZashiInfoRow
@ -20,7 +21,6 @@ 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.colors.ZashiColors import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.component.ZashiBaseSettingsOptIn
@Composable @Composable
fun CrashReportOptInView(state: CrashReportOptInState) { fun CrashReportOptInView(state: CrashReportOptInState) {
@ -76,11 +76,12 @@ private fun Preview() =
ZcashTheme { ZcashTheme {
BlankSurface { BlankSurface {
CrashReportOptInView( CrashReportOptInView(
state = CrashReportOptInState( state =
onOptInClick = {}, CrashReportOptInState(
onBack = {}, onOptInClick = {},
onOptOutClick = {}, onBack = {},
) onOptOutClick = {},
)
) )
} }
} }

View File

@ -9,14 +9,15 @@ import kotlinx.coroutines.flow.StateFlow
class CrashReportOptInViewModel( class CrashReportOptInViewModel(
private val exchangeRateRepository: ExchangeRateRepository, private val exchangeRateRepository: ExchangeRateRepository,
private val navigationRouter: NavigationRouter private val navigationRouter: NavigationRouter
): ViewModel() { ) : ViewModel() {
val state: StateFlow<CrashReportOptInState> = MutableStateFlow( val state: StateFlow<CrashReportOptInState> =
CrashReportOptInState( MutableStateFlow(
onBack = ::dismissOptInExchangeRateUsd, CrashReportOptInState(
onOptInClick = ::optInExchangeRateUsd, onBack = ::dismissOptInExchangeRateUsd,
onOptOutClick = ::onSkipClick onOptInClick = ::optInExchangeRateUsd,
onOptOutClick = ::onSkipClick
)
) )
)
private fun onSkipClick() { private fun onSkipClick() {
exchangeRateRepository.optInExchangeRateUsd(false) exchangeRateRepository.optInExchangeRateUsd(false)
@ -31,4 +32,4 @@ class CrashReportOptInViewModel(
private fun dismissOptInExchangeRateUsd() { private fun dismissOptInExchangeRateUsd() {
navigationRouter.back() navigationRouter.back()
} }
} }

View File

@ -6,7 +6,6 @@ import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.NavigationRouter 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.WalletBackupAvailability
import co.electriccoin.zcash.ui.common.repository.ShieldFundsData import co.electriccoin.zcash.ui.common.repository.ShieldFundsData
import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepository import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepository
import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase
@ -32,43 +31,42 @@ class ShieldFundsInfoViewModel(
private val remindShieldFundsLater: RemindShieldFundsLaterUseCase, private val remindShieldFundsLater: RemindShieldFundsLaterUseCase,
private val shieldFunds: ShieldFundsUseCase, private val shieldFunds: ShieldFundsUseCase,
) : ViewModel() { ) : ViewModel() {
private val lockoutDuration =
private val lockoutDuration = shieldFundsRepository shieldFundsRepository
.availability .availability
.filterIsInstance<ShieldFundsData.Available>() .filterIsInstance<ShieldFundsData.Available>()
.take(1) .take(1)
.map { it.lockoutDuration } .map { it.lockoutDuration }
.stateIn( .stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(), started = SharingStarted.WhileSubscribed(),
initialValue = null initialValue = null
) )
val state: StateFlow<ShieldFundsInfoState?> = val state: StateFlow<ShieldFundsInfoState?> =
combine( combine(
getSelectedWalletAccount.observe(), getSelectedWalletAccount.observe(),
lockoutDuration.filterNotNull(), lockoutDuration.filterNotNull(),
) { account, lockoutDuration -> ) { account, lockoutDuration ->
ShieldFundsInfoState( ShieldFundsInfoState(
onBack = ::onBack, onBack = ::onBack,
primaryButton = primaryButton =
ButtonState( ButtonState(
onClick = ::onShieldClick, onClick = ::onShieldClick,
text = stringRes(R.string.home_info_transparent_balance_shield) text = stringRes(R.string.home_info_transparent_balance_shield)
), ),
secondaryButton = secondaryButton =
ButtonState( ButtonState(
onClick = ::onRemindMeClick, onClick = ::onRemindMeClick,
text = stringRes(R.string.general_remind_me_in, stringRes(lockoutDuration.res)) text = stringRes(R.string.general_remind_me_in, stringRes(lockoutDuration.res))
), ),
transparentAmount = account?.transparent?.balance ?: Zatoshi(0) transparentAmount = account?.transparent?.balance ?: Zatoshi(0)
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = null
) )
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = null
)
private fun onRemindMeClick() = viewModelScope.launch { remindShieldFundsLater() } private fun onRemindMeClick() = viewModelScope.launch { remindShieldFundsLater() }
@ -76,4 +74,3 @@ class ShieldFundsInfoViewModel(
private fun onShieldClick() = shieldFunds(closeCurrentScreen = true) private fun onShieldClick() = shieldFunds(closeCurrentScreen = true)
} }

View File

@ -58,7 +58,6 @@ fun MainActivity.OnboardingNavigation() {
val flexaViewModel = koinViewModel<FlexaViewModel>() val flexaViewModel = koinViewModel<FlexaViewModel>()
val messageAvailabilityDataSource = koinInject<MessageAvailabilityDataSource>() val messageAvailabilityDataSource = koinInject<MessageAvailabilityDataSource>()
val navigator: Navigator = val navigator: Navigator =
remember( remember(
navController, navController,

View File

@ -17,4 +17,6 @@ fun AndroidRestoreBDDate(args: RestoreBDDate) {
} }
@Serializable @Serializable
data class RestoreBDDate(val seed: String) data class RestoreBDDate(
val seed: String
)

View File

@ -31,18 +31,18 @@ class RestoreBDDateViewModel(
private val navigationRouter: NavigationRouter, private val navigationRouter: NavigationRouter,
private val context: Context, private val context: Context,
) : ViewModel() { ) : ViewModel() {
@Suppress("MagicNumber")
private val selection = MutableStateFlow<YearMonth>(YearMonth.of(2018, 10)) private val selection = MutableStateFlow<YearMonth>(YearMonth.of(2018, 10))
val state: StateFlow<RestoreBDDateState?> = selection val state: StateFlow<RestoreBDDateState?> =
.map { selection
createState(it) .map {
} createState(it)
.stateIn( }.stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = null initialValue = null
) )
private fun createState(selection: YearMonth) = private fun createState(selection: YearMonth) =
RestoreBDDateState( RestoreBDDateState(
@ -59,16 +59,19 @@ class RestoreBDDateViewModel(
private fun onEstimateClick() { private fun onEstimateClick() {
viewModelScope.launch { viewModelScope.launch {
val instant = selection.value.atDay(1) val instant =
.atStartOfDay() selection.value
.atZone(ZoneId.systemDefault()) .atDay(1)
.toInstant() .atStartOfDay()
.toKotlinInstant() .atZone(ZoneId.systemDefault())
val bday = SdkSynchronizer.estimateBirthdayHeight( .toInstant()
context = context, .toKotlinInstant()
date = instant, val bday =
network = ZcashNetwork.fromResources(context) SdkSynchronizer.estimateBirthdayHeight(
) context = context,
date = instant,
network = ZcashNetwork.fromResources(context)
)
navigationRouter.forward(RestoreBDEstimation(seed = args.seed, blockHeight = bday.value)) navigationRouter.forward(RestoreBDEstimation(seed = args.seed, blockHeight = bday.value))
} }
} }

View File

@ -75,8 +75,8 @@ 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.BalanceWidgetState
import co.electriccoin.zcash.ui.screen.balances.BalanceWidget import co.electriccoin.zcash.ui.screen.balances.BalanceWidget
import co.electriccoin.zcash.ui.screen.balances.BalanceWidgetState
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
import co.electriccoin.zcash.ui.screen.send.model.MemoState import co.electriccoin.zcash.ui.screen.send.model.MemoState
@ -331,7 +331,7 @@ private fun SendForm(
recipientAddressState.type is AddressType.Valid && recipientAddressState.type is AddressType.Valid &&
recipientAddressState.type !is AddressType.Transparent && recipientAddressState.type !is AddressType.Transparent &&
recipientAddressState.type !is AddressType.Tex recipientAddressState.type !is AddressType.Tex
) )
SendFormAmountTextField( SendFormAmountTextField(
amountState = amountState, amountState = amountState,
@ -653,9 +653,10 @@ fun SendFormAmountTextField(
) )
}, },
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
innerModifier = ZashiTextFieldDefaults innerModifier =
.innerModifier ZashiTextFieldDefaults
.testTag(SendTag.SEND_AMOUNT_FIELD), .innerModifier
.testTag(SendTag.SEND_AMOUNT_FIELD),
error = amountError, error = amountError,
placeholder = { placeholder = {
Text( Text(

View File

@ -6,7 +6,6 @@ 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.common.viewmodel.WalletViewModel
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf

View File

@ -6,7 +6,6 @@ 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.common.viewmodel.WalletViewModel
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@Composable @Composable

View File

@ -17,4 +17,6 @@ internal fun AndroidWalletBackup(args: WalletBackup) {
} }
@Serializable @Serializable
data class WalletBackup(val isOpenedFromSeedBackupInfo: Boolean) data class WalletBackup(
val isOpenedFromSeedBackupInfo: Boolean
)

View File

@ -291,11 +291,12 @@ private fun RevealedPreview() =
isRevealed = true, isRevealed = true,
tooltip = SeedSecretStateTooltip(title = stringRes(""), message = stringRes("")), tooltip = SeedSecretStateTooltip(title = stringRes(""), message = stringRes("")),
) {}, ) {},
secondaryButton = ButtonState( secondaryButton =
text = stringRes("Text"), ButtonState(
icon = R.drawable.ic_seed_show, text = stringRes("Text"),
onClick = {}, icon = R.drawable.ic_seed_show,
), onClick = {},
),
primaryButton = primaryButton =
ButtonState( ButtonState(
text = stringRes("Text"), text = stringRes("Text"),
@ -331,11 +332,12 @@ private fun HiddenPreview() =
isRevealed = false, isRevealed = false,
tooltip = SeedSecretStateTooltip(title = stringRes(""), message = stringRes("")), tooltip = SeedSecretStateTooltip(title = stringRes(""), message = stringRes("")),
) {}, ) {},
secondaryButton = ButtonState( secondaryButton =
text = stringRes("Text"), ButtonState(
icon = R.drawable.ic_seed_show, text = stringRes("Text"),
onClick = {}, icon = R.drawable.ic_seed_show,
), onClick = {},
),
primaryButton = primaryButton =
ButtonState( ButtonState(
text = stringRes("Text"), text = stringRes("Text"),

View File

@ -34,24 +34,25 @@ class WalletBackupViewModel(
private val onUserSavedWalletBackup: OnUserSavedWalletBackupUseCase, private val onUserSavedWalletBackup: OnUserSavedWalletBackupUseCase,
private val remindWalletBackupLater: RemindWalletBackupLaterUseCase, private val remindWalletBackupLater: RemindWalletBackupLaterUseCase,
) : ViewModel() { ) : ViewModel() {
private val lockoutDuration =
private val lockoutDuration = walletBackupDataSource walletBackupDataSource
.observe() .observe()
.filterIsInstance<WalletBackupAvailability.Available>() .filterIsInstance<WalletBackupAvailability.Available>()
.take(1) .take(1)
.map { it.lockoutDuration } .map { it.lockoutDuration }
.stateIn( .stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.WhileSubscribed(), started = SharingStarted.WhileSubscribed(),
initialValue = null initialValue = null
) )
private val isRevealed = MutableStateFlow(false) private val isRevealed = MutableStateFlow(false)
private val isRemindMeLaterButtonVisible = isRevealed private val isRemindMeLaterButtonVisible =
.map { isRevealed -> isRevealed
isRevealed && args.isOpenedFromSeedBackupInfo .map { isRevealed ->
} isRevealed && args.isOpenedFromSeedBackupInfo
}
private val observableWallet = observePersistableWallet() private val observableWallet = observePersistableWallet()
@ -63,14 +64,16 @@ class WalletBackupViewModel(
lockoutDuration lockoutDuration
) { isRevealed, isRemindMeLaterButtonVisible, wallet, lockoutDuration -> ) { isRevealed, isRemindMeLaterButtonVisible, wallet, lockoutDuration ->
WalletBackupState( WalletBackupState(
secondaryButton = ButtonState( secondaryButton =
text = if (lockoutDuration != null) { ButtonState(
stringRes(R.string.general_remind_me_in, stringRes(lockoutDuration.res)) text =
} else { if (lockoutDuration != null) {
stringRes(R.string.general_remind_me_later) stringRes(R.string.general_remind_me_in, stringRes(lockoutDuration.res))
}, } else {
onClick = ::onRemindMeLaterClick stringRes(R.string.general_remind_me_later)
).takeIf { isRemindMeLaterButtonVisible }, },
onClick = ::onRemindMeLaterClick
).takeIf { isRemindMeLaterButtonVisible },
primaryButton = primaryButton =
ButtonState( ButtonState(
text = text =
@ -80,11 +83,12 @@ class WalletBackupViewModel(
isRevealed -> stringRes(R.string.seed_recovery_hide_button) isRevealed -> stringRes(R.string.seed_recovery_hide_button)
else -> stringRes(R.string.seed_recovery_reveal_button) else -> stringRes(R.string.seed_recovery_reveal_button)
}, },
onClick = if (isRevealed && args.isOpenedFromSeedBackupInfo) { onClick =
{ onWalletBackupSavedClick() } if (isRevealed && args.isOpenedFromSeedBackupInfo) {
} else { { onWalletBackupSavedClick() }
{ onRevealClick() } } else {
}, { onRevealClick() }
},
isEnabled = wallet != null, isEnabled = wallet != null,
isLoading = wallet == null, isLoading = wallet == null,
icon = icon =
@ -144,4 +148,3 @@ class WalletBackupViewModel(
private fun onBack() = navigationRouter.back() private fun onBack() = navigationRouter.back()
} }

View File

@ -48,8 +48,8 @@ import co.electriccoin.zcash.ui.screen.balances.BalanceTag
import co.electriccoin.zcash.ui.screen.home.HomeTags import co.electriccoin.zcash.ui.screen.home.HomeTags
import co.electriccoin.zcash.ui.screen.restore.height.RestoreBDHeightTags import co.electriccoin.zcash.ui.screen.restore.height.RestoreBDHeightTags
import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedTag import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedTag
import co.electriccoin.zcash.ui.screen.walletbackup.WalletBackup
import co.electriccoin.zcash.ui.screen.send.SendTag import co.electriccoin.zcash.ui.screen.send.SendTag
import co.electriccoin.zcash.ui.screen.walletbackup.WalletBackup
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext