[#310] [Scaffold] Progress Status Circular Bar
* [#310] [Scaffold] Progress Status Circular Bar - Added Z to Home ZECs balance - Added USD balance text to Home - Prepared Text extension functions for both - Provided Zatoshi to USD conversion methods + filed related SDK conversion issue * Update Home screen UI with progress bars - Implemented scaffolding UI of progress bars - Added related texts to strings * Update Home screen UI with progress bars - Connected some of the implemented UI elements to SDK values - Added app update information to Home screen - Update WalletSnapshot with progress field * Update Home screen UI with progress bars - Capturing and handling errors from SDK Synchronizer. - Added related error strings. - Simplified Home screen UI. * Zboto font added. Load it in runtime. Import to Typography. * Updated ZEC sign icon. * Draw ZEC balance with Zboto font * Simplify Home screen balances assigning * Switch to PercentDecimal progress representatiton * Support different locales while working with fiat currency * Fix bug in checking of fiat currency value * Generalize strings to provide possibility of other fiat currencies * Add fiat currency conversion states mechanism * Add TODO comment with reference to follow up SynchronizerError issue * Add WalletDisplayValues to simplify HomeView composable * Add CurrencyConversion class for connection to Price API (and convert Zatoshi to fiat currency) * Add basic HomeView tests * Add basic HomeViewIntegration test * Review changes - Used Duration API for times - Allow injecting clock into currency conversion - Moved FiatCurrencyConversionRateState to sdk-ext-ui because I suspect that we’ll consider this to be a UI object. I based this on the fact that current/stale cutoff values are arbitrary and probably should be the domain of the UI rather than the SDK. - Added some tests, although additional coverage is needed - Added fixtures for model objects * Minor code refactoring - Move UpdateInfoFixture class to fixture dir - Remove unnecessary annotation - Add common application context method to test suite - Fix Test class import - Move several WalletSnapshotFixture parameters to const fields * Add WalletDisplayValuesTest to cover the model class. * Fix import after changes merged * Use the new MonetarySeparatorsFixture in related tests * Add a few basic Zatoshi -> USD conversion tests * Turn on core lib desugaring for sdk-ext-ui-lib module * Make WalletDisplayValues a data class I think there may be some instances where this can help with recomposition * Add preference key for fiat currency This allows us to configure reading the value with observers correctly, even if we don’t allow the user to change it right now. * Delegate symbol and formatting to JVM * Add tests for Locale Co-authored-by: Carter Jernigan <git@carterjernigan.com>
This commit is contained in:
parent
f31cf5b486
commit
32c20953f7
|
@ -13,6 +13,7 @@ dependencies {
|
|||
implementation(libs.kotlin.stdlib)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
api(libs.zcash.sdk)
|
||||
api(libs.zcash.bip39)
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package cash.z.ecc.sdk.fixture
|
||||
|
||||
import cash.z.ecc.sdk.model.CurrencyConversion
|
||||
import cash.z.ecc.sdk.model.FiatCurrency
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.toInstant
|
||||
|
||||
object CurrencyConversionFixture {
|
||||
val FIAT_CURRENCY = FiatCurrencyFixture.new()
|
||||
val TIMESTAMP = "2022-07-08T11:51:44Z".toInstant()
|
||||
const val PRICE_OF_ZEC = 54.98
|
||||
|
||||
fun new(
|
||||
fiatCurrency: FiatCurrency = FIAT_CURRENCY,
|
||||
timestamp: Instant = TIMESTAMP,
|
||||
priceOfZec: Double = PRICE_OF_ZEC
|
||||
) = CurrencyConversion(fiatCurrency, timestamp, priceOfZec)
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package cash.z.ecc.sdk.fixture
|
||||
|
||||
import cash.z.ecc.sdk.model.FiatCurrency
|
||||
|
||||
object FiatCurrencyFixture {
|
||||
const val USD = "USD"
|
||||
|
||||
fun new(code: String = USD) = FiatCurrency(code)
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package cash.z.ecc.sdk.model
|
||||
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
/**
|
||||
* Represents a snapshot in time of a currency conversion rate.
|
||||
*
|
||||
* @param fiatCurrency The fiat currency for this conversion.
|
||||
* @param timestamp The timestamp when this conversion was obtained. This value is returned by
|
||||
* the server so it shouldn't have issues with client-side clock inaccuracy.
|
||||
* @param priceOfZec The conversion rate of ZEC to the fiat currency.
|
||||
*/
|
||||
data class CurrencyConversion(
|
||||
val fiatCurrency: FiatCurrency,
|
||||
val timestamp: Instant,
|
||||
val priceOfZec: Double
|
||||
) {
|
||||
init {
|
||||
require(priceOfZec > 0) { "priceOfZec must be greater than 0" }
|
||||
require(priceOfZec.isFinite()) { "priceOfZec must be finite" }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an ISO 4217 currency code.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
data class FiatCurrency(val code: String) {
|
||||
init {
|
||||
require(code.length == 3) { "Fiat currency code must be 3 characters long." }
|
||||
|
||||
// TODO [#532] https://github.com/zcash/secant-android-wallet/issues/532
|
||||
// Add another check to make sure the code is in the known ISO currency code list.
|
||||
}
|
||||
}
|
|
@ -8,12 +8,18 @@ plugins {
|
|||
|
||||
android {
|
||||
resourcePrefix = "co_electriccoin_zcash_"
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring(libs.desugaring)
|
||||
implementation(projects.sdkExtLib)
|
||||
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
|
||||
androidTestImplementation(libs.bundles.androidx.test)
|
||||
androidTestImplementation(libs.kotlin.test)
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
package cash.z.ecc.sdk.ext.ui.model
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.sdk.ext.ui.fixture.LocaleFixture
|
||||
import cash.z.ecc.sdk.ext.ui.fixture.MonetarySeparatorsFixture
|
||||
import cash.z.ecc.sdk.ext.ui.toFiatCurrencyState
|
||||
import cash.z.ecc.sdk.fixture.CurrencyConversionFixture
|
||||
import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class FiatCurrencyConversionRateStateTest {
|
||||
@Test
|
||||
@SmallTest
|
||||
fun future_near() {
|
||||
val zatoshi = ZatoshiFixture.new()
|
||||
|
||||
val frozenClock = FrozenClock(
|
||||
CurrencyConversionFixture.TIMESTAMP - FiatCurrencyConversionRateState.FUTURE_CUTOFF_AGE_INCLUSIVE
|
||||
)
|
||||
|
||||
val currencyConversion = CurrencyConversionFixture.new()
|
||||
|
||||
val result = zatoshi.toFiatCurrencyState(currencyConversion, LocaleFixture.new(), MonetarySeparatorsFixture.new(), frozenClock)
|
||||
|
||||
assertIs<FiatCurrencyConversionRateState.Current>(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun future_far() {
|
||||
val zatoshi = ZatoshiFixture.new()
|
||||
|
||||
val frozenClock = FrozenClock(
|
||||
CurrencyConversionFixture.TIMESTAMP - FiatCurrencyConversionRateState.FUTURE_CUTOFF_AGE_INCLUSIVE - 1.seconds
|
||||
)
|
||||
|
||||
val currencyConversion = CurrencyConversionFixture.new()
|
||||
|
||||
val result = zatoshi.toFiatCurrencyState(currencyConversion, LocaleFixture.new(), MonetarySeparatorsFixture.new(), frozenClock)
|
||||
|
||||
assertIs<FiatCurrencyConversionRateState.Unavailable>(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun current() {
|
||||
val zatoshi = ZatoshiFixture.new()
|
||||
|
||||
val frozenClock = FrozenClock(CurrencyConversionFixture.TIMESTAMP)
|
||||
|
||||
val currencyConversion = CurrencyConversionFixture.new(
|
||||
timestamp = CurrencyConversionFixture.TIMESTAMP - 1.seconds
|
||||
)
|
||||
|
||||
val result = zatoshi.toFiatCurrencyState(currencyConversion, LocaleFixture.new(), MonetarySeparatorsFixture.new(), frozenClock)
|
||||
|
||||
assertIs<FiatCurrencyConversionRateState.Current>(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun stale() {
|
||||
val zatoshi = ZatoshiFixture.new()
|
||||
|
||||
val frozenClock = FrozenClock(CurrencyConversionFixture.TIMESTAMP)
|
||||
|
||||
val currencyConversion = CurrencyConversionFixture.new(
|
||||
timestamp = CurrencyConversionFixture.TIMESTAMP - FiatCurrencyConversionRateState.CURRENT_CUTOFF_AGE_INCLUSIVE - 1.seconds
|
||||
)
|
||||
|
||||
val result = zatoshi.toFiatCurrencyState(currencyConversion, LocaleFixture.new(), MonetarySeparatorsFixture.new(), frozenClock)
|
||||
|
||||
assertIs<FiatCurrencyConversionRateState.Stale>(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun too_stale() {
|
||||
val zatoshi = ZatoshiFixture.new()
|
||||
|
||||
val frozenClock = FrozenClock(CurrencyConversionFixture.TIMESTAMP)
|
||||
|
||||
val currencyConversion = CurrencyConversionFixture.new(
|
||||
timestamp = CurrencyConversionFixture.TIMESTAMP - FiatCurrencyConversionRateState.STALE_CUTOFF_AGE_INCLUSIVE - 1.seconds
|
||||
)
|
||||
|
||||
val result = zatoshi.toFiatCurrencyState(currencyConversion, LocaleFixture.new(), MonetarySeparatorsFixture.new(), frozenClock)
|
||||
|
||||
assertIs<FiatCurrencyConversionRateState.Unavailable>(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun null_conversion_rate() {
|
||||
val zatoshi = ZatoshiFixture.new()
|
||||
|
||||
val result = zatoshi.toFiatCurrencyState(null, LocaleFixture.new(), MonetarySeparatorsFixture.new())
|
||||
|
||||
assertIs<FiatCurrencyConversionRateState.Unavailable>(result)
|
||||
}
|
||||
}
|
||||
|
||||
private class FrozenClock(private val timestamp: Instant) : Clock {
|
||||
override fun now() = timestamp
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package cash.z.ecc.sdk.ext.ui.model
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class LocaleTest {
|
||||
@Test
|
||||
@SmallTest
|
||||
fun toKotlinLocale() {
|
||||
val javaLocale = java.util.Locale.forLanguageTag("en-US")
|
||||
|
||||
val kotlinLocale = javaLocale.toKotlinLocale()
|
||||
assertEquals("en", kotlinLocale.language)
|
||||
assertEquals("US", kotlinLocale.region)
|
||||
assertEquals(null, kotlinLocale.variant)
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun toJavaLocale() {
|
||||
val kotlinLocale = cash.z.ecc.sdk.ext.ui.model.Locale("en", "US", null)
|
||||
val javaLocale = kotlinLocale.toJavaLocale()
|
||||
assertEquals("en-US", javaLocale.toLanguageTag())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package cash.z.ecc.sdk.ext.ui.model
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.sdk.ext.ui.fixture.LocaleFixture
|
||||
import cash.z.ecc.sdk.ext.ui.fixture.MonetarySeparatorsFixture
|
||||
import cash.z.ecc.sdk.ext.ui.toFiatString
|
||||
import cash.z.ecc.sdk.fixture.CurrencyConversionFixture
|
||||
import cash.z.ecc.sdk.fixture.ZatoshiFixture
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ZatoshiExtTest {
|
||||
companion object {
|
||||
private val EN_US_SEPARATORS = MonetarySeparatorsFixture.new()
|
||||
private val CURRENCY_CONVERSION = CurrencyConversionFixture.new()
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun zero_zatoshi_to_fiat_conversion_test() {
|
||||
val zatoshi = ZatoshiFixture.new(0L)
|
||||
val fiatString = zatoshi.toFiatString(CURRENCY_CONVERSION, LocaleFixture.new(), EN_US_SEPARATORS)
|
||||
|
||||
fiatString.also {
|
||||
assertNotNull(it)
|
||||
assertTrue(it.isNotEmpty())
|
||||
assertTrue(it.contains("0"))
|
||||
assertTrue(it.isValidNumber(EN_US_SEPARATORS))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun regular_zatoshi_to_fiat_conversion_test() {
|
||||
val zatoshi = ZatoshiFixture.new(123_456_789L)
|
||||
val fiatString = zatoshi.toFiatString(CURRENCY_CONVERSION, LocaleFixture.new(), EN_US_SEPARATORS)
|
||||
|
||||
fiatString.also {
|
||||
assertNotNull(it)
|
||||
assertTrue(it.isNotEmpty())
|
||||
assertTrue(it.isValidNumber(EN_US_SEPARATORS))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun rounded_zatoshi_to_fiat_conversion_test() {
|
||||
val roundedZatoshi = ZatoshiFixture.new(100_000_000L)
|
||||
val roundedCurrencyConversion = CurrencyConversionFixture.new(
|
||||
priceOfZec = 100.0
|
||||
)
|
||||
|
||||
val fiatString = roundedZatoshi.toFiatString(
|
||||
roundedCurrencyConversion,
|
||||
LocaleFixture.new(),
|
||||
EN_US_SEPARATORS
|
||||
)
|
||||
|
||||
fiatString.also {
|
||||
assertNotNull(it)
|
||||
assertTrue(it.isNotEmpty())
|
||||
assertTrue(it.isValidNumber(EN_US_SEPARATORS))
|
||||
assertTrue("$100${EN_US_SEPARATORS.decimal}00" == it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Char.isDigitOrSeparator(separators: MonetarySeparators): Boolean {
|
||||
return this.isDigit() || this == separators.decimal || this == separators.grouping
|
||||
}
|
||||
|
||||
private fun String.isValidNumber(separators: MonetarySeparators): Boolean {
|
||||
return this
|
||||
.drop(1) // remove currency symbol
|
||||
.all { return it.isDigitOrSeparator(separators) }
|
||||
}
|
|
@ -5,6 +5,7 @@ import android.content.res.Configuration
|
|||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.ext.ui.fixture.MonetarySeparatorsFixture
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
|
@ -15,7 +16,7 @@ import kotlin.test.assertNull
|
|||
class ZecStringTest {
|
||||
|
||||
companion object {
|
||||
private val EN_US_MONETARY_SEPARATORS = MonetarySeparators(',', '.')
|
||||
private val EN_US_MONETARY_SEPARATORS = MonetarySeparatorsFixture.new()
|
||||
private val context = run {
|
||||
val applicationContext = ApplicationProvider.getApplicationContext<Context>()
|
||||
val enUsConfiguration = Configuration(applicationContext.resources.configuration).apply {
|
||||
|
|
|
@ -3,7 +3,7 @@ package cash.z.ecc.sdk.ext.ui.regex
|
|||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.sdk.ext.ui.R
|
||||
import cash.z.ecc.sdk.ext.ui.ZecStringExt
|
||||
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
||||
import cash.z.ecc.sdk.ext.ui.fixture.MonetarySeparatorsFixture
|
||||
import cash.z.ecc.sdk.ext.ui.test.getStringResourceWithArgs
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFalse
|
||||
|
@ -13,7 +13,7 @@ import kotlin.test.assertTrue
|
|||
class ZecStringExtTest {
|
||||
|
||||
companion object {
|
||||
private val EN_US_SEPARATORS = MonetarySeparators(',', '.')
|
||||
private val EN_US_SEPARATORS = MonetarySeparatorsFixture.new()
|
||||
}
|
||||
|
||||
private fun getContinuousRegex(): Regex {
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
@file:Suppress("ktlint:filename")
|
||||
|
||||
package cash.z.ecc.sdk.ext.ui
|
||||
|
||||
import cash.z.ecc.android.sdk.ext.Conversions
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.ext.ui.model.FiatCurrencyConversionRateState
|
||||
import cash.z.ecc.sdk.ext.ui.model.Locale
|
||||
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
||||
import cash.z.ecc.sdk.ext.ui.model.toJavaLocale
|
||||
import cash.z.ecc.sdk.model.CurrencyConversion
|
||||
import kotlinx.datetime.Clock
|
||||
import java.math.BigDecimal
|
||||
import java.math.MathContext
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import java.util.Currency
|
||||
import kotlin.time.Duration
|
||||
|
||||
fun Zatoshi.toFiatCurrencyState(
|
||||
currencyConversion: CurrencyConversion?,
|
||||
locale: Locale,
|
||||
monetarySeparators: MonetarySeparators,
|
||||
clock: Clock = Clock.System
|
||||
): FiatCurrencyConversionRateState {
|
||||
if (currencyConversion == null) {
|
||||
return FiatCurrencyConversionRateState.Unavailable
|
||||
}
|
||||
|
||||
val fiatCurrencyConversionRate = toFiatString(currencyConversion, locale, monetarySeparators)
|
||||
|
||||
val currentSystemTime = clock.now()
|
||||
|
||||
val age = currentSystemTime - currencyConversion.timestamp
|
||||
|
||||
return if (age < Duration.ZERO && age.absoluteValue > FiatCurrencyConversionRateState.FUTURE_CUTOFF_AGE_INCLUSIVE) {
|
||||
// Special case if the device's clock is set to the future.
|
||||
// TODO [#535]: Consider using NTP requests to get the correct time instead of relying on the device's clock.
|
||||
FiatCurrencyConversionRateState.Unavailable
|
||||
} else if (age <= FiatCurrencyConversionRateState.CURRENT_CUTOFF_AGE_INCLUSIVE) {
|
||||
FiatCurrencyConversionRateState.Current(fiatCurrencyConversionRate)
|
||||
} else if (age <= FiatCurrencyConversionRateState.STALE_CUTOFF_AGE_INCLUSIVE) {
|
||||
FiatCurrencyConversionRateState.Stale(fiatCurrencyConversionRate)
|
||||
} else {
|
||||
FiatCurrencyConversionRateState.Unavailable
|
||||
}
|
||||
}
|
||||
|
||||
fun Zatoshi.toFiatString(
|
||||
currencyConversion: CurrencyConversion,
|
||||
locale: Locale,
|
||||
monetarySeparators: MonetarySeparators
|
||||
) =
|
||||
convertZatoshiToZecDecimal()
|
||||
.convertZecDecimalToFiatDecimal(BigDecimal(currencyConversion.priceOfZec))
|
||||
.convertFiatDecimalToFiatString(
|
||||
Currency.getInstance(currencyConversion.fiatCurrency.code),
|
||||
locale.toJavaLocale(),
|
||||
monetarySeparators
|
||||
)
|
||||
|
||||
private fun Zatoshi.convertZatoshiToZecDecimal(): BigDecimal {
|
||||
return BigDecimal(value, MathContext.DECIMAL128).divide(
|
||||
Conversions.ONE_ZEC_IN_ZATOSHI,
|
||||
MathContext.DECIMAL128
|
||||
).setScale(Conversions.ZEC_FORMATTER.maximumFractionDigits, RoundingMode.HALF_EVEN)
|
||||
}
|
||||
|
||||
private fun BigDecimal.convertZecDecimalToFiatDecimal(zecPrice: BigDecimal): BigDecimal {
|
||||
return multiply(zecPrice, MathContext.DECIMAL128)
|
||||
}
|
||||
|
||||
private fun BigDecimal.convertFiatDecimalToFiatString(
|
||||
fiatCurrency: Currency,
|
||||
locale: java.util.Locale,
|
||||
monetarySeparators: MonetarySeparators
|
||||
): String {
|
||||
return NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = fiatCurrency
|
||||
roundingMode = RoundingMode.HALF_EVEN
|
||||
if (this is DecimalFormat) {
|
||||
decimalFormatSymbols.apply {
|
||||
decimalSeparator = monetarySeparators.decimal
|
||||
monetaryDecimalSeparator = monetarySeparators.decimal
|
||||
groupingSeparator = monetarySeparators.grouping
|
||||
}
|
||||
}
|
||||
}.format(this)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package cash.z.ecc.sdk.ext.ui.fixture
|
||||
|
||||
import cash.z.ecc.sdk.ext.ui.model.Locale
|
||||
|
||||
object LocaleFixture {
|
||||
const val LANGUAGE = "en"
|
||||
const val COUNTRY = "US"
|
||||
val VARIANT: String? = null
|
||||
|
||||
fun new(
|
||||
language: String = LANGUAGE,
|
||||
country: String? = COUNTRY,
|
||||
variant: String? = VARIANT
|
||||
) = Locale(language, country, variant)
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package cash.z.ecc.sdk.ext.ui.fixture
|
||||
|
||||
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
||||
|
||||
object MonetarySeparatorsFixture {
|
||||
const val US_GROUPING_SEPARATOR = ','
|
||||
const val US_DECIMAL_SEPARATOR = '.'
|
||||
|
||||
fun new(
|
||||
grouping: Char = US_GROUPING_SEPARATOR,
|
||||
decimal: Char = US_DECIMAL_SEPARATOR
|
||||
) = MonetarySeparators(grouping, decimal)
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package cash.z.ecc.sdk.ext.ui.model
|
||||
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
/**
|
||||
* Represents a state of current fiat currency conversion to ZECs.
|
||||
*/
|
||||
sealed class FiatCurrencyConversionRateState {
|
||||
/**
|
||||
* @param formattedFiatValue A fiat value formatted as a localized string. E.g. $1.00.
|
||||
*/
|
||||
data class Current(val formattedFiatValue: String) : FiatCurrencyConversionRateState()
|
||||
|
||||
/**
|
||||
* @param formattedFiatValue A fiat value formatted as a localized string. E.g. $1.00.
|
||||
*/
|
||||
data class Stale(val formattedFiatValue: String) : FiatCurrencyConversionRateState()
|
||||
object Unavailable : FiatCurrencyConversionRateState()
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Cutoff negative age. Some users may intentionally set their clock forward 10 minutes
|
||||
* because they're always late to things. This allows the app to mostly work for those users,
|
||||
* while still failing if the clock is way off.
|
||||
*/
|
||||
val FUTURE_CUTOFF_AGE_INCLUSIVE = 10.minutes
|
||||
|
||||
/**
|
||||
* Cutoff age for next attempt to refresh the conversion rate from the API.
|
||||
*/
|
||||
val CURRENT_CUTOFF_AGE_INCLUSIVE = 1.minutes
|
||||
|
||||
/**
|
||||
* Cutoff age for displaying conversion rate from prior app launches or background refresh.
|
||||
*/
|
||||
val STALE_CUTOFF_AGE_INCLUSIVE = 1.days
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package cash.z.ecc.sdk.ext.ui.model
|
||||
|
||||
data class Locale(val language: String, val region: String?, val variant: String?) {
|
||||
companion object
|
||||
}
|
||||
|
||||
fun Locale.toJavaLocale(): java.util.Locale {
|
||||
return if (!region.isNullOrEmpty() && !variant.isNullOrEmpty()) {
|
||||
java.util.Locale(language, region, variant)
|
||||
} else if (!region.isNullOrEmpty() && variant.isNullOrEmpty()) {
|
||||
java.util.Locale(language, region)
|
||||
} else {
|
||||
java.util.Locale(language)
|
||||
}
|
||||
}
|
||||
|
||||
fun java.util.Locale.toKotlinLocale(): Locale {
|
||||
val resultCountry = if (country.isNullOrEmpty()) {
|
||||
null
|
||||
} else {
|
||||
country
|
||||
}
|
||||
|
||||
val resultVariant = if (variant.isNullOrEmpty()) {
|
||||
null
|
||||
} else {
|
||||
variant
|
||||
}
|
||||
|
||||
return Locale(language, resultCountry, resultVariant)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package co.electriccoin.zcash.ui.design.compat
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.FontRes
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import co.electriccoin.zcash.spackle.AndroidApiVersion
|
||||
import co.electriccoin.zcash.ui.design.R
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
object FontCompat {
|
||||
fun isFontPrefetchNeeded() = !AndroidApiVersion.isAtLeastO
|
||||
|
||||
suspend fun prefetchFontsLegacy(context: Context) {
|
||||
prefetchFontLegacy(context, R.font.zboto)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-fetches fonts on Android N (API 25) and below.
|
||||
*/
|
||||
/*
|
||||
* ResourcesCompat is used implicitly by Compose on older Android versions.
|
||||
* The backwards compatibility library performs disk IO and then
|
||||
* caches the results. This moves that IO off the main thread, to prevent ANRs and
|
||||
* jank during app startup.
|
||||
*/
|
||||
private suspend fun prefetchFontLegacy(context: Context, @FontRes fontRes: Int) =
|
||||
withContext(Dispatchers.IO) {
|
||||
ResourcesCompat.getFont(context, fontRes)
|
||||
}
|
||||
}
|
|
@ -5,8 +5,11 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import co.electriccoin.zcash.ui.design.R
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
|
||||
@Composable
|
||||
|
@ -35,6 +38,21 @@ fun Body(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Small(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
textAlign: TextAlign
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = modifier,
|
||||
textAlign = textAlign
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListItem(
|
||||
text: String,
|
||||
|
@ -77,3 +95,36 @@ fun Reference(
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass amount of ZECs you want to display and the component appends ZEC symbol to it. We're using
|
||||
* a custom font here, which is Roboto modified to replace the dollar symbol with the ZEC symbol internally.
|
||||
*
|
||||
* @param amount of ZECs to be displayed
|
||||
* @param modifier to modify the Text UI element as needed
|
||||
*/
|
||||
@Composable
|
||||
fun HeaderWithZecIcon(
|
||||
amount: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.amount_with_zec_currency_symbol, amount),
|
||||
style = ZcashTheme.typography.zecBalance,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BodyWithFiatCurrencySymbol(
|
||||
amount: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Text(
|
||||
text = amount,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.Immutable
|
|||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.text.ExperimentalTextApi
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.googlefonts.Font
|
||||
|
@ -33,6 +34,10 @@ private val RubikFontFamily = FontFamily(
|
|||
Font(googleFont = RubikFont, fontProvider = provider, weight = FontWeight.Bold) // W700
|
||||
)
|
||||
|
||||
private val Zboto = FontFamily(
|
||||
Font(R.font.zboto, FontWeight.Normal)
|
||||
)
|
||||
|
||||
// If you change this definition of our Typography, don't forget to check if you use only
|
||||
// the defined font weights above, otherwise the closest one will be used.
|
||||
internal val Typography = Typography(
|
||||
|
@ -61,7 +66,8 @@ internal val Typography = Typography(
|
|||
@Immutable
|
||||
data class ExtendedTypography(
|
||||
val chipIndex: TextStyle,
|
||||
val listItem: TextStyle
|
||||
val listItem: TextStyle,
|
||||
val zecBalance: TextStyle
|
||||
)
|
||||
|
||||
val LocalExtendedTypography = staticCompositionLocalOf {
|
||||
|
@ -73,6 +79,11 @@ val LocalExtendedTypography = staticCompositionLocalOf {
|
|||
),
|
||||
listItem = Typography.bodyLarge.copy(
|
||||
fontSize = 24.sp
|
||||
),
|
||||
zecBalance = TextStyle(
|
||||
fontFamily = Zboto,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 30.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="@android:color/black"
|
||||
android:pathData="M22.55,42V37.8Q19.7,37.3 17.875,35.625Q16.05,33.95 15.25,31.4L18.05,30.25Q18.9,32.65 20.5,33.825Q22.1,35 24.35,35Q26.75,35 28.3,33.8Q29.85,32.6 29.85,30.5Q29.85,28.3 28.475,27.1Q27.1,25.9 23.3,24.65Q19.7,23.5 17.925,21.6Q16.15,19.7 16.15,16.85Q16.15,14.1 17.925,12.25Q19.7,10.4 22.55,10.15V6H25.55V10.15Q27.8,10.4 29.425,11.625Q31.05,12.85 31.9,14.75L29.1,15.95Q28.4,14.35 27.225,13.625Q26.05,12.9 24.15,12.9Q21.85,12.9 20.5,13.95Q19.15,15 19.15,16.8Q19.15,18.7 20.65,19.875Q22.15,21.05 26.2,22.3Q29.6,23.35 31.225,25.325Q32.85,27.3 32.85,30.3Q32.85,33.45 31,35.375Q29.15,37.3 25.55,37.85V42Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="3.17dp"
|
||||
android:height="5.57dp"
|
||||
android:viewportWidth="3.17"
|
||||
android:viewportHeight="5.57">
|
||||
<path
|
||||
android:pathData="M1.96,5.57l0,-0.75l1.21,0l0,-0.84l-1.96,0l1.62,-2.18l0.34,-0.44l0,0l0,-0.61l-1.21,0l0,-0.75l-0.75,0l0,0.75l-1.21,0l0,0.84l1.96,0l-1.62,2.12l-0.34,0.44l0,0.67l1.21,0l0,0.75l0.75,0z"
|
||||
android:fillColor="#1d181a"/>
|
||||
</vector>
|
Binary file not shown.
|
@ -0,0 +1,4 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="amount_with_zec_currency_symbol" formatted="true">$<xliff:g id="zec_amount" example="123">%1$s</xliff:g></string>
|
||||
<string name="amount_with_fiat_currency_symbol" formatted="true"><xliff:g id="fiat_currency_symbol" example="$">%1$s</xliff:g><xliff:g id="amount" example="123">%2$s</xliff:g></string>
|
||||
</resources>
|
|
@ -5,8 +5,8 @@ import android.content.Context
|
|||
import androidx.activity.ComponentActivity
|
||||
import co.electriccoin.zcash.spackle.getPackageInfoCompat
|
||||
import co.electriccoin.zcash.spackle.versionCodeCompat
|
||||
import co.electriccoin.zcash.ui.fixture.UpdateInfoFixture
|
||||
import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker
|
||||
import co.electriccoin.zcash.ui.screen.update.fixture.UpdateInfoFixture
|
||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
|
||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
||||
import com.google.android.play.core.appupdate.AppUpdateInfo
|
||||
|
|
|
@ -4,9 +4,9 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.test.filters.MediumTest
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.fixture.UpdateInfoFixture
|
||||
import co.electriccoin.zcash.ui.integration.test.common.IntegrationTestingActivity
|
||||
import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker
|
||||
import co.electriccoin.zcash.ui.screen.update.fixture.UpdateInfoFixture
|
||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
|
||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
||||
import co.electriccoin.zcash.ui.screen.update.viewmodel.UpdateViewModel
|
||||
|
|
|
@ -13,13 +13,11 @@ import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
|
|||
import co.electriccoin.zcash.ui.screen.about.model.VersionInfo
|
||||
import co.electriccoin.zcash.ui.screen.about.view.About
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class AboutViewTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
package co.electriccoin.zcash.ui.screen.home
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.screen.home.view.Home
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class HomeTestSetup(
|
||||
private val composeTestRule: ComposeContentTestRule,
|
||||
private val walletSnapshot: WalletSnapshot
|
||||
) {
|
||||
private val onScanCount = AtomicInteger(0)
|
||||
private val onProfileCount = AtomicInteger(0)
|
||||
private val onSendCount = AtomicInteger(0)
|
||||
private val onRequestCount = AtomicInteger(0)
|
||||
|
||||
fun getOnScanCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onScanCount.get()
|
||||
}
|
||||
|
||||
fun getOnProfileCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onProfileCount.get()
|
||||
}
|
||||
|
||||
fun getOnSendCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onSendCount.get()
|
||||
}
|
||||
|
||||
fun getOnRequestCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onRequestCount.get()
|
||||
}
|
||||
|
||||
fun getWalletSnapshot(): WalletSnapshot {
|
||||
composeTestRule.waitForIdle()
|
||||
return walletSnapshot
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getDefaultContent() {
|
||||
Home(
|
||||
walletSnapshot,
|
||||
emptyList(),
|
||||
goScan = {
|
||||
onScanCount.incrementAndGet()
|
||||
},
|
||||
goProfile = {
|
||||
onProfileCount.incrementAndGet()
|
||||
},
|
||||
goSend = {
|
||||
onSendCount.incrementAndGet()
|
||||
},
|
||||
goRequest = {
|
||||
onRequestCount.incrementAndGet()
|
||||
},
|
||||
resetSdk = {},
|
||||
wipeEntireWallet = {},
|
||||
isDebugMenuEnabled = false,
|
||||
updateAvailable = false
|
||||
)
|
||||
}
|
||||
|
||||
fun setDefaultContent() {
|
||||
composeTestRule.setContent {
|
||||
ZcashTheme {
|
||||
getDefaultContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package co.electriccoin.zcash.ui.screen.home.integration
|
||||
|
||||
import androidx.compose.ui.test.assertHeightIsAtLeast
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertWidthIsAtLeast
|
||||
import androidx.compose.ui.test.junit4.StateRestorationTester
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.sdk.model.PercentDecimal
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.screen.home.HomeTag
|
||||
import co.electriccoin.zcash.ui.screen.home.HomeTestSetup
|
||||
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class HomeViewIntegrationTest : UiTestPrerequisites() {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
private fun newTestSetup(walletSnapshot: WalletSnapshot) = HomeTestSetup(
|
||||
composeTestRule,
|
||||
walletSnapshot
|
||||
)
|
||||
|
||||
// This is just basic sanity check that we still have UI set up as expected after the state restore
|
||||
@Test
|
||||
@MediumTest
|
||||
fun wallet_snapshot_restoration() {
|
||||
val restorationTester = StateRestorationTester(composeTestRule)
|
||||
val walletSnapshot = WalletSnapshotFixture.new(
|
||||
status = Synchronizer.Status.DOWNLOADING,
|
||||
progress = PercentDecimal(0.5f)
|
||||
)
|
||||
val testSetup = newTestSetup(walletSnapshot)
|
||||
|
||||
restorationTester.setContent {
|
||||
testSetup.getDefaultContent()
|
||||
}
|
||||
|
||||
assertNotEquals(WalletSnapshotFixture.STATUS, testSetup.getWalletSnapshot().status)
|
||||
assertEquals(Synchronizer.Status.DOWNLOADING, testSetup.getWalletSnapshot().status)
|
||||
|
||||
assertNotEquals(WalletSnapshotFixture.PROGRESS, testSetup.getWalletSnapshot().progress)
|
||||
assertEquals(0.5f, testSetup.getWalletSnapshot().progress.decimal)
|
||||
|
||||
restorationTester.emulateSavedInstanceStateRestore()
|
||||
|
||||
assertNotEquals(WalletSnapshotFixture.STATUS, testSetup.getWalletSnapshot().status)
|
||||
assertEquals(Synchronizer.Status.DOWNLOADING, testSetup.getWalletSnapshot().status)
|
||||
|
||||
assertNotEquals(WalletSnapshotFixture.PROGRESS, testSetup.getWalletSnapshot().progress)
|
||||
assertEquals(0.5f, testSetup.getWalletSnapshot().progress.decimal)
|
||||
|
||||
composeTestRule.onNodeWithTag(HomeTag.PROGRESS).also {
|
||||
it.assertIsDisplayed()
|
||||
it.assertHeightIsAtLeast(1.dp)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(HomeTag.PROGRESS).also {
|
||||
it.assertIsDisplayed()
|
||||
it.assertHeightIsAtLeast(1.dp)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(HomeTag.SINGLE_LINE_TEXT).also {
|
||||
it.assertIsDisplayed()
|
||||
it.assertWidthIsAtLeast(1.dp)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package co.electriccoin.zcash.ui.screen.home.model
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.sdk.ext.ui.model.FiatCurrencyConversionRateState
|
||||
import cash.z.ecc.sdk.ext.ui.model.toZecString
|
||||
import cash.z.ecc.sdk.model.PercentDecimal
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.test.getAppContext
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class WalletDisplayValuesTest {
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun download_running_test() {
|
||||
val walletSnapshot = WalletSnapshotFixture.new(
|
||||
progress = PercentDecimal.ONE_HUNDRED_PERCENT,
|
||||
status = Synchronizer.Status.SCANNING,
|
||||
orchardBalance = WalletSnapshotFixture.ORCHARD_BALANCE,
|
||||
saplingBalance = WalletSnapshotFixture.SAPLING_BALANCE,
|
||||
transparentBalance = WalletSnapshotFixture.TRANSPARENT_BALANCE
|
||||
)
|
||||
val values = WalletDisplayValues.getNextValues(
|
||||
getAppContext(),
|
||||
walletSnapshot,
|
||||
false
|
||||
)
|
||||
|
||||
assertNotNull(values)
|
||||
assertEquals(1f, values.progress.decimal)
|
||||
assertEquals(walletSnapshot.totalBalance().toZecString(), values.zecAmountText)
|
||||
assertEquals(getStringResource(R.string.home_status_syncing_catchup), values.statusText)
|
||||
// TODO [#578] https://github.com/zcash/zcash-android-wallet-sdk/issues/578
|
||||
assertEquals(FiatCurrencyConversionRateState.Unavailable, values.fiatCurrencyAmountState)
|
||||
assertEquals(getStringResource(R.string.fiat_currency_conversion_rate_unavailable), values.fiatCurrencyAmountText)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
package co.electriccoin.zcash.ui.screen.home.view
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import androidx.compose.ui.test.junit4.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.test.filters.MediumTest
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.screen.home.HomeTag
|
||||
import co.electriccoin.zcash.ui.screen.home.HomeTestSetup
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class HomeViewTest : UiTestPrerequisites() {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun check_all_elementary_ui_elements_displayed() {
|
||||
newTestSetup()
|
||||
|
||||
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.home_scan_content_description)).also {
|
||||
it.assertIsDisplayed()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.home_profile_content_description)).also {
|
||||
it.assertIsDisplayed()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(HomeTag.STATUS_VIEWS).also {
|
||||
it.assertIsDisplayed()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.home_button_send)).also {
|
||||
it.assertIsDisplayed()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.home_button_request)).also {
|
||||
it.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun click_scan_button() {
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
assertEquals(0, testSetup.getOnScanCount())
|
||||
|
||||
composeTestRule.clickScan()
|
||||
|
||||
assertEquals(1, testSetup.getOnScanCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun click_profile_button() {
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
assertEquals(0, testSetup.getOnProfileCount())
|
||||
|
||||
composeTestRule.clickProfile()
|
||||
|
||||
assertEquals(1, testSetup.getOnProfileCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun click_send_button() {
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
assertEquals(0, testSetup.getOnSendCount())
|
||||
|
||||
composeTestRule.clickSend()
|
||||
|
||||
assertEquals(1, testSetup.getOnSendCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun click_request_button() {
|
||||
val testSetup = newTestSetup()
|
||||
|
||||
assertEquals(0, testSetup.getOnRequestCount())
|
||||
|
||||
composeTestRule.clickRequest()
|
||||
|
||||
assertEquals(1, testSetup.getOnRequestCount())
|
||||
}
|
||||
|
||||
private fun newTestSetup() = HomeTestSetup(
|
||||
composeTestRule,
|
||||
WalletSnapshotFixture.new()
|
||||
).apply {
|
||||
setDefaultContent()
|
||||
}
|
||||
}
|
||||
|
||||
fun ComposeContentTestRule.clickScan() {
|
||||
onNodeWithContentDescription(getStringResource(R.string.home_scan_content_description)).also {
|
||||
it.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.clickProfile() {
|
||||
onNodeWithContentDescription(getStringResource(R.string.home_profile_content_description)).also {
|
||||
it.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.clickSend() {
|
||||
onNodeWithText(getStringResource(R.string.home_button_send)).also {
|
||||
it.performScrollTo()
|
||||
it.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.clickRequest() {
|
||||
onNodeWithText(getStringResource(R.string.home_button_request)).also {
|
||||
it.performScrollTo()
|
||||
it.performClick()
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.screen.onboarding.view
|
||||
package co.electriccoin.zcash.ui.screen.onboarding
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -6,6 +6,7 @@ import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
|||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.model.OnboardingStage
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.state.OnboardingState
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.view.Onboarding
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class OnboardingTestSetup(
|
|
@ -7,14 +7,13 @@ import androidx.test.filters.MediumTest
|
|||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.UiTestingActivity
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.OnboardingTestSetup
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.model.OnboardingStage
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.view.OnboardingTestSetup
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
// TODO [#382]: https://github.com/zcash/secant-android-wallet/issues/382
|
||||
class OnboardingActivityTest : UiTestPrerequisites() {
|
||||
@get:Rule
|
||||
val composeTestRule = createAndroidComposeRule<UiTestingActivity>()
|
||||
|
|
|
@ -7,14 +7,13 @@ import androidx.compose.ui.test.performClick
|
|||
import androidx.test.filters.MediumTest
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.OnboardingTestSetup
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.model.OnboardingStage
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.view.OnboardingTestSetup
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
// TODO [#382]: https://github.com/zcash/secant-android-wallet/issues/382
|
||||
class OnboardingIntegrationTest : UiTestPrerequisites() {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
|
|
@ -8,6 +8,7 @@ import androidx.compose.ui.test.performClick
|
|||
import androidx.test.filters.MediumTest
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.OnboardingTestSetup
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.model.OnboardingStage
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
import org.junit.Assert.assertEquals
|
||||
|
|
|
@ -14,7 +14,6 @@ import androidx.compose.ui.test.onNodeWithTag
|
|||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.sdk.fixture.SeedPhraseFixture
|
||||
|
@ -25,6 +24,7 @@ import co.electriccoin.zcash.ui.design.component.CommonTag
|
|||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.screen.restore.RestoreTag
|
||||
import co.electriccoin.zcash.ui.screen.restore.state.WordList
|
||||
import co.electriccoin.zcash.ui.test.getAppContext
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
|
@ -48,7 +48,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
|||
it.assertIsFocused()
|
||||
}
|
||||
|
||||
val inputMethodManager = ApplicationProvider.getApplicationContext<Context>().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
val inputMethodManager = getAppContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
assertTrue(inputMethodManager.isAcceptingText)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
package co.electriccoin.zcash.ui.screen.scan.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.Settings
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.filters.SmallTest
|
||||
import co.electriccoin.zcash.ui.test.getAppContext
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertContains
|
||||
import kotlin.test.assertEquals
|
||||
|
@ -14,13 +13,13 @@ class SettingsUtilTest {
|
|||
|
||||
companion object {
|
||||
val SETTINGS_URI = SettingsUtil.SETTINGS_URI_PREFIX +
|
||||
ApplicationProvider.getApplicationContext<Context>().packageName
|
||||
getAppContext().packageName
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun check_intent_to_settings() {
|
||||
val intent = SettingsUtil.newSettingsIntent(ApplicationProvider.getApplicationContext<Context>().packageName)
|
||||
val intent = SettingsUtil.newSettingsIntent(getAppContext().packageName)
|
||||
assertNotNull(intent)
|
||||
assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, intent.action)
|
||||
assertContains(intent.categories, Intent.CATEGORY_DEFAULT)
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package co.electriccoin.zcash.ui.screen.send.ext
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.filters.SmallTest
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
||||
import co.electriccoin.zcash.ui.test.getAppContext
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class WalletAddressExtTest {
|
||||
|
@ -14,7 +14,7 @@ class WalletAddressExtTest {
|
|||
@SmallTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun testAbbreviated() = runTest {
|
||||
val actual = WalletAddressFixture.shieldedSapling().abbreviated(ApplicationProvider.getApplicationContext())
|
||||
val actual = WalletAddressFixture.shieldedSapling().abbreviated(getAppContext())
|
||||
|
||||
// TODO [#248]: The expected value should probably be reversed if the locale is RTL
|
||||
val expected = "ztest…rxnwg"
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package co.electriccoin.zcash.ui.screen.support.model
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import co.electriccoin.zcash.ui.test.getAppContext
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.test.Test
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
|
@ -11,7 +11,7 @@ class SupportInfoTest {
|
|||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun filter_time() = runTest {
|
||||
val supportInfo = SupportInfo.new(ApplicationProvider.getApplicationContext())
|
||||
val supportInfo = SupportInfo.new(getAppContext())
|
||||
|
||||
val individualExpected = supportInfo.timeInfo.toSupportString()
|
||||
|
||||
|
@ -25,7 +25,7 @@ class SupportInfoTest {
|
|||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun filter_app() = runTest {
|
||||
val supportInfo = SupportInfo.new(ApplicationProvider.getApplicationContext())
|
||||
val supportInfo = SupportInfo.new(getAppContext())
|
||||
|
||||
val individualExpected = supportInfo.appInfo.toSupportString()
|
||||
|
||||
|
@ -39,7 +39,7 @@ class SupportInfoTest {
|
|||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun filter_os() = runTest {
|
||||
val supportInfo = SupportInfo.new(ApplicationProvider.getApplicationContext())
|
||||
val supportInfo = SupportInfo.new(getAppContext())
|
||||
|
||||
val individualExpected = supportInfo.operatingSystemInfo.toSupportString()
|
||||
|
||||
|
@ -53,7 +53,7 @@ class SupportInfoTest {
|
|||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun filter_device() = runTest {
|
||||
val supportInfo = SupportInfo.new(ApplicationProvider.getApplicationContext())
|
||||
val supportInfo = SupportInfo.new(getAppContext())
|
||||
|
||||
val individualExpected = supportInfo.deviceInfo.toSupportString()
|
||||
|
||||
|
@ -67,7 +67,7 @@ class SupportInfoTest {
|
|||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun filter_crash() = runTest {
|
||||
val supportInfo = SupportInfo.new(ApplicationProvider.getApplicationContext())
|
||||
val supportInfo = SupportInfo.new(getAppContext())
|
||||
|
||||
val individualExpected = supportInfo.crashInfo.toCrashSupportString()
|
||||
|
||||
|
@ -81,7 +81,7 @@ class SupportInfoTest {
|
|||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun filter_environment() = runTest {
|
||||
val supportInfo = SupportInfo.new(ApplicationProvider.getApplicationContext())
|
||||
val supportInfo = SupportInfo.new(getAppContext())
|
||||
|
||||
val individualExpected = supportInfo.environmentInfo.toSupportString()
|
||||
|
||||
|
@ -95,7 +95,7 @@ class SupportInfoTest {
|
|||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun filter_permission() = runTest {
|
||||
val supportInfo = SupportInfo.new(ApplicationProvider.getApplicationContext())
|
||||
val supportInfo = SupportInfo.new(getAppContext())
|
||||
|
||||
val individualExpected = supportInfo.permissionInfo.toPermissionSupportString()
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
package co.electriccoin.zcash.ui.screen.update.fixture
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import kotlin.test.Test
|
||||
import co.electriccoin.zcash.ui.fixture.UpdateInfoFixture
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class UpdateInfoFixtureTest {
|
||||
|
|
|
@ -6,8 +6,8 @@ import androidx.compose.ui.test.onNodeWithText
|
|||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.filters.MediumTest
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.fixture.UpdateInfoFixture
|
||||
import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker
|
||||
import co.electriccoin.zcash.ui.screen.update.fixture.UpdateInfoFixture
|
||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
|
||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
||||
import co.electriccoin.zcash.ui.screen.update.view.UpdateViewTestSetup
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
package co.electriccoin.zcash.ui.screen.update.util
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.filters.SmallTest
|
||||
import co.electriccoin.zcash.ui.test.getAppContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Test
|
||||
|
@ -13,13 +12,13 @@ class PlayStoreUtilTest {
|
|||
|
||||
companion object {
|
||||
val PLAY_STORE_URI = PlayStoreUtil.PLAY_STORE_APP_URI +
|
||||
ApplicationProvider.getApplicationContext<Context>().packageName
|
||||
getAppContext().packageName
|
||||
}
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun check_intent_for_store() {
|
||||
val intent = PlayStoreUtil.newActivityIntent(ApplicationProvider.getApplicationContext())
|
||||
val intent = PlayStoreUtil.newActivityIntent(getAppContext())
|
||||
assertNotNull(intent)
|
||||
assertEquals(intent.action, Intent.ACTION_VIEW)
|
||||
assertContains(PLAY_STORE_URI, intent.data.toString())
|
||||
|
|
|
@ -4,12 +4,12 @@ import android.app.Activity
|
|||
import android.content.Context
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.sdk.ext.onFirst
|
||||
import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp
|
||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
|
||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
||||
import co.electriccoin.zcash.ui.test.getAppContext
|
||||
import com.google.android.play.core.install.model.ActivityResult
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -26,7 +26,7 @@ class AppUpdateCheckerImpTest {
|
|||
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
companion object {
|
||||
val context: Context = ApplicationProvider.getApplicationContext()
|
||||
val context: Context = getAppContext()
|
||||
val updateChecker = AppUpdateCheckerImp.new()
|
||||
}
|
||||
|
||||
|
|
|
@ -10,9 +10,9 @@ import androidx.test.espresso.Espresso
|
|||
import androidx.test.filters.MediumTest
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.fixture.UpdateInfoFixture
|
||||
import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker
|
||||
import co.electriccoin.zcash.ui.screen.update.UpdateTag
|
||||
import co.electriccoin.zcash.ui.screen.update.fixture.UpdateInfoFixture
|
||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
|
||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
|
|
|
@ -4,6 +4,8 @@ import android.content.Context
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
|
||||
fun getStringResource(@StringRes resId: Int) = ApplicationProvider.getApplicationContext<Context>().getString(resId)
|
||||
fun getAppContext(): Context = ApplicationProvider.getApplicationContext()
|
||||
|
||||
fun getStringResourceWithArgs(@StringRes resId: Int, vararg formatArgs: String) = ApplicationProvider.getApplicationContext<Context>().getString(resId, *formatArgs)
|
||||
fun getStringResource(@StringRes resId: Int) = getAppContext().getString(resId)
|
||||
|
||||
fun getStringResourceWithArgs(@StringRes resId: Int, vararg formatArgs: String) = getAppContext().getString(resId, *formatArgs)
|
||||
|
|
|
@ -24,6 +24,7 @@ import androidx.navigation.compose.composable
|
|||
import androidx.navigation.compose.rememberNavController
|
||||
import cash.z.ecc.sdk.model.ZecRequest
|
||||
import cash.z.ecc.sdk.send
|
||||
import co.electriccoin.zcash.ui.design.compat.FontCompat
|
||||
import co.electriccoin.zcash.ui.design.component.ConfigurationOverride
|
||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||
import co.electriccoin.zcash.ui.design.component.Override
|
||||
|
@ -82,8 +83,15 @@ class MainActivity : ComponentActivity() {
|
|||
|
||||
setupSplashScreen()
|
||||
|
||||
if (FontCompat.isFontPrefetchNeeded()) {
|
||||
lifecycleScope.launch {
|
||||
FontCompat.prefetchFontsLegacy(applicationContext)
|
||||
setupUiContent()
|
||||
}
|
||||
} else {
|
||||
setupUiContent()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSplashScreen() {
|
||||
val splashScreen = installSplashScreen()
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
@file:Suppress("ktlint:filename")
|
||||
|
||||
package co.electriccoin.zcash.ui.common
|
||||
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
|
||||
fun Locale.toKotlinLocale() = cash.z.ecc.sdk.ext.ui.model.Locale(language, region, script)
|
|
@ -1,4 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.screen.update.fixture
|
||||
package co.electriccoin.zcash.ui.fixture
|
||||
|
||||
import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker
|
||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
|
|
@ -4,18 +4,38 @@ import cash.z.ecc.android.sdk.Synchronizer
|
|||
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.model.PercentDecimal
|
||||
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.SynchronizerError
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
object WalletSnapshotFixture {
|
||||
|
||||
val STATUS = Synchronizer.Status.SYNCED
|
||||
val PROGRESS = PercentDecimal.ZERO_PERCENT
|
||||
val TRANSPARENT_BALANCE: WalletBalance = WalletBalance(Zatoshi(8), Zatoshi(1))
|
||||
val ORCHARD_BALANCE: WalletBalance = WalletBalance(Zatoshi(5), Zatoshi(2))
|
||||
val SAPLING_BALANCE: WalletBalance = WalletBalance(Zatoshi(4), Zatoshi(4))
|
||||
|
||||
// Should fill in with non-empty values for better example values in tests and UI previews
|
||||
@Suppress("LongParameterList")
|
||||
fun new(
|
||||
status: Synchronizer.Status = Synchronizer.Status.SYNCED,
|
||||
status: Synchronizer.Status = STATUS,
|
||||
processorInfo: CompactBlockProcessor.ProcessorInfo = CompactBlockProcessor.ProcessorInfo(),
|
||||
orchardBalance: WalletBalance = WalletBalance(Zatoshi(5), Zatoshi(2)),
|
||||
saplingBalance: WalletBalance = WalletBalance(Zatoshi(4), Zatoshi(4)),
|
||||
transparentBalance: WalletBalance = WalletBalance(Zatoshi(8), Zatoshi(1)),
|
||||
pendingCount: Int = 0
|
||||
) = WalletSnapshot(status, processorInfo, orchardBalance, saplingBalance, transparentBalance, pendingCount)
|
||||
orchardBalance: WalletBalance = ORCHARD_BALANCE,
|
||||
saplingBalance: WalletBalance = SAPLING_BALANCE,
|
||||
transparentBalance: WalletBalance = TRANSPARENT_BALANCE,
|
||||
pendingCount: Int = 0,
|
||||
progress: PercentDecimal = PROGRESS,
|
||||
synchronizerError: SynchronizerError? = null
|
||||
) = WalletSnapshot(
|
||||
status,
|
||||
processorInfo,
|
||||
orchardBalance,
|
||||
saplingBalance,
|
||||
transparentBalance,
|
||||
pendingCount,
|
||||
progress,
|
||||
synchronizerError
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package co.electriccoin.zcash.ui.preference
|
||||
|
||||
import cash.z.ecc.sdk.model.FiatCurrency
|
||||
import co.electriccoin.zcash.preference.api.PreferenceProvider
|
||||
import co.electriccoin.zcash.preference.model.entry.Key
|
||||
import co.electriccoin.zcash.preference.model.entry.PreferenceDefault
|
||||
|
||||
data class FiatCurrencyPreferenceDefault(
|
||||
override val key: Key
|
||||
) : PreferenceDefault<FiatCurrency> {
|
||||
|
||||
override suspend fun getValue(preferenceProvider: PreferenceProvider) =
|
||||
preferenceProvider.getString(key)?.let { FiatCurrency(it) } ?: FiatCurrency("USD")
|
||||
|
||||
override suspend fun putValue(
|
||||
preferenceProvider: PreferenceProvider,
|
||||
newValue: FiatCurrency
|
||||
) = preferenceProvider.putString(key, newValue.code)
|
||||
}
|
|
@ -9,4 +9,9 @@ object StandardPreferenceKeys {
|
|||
* Whether the user has completed the backup flow for a newly created wallet.
|
||||
*/
|
||||
val IS_USER_BACKUP_COMPLETE = BooleanPreferenceDefault(Key("is_user_backup_complete"), false)
|
||||
|
||||
/**
|
||||
* The fiat currency that the user prefers.
|
||||
*/
|
||||
val PREFERRED_FIAT_CURRENCY = FiatCurrencyPreferenceDefault(Key("preferred_fiat_currency_code"))
|
||||
}
|
||||
|
|
|
@ -13,8 +13,11 @@ import co.electriccoin.zcash.ui.BuildConfig
|
|||
import co.electriccoin.zcash.ui.MainActivity
|
||||
import co.electriccoin.zcash.ui.screen.backup.viewmodel.BackupViewModel
|
||||
import co.electriccoin.zcash.ui.screen.home.view.Home
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.CheckUpdateViewModel
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
||||
import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp
|
||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
||||
|
||||
@Composable
|
||||
internal fun MainActivity.WrapHome(
|
||||
|
@ -40,6 +43,17 @@ internal fun WrapHome(
|
|||
goSend: () -> Unit,
|
||||
goRequest: () -> Unit
|
||||
) {
|
||||
// we want to show information about app update, if available
|
||||
val checkUpdateViewModel by activity.viewModels<CheckUpdateViewModel> {
|
||||
CheckUpdateViewModel.CheckUpdateViewModelFactory(
|
||||
activity.application,
|
||||
AppUpdateCheckerImp.new()
|
||||
)
|
||||
}
|
||||
val updateAvailable = checkUpdateViewModel.updateInfo.collectAsState().value.let {
|
||||
it?.appUpdateInfo != null && it.state == UpdateState.Prepared
|
||||
}
|
||||
|
||||
val walletViewModel by activity.viewModels<WalletViewModel>()
|
||||
|
||||
val walletSnapshot = walletViewModel.walletSnapshot.collectAsState().value
|
||||
|
@ -76,7 +90,8 @@ internal fun WrapHome(
|
|||
|
||||
val backupViewModel by activity.viewModels<BackupViewModel>()
|
||||
backupViewModel.backupState.goToBeginning()
|
||||
}
|
||||
},
|
||||
updateAvailable = updateAvailable
|
||||
)
|
||||
|
||||
activity.reportFullyDrawn()
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package co.electriccoin.zcash.ui.screen.home
|
||||
|
||||
/**
|
||||
* These are only used for automated testing.
|
||||
*/
|
||||
object HomeTag {
|
||||
const val STATUS_VIEWS = "status_views"
|
||||
const val PROGRESS = "progress_bar"
|
||||
const val SINGLE_LINE_TEXT = "single_line_text"
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package co.electriccoin.zcash.ui.screen.home.model
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import cash.z.ecc.android.sdk.Synchronizer
|
||||
import cash.z.ecc.sdk.ext.ui.model.FiatCurrencyConversionRateState
|
||||
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
||||
import cash.z.ecc.sdk.ext.ui.model.toZecString
|
||||
import cash.z.ecc.sdk.ext.ui.toFiatCurrencyState
|
||||
import cash.z.ecc.sdk.model.PercentDecimal
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.toKotlinLocale
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
data class WalletDisplayValues(
|
||||
val progress: PercentDecimal,
|
||||
val zecAmountText: String,
|
||||
val statusText: String,
|
||||
val fiatCurrencyAmountState: FiatCurrencyConversionRateState,
|
||||
val fiatCurrencyAmountText: String
|
||||
) {
|
||||
companion object {
|
||||
@Suppress("MagicNumber", "LongMethod")
|
||||
internal fun getNextValues(
|
||||
context: Context,
|
||||
walletSnapshot: WalletSnapshot,
|
||||
updateAvailable: Boolean
|
||||
): WalletDisplayValues {
|
||||
var progress = PercentDecimal.ZERO_PERCENT
|
||||
val zecAmountText = walletSnapshot.totalBalance().toZecString()
|
||||
var statusText = ""
|
||||
// TODO [#578] https://github.com/zcash/zcash-android-wallet-sdk/issues/578
|
||||
// We'll ideally provide a "fresh" currencyConversion object here
|
||||
val fiatCurrencyAmountState = walletSnapshot.spendableBalance().toFiatCurrencyState(
|
||||
null,
|
||||
Locale.current.toKotlinLocale(),
|
||||
MonetarySeparators.current()
|
||||
)
|
||||
var fiatCurrencyAmountText = getFiatCurrencyRateValue(context, fiatCurrencyAmountState)
|
||||
|
||||
when (walletSnapshot.status) {
|
||||
Synchronizer.Status.PREPARING,
|
||||
Synchronizer.Status.DOWNLOADING,
|
||||
Synchronizer.Status.VALIDATING -> {
|
||||
progress = walletSnapshot.progress
|
||||
val progressPercent = (walletSnapshot.progress.decimal * 100).roundToInt()
|
||||
// we add "so far" to the amount
|
||||
if (fiatCurrencyAmountState != FiatCurrencyConversionRateState.Unavailable) {
|
||||
fiatCurrencyAmountText = context.getString(
|
||||
R.string.home_status_syncing_amount_suffix,
|
||||
fiatCurrencyAmountText
|
||||
)
|
||||
}
|
||||
statusText = context.getString(R.string.home_status_syncing_format, progressPercent)
|
||||
}
|
||||
Synchronizer.Status.SCANNING -> {
|
||||
// SDK provides us only one progress, which keeps on 100 in the scanning state
|
||||
progress = PercentDecimal.ONE_HUNDRED_PERCENT
|
||||
statusText = context.getString(R.string.home_status_syncing_catchup)
|
||||
}
|
||||
Synchronizer.Status.SYNCED,
|
||||
Synchronizer.Status.ENHANCING -> {
|
||||
statusText = if (updateAvailable) {
|
||||
context.getString(R.string.home_status_update)
|
||||
} else {
|
||||
context.getString(R.string.home_status_up_to_date)
|
||||
}
|
||||
}
|
||||
Synchronizer.Status.DISCONNECTED -> {
|
||||
statusText = context.getString(
|
||||
R.string.home_status_error,
|
||||
context.getString(R.string.home_status_error_connection)
|
||||
)
|
||||
}
|
||||
Synchronizer.Status.STOPPED -> {
|
||||
statusText = context.getString(R.string.home_status_stopped)
|
||||
}
|
||||
}
|
||||
|
||||
// more detailed error message
|
||||
walletSnapshot.synchronizerError?.let {
|
||||
statusText = context.getString(
|
||||
R.string.home_status_error,
|
||||
walletSnapshot.synchronizerError.getCauseMessage()
|
||||
?: context.getString(R.string.home_status_error_unknown)
|
||||
)
|
||||
}
|
||||
|
||||
return WalletDisplayValues(
|
||||
progress = progress,
|
||||
zecAmountText = zecAmountText,
|
||||
statusText = statusText,
|
||||
fiatCurrencyAmountState = fiatCurrencyAmountState,
|
||||
fiatCurrencyAmountText = fiatCurrencyAmountText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFiatCurrencyRateValue(
|
||||
context: Context,
|
||||
fiatCurrencyAmountState: FiatCurrencyConversionRateState
|
||||
): String {
|
||||
return fiatCurrencyAmountState.let { state ->
|
||||
when (state) {
|
||||
is FiatCurrencyConversionRateState.Current -> state.formattedFiatValue
|
||||
is FiatCurrencyConversionRateState.Stale -> state.formattedFiatValue
|
||||
is FiatCurrencyConversionRateState.Unavailable -> {
|
||||
context.getString(R.string.fiat_currency_conversion_rate_unavailable)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@ import cash.z.ecc.android.sdk.block.CompactBlockProcessor
|
|||
import cash.z.ecc.android.sdk.ext.ZcashSdk
|
||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.model.PercentDecimal
|
||||
import co.electriccoin.zcash.ui.screen.home.viewmodel.SynchronizerError
|
||||
|
||||
// TODO [#292]: Should be moved to SDK-EXT-UI module.
|
||||
data class WalletSnapshot(
|
||||
|
@ -13,7 +15,9 @@ data class WalletSnapshot(
|
|||
val orchardBalance: WalletBalance,
|
||||
val saplingBalance: WalletBalance,
|
||||
val transparentBalance: WalletBalance,
|
||||
val pendingCount: Int
|
||||
val pendingCount: Int,
|
||||
val progress: PercentDecimal,
|
||||
val synchronizerError: SynchronizerError?
|
||||
) {
|
||||
// Note: the wallet is effectively empty if it cannot cover the miner's fee
|
||||
val hasFunds = saplingBalance.available.value >
|
||||
|
|
|
@ -1,17 +1,25 @@
|
|||
package co.electriccoin.zcash.ui.screen.home.view
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.QrCodeScanner
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
|
@ -25,24 +33,32 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.android.sdk.db.entity.Transaction
|
||||
import cash.z.ecc.sdk.ext.ui.model.toZecString
|
||||
import cash.z.ecc.sdk.ext.ui.model.FiatCurrencyConversionRateState
|
||||
import cash.z.ecc.sdk.model.PercentDecimal
|
||||
import co.electriccoin.zcash.crash.android.CrashReporter
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||
import co.electriccoin.zcash.ui.design.component.Body
|
||||
import co.electriccoin.zcash.ui.design.component.BodyWithFiatCurrencySymbol
|
||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||
import co.electriccoin.zcash.ui.design.component.Header
|
||||
import co.electriccoin.zcash.ui.design.component.HeaderWithZecIcon
|
||||
import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
||||
import co.electriccoin.zcash.ui.design.component.TertiaryButton
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.screen.home.HomeTag
|
||||
import co.electriccoin.zcash.ui.screen.home.model.WalletDisplayValues
|
||||
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
|
||||
import co.electriccoin.zcash.ui.screen.home.model.totalBalance
|
||||
import java.lang.RuntimeException
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
@ -56,9 +72,10 @@ fun ComposablePreview() {
|
|||
goProfile = {},
|
||||
goSend = {},
|
||||
goRequest = {},
|
||||
isDebugMenuEnabled = false,
|
||||
resetSdk = {},
|
||||
wipeEntireWallet = {}
|
||||
wipeEntireWallet = {},
|
||||
isDebugMenuEnabled = false,
|
||||
updateAvailable = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -76,7 +93,8 @@ fun Home(
|
|||
goRequest: () -> Unit,
|
||||
resetSdk: () -> Unit,
|
||||
wipeEntireWallet: () -> Unit,
|
||||
isDebugMenuEnabled: Boolean
|
||||
isDebugMenuEnabled: Boolean,
|
||||
updateAvailable: Boolean
|
||||
) {
|
||||
Scaffold(topBar = {
|
||||
HomeTopAppBar(isDebugMenuEnabled, resetSdk, wipeEntireWallet)
|
||||
|
@ -88,7 +106,8 @@ fun Home(
|
|||
goScan = goScan,
|
||||
goProfile = goProfile,
|
||||
goSend = goSend,
|
||||
goRequest = goRequest
|
||||
goRequest = goRequest,
|
||||
updateAvailable = updateAvailable
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -163,9 +182,10 @@ private fun HomeMainContent(
|
|||
goScan: () -> Unit,
|
||||
goProfile: () -> Unit,
|
||||
goSend: () -> Unit,
|
||||
goRequest: () -> Unit
|
||||
goRequest: () -> Unit,
|
||||
updateAvailable: Boolean
|
||||
) {
|
||||
Column {
|
||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
|
@ -189,33 +209,133 @@ private fun HomeMainContent(
|
|||
)
|
||||
}
|
||||
}
|
||||
Status(walletSnapshot)
|
||||
|
||||
Status(walletSnapshot, updateAvailable)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
PrimaryButton(onClick = goSend, text = stringResource(R.string.home_button_send))
|
||||
|
||||
TertiaryButton(onClick = goRequest, text = stringResource(R.string.home_button_request))
|
||||
|
||||
History(transactionHistory)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Status(walletSnapshot: WalletSnapshot) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Header(text = walletSnapshot.totalBalance().toZecString())
|
||||
@Suppress("LongMethod", "MagicNumber")
|
||||
private fun Status(
|
||||
walletSnapshot: WalletSnapshot,
|
||||
updateAvailable: Boolean
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
val contentSizeRatioRatio = if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
0.45f
|
||||
} else {
|
||||
0.9f
|
||||
}
|
||||
|
||||
// UI parts sizes
|
||||
val progressCircleStroke = 12.dp
|
||||
val progressCirclePadding = progressCircleStroke + 6.dp
|
||||
val contentPadding = progressCircleStroke + progressCirclePadding + 10.dp
|
||||
|
||||
val walletDisplayValues = WalletDisplayValues.getNextValues(
|
||||
LocalContext.current,
|
||||
walletSnapshot,
|
||||
updateAvailable
|
||||
)
|
||||
|
||||
// wrapper box
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(HomeTag.STATUS_VIEWS),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// relatively sized box
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(contentSizeRatioRatio)
|
||||
.aspectRatio(1f),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// progress circle
|
||||
if (walletDisplayValues.progress.decimal > PercentDecimal.ZERO_PERCENT.decimal) {
|
||||
CircularProgressIndicator(
|
||||
progress = walletDisplayValues.progress.decimal,
|
||||
color = Color.Gray,
|
||||
strokeWidth = progressCircleStroke,
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.padding(progressCirclePadding)
|
||||
.testTag(HomeTag.PROGRESS)
|
||||
)
|
||||
}
|
||||
|
||||
// texts
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.wrapContentSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
if (walletDisplayValues.zecAmountText.isNotEmpty()) {
|
||||
HeaderWithZecIcon(amount = walletDisplayValues.zecAmountText)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
when (walletDisplayValues.fiatCurrencyAmountState) {
|
||||
is FiatCurrencyConversionRateState.Current -> {
|
||||
BodyWithFiatCurrencySymbol(
|
||||
amount = walletDisplayValues.fiatCurrencyAmountText
|
||||
)
|
||||
}
|
||||
is FiatCurrencyConversionRateState.Stale -> {
|
||||
// Note: we should show information about staleness too
|
||||
BodyWithFiatCurrencySymbol(
|
||||
amount = walletDisplayValues.fiatCurrencyAmountText
|
||||
)
|
||||
}
|
||||
is FiatCurrencyConversionRateState.Unavailable -> {
|
||||
Body(text = walletDisplayValues.fiatCurrencyAmountText)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
if (walletDisplayValues.statusText.isNotEmpty()) {
|
||||
Body(
|
||||
text = stringResource(
|
||||
id = R.string.home_status_shielding_format,
|
||||
walletSnapshot.saplingBalance.total.toZecString()
|
||||
)
|
||||
text = walletDisplayValues.statusText,
|
||||
modifier = Modifier.testTag(HomeTag.SINGLE_LINE_TEXT)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("MagicNumber")
|
||||
private fun History(transactionHistory: List<Transaction>) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
LazyColumn {
|
||||
if (transactionHistory.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
// here we need to use a fixed height to avoid nested columns vertical scrolling problem
|
||||
// we'll refactor this part to a dedicated bottom sheet later
|
||||
val historyPart = LocalConfiguration.current.screenHeightDp / 3
|
||||
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(historyPart.dp)
|
||||
) {
|
||||
items(transactionHistory) {
|
||||
Text(it.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,11 @@ import cash.z.ecc.android.sdk.db.entity.isSubmitSuccess
|
|||
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
||||
import cash.z.ecc.sdk.model.FiatCurrency
|
||||
import cash.z.ecc.sdk.model.PercentDecimal
|
||||
import cash.z.ecc.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.sdk.model.WalletAddresses
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceKeys
|
||||
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceSingleton
|
||||
|
@ -25,9 +28,12 @@ import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
|
|||
import co.electriccoin.zcash.work.WorkIds
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
|
@ -63,6 +69,18 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
|||
null
|
||||
)
|
||||
|
||||
/**
|
||||
* A flow of the user's preferred fiat currency.
|
||||
*/
|
||||
val preferredFiatCurrency: StateFlow<FiatCurrency?> = flow<FiatCurrency?> {
|
||||
val preferenceProvider = StandardPreferenceSingleton.getInstance(application)
|
||||
emitAll(StandardPreferenceKeys.PREFERRED_FIAT_CURRENCY.observe(preferenceProvider))
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
null
|
||||
)
|
||||
|
||||
/**
|
||||
* A flow of whether a backup of the user's wallet has been performed.
|
||||
*/
|
||||
|
@ -241,6 +259,68 @@ sealed class SecretState {
|
|||
class Ready(val persistableWallet: PersistableWallet) : SecretState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents all kind of Synchronizer errors
|
||||
*/
|
||||
// TODO [#529] https://github.com/zcash/secant-android-wallet/issues/529
|
||||
sealed class SynchronizerError {
|
||||
abstract fun getCauseMessage(): String?
|
||||
|
||||
class Critical(val error: Throwable?) : SynchronizerError() {
|
||||
override fun getCauseMessage(): String? = error?.localizedMessage
|
||||
}
|
||||
|
||||
class Processor(val error: Throwable?) : SynchronizerError() {
|
||||
override fun getCauseMessage(): String? = error?.localizedMessage
|
||||
}
|
||||
|
||||
class Submission(val error: Throwable?) : SynchronizerError() {
|
||||
override fun getCauseMessage(): String? = error?.localizedMessage
|
||||
}
|
||||
|
||||
class Setup(val error: Throwable?) : SynchronizerError() {
|
||||
override fun getCauseMessage(): String? = error?.localizedMessage
|
||||
}
|
||||
|
||||
class Chain(val x: Int, val y: Int) : SynchronizerError() {
|
||||
override fun getCauseMessage(): String = "$x, $y"
|
||||
}
|
||||
}
|
||||
|
||||
private fun Synchronizer.toCommonError(): Flow<SynchronizerError?> = callbackFlow {
|
||||
// just for initial default value emit
|
||||
trySend(null)
|
||||
|
||||
onCriticalErrorHandler = {
|
||||
Twig.error { "WALLET - Error Critical: $it" }
|
||||
trySend(SynchronizerError.Critical(it))
|
||||
false
|
||||
}
|
||||
onProcessorErrorHandler = {
|
||||
Twig.error { "WALLET - Error Processor: $it" }
|
||||
trySend(SynchronizerError.Processor(it))
|
||||
false
|
||||
}
|
||||
onSubmissionErrorHandler = {
|
||||
Twig.error { "WALLET - Error Submission: $it" }
|
||||
trySend(SynchronizerError.Submission(it))
|
||||
false
|
||||
}
|
||||
onSetupErrorHandler = {
|
||||
Twig.error { "WALLET - Error Setup: $it" }
|
||||
trySend(SynchronizerError.Setup(it))
|
||||
false
|
||||
}
|
||||
onChainErrorHandler = { x, y ->
|
||||
Twig.error { "WALLET - Error Chain: $x, $y" }
|
||||
trySend(SynchronizerError.Chain(x, y))
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
// nothing to close here
|
||||
}
|
||||
}
|
||||
|
||||
// No good way around needing magic numbers for the indices
|
||||
@Suppress("MagicNumber")
|
||||
private fun Synchronizer.toWalletSnapshot() =
|
||||
|
@ -250,7 +330,9 @@ private fun Synchronizer.toWalletSnapshot() =
|
|||
orchardBalances, // 2
|
||||
saplingBalances, // 3
|
||||
transparentBalances, // 4
|
||||
pendingTransactions.distinctUntilChanged() // 5
|
||||
pendingTransactions.distinctUntilChanged(), // 5
|
||||
progress, // 6
|
||||
toCommonError() // 7
|
||||
) { flows ->
|
||||
val pendingCount = (flows[5] as List<*>)
|
||||
.filterIsInstance(PendingTransaction::class.java)
|
||||
|
@ -261,13 +343,22 @@ private fun Synchronizer.toWalletSnapshot() =
|
|||
val saplingBalance = flows[3] as WalletBalance?
|
||||
val transparentBalance = flows[4] as WalletBalance?
|
||||
|
||||
val progressPercentDecimal = (flows[6] as Int).let { value ->
|
||||
if (value > PercentDecimal.MAX || value < PercentDecimal.MIN) {
|
||||
PercentDecimal.ZERO_PERCENT
|
||||
}
|
||||
PercentDecimal((flows[6] as Int) / 100f)
|
||||
}
|
||||
|
||||
WalletSnapshot(
|
||||
flows[0] as Synchronizer.Status,
|
||||
flows[1] as CompactBlockProcessor.ProcessorInfo,
|
||||
orchardBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0)),
|
||||
saplingBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0)),
|
||||
transparentBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0)),
|
||||
pendingCount
|
||||
pendingCount,
|
||||
progressPercentDecimal,
|
||||
flows[7] as SynchronizerError?
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -33,8 +33,8 @@ import co.electriccoin.zcash.ui.design.component.PrimaryButton
|
|||
import co.electriccoin.zcash.ui.design.component.Reference
|
||||
import co.electriccoin.zcash.ui.design.component.TertiaryButton
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.fixture.UpdateInfoFixture
|
||||
import co.electriccoin.zcash.ui.screen.update.UpdateTag
|
||||
import co.electriccoin.zcash.ui.screen.update.fixture.UpdateInfoFixture
|
||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
|
||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
||||
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="59dp"
|
||||
android:height="105dp"
|
||||
android:viewportWidth="59"
|
||||
android:viewportHeight="105">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M59,26.586V13.911H36.429V0H22.567V13.911H0V30.7H34.987L6.375,70.258 0,78.4V91.074H22.567v13.867H24.23V105H34.767v-0.059h1.663V91.074H59V74.284H24.01L52.621,34.727 59,26.586Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,3 @@
|
|||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="fiat_currency_conversion_rate_unavailable">Unavailable</string>
|
||||
</resources>
|
|
@ -2,6 +2,22 @@
|
|||
<string name="home_scan_content_description">Scan</string>
|
||||
<string name="home_profile_content_description">Profile</string>
|
||||
<string name="home_button_send">Send</string>
|
||||
<string name="home_status_shielding_format" formatted="true">Shielding <xliff:g id="shielded_amount" example=".023">%1$s</xliff:g></string>
|
||||
<string name="home_button_request">Request ZEC</string>
|
||||
|
||||
<string name="home_status_syncing_format" formatted="true">Syncing - <xliff:g id="synced_percent" example="50">%1$d</xliff:g>%%</string> <!-- double %% for escaping -->
|
||||
<string name="home_status_syncing_catchup">Syncing</string>
|
||||
<string name="home_status_syncing_amount_suffix" formatted="true"><xliff:g id="amount_prefix" example="123$">%1$s</xliff:g> so far</string>
|
||||
<string name="home_status_syncing_additional_information">We will show you funds as we discover them.</string>
|
||||
<string name="home_status_up_to_date">Up-to-date</string>
|
||||
<string name="home_status_sending_format" formatted="true">Sending <xliff:g id="sending_amount" example=".023">%1$s</xliff:g></string>
|
||||
<string name="home_status_receiving_format" formatted="true">Receiving <xliff:g id="receiving_amount" example=".023">%1$s</xliff:g> ZEC</string>
|
||||
<string name="home_status_shielding_format" formatted="true">Shielding <xliff:g id="shielding_amount" example=".023">%1$s</xliff:g> ZEC</string>
|
||||
<string name="home_status_update">Please Update</string>
|
||||
<string name="home_status_error" formatted="true">Error: <xliff:g id="error_type" example="Lost connection">%1$s</xliff:g></string>
|
||||
<string name="home_status_error_connection">Disconnected</string>
|
||||
<string name="home_status_error_unknown">Unknown cause</string>
|
||||
<string name="home_status_stopped">Synchronizer stopped</string>
|
||||
<string name="home_status_updating_blockheight">Updating blockheight</string>
|
||||
<string name="home_status_fiat_currency_price_out_of_date" formatted="true"><xliff:g id="fiat_currency" example="USD">%1$s</xliff:g> price out-of-date</string>
|
||||
<string name="home_status_spendable" formatted="true">Fully spendable in <xliff:g id="spendable_time" example="2 minutes">%1$s</xliff:g></string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue