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 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) =
preferenceProvider.getLong(key)?.let { Instant.ofEpochMilli(it) }
@ -11,4 +13,4 @@ class TimestampPreferenceDefault(override val key: PreferenceKey): PreferenceDef
preferenceProvider: PreferenceProvider,
newValue: Instant?
) = preferenceProvider.putLong(key, newValue?.toEpochMilli())
}
}

View File

@ -65,22 +65,23 @@ fun ZashiBigIconButton(
Modifier.background(darkBgGradient)
Surface(
modifier = modifier
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
event.changes.forEach { change ->
if (change.changedToDown()) {
isPressed = true
}
if (change.changedToUp()) {
isPressed = false
modifier =
modifier
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
event.changes.forEach { change ->
if (change.changedToDown()) {
isPressed = true
}
if (change.changedToUp()) {
isPressed = false
}
}
}
}
}
},
},
onClick = state.onClick,
color = ZashiColors.Surfaces.bgPrimary,
shape = RoundedCornerShape(22.dp),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.flowOf
import java.time.Instant
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds
interface ShieldFundsDataSource {
suspend fun observe(forAccount: AccountUuid): Flow<ShieldFundsAvailability>
@ -26,34 +25,37 @@ interface ShieldFundsDataSource {
class ShieldFundsDataSourceImpl(
private val shieldFundsRemindMeCountStorageProvider: ShieldFundsRemindMeCountStorageProvider,
private val shieldFundsRemindMeTimestampStorageProvider: ShieldFundsRemindMeTimestampStorageProvider
): ShieldFundsDataSource {
) : ShieldFundsDataSource {
@OptIn(ExperimentalCoroutinesApi::class)
override suspend fun observe(forAccount: AccountUuid): Flow<ShieldFundsAvailability> = combine(
shieldFundsRemindMeCountStorageProvider.observe(forAccount),
shieldFundsRemindMeTimestampStorageProvider.observe(forAccount)
) { count, timestamp ->
count to timestamp
}.flatMapLatest {(count, timestamp) ->
when {
timestamp == null -> flowOf(ShieldFundsAvailability.Available(ShieldFundsLockoutDuration.TWO_DAYS))
count == 1 -> calculateNext(
lastTimestamp = timestamp,
lastLockoutDuration = ShieldFundsLockoutDuration.TWO_DAYS,
nextLockoutDuration = ShieldFundsLockoutDuration.TWO_WEEKS
)
override suspend fun observe(forAccount: AccountUuid): Flow<ShieldFundsAvailability> =
combine(
shieldFundsRemindMeCountStorageProvider.observe(forAccount),
shieldFundsRemindMeTimestampStorageProvider.observe(forAccount)
) { count, timestamp ->
count to timestamp
}.flatMapLatest { (count, timestamp) ->
when {
timestamp == null -> flowOf(ShieldFundsAvailability.Available(ShieldFundsLockoutDuration.TWO_DAYS))
count == 1 ->
calculateNext(
lastTimestamp = timestamp,
lastLockoutDuration = ShieldFundsLockoutDuration.TWO_DAYS,
nextLockoutDuration = ShieldFundsLockoutDuration.TWO_WEEKS
)
else -> calculateNext(
lastTimestamp = timestamp,
lastLockoutDuration = if (count == 2) {
ShieldFundsLockoutDuration.TWO_WEEKS
} else {
ShieldFundsLockoutDuration.ONE_MONTH
},
nextLockoutDuration = ShieldFundsLockoutDuration.ONE_MONTH
)
else ->
calculateNext(
lastTimestamp = timestamp,
lastLockoutDuration =
if (count == 2) {
ShieldFundsLockoutDuration.TWO_WEEKS
} else {
ShieldFundsLockoutDuration.ONE_MONTH
},
nextLockoutDuration = ShieldFundsLockoutDuration.ONE_MONTH
)
}
}
}
override suspend fun remindMeLater(forAccount: AccountUuid) {
val count = shieldFundsRemindMeCountStorageProvider.get(forAccount)
@ -83,11 +85,17 @@ class ShieldFundsDataSourceImpl(
}
sealed interface ShieldFundsAvailability {
data class Available(val lockoutDuration: ShieldFundsLockoutDuration) : ShieldFundsAvailability
data class Available(
val lockoutDuration: ShieldFundsLockoutDuration
) : 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_WEEKS(2.days, R.string.general_remind_me_in_two_weeks),
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 kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds
interface WalletBackupDataSource {
fun observe(): Flow<WalletBackupAvailability>
@ -30,35 +29,38 @@ class WalletBackupDataSourceImpl(
private val walletBackupRemindMeCountStorageProvider: WalletBackupRemindMeCountStorageProvider,
private val walletBackupRemindMeTimestampStorageProvider: WalletBackupRemindMeTimestampStorageProvider
) : WalletBackupDataSource {
@OptIn(ExperimentalCoroutinesApi::class)
override fun observe(): Flow<WalletBackupAvailability> = combine(
walletBackupFlagStorageProvider.observe(),
walletBackupRemindMeCountStorageProvider.observe(),
walletBackupRemindMeTimestampStorageProvider.observe()
) { isBackedUp, count, timestamp ->
Triple(isBackedUp, count, timestamp)
}.flatMapLatest { (isBackedUp, count, timestamp) ->
when {
isBackedUp -> flowOf(WalletBackupAvailability.Unavailable)
timestamp == null -> flowOf(WalletBackupAvailability.Available(WalletBackupLockoutDuration.TWO_DAYS))
count == 1 -> calculateNext(
lastTimestamp = timestamp,
lastLockoutDuration = WalletBackupLockoutDuration.TWO_DAYS,
nextLockoutDuration = WalletBackupLockoutDuration.TWO_WEEKS
)
override fun observe(): Flow<WalletBackupAvailability> =
combine(
walletBackupFlagStorageProvider.observe(),
walletBackupRemindMeCountStorageProvider.observe(),
walletBackupRemindMeTimestampStorageProvider.observe()
) { isBackedUp, count, timestamp ->
Triple(isBackedUp, count, timestamp)
}.flatMapLatest { (isBackedUp, count, timestamp) ->
when {
isBackedUp -> flowOf(WalletBackupAvailability.Unavailable)
timestamp == null -> flowOf(WalletBackupAvailability.Available(WalletBackupLockoutDuration.TWO_DAYS))
count == 1 ->
calculateNext(
lastTimestamp = timestamp,
lastLockoutDuration = WalletBackupLockoutDuration.TWO_DAYS,
nextLockoutDuration = WalletBackupLockoutDuration.TWO_WEEKS
)
else -> calculateNext(
lastTimestamp = timestamp,
lastLockoutDuration = if (count == 2) {
WalletBackupLockoutDuration.TWO_WEEKS
} else {
WalletBackupLockoutDuration.ONE_MONTH
},
nextLockoutDuration = WalletBackupLockoutDuration.ONE_MONTH
)
else ->
calculateNext(
lastTimestamp = timestamp,
lastLockoutDuration =
if (count == 2) {
WalletBackupLockoutDuration.TWO_WEEKS
} else {
WalletBackupLockoutDuration.ONE_MONTH
},
nextLockoutDuration = WalletBackupLockoutDuration.ONE_MONTH
)
}
}
}
override suspend fun onUserSavedWalletBackup() {
walletBackupFlagStorageProvider.store(true)
@ -92,11 +94,17 @@ class WalletBackupDataSourceImpl(
}
sealed interface WalletBackupAvailability {
data class Available(val lockoutDuration: WalletBackupLockoutDuration) : WalletBackupAvailability
data class Available(
val lockoutDuration: WalletBackupLockoutDuration
) : 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_WEEKS(14.days, R.string.general_remind_me_in_two_weeks),
ONE_MONTH(30.days, R.string.general_remind_me_in_two_months),

View File

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

View File

@ -53,17 +53,10 @@ sealed interface WalletAccount : Comparable<WalletAccount> {
val isShieldedPending: Boolean
get() = pendingShieldedBalance > Zatoshi(0)
@Suppress("MagicNumber")
val isShieldingAvailable: Boolean
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
}

View File

@ -1,10 +1,7 @@
package co.electriccoin.zcash.ui.common.model
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.processor.CompactBlockProcessor
import cash.z.ecc.android.sdk.model.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
// 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>
abstract class BaseBooleanStorageProvider(key: PreferenceKey) : BaseStorageProvider<Boolean>(), BooleanStorageProvider {
abstract class BaseBooleanStorageProvider(
key: PreferenceKey
) : BaseStorageProvider<Boolean>(),
BooleanStorageProvider {
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>
abstract class BaseIntStorageProvider(key: PreferenceKey) : IntStorageProvider, BaseStorageProvider<Int>() {
abstract class BaseIntStorageProvider(
key: PreferenceKey
) : BaseStorageProvider<Int>(),
IntStorageProvider {
override val default = IntegerPreferenceDefault(key = key, defaultValue = 0)
}

View File

@ -7,4 +7,5 @@ interface RestoreTimestampStorageProvider : TimestampStorageProvider
class RestoreTimestampStorageProviderImpl(
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 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.PreferenceKey
import kotlinx.coroutines.flow.Flow
@ -18,22 +17,21 @@ interface ShieldFundsRemindMeCountStorageProvider {
class ShieldFundsRemindMeCountStorageProviderImpl(
private val encryptedPreferenceProvider: EncryptedPreferenceProvider
) : ShieldFundsRemindMeCountStorageProvider {
@OptIn(ExperimentalStdlibApi::class)
private fun getDefault(forAccount: AccountUuid): IntegerPreferenceDefault {
val key = PreferenceKey("shield_funds_remind_me_count_${forAccount.value.toHexString()}")
return IntegerPreferenceDefault(key = key, defaultValue = 0)
}
override suspend fun get(forAccount: AccountUuid): Int {
return getDefault(forAccount).getValue(encryptedPreferenceProvider())
}
override suspend fun get(forAccount: AccountUuid): Int =
getDefault(forAccount)
.getValue(encryptedPreferenceProvider())
override suspend fun store(forAccount: AccountUuid, amount: Int) {
getDefault(forAccount).putValue(encryptedPreferenceProvider(), amount)
}
override suspend fun observe(forAccount: AccountUuid): Flow<Int> {
return getDefault(forAccount).observe(encryptedPreferenceProvider())
}
override suspend fun observe(forAccount: AccountUuid): Flow<Int> =
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 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.TimestampPreferenceDefault
import kotlinx.coroutines.flow.Flow
import java.time.Instant
@ -17,22 +17,20 @@ interface ShieldFundsRemindMeTimestampStorageProvider {
class ShieldFundsRemindMeTimestampStorageProviderImpl(
private val encryptedPreferenceProvider: EncryptedPreferenceProvider
) : ShieldFundsRemindMeTimestampStorageProvider {
) : ShieldFundsRemindMeTimestampStorageProvider {
@OptIn(ExperimentalStdlibApi::class)
private fun getDefault(forAccount: AccountUuid): TimestampPreferenceDefault {
val key = PreferenceKey("shield_funds_remind_me_timestamp_${forAccount.value.toHexString()}")
return TimestampPreferenceDefault(key = key)
}
override suspend fun get(forAccount: AccountUuid): Instant? {
return getDefault(forAccount).getValue(encryptedPreferenceProvider())
}
override suspend fun get(forAccount: AccountUuid) = getDefault(forAccount).getValue(encryptedPreferenceProvider())
override suspend fun store(forAccount: AccountUuid, timestamp: Instant) {
getDefault(forAccount).putValue(encryptedPreferenceProvider(), timestamp)
}
override suspend fun observe(forAccount: AccountUuid): Flow<Instant?> {
return getDefault(forAccount).observe(encryptedPreferenceProvider())
}
override suspend fun observe(forAccount: AccountUuid): Flow<Instant?> =
getDefault(forAccount)
.observe(encryptedPreferenceProvider())
}

View File

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

View File

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

View File

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

View File

@ -7,4 +7,5 @@ interface WalletBackupConsentStorageProvider : BooleanStorageProvider
class WalletBackupConsentStorageProviderImpl(
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(
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(
override val preferenceHolder: StandardPreferenceProvider,
) : BaseStorageProvider<WalletRestoringState>(), WalletRestoringStateProvider {
) : BaseStorageProvider<WalletRestoringState>(),
WalletRestoringStateProvider {
override val default: PreferenceDefault<WalletRestoringState> = WalletRestoringStatePreferenceDefault()
}
private class WalletRestoringStatePreferenceDefault : PreferenceDefault<WalletRestoringState> {
private val internal = StandardPreferenceKeys.WALLET_RESTORING_STATE
override val key: PreferenceKey = internal.key

View File

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

View File

@ -28,7 +28,6 @@ interface HomeMessageCacheRepository {
class HomeMessageCacheRepositoryImpl(
private val messageAvailabilityDataSource: MessageAvailabilityDataSource
) : HomeMessageCacheRepository {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
override var lastShownMessage: HomeMessageData? = null
@ -42,8 +41,7 @@ class HomeMessageCacheRepositoryImpl(
lastShownMessage = null
lastMessage = null
}
}
.launchIn(scope)
}.launchIn(scope)
}
override fun reset() {
@ -52,21 +50,33 @@ class HomeMessageCacheRepositoryImpl(
}
}
@Suppress("MagicNumber")
sealed interface HomeMessageData {
val priority: Int
data class Error(val synchronizerError: SynchronizerError) : RuntimeMessage()
data class Error(
val synchronizerError: SynchronizerError
) : 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 Backup : Prioritized {
override val priority: Int = 4
}
data class ShieldFunds(val zatoshi: Zatoshi) : Prioritized {
data class ShieldFunds(
val zatoshi: Zatoshi
) : Prioritized {
override val priority: Int = 3
}
@ -82,6 +92,7 @@ sealed interface HomeMessageData {
/**
* Message which always is shown.
*/
@Suppress("MagicNumber")
sealed class RuntimeMessage : HomeMessageData {
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.
*/
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.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
interface ShieldFundsRepository {
val availability: Flow<ShieldFundsData>
@ -25,31 +24,33 @@ class ShieldFundsRepositoryImpl(
private val messageAvailabilityDataSource: MessageAvailabilityDataSource,
) : ShieldFundsRepository {
@OptIn(ExperimentalCoroutinesApi::class)
override val availability: Flow<ShieldFundsData> = accountDataSource
.selectedAccount
.flatMapLatest { account ->
when {
account == null -> flowOf(ShieldFundsData.Unavailable)
override val availability: Flow<ShieldFundsData> =
accountDataSource
.selectedAccount
.flatMapLatest { account ->
when {
account == null -> flowOf(ShieldFundsData.Unavailable)
account.isShieldingAvailable ->
combine(
messageAvailabilityDataSource.canShowShieldMessage,
shieldFundsDataSource.observe(account.sdkAccount.accountUuid)
) { canShowShieldMessage, availability ->
when {
!canShowShieldMessage -> ShieldFundsData.Unavailable
availability is ShieldFundsAvailability.Available -> ShieldFundsData.Available(
lockoutDuration = availability.lockoutDuration,
amount = account.transparent.balance
)
account.isShieldingAvailable ->
combine(
messageAvailabilityDataSource.canShowShieldMessage,
shieldFundsDataSource.observe(account.sdkAccount.accountUuid)
) { canShowShieldMessage, availability ->
when {
!canShowShieldMessage -> ShieldFundsData.Unavailable
availability is ShieldFundsAvailability.Available ->
ShieldFundsData.Available(
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() {
shieldFundsDataSource.remindMeLater(

View File

@ -73,23 +73,25 @@ class TransactionRepositoryImpl(
)
@OptIn(ExperimentalCoroutinesApi::class)
override val currentTransactions: Flow<List<Transaction>?> = accountDataSource.selectedAccount
.distinctUntilChangedBy { it?.sdkAccount?.accountUuid }
.flatMapLatest { selected ->
if (selected is ZashiAccount) {
zashiTransactions
} else {
observeTransactions(
accountFlow = accountDataSource.selectedAccount.map { it?.sdkAccount?.accountUuid }
.distinctUntilChanged()
)
}
}
.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(5.seconds, Duration.ZERO),
initialValue = null
)
override val currentTransactions: Flow<List<Transaction>?> =
accountDataSource.selectedAccount
.distinctUntilChangedBy { it?.sdkAccount?.accountUuid }
.flatMapLatest { selected ->
if (selected is ZashiAccount) {
zashiTransactions
} else {
observeTransactions(
accountFlow =
accountDataSource.selectedAccount
.map { it?.sdkAccount?.accountUuid }
.distinctUntilChanged()
)
}
}.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(5.seconds, Duration.ZERO),
initialValue = null
)
@OptIn(ExperimentalCoroutinesApi::class)
private fun TransactionRepositoryImpl.observeTransactions(accountFlow: Flow<AccountUuid?>) =
@ -123,6 +125,7 @@ class TransactionRepositoryImpl(
}
}
@Suppress("CyclomaticComplexMethod")
private suspend fun createTransactions(
transactions: List<TransactionOverview>,
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.model.BlockHeight
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.SeedPhrase
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.zcash.preference.EncryptedPreferenceProvider
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.RestoreTimestampDataSource
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.WalletRestoringStateProvider
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.StandardPreferenceKeys
import co.electriccoin.zcash.ui.screen.chooseserver.AvailableServerProvider
@ -36,13 +33,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll
@ -51,7 +46,6 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
@ -196,13 +190,14 @@ class WalletRepositoryImpl(
/**
* A flow of the wallet block synchronization state.
*/
override val walletRestoringState: StateFlow<WalletRestoringState> = walletRestoringStateProvider
.observe()
.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = WalletRestoringState.NONE
)
override val walletRestoringState: StateFlow<WalletRestoringState> =
walletRestoringStateProvider
.observe()
.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = WalletRestoringState.NONE
)
/**
* 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 walletRestoringStateProvider: WalletRestoringStateProvider
) : WalletSnapshotRepository {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
@OptIn(ExperimentalCoroutinesApi::class)
@ -38,8 +37,7 @@ class WalletSnapshotRepositoryImpl(
status to restoringState
}
}
}
.collect { (status, restoringState) ->
}.collect { (status, restoringState) ->
// 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)) {
walletRestoringStateProvider.store(WalletRestoringState.SYNCING)

View File

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

View File

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

View File

@ -29,8 +29,19 @@ class NavigateToErrorUseCase(
}
sealed interface ErrorArgs {
data class SyncError(val synchronizerError: SynchronizerError) : ErrorArgs
data class ShieldingError(val error: SubmitResult.Failure): ErrorArgs
data class ShieldingGeneralError(val exception: Exception): ErrorArgs
data class General(val exception: Exception): ErrorArgs
data class SyncError(
val synchronizerError: SynchronizerError
) : 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()
navigationRouter.backToRoot()
}
}
}

View File

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

View File

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

View File

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

View File

@ -46,14 +46,16 @@ class ShieldFundsUseCase(
}
}
@Suppress("TooGenericExceptionCaught")
private suspend fun shieldZashiFunds() {
try {
zashiProposalRepository.createShieldProposal()
zashiProposalRepository.submitTransaction()
val result = zashiProposalRepository.submitState
.filterIsInstance<SubmitProposalState.Result>()
.first()
.submitResult
val result =
zashiProposalRepository.submitState
.filterIsInstance<SubmitProposalState.Result>()
.first()
.submitResult
if (result is SubmitResult.Failure) {
navigateToError(ErrorArgs.ShieldingError(result))
@ -65,6 +67,7 @@ class ShieldFundsUseCase(
}
}
@Suppress("TooGenericExceptionCaught")
private suspend fun createKeystoneShieldProposal() {
try {
keystoneProposalRepository.createShieldProposal()
@ -75,4 +78,4 @@ class ShieldFundsUseCase(
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.common.model.WalletRestoringState
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.NavigateToWalletBackupUseCase
import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
import co.electriccoin.zcash.ui.design.util.stringRes

View File

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

View File

@ -41,10 +41,11 @@ internal fun BalanceWidgetButton(
state: BalanceButtonState,
modifier: Modifier = Modifier,
) {
val colors = ZashiButtonDefaults.secondaryColors(
containerColor = ZashiColors.Surfaces.bgPrimary,
borderColor = ZashiColors.Utility.Gray.utilityGray100
)
val colors =
ZashiButtonDefaults.secondaryColors(
containerColor = ZashiColors.Surfaces.bgPrimary,
borderColor = ZashiColors.Utility.Gray.utilityGray100
)
val borderColor = colors.borderColor
Button(
@ -53,10 +54,11 @@ internal fun BalanceWidgetButton(
shape = RoundedCornerShape(ZashiDimensions.Radius.radiusIg),
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 10.dp),
colors = colors.toButtonColors(),
elevation = ButtonDefaults.buttonElevation(
defaultElevation = 1.dp,
pressedElevation = 0.dp
),
elevation =
ButtonDefaults.buttonElevation(
defaultElevation = 1.dp,
pressedElevation = 0.dp
),
border = borderColor.takeIf { it != Color.Unspecified }?.let { BorderStroke(1.dp, it) },
content = {
Row(
@ -110,15 +112,17 @@ data class BalanceButtonState(
@PreviewScreens
@Composable
private fun Preview() = ZcashTheme {
BlankSurface {
BalanceWidgetButton(
state = BalanceButtonState(
icon = R.drawable.ic_help,
text = stringRes("text"),
amount = Zatoshi(1000),
onClick = {}
private fun Preview() =
ZcashTheme {
BlankSurface {
BalanceWidgetButton(
state =
BalanceButtonState(
icon = R.drawable.ic_help,
text = stringRes("text"),
amount = Zatoshi(1000),
onClick = {}
)
)
)
}
}
}

View File

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

View File

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

View File

@ -108,11 +108,12 @@ private fun BalanceActionRow(state: BalanceActionRowState) {
)
Spacer(1f)
when (state.icon) {
is ImageResource.ByDrawable -> Image(
modifier = Modifier.size(20.dp),
painter = painterResource(state.icon.resource),
contentDescription = null
)
is ImageResource.ByDrawable ->
Image(
modifier = Modifier.size(20.dp),
painter = painterResource(state.icon.resource),
contentDescription = null
)
ImageResource.Loading -> LottieProgress(modifier = Modifier.size(20.dp))
is ImageResource.DisplayString -> {
// do nothing
@ -122,11 +123,12 @@ private fun BalanceActionRow(state: BalanceActionRowState) {
SelectionContainer {
Text(
text = state.value.getValue(),
color = if (state.icon is ImageResource.Loading) {
ZashiColors.Text.textTertiary
} else {
ZashiColors.Text.textPrimary
},
color =
if (state.icon is ImageResource.Loading) {
ZashiColors.Text.textTertiary
} else {
ZashiColors.Text.textPrimary
},
style = ZashiTypography.textSm,
fontWeight = FontWeight.Medium
)
@ -176,9 +178,11 @@ private fun BalanceShieldButton(state: BalanceShieldButtonState) {
}
Spacer(1f)
ZashiButton(
state = ButtonState(
text = stringRes(R.string.balance_action_shield),
onClick = state.onShieldClick)
state =
ButtonState(
text = stringRes(R.string.balance_action_shield),
onClick = state.onShieldClick
)
)
}
}
@ -187,31 +191,36 @@ private fun BalanceShieldButton(state: BalanceShieldButtonState) {
@OptIn(ExperimentalMaterial3Api::class)
@PreviewScreens
@Composable
private fun Preview() = ZcashTheme {
BalanceActionView(
state = BalanceActionState(
title = stringRes("Title"),
message = stringRes("Subtitle"),
positive = ButtonState(
text = stringRes("Positive")
),
onBack = {},
rows = listOf(
BalanceActionRowState(
title = stringRes("Row"),
icon = loadingImageRes(),
value = stringRes("Value")
),
BalanceActionRowState(
title = stringRes("Row"),
icon = imageRes(R.drawable.ic_balance_shield),
value = stringRes("Value")
private fun Preview() =
ZcashTheme {
BalanceActionView(
state =
BalanceActionState(
title = stringRes("Title"),
message = stringRes("Subtitle"),
positive =
ButtonState(
text = stringRes("Positive")
),
onBack = {},
rows =
listOf(
BalanceActionRowState(
title = stringRes("Row"),
icon = loadingImageRes(),
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 shieldFunds: ShieldFundsUseCase,
) : ViewModel() {
val state = accountDataSource.selectedAccount
.mapNotNull {
createState(it)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = createState(accountDataSource.allAccounts.value.orEmpty().firstOrNull { it.isSelected })
)
val state =
accountDataSource.selectedAccount
.mapNotNull {
createState(it)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue =
createState(
accountDataSource.allAccounts.value
.orEmpty()
.firstOrNull { it.isSelected }
)
)
private fun createState(account: WalletAccount?): BalanceActionState? {
if (account == null) return null
@ -47,15 +52,16 @@ class BalanceActionViewModel(
}
private fun createMessage(account: WalletAccount): StringResource {
val pending = when {
account.totalShieldedBalance == account.spendableShieldedBalance ->
stringRes(R.string.balance_action_all_shielded)
val pending =
when {
account.totalShieldedBalance == account.spendableShieldedBalance ->
stringRes(R.string.balance_action_all_shielded)
account.totalShieldedBalance > account.spendableShieldedBalance ->
stringRes(R.string.balance_action_pending)
account.totalShieldedBalance > account.spendableShieldedBalance ->
stringRes(R.string.balance_action_pending)
else -> null
}
else -> null
}
val shielding = stringRes(R.string.balance_action_shield_message).takeIf { account.isShieldingAvailable }
@ -66,51 +72,54 @@ class BalanceActionViewModel(
}
}
private fun createPositiveButton(account: WalletAccount) = ButtonState(
text = if (account.isShieldingAvailable) {
stringRes(R.string.general_dismiss)
} else {
stringRes(R.string.general_ok)
},
onClick = ::onBack
)
private fun createPositiveButton(account: WalletAccount) =
ButtonState(
text =
if (account.isShieldingAvailable) {
stringRes(R.string.general_dismiss)
} else {
stringRes(R.string.general_ok)
},
onClick = ::onBack
)
private fun createInfoRows(account: WalletAccount) = listOfNotNull(
BalanceActionRowState(
title = stringRes(R.string.balance_action_info_shielded),
icon = imageRes(R.drawable.ic_balance_shield),
value = stringRes(R.string.general_zec, stringRes(account.spendableShieldedBalance))
),
when {
account.totalShieldedBalance > account.spendableShieldedBalance && account.isShieldedPending ->
BalanceActionRowState(
title = stringRes(R.string.balance_action_info_pending),
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)
private fun createInfoRows(account: WalletAccount) =
listOfNotNull(
BalanceActionRowState(
title = stringRes(R.string.balance_action_info_shielded),
icon = imageRes(R.drawable.ic_balance_shield),
value = stringRes(R.string.general_zec, stringRes(account.spendableShieldedBalance))
),
when {
account.totalShieldedBalance > account.spendableShieldedBalance && account.isShieldedPending ->
BalanceActionRowState(
title = stringRes(R.string.balance_action_info_pending),
icon = loadingImageRes(),
value = stringRes(R.string.general_zec, stringRes(account.pendingShieldedBalance))
)
)
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? {
return BalanceShieldButtonState(
else -> null
},
)
private fun createShieldButtonState(account: WalletAccount): BalanceShieldButtonState? =
BalanceShieldButtonState(
amount = account.transparent.balance,
onShieldClick = ::onShieldClick
).takeIf { account.isShieldingAvailable }
}
private fun onBack() = navigationRouter.back()
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.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R

View File

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

View File

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

View File

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

View File

@ -33,96 +33,115 @@ class ErrorViewModel(
private fun onBack() = navigationRouter.back()
private fun createState(args: ErrorArgs): ErrorState = when (args) {
is ErrorArgs.SyncError -> createSyncErrorState(args)
is ErrorArgs.ShieldingError -> createShieldingErrorState(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 createState(args: ErrorArgs): ErrorState =
when (args) {
is ErrorArgs.SyncError -> createSyncErrorState(args)
is ErrorArgs.ShieldingError -> createShieldingErrorState(args)
is ErrorArgs.General -> createGeneralErrorState(args)
is ErrorArgs.ShieldingGeneralError -> createGeneralShieldingErrorState(args)
}
}
private fun sendReportClick(args: ErrorArgs.SyncError) = viewModelScope.launch {
withContext(NonCancellable) {
navigationRouter.back()
sendEmailUseCase(args.synchronizerError)
}
}
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 sendReportClick(exception: Exception) = viewModelScope.launch {
withContext(NonCancellable) {
navigationRouter.back()
sendEmailUseCase(exception)
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 {
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 onBack: () -> Unit,
val onSkipClick: () -> Unit,
)
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,15 +18,16 @@ import org.koin.core.parameter.parametersOf
@Composable
internal fun AndroidHome() {
val topAppBarViewModel = koinActivityViewModel<ZashiTopAppBarViewModel>()
val balanceWidgetViewModel = koinViewModel<BalanceWidgetViewModel> {
parametersOf(
BalanceWidgetArgs(
isBalanceButtonEnabled = false,
isExchangeRateButtonEnabled = true,
showDust = false,
val balanceWidgetViewModel =
koinViewModel<BalanceWidgetViewModel> {
parametersOf(
BalanceWidgetArgs(
isBalanceButtonEnabled = false,
isExchangeRateButtonEnabled = true,
showDust = false,
)
)
)
}
}
val homeViewModel = koinViewModel<HomeViewModel>()
val transactionHistoryWidgetViewModel = koinViewModel<TransactionHistoryWidgetViewModel>()
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.restoring.WalletRestoringMessage
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.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.WalletUpdatingMessageState
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> {
val delayMs = delay?.inWholeMilliseconds?.toInt() ?: 0
return tween(

View File

@ -107,8 +107,7 @@ private fun Container(
0f to ZashiLightColors.Utility.Purple.utilityPurple500,
1f to ZashiLightColors.Utility.Purple.utilityPurple900,
)
)
.clickable(onClick = onClick)
).clickable(onClick = onClick)
.padding(contentPadding),
) {
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.fixture.BalanceStateFixture
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.BalanceWidgetState
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.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.WalletDisconnectedMessageState
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.CrashReportOptIn
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.shieldfunds.ShieldFundsInfo
@ -53,6 +53,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@Suppress("TooManyFunctions")
class HomeViewModel(
getHomeMessage: GetHomeMessageUseCase,
getVersionInfoProvider: GetVersionInfoProvider,
@ -63,15 +64,15 @@ class HomeViewModel(
private val shieldFunds: ShieldFundsUseCase,
private val navigateToError: NavigateToErrorUseCase
) : ViewModel() {
private val messageState = getHomeMessage
.observe()
.map { createMessageState(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = null
)
private val messageState =
getHomeMessage
.observe()
.map { createMessageState(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = null
)
private val isRestoreDialogVisible: Flow<Boolean?> =
isRestoreSuccessDialogVisible
@ -155,54 +156,65 @@ class HomeViewModel(
message = messageState
)
private fun createMessageState(it: HomeMessageData?) = when (it) {
is HomeMessageData.Backup -> WalletBackupMessageState(
onClick = ::onWalletBackupMessageClick,
onButtonClick = ::onWalletBackupMessageButtonClick,
)
private fun createMessageState(it: HomeMessageData?) =
when (it) {
is HomeMessageData.Backup ->
WalletBackupMessageState(
onClick = ::onWalletBackupMessageClick,
onButtonClick = ::onWalletBackupMessageButtonClick,
)
HomeMessageData.Disconnected -> WalletDisconnectedMessageState(
onClick = ::onWalletDisconnectedMessageClick
)
HomeMessageData.Disconnected ->
WalletDisconnectedMessageState(
onClick = ::onWalletDisconnectedMessageClick
)
HomeMessageData.EnableCurrencyConversion -> EnableCurrencyConversionMessageState(
onClick = ::onEnableCurrencyConversionClick,
onButtonClick = ::onEnableCurrencyConversionClick
)
HomeMessageData.EnableCurrencyConversion ->
EnableCurrencyConversionMessageState(
onClick = ::onEnableCurrencyConversionClick,
onButtonClick = ::onEnableCurrencyConversionClick
)
is HomeMessageData.Error -> WalletErrorMessageState(
onClick = { onWalletErrorMessageClick(it) }
)
is HomeMessageData.Error ->
WalletErrorMessageState(
onClick = { onWalletErrorMessageClick(it) }
)
is HomeMessageData.Restoring -> WalletRestoringMessageState(
progress = it.progress,
onClick = ::onWalletRestoringMessageClick
)
is HomeMessageData.Restoring ->
WalletRestoringMessageState(
progress = it.progress,
onClick = ::onWalletRestoringMessageClick
)
is HomeMessageData.Syncing -> WalletSyncingMessageState(
progress = it.progress,
onClick = ::onWalletSyncingMessageClick
)
is HomeMessageData.Syncing ->
WalletSyncingMessageState(
progress = it.progress,
onClick = ::onWalletSyncingMessageClick
)
is HomeMessageData.ShieldFunds -> ShieldFundsMessageState(
subtitle = stringRes(
R.string.home_message_transparent_balance_subtitle,
stringRes(it.zatoshi)
),
onClick = ::onShieldFundsMessageClick,
onButtonClick = ::onShieldFundsMessageButtonClick,
)
is HomeMessageData.ShieldFunds ->
ShieldFundsMessageState(
subtitle =
stringRes(
R.string.home_message_transparent_balance_subtitle,
stringRes(it.zatoshi)
),
onClick = ::onShieldFundsMessageClick,
onButtonClick = ::onShieldFundsMessageButtonClick,
)
HomeMessageData.Updating -> WalletUpdatingMessageState(
onClick = ::onWalletUpdatingMessageClick
)
HomeMessageData.Updating ->
WalletUpdatingMessageState(
onClick = ::onWalletUpdatingMessageClick
)
HomeMessageData.CrashReport -> CrashReportMessageState(
onClick = ::onCrashReportMessageClick,
onButtonClick = ::onCrashReportMessageClick
)
null -> null
}
HomeMessageData.CrashReport ->
CrashReportMessageState(
onClick = ::onCrashReportMessageClick,
onButtonClick = ::onCrashReportMessageClick
)
null -> null
}
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 co.electriccoin.zcash.ui.R
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.Spacer
import co.electriccoin.zcash.ui.design.component.VerticalSpacer
import co.electriccoin.zcash.ui.design.component.ZashiButton
import co.electriccoin.zcash.ui.design.component.ZashiIconButton
import co.electriccoin.zcash.ui.design.component.ZashiInfoRow

View File

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

View File

@ -42,11 +42,11 @@ fun WalletBackupInfoView(
state = state
) {
Column(
modifier = Modifier
.weight(1f, false)
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp)
modifier =
Modifier
.weight(1f, false)
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp)
) {
Image(
painter = painterResource(R.drawable.ic_info_backup),
@ -118,20 +118,23 @@ private fun Preview() =
WalletBackupInfoView(
WalletBackupInfoState(
onBack = {},
secondaryButton = ButtonState(
text = stringRes(R.string.general_remind_me_later),
onClick = {},
isEnabled = false
),
primaryButton = ButtonState(
text = stringRes(R.string.general_ok),
onClick = {}
),
checkboxState = CheckboxState(
isChecked = false,
onClick = {},
text = stringRes(R.string.home_info_backup_checkbox)
)
secondaryButton =
ButtonState(
text = stringRes(R.string.general_remind_me_later),
onClick = {},
isEnabled = false
),
primaryButton =
ButtonState(
text = stringRes(R.string.general_ok),
onClick = {}
),
checkboxState =
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 remindWalletBackupLater: RemindWalletBackupLaterUseCase,
) : ViewModel() {
private val isConsentChecked = MutableStateFlow(false)
private val lockoutDuration = walletBackupDataSource
.observe()
.filterIsInstance<WalletBackupAvailability.Available>()
.take(1)
.map { it.lockoutDuration }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = null
)
private val lockoutDuration =
walletBackupDataSource
.observe()
.filterIsInstance<WalletBackupAvailability.Available>()
.take(1)
.map { it.lockoutDuration }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = null
)
val state: StateFlow<WalletBackupInfoState?> = combine(
lockoutDuration.filterNotNull(),
isConsentChecked,
walletBackupConsentStorageProvider.observe().take(1)
) { lockout, isConsentChecked, isConsentSaved ->
val state: StateFlow<WalletBackupInfoState?> =
combine(
lockoutDuration.filterNotNull(),
isConsentChecked,
walletBackupConsentStorageProvider.observe().take(1)
) { lockout, isConsentChecked, isConsentSaved ->
WalletBackupInfoState(
onBack = ::onBack,
secondaryButton = ButtonState(
text = stringRes(R.string.general_remind_me_in, stringRes(lockout.res)),
onClick = ::onRemindMeLaterClick,
isEnabled = isConsentChecked || isConsentSaved
),
checkboxState = CheckboxState(
isChecked = isConsentChecked,
onClick = ::onConsentClick,
text = stringRes(R.string.home_info_backup_checkbox)
).takeIf { !isConsentSaved },
primaryButton = ButtonState(
text = stringRes(R.string.general_ok),
onClick = ::onPrimaryClick
)
secondaryButton =
ButtonState(
text = stringRes(R.string.general_remind_me_in, stringRes(lockout.res)),
onClick = ::onRemindMeLaterClick,
isEnabled = isConsentChecked || isConsentSaved
),
checkboxState =
CheckboxState(
isChecked = isConsentChecked,
onClick = ::onConsentClick,
text = stringRes(R.string.home_info_backup_checkbox)
).takeIf { !isConsentSaved },
primaryButton =
ButtonState(
text = stringRes(R.string.general_ok),
onClick = ::onPrimaryClick
)
)
}
.stateIn(
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
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
@Composable

View File

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

View File

@ -9,14 +9,15 @@ import kotlinx.coroutines.flow.StateFlow
class CrashReportOptInViewModel(
private val exchangeRateRepository: ExchangeRateRepository,
private val navigationRouter: NavigationRouter
): ViewModel() {
val state: StateFlow<CrashReportOptInState> = MutableStateFlow(
CrashReportOptInState(
onBack = ::dismissOptInExchangeRateUsd,
onOptInClick = ::optInExchangeRateUsd,
onOptOutClick = ::onSkipClick
) : ViewModel() {
val state: StateFlow<CrashReportOptInState> =
MutableStateFlow(
CrashReportOptInState(
onBack = ::dismissOptInExchangeRateUsd,
onOptInClick = ::optInExchangeRateUsd,
onOptOutClick = ::onSkipClick
)
)
)
private fun onSkipClick() {
exchangeRateRepository.optInExchangeRateUsd(false)
@ -31,4 +32,4 @@ class CrashReportOptInViewModel(
private fun dismissOptInExchangeRateUsd() {
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 co.electriccoin.zcash.ui.NavigationRouter
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.ShieldFundsRepository
import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase
@ -32,43 +31,42 @@ class ShieldFundsInfoViewModel(
private val remindShieldFundsLater: RemindShieldFundsLaterUseCase,
private val shieldFunds: ShieldFundsUseCase,
) : ViewModel() {
private val lockoutDuration = shieldFundsRepository
.availability
.filterIsInstance<ShieldFundsData.Available>()
.take(1)
.map { it.lockoutDuration }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = null
)
private val lockoutDuration =
shieldFundsRepository
.availability
.filterIsInstance<ShieldFundsData.Available>()
.take(1)
.map { it.lockoutDuration }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = null
)
val state: StateFlow<ShieldFundsInfoState?> =
combine(
getSelectedWalletAccount.observe(),
lockoutDuration.filterNotNull(),
) { account, lockoutDuration ->
ShieldFundsInfoState(
onBack = ::onBack,
primaryButton =
ButtonState(
onClick = ::onShieldClick,
text = stringRes(R.string.home_info_transparent_balance_shield)
),
secondaryButton =
ButtonState(
onClick = ::onRemindMeClick,
text = stringRes(R.string.general_remind_me_in, stringRes(lockoutDuration.res))
),
transparentAmount = account?.transparent?.balance ?: Zatoshi(0)
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = null
ShieldFundsInfoState(
onBack = ::onBack,
primaryButton =
ButtonState(
onClick = ::onShieldClick,
text = stringRes(R.string.home_info_transparent_balance_shield)
),
secondaryButton =
ButtonState(
onClick = ::onRemindMeClick,
text = stringRes(R.string.general_remind_me_in, stringRes(lockoutDuration.res))
),
transparentAmount = account?.transparent?.balance ?: Zatoshi(0)
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = null
)
private fun onRemindMeClick() = viewModelScope.launch { remindShieldFundsLater() }
@ -76,4 +74,3 @@ class ShieldFundsInfoViewModel(
private fun onShieldClick() = shieldFunds(closeCurrentScreen = true)
}

View File

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

View File

@ -17,4 +17,6 @@ fun AndroidRestoreBDDate(args: RestoreBDDate) {
}
@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 context: Context,
) : ViewModel() {
@Suppress("MagicNumber")
private val selection = MutableStateFlow<YearMonth>(YearMonth.of(2018, 10))
val state: StateFlow<RestoreBDDateState?> = selection
.map {
createState(it)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = null
)
val state: StateFlow<RestoreBDDateState?> =
selection
.map {
createState(it)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = null
)
private fun createState(selection: YearMonth) =
RestoreBDDateState(
@ -59,16 +59,19 @@ class RestoreBDDateViewModel(
private fun onEstimateClick() {
viewModelScope.launch {
val instant = selection.value.atDay(1)
.atStartOfDay()
.atZone(ZoneId.systemDefault())
.toInstant()
.toKotlinInstant()
val bday = SdkSynchronizer.estimateBirthdayHeight(
context = context,
date = instant,
network = ZcashNetwork.fromResources(context)
)
val instant =
selection.value
.atDay(1)
.atStartOfDay()
.atZone(ZoneId.systemDefault())
.toInstant()
.toKotlinInstant()
val bday =
SdkSynchronizer.estimateBirthdayHeight(
context = context,
date = instant,
network = ZcashNetwork.fromResources(context)
)
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.fixture.BalanceStateFixture
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.BalanceWidgetState
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.MemoState
@ -331,7 +331,7 @@ private fun SendForm(
recipientAddressState.type is AddressType.Valid &&
recipientAddressState.type !is AddressType.Transparent &&
recipientAddressState.type !is AddressType.Tex
)
)
SendFormAmountTextField(
amountState = amountState,
@ -653,9 +653,10 @@ fun SendFormAmountTextField(
)
},
modifier = Modifier.weight(1f),
innerModifier = ZashiTextFieldDefaults
.innerModifier
.testTag(SendTag.SEND_AMOUNT_FIELD),
innerModifier =
ZashiTextFieldDefaults
.innerModifier
.testTag(SendTag.SEND_AMOUNT_FIELD),
error = amountError,
placeholder = {
Text(

View File

@ -6,7 +6,6 @@ import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel
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.core.parameter.parametersOf

View File

@ -6,7 +6,6 @@ import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppBarViewModel
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import org.koin.androidx.compose.koinViewModel
@Composable

View File

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

View File

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

View File

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