[#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.kotlin.stdlib)
|
||||||
implementation(libs.kotlinx.coroutines.android)
|
implementation(libs.kotlinx.coroutines.android)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
api(libs.zcash.sdk)
|
api(libs.zcash.sdk)
|
||||||
api(libs.zcash.bip39)
|
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 {
|
android {
|
||||||
resourcePrefix = "co_electriccoin_zcash_"
|
resourcePrefix = "co_electriccoin_zcash_"
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
coreLibraryDesugaring(libs.desugaring)
|
||||||
implementation(projects.sdkExtLib)
|
implementation(projects.sdkExtLib)
|
||||||
|
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
implementation(libs.kotlinx.datetime)
|
||||||
|
|
||||||
androidTestImplementation(libs.bundles.androidx.test)
|
androidTestImplementation(libs.bundles.androidx.test)
|
||||||
androidTestImplementation(libs.kotlin.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.core.app.ApplicationProvider
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
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.Assert.assertEquals
|
||||||
import org.junit.Ignore
|
import org.junit.Ignore
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -15,7 +16,7 @@ import kotlin.test.assertNull
|
||||||
class ZecStringTest {
|
class ZecStringTest {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val EN_US_MONETARY_SEPARATORS = MonetarySeparators(',', '.')
|
private val EN_US_MONETARY_SEPARATORS = MonetarySeparatorsFixture.new()
|
||||||
private val context = run {
|
private val context = run {
|
||||||
val applicationContext = ApplicationProvider.getApplicationContext<Context>()
|
val applicationContext = ApplicationProvider.getApplicationContext<Context>()
|
||||||
val enUsConfiguration = Configuration(applicationContext.resources.configuration).apply {
|
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 androidx.test.filters.SmallTest
|
||||||
import cash.z.ecc.sdk.ext.ui.R
|
import cash.z.ecc.sdk.ext.ui.R
|
||||||
import cash.z.ecc.sdk.ext.ui.ZecStringExt
|
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 cash.z.ecc.sdk.ext.ui.test.getStringResourceWithArgs
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
|
@ -13,7 +13,7 @@ import kotlin.test.assertTrue
|
||||||
class ZecStringExtTest {
|
class ZecStringExtTest {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val EN_US_SEPARATORS = MonetarySeparators(',', '.')
|
private val EN_US_SEPARATORS = MonetarySeparatorsFixture.new()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getContinuousRegex(): Regex {
|
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.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.TextStyle
|
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
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
|
|
||||||
@Composable
|
@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
|
@Composable
|
||||||
fun ListItem(
|
fun ListItem(
|
||||||
text: String,
|
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.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.text.ExperimentalTextApi
|
import androidx.compose.ui.text.ExperimentalTextApi
|
||||||
import androidx.compose.ui.text.TextStyle
|
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.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.googlefonts.Font
|
import androidx.compose.ui.text.googlefonts.Font
|
||||||
|
@ -33,6 +34,10 @@ private val RubikFontFamily = FontFamily(
|
||||||
Font(googleFont = RubikFont, fontProvider = provider, weight = FontWeight.Bold) // W700
|
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
|
// 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.
|
// the defined font weights above, otherwise the closest one will be used.
|
||||||
internal val Typography = Typography(
|
internal val Typography = Typography(
|
||||||
|
@ -61,7 +66,8 @@ internal val Typography = Typography(
|
||||||
@Immutable
|
@Immutable
|
||||||
data class ExtendedTypography(
|
data class ExtendedTypography(
|
||||||
val chipIndex: TextStyle,
|
val chipIndex: TextStyle,
|
||||||
val listItem: TextStyle
|
val listItem: TextStyle,
|
||||||
|
val zecBalance: TextStyle
|
||||||
)
|
)
|
||||||
|
|
||||||
val LocalExtendedTypography = staticCompositionLocalOf {
|
val LocalExtendedTypography = staticCompositionLocalOf {
|
||||||
|
@ -73,6 +79,11 @@ val LocalExtendedTypography = staticCompositionLocalOf {
|
||||||
),
|
),
|
||||||
listItem = Typography.bodyLarge.copy(
|
listItem = Typography.bodyLarge.copy(
|
||||||
fontSize = 24.sp
|
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 androidx.activity.ComponentActivity
|
||||||
import co.electriccoin.zcash.spackle.getPackageInfoCompat
|
import co.electriccoin.zcash.spackle.getPackageInfoCompat
|
||||||
import co.electriccoin.zcash.spackle.versionCodeCompat
|
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.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.UpdateInfo
|
||||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
||||||
import com.google.android.play.core.appupdate.AppUpdateInfo
|
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.lifecycle.viewModelScope
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
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.integration.test.common.IntegrationTestingActivity
|
||||||
import co.electriccoin.zcash.ui.screen.update.AppUpdateChecker
|
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.UpdateInfo
|
||||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
||||||
import co.electriccoin.zcash.ui.screen.update.viewmodel.UpdateViewModel
|
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.model.VersionInfo
|
||||||
import co.electriccoin.zcash.ui.screen.about.view.About
|
import co.electriccoin.zcash.ui.screen.about.view.About
|
||||||
import co.electriccoin.zcash.ui.test.getStringResource
|
import co.electriccoin.zcash.ui.test.getStringResource
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
class AboutViewTest {
|
class AboutViewTest {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val composeTestRule = createComposeRule()
|
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 android.annotation.SuppressLint
|
||||||
import androidx.compose.runtime.Composable
|
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.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.screen.onboarding.model.OnboardingStage
|
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.state.OnboardingState
|
||||||
|
import co.electriccoin.zcash.ui.screen.onboarding.view.Onboarding
|
||||||
import java.util.concurrent.atomic.AtomicInteger
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
|
||||||
class OnboardingTestSetup(
|
class OnboardingTestSetup(
|
|
@ -7,14 +7,13 @@ import androidx.test.filters.MediumTest
|
||||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||||
import co.electriccoin.zcash.ui.R
|
import co.electriccoin.zcash.ui.R
|
||||||
import co.electriccoin.zcash.ui.common.UiTestingActivity
|
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.model.OnboardingStage
|
||||||
import co.electriccoin.zcash.ui.screen.onboarding.view.OnboardingTestSetup
|
|
||||||
import co.electriccoin.zcash.ui.test.getStringResource
|
import co.electriccoin.zcash.ui.test.getStringResource
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
// TODO [#382]: https://github.com/zcash/secant-android-wallet/issues/382
|
|
||||||
class OnboardingActivityTest : UiTestPrerequisites() {
|
class OnboardingActivityTest : UiTestPrerequisites() {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val composeTestRule = createAndroidComposeRule<UiTestingActivity>()
|
val composeTestRule = createAndroidComposeRule<UiTestingActivity>()
|
||||||
|
|
|
@ -7,14 +7,13 @@ import androidx.compose.ui.test.performClick
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||||
import co.electriccoin.zcash.ui.R
|
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.model.OnboardingStage
|
||||||
import co.electriccoin.zcash.ui.screen.onboarding.view.OnboardingTestSetup
|
|
||||||
import co.electriccoin.zcash.ui.test.getStringResource
|
import co.electriccoin.zcash.ui.test.getStringResource
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
// TODO [#382]: https://github.com/zcash/secant-android-wallet/issues/382
|
|
||||||
class OnboardingIntegrationTest : UiTestPrerequisites() {
|
class OnboardingIntegrationTest : UiTestPrerequisites() {
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val composeTestRule = createComposeRule()
|
val composeTestRule = createComposeRule()
|
||||||
|
|
|
@ -8,6 +8,7 @@ import androidx.compose.ui.test.performClick
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||||
import co.electriccoin.zcash.ui.R
|
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.model.OnboardingStage
|
||||||
import co.electriccoin.zcash.ui.test.getStringResource
|
import co.electriccoin.zcash.ui.test.getStringResource
|
||||||
import org.junit.Assert.assertEquals
|
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.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.compose.ui.test.performTextInput
|
import androidx.compose.ui.test.performTextInput
|
||||||
import androidx.test.core.app.ApplicationProvider
|
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import cash.z.ecc.android.bip39.Mnemonics
|
import cash.z.ecc.android.bip39.Mnemonics
|
||||||
import cash.z.ecc.sdk.fixture.SeedPhraseFixture
|
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.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.screen.restore.RestoreTag
|
import co.electriccoin.zcash.ui.screen.restore.RestoreTag
|
||||||
import co.electriccoin.zcash.ui.screen.restore.state.WordList
|
import co.electriccoin.zcash.ui.screen.restore.state.WordList
|
||||||
|
import co.electriccoin.zcash.ui.test.getAppContext
|
||||||
import co.electriccoin.zcash.ui.test.getStringResource
|
import co.electriccoin.zcash.ui.test.getStringResource
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
|
@ -48,7 +48,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
it.assertIsFocused()
|
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)
|
assertTrue(inputMethodManager.isAcceptingText)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
package co.electriccoin.zcash.ui.screen.scan.util
|
package co.electriccoin.zcash.ui.screen.scan.util
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import androidx.test.core.app.ApplicationProvider
|
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
|
import co.electriccoin.zcash.ui.test.getAppContext
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertContains
|
import kotlin.test.assertContains
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
@ -14,13 +13,13 @@ class SettingsUtilTest {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val SETTINGS_URI = SettingsUtil.SETTINGS_URI_PREFIX +
|
val SETTINGS_URI = SettingsUtil.SETTINGS_URI_PREFIX +
|
||||||
ApplicationProvider.getApplicationContext<Context>().packageName
|
getAppContext().packageName
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun check_intent_to_settings() {
|
fun check_intent_to_settings() {
|
||||||
val intent = SettingsUtil.newSettingsIntent(ApplicationProvider.getApplicationContext<Context>().packageName)
|
val intent = SettingsUtil.newSettingsIntent(getAppContext().packageName)
|
||||||
assertNotNull(intent)
|
assertNotNull(intent)
|
||||||
assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, intent.action)
|
assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, intent.action)
|
||||||
assertContains(intent.categories, Intent.CATEGORY_DEFAULT)
|
assertContains(intent.categories, Intent.CATEGORY_DEFAULT)
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package co.electriccoin.zcash.ui.screen.send.ext
|
package co.electriccoin.zcash.ui.screen.send.ext
|
||||||
|
|
||||||
import androidx.test.core.app.ApplicationProvider
|
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
||||||
|
import co.electriccoin.zcash.ui.test.getAppContext
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlin.test.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class WalletAddressExtTest {
|
class WalletAddressExtTest {
|
||||||
|
@ -14,7 +14,7 @@ class WalletAddressExtTest {
|
||||||
@SmallTest
|
@SmallTest
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun testAbbreviated() = runTest {
|
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
|
// TODO [#248]: The expected value should probably be reversed if the locale is RTL
|
||||||
val expected = "ztest…rxnwg"
|
val expected = "ztest…rxnwg"
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package co.electriccoin.zcash.ui.screen.support.model
|
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.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlin.test.Test
|
import org.junit.Test
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ class SupportInfoTest {
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun filter_time() = runTest {
|
fun filter_time() = runTest {
|
||||||
val supportInfo = SupportInfo.new(ApplicationProvider.getApplicationContext())
|
val supportInfo = SupportInfo.new(getAppContext())
|
||||||
|
|
||||||
val individualExpected = supportInfo.timeInfo.toSupportString()
|
val individualExpected = supportInfo.timeInfo.toSupportString()
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ class SupportInfoTest {
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun filter_app() = runTest {
|
fun filter_app() = runTest {
|
||||||
val supportInfo = SupportInfo.new(ApplicationProvider.getApplicationContext())
|
val supportInfo = SupportInfo.new(getAppContext())
|
||||||
|
|
||||||
val individualExpected = supportInfo.appInfo.toSupportString()
|
val individualExpected = supportInfo.appInfo.toSupportString()
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ class SupportInfoTest {
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun filter_os() = runTest {
|
fun filter_os() = runTest {
|
||||||
val supportInfo = SupportInfo.new(ApplicationProvider.getApplicationContext())
|
val supportInfo = SupportInfo.new(getAppContext())
|
||||||
|
|
||||||
val individualExpected = supportInfo.operatingSystemInfo.toSupportString()
|
val individualExpected = supportInfo.operatingSystemInfo.toSupportString()
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ class SupportInfoTest {
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun filter_device() = runTest {
|
fun filter_device() = runTest {
|
||||||
val supportInfo = SupportInfo.new(ApplicationProvider.getApplicationContext())
|
val supportInfo = SupportInfo.new(getAppContext())
|
||||||
|
|
||||||
val individualExpected = supportInfo.deviceInfo.toSupportString()
|
val individualExpected = supportInfo.deviceInfo.toSupportString()
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ class SupportInfoTest {
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun filter_crash() = runTest {
|
fun filter_crash() = runTest {
|
||||||
val supportInfo = SupportInfo.new(ApplicationProvider.getApplicationContext())
|
val supportInfo = SupportInfo.new(getAppContext())
|
||||||
|
|
||||||
val individualExpected = supportInfo.crashInfo.toCrashSupportString()
|
val individualExpected = supportInfo.crashInfo.toCrashSupportString()
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ class SupportInfoTest {
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun filter_environment() = runTest {
|
fun filter_environment() = runTest {
|
||||||
val supportInfo = SupportInfo.new(ApplicationProvider.getApplicationContext())
|
val supportInfo = SupportInfo.new(getAppContext())
|
||||||
|
|
||||||
val individualExpected = supportInfo.environmentInfo.toSupportString()
|
val individualExpected = supportInfo.environmentInfo.toSupportString()
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ class SupportInfoTest {
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun filter_permission() = runTest {
|
fun filter_permission() = runTest {
|
||||||
val supportInfo = SupportInfo.new(ApplicationProvider.getApplicationContext())
|
val supportInfo = SupportInfo.new(getAppContext())
|
||||||
|
|
||||||
val individualExpected = supportInfo.permissionInfo.toPermissionSupportString()
|
val individualExpected = supportInfo.permissionInfo.toPermissionSupportString()
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
package co.electriccoin.zcash.ui.screen.update.fixture
|
package co.electriccoin.zcash.ui.screen.update.fixture
|
||||||
|
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
import kotlin.test.Test
|
import co.electriccoin.zcash.ui.fixture.UpdateInfoFixture
|
||||||
|
import org.junit.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class UpdateInfoFixtureTest {
|
class UpdateInfoFixtureTest {
|
||||||
|
|
|
@ -6,8 +6,8 @@ import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import co.electriccoin.zcash.ui.R
|
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.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.UpdateInfo
|
||||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
||||||
import co.electriccoin.zcash.ui.screen.update.view.UpdateViewTestSetup
|
import co.electriccoin.zcash.ui.screen.update.view.UpdateViewTestSetup
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
package co.electriccoin.zcash.ui.screen.update.util
|
package co.electriccoin.zcash.ui.screen.update.util
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.test.core.app.ApplicationProvider
|
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
|
import co.electriccoin.zcash.ui.test.getAppContext
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertNotNull
|
import org.junit.Assert.assertNotNull
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -13,13 +12,13 @@ class PlayStoreUtilTest {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val PLAY_STORE_URI = PlayStoreUtil.PLAY_STORE_APP_URI +
|
val PLAY_STORE_URI = PlayStoreUtil.PLAY_STORE_APP_URI +
|
||||||
ApplicationProvider.getApplicationContext<Context>().packageName
|
getAppContext().packageName
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@SmallTest
|
@SmallTest
|
||||||
fun check_intent_for_store() {
|
fun check_intent_for_store() {
|
||||||
val intent = PlayStoreUtil.newActivityIntent(ApplicationProvider.getApplicationContext())
|
val intent = PlayStoreUtil.newActivityIntent(getAppContext())
|
||||||
assertNotNull(intent)
|
assertNotNull(intent)
|
||||||
assertEquals(intent.action, Intent.ACTION_VIEW)
|
assertEquals(intent.action, Intent.ACTION_VIEW)
|
||||||
assertContains(PLAY_STORE_URI, intent.data.toString())
|
assertContains(PLAY_STORE_URI, intent.data.toString())
|
||||||
|
|
|
@ -4,12 +4,12 @@ import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.test.core.app.ApplicationProvider
|
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import cash.z.ecc.android.sdk.ext.onFirst
|
import cash.z.ecc.android.sdk.ext.onFirst
|
||||||
import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp
|
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.UpdateInfo
|
||||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
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 com.google.android.play.core.install.model.ActivityResult
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
@ -26,7 +26,7 @@ class AppUpdateCheckerImpTest {
|
||||||
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
|
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val context: Context = ApplicationProvider.getApplicationContext()
|
val context: Context = getAppContext()
|
||||||
val updateChecker = AppUpdateCheckerImp.new()
|
val updateChecker = AppUpdateCheckerImp.new()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,9 @@ import androidx.test.espresso.Espresso
|
||||||
import androidx.test.filters.MediumTest
|
import androidx.test.filters.MediumTest
|
||||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||||
import co.electriccoin.zcash.ui.R
|
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.AppUpdateChecker
|
||||||
import co.electriccoin.zcash.ui.screen.update.UpdateTag
|
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.UpdateInfo
|
||||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
||||||
import co.electriccoin.zcash.ui.test.getStringResource
|
import co.electriccoin.zcash.ui.test.getStringResource
|
||||||
|
|
|
@ -4,6 +4,8 @@ import android.content.Context
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.test.core.app.ApplicationProvider
|
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 androidx.navigation.compose.rememberNavController
|
||||||
import cash.z.ecc.sdk.model.ZecRequest
|
import cash.z.ecc.sdk.model.ZecRequest
|
||||||
import cash.z.ecc.sdk.send
|
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.ConfigurationOverride
|
||||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||||
import co.electriccoin.zcash.ui.design.component.Override
|
import co.electriccoin.zcash.ui.design.component.Override
|
||||||
|
@ -82,8 +83,15 @@ class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
setupSplashScreen()
|
setupSplashScreen()
|
||||||
|
|
||||||
|
if (FontCompat.isFontPrefetchNeeded()) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
FontCompat.prefetchFontsLegacy(applicationContext)
|
||||||
setupUiContent()
|
setupUiContent()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setupUiContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupSplashScreen() {
|
private fun setupSplashScreen() {
|
||||||
val splashScreen = installSplashScreen()
|
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.AppUpdateChecker
|
||||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo
|
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.block.CompactBlockProcessor
|
||||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
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.model.WalletSnapshot
|
||||||
|
import co.electriccoin.zcash.ui.screen.home.viewmodel.SynchronizerError
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
object WalletSnapshotFixture {
|
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
|
// Should fill in with non-empty values for better example values in tests and UI previews
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
fun new(
|
fun new(
|
||||||
status: Synchronizer.Status = Synchronizer.Status.SYNCED,
|
status: Synchronizer.Status = STATUS,
|
||||||
processorInfo: CompactBlockProcessor.ProcessorInfo = CompactBlockProcessor.ProcessorInfo(),
|
processorInfo: CompactBlockProcessor.ProcessorInfo = CompactBlockProcessor.ProcessorInfo(),
|
||||||
orchardBalance: WalletBalance = WalletBalance(Zatoshi(5), Zatoshi(2)),
|
orchardBalance: WalletBalance = ORCHARD_BALANCE,
|
||||||
saplingBalance: WalletBalance = WalletBalance(Zatoshi(4), Zatoshi(4)),
|
saplingBalance: WalletBalance = SAPLING_BALANCE,
|
||||||
transparentBalance: WalletBalance = WalletBalance(Zatoshi(8), Zatoshi(1)),
|
transparentBalance: WalletBalance = TRANSPARENT_BALANCE,
|
||||||
pendingCount: Int = 0
|
pendingCount: Int = 0,
|
||||||
) = WalletSnapshot(status, processorInfo, orchardBalance, saplingBalance, transparentBalance, pendingCount)
|
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.
|
* 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)
|
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.MainActivity
|
||||||
import co.electriccoin.zcash.ui.screen.backup.viewmodel.BackupViewModel
|
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.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.home.viewmodel.WalletViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
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
|
@Composable
|
||||||
internal fun MainActivity.WrapHome(
|
internal fun MainActivity.WrapHome(
|
||||||
|
@ -40,6 +43,17 @@ internal fun WrapHome(
|
||||||
goSend: () -> Unit,
|
goSend: () -> Unit,
|
||||||
goRequest: () -> 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 walletViewModel by activity.viewModels<WalletViewModel>()
|
||||||
|
|
||||||
val walletSnapshot = walletViewModel.walletSnapshot.collectAsState().value
|
val walletSnapshot = walletViewModel.walletSnapshot.collectAsState().value
|
||||||
|
@ -76,7 +90,8 @@ internal fun WrapHome(
|
||||||
|
|
||||||
val backupViewModel by activity.viewModels<BackupViewModel>()
|
val backupViewModel by activity.viewModels<BackupViewModel>()
|
||||||
backupViewModel.backupState.goToBeginning()
|
backupViewModel.backupState.goToBeginning()
|
||||||
}
|
},
|
||||||
|
updateAvailable = updateAvailable
|
||||||
)
|
)
|
||||||
|
|
||||||
activity.reportFullyDrawn()
|
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.ext.ZcashSdk
|
||||||
import cash.z.ecc.android.sdk.model.WalletBalance
|
import cash.z.ecc.android.sdk.model.WalletBalance
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
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.
|
// TODO [#292]: Should be moved to SDK-EXT-UI module.
|
||||||
data class WalletSnapshot(
|
data class WalletSnapshot(
|
||||||
|
@ -13,7 +15,9 @@ data class WalletSnapshot(
|
||||||
val orchardBalance: WalletBalance,
|
val orchardBalance: WalletBalance,
|
||||||
val saplingBalance: WalletBalance,
|
val saplingBalance: WalletBalance,
|
||||||
val transparentBalance: 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
|
// Note: the wallet is effectively empty if it cannot cover the miner's fee
|
||||||
val hasFunds = saplingBalance.available.value >
|
val hasFunds = saplingBalance.available.value >
|
||||||
|
|
|
@ -1,17 +1,25 @@
|
||||||
package co.electriccoin.zcash.ui.screen.home.view
|
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.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.wrapContentSize
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Person
|
import androidx.compose.material.icons.filled.Person
|
||||||
import androidx.compose.material.icons.filled.QrCodeScanner
|
import androidx.compose.material.icons.filled.QrCodeScanner
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
@ -25,24 +33,32 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
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.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.crash.android.CrashReporter
|
||||||
import co.electriccoin.zcash.ui.R
|
import co.electriccoin.zcash.ui.R
|
||||||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||||
import co.electriccoin.zcash.ui.design.component.Body
|
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.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.PrimaryButton
|
||||||
import co.electriccoin.zcash.ui.design.component.TertiaryButton
|
import co.electriccoin.zcash.ui.design.component.TertiaryButton
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
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.WalletSnapshot
|
||||||
import co.electriccoin.zcash.ui.screen.home.model.totalBalance
|
|
||||||
import java.lang.RuntimeException
|
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -56,9 +72,10 @@ fun ComposablePreview() {
|
||||||
goProfile = {},
|
goProfile = {},
|
||||||
goSend = {},
|
goSend = {},
|
||||||
goRequest = {},
|
goRequest = {},
|
||||||
isDebugMenuEnabled = false,
|
|
||||||
resetSdk = {},
|
resetSdk = {},
|
||||||
wipeEntireWallet = {}
|
wipeEntireWallet = {},
|
||||||
|
isDebugMenuEnabled = false,
|
||||||
|
updateAvailable = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,7 +93,8 @@ fun Home(
|
||||||
goRequest: () -> Unit,
|
goRequest: () -> Unit,
|
||||||
resetSdk: () -> Unit,
|
resetSdk: () -> Unit,
|
||||||
wipeEntireWallet: () -> Unit,
|
wipeEntireWallet: () -> Unit,
|
||||||
isDebugMenuEnabled: Boolean
|
isDebugMenuEnabled: Boolean,
|
||||||
|
updateAvailable: Boolean
|
||||||
) {
|
) {
|
||||||
Scaffold(topBar = {
|
Scaffold(topBar = {
|
||||||
HomeTopAppBar(isDebugMenuEnabled, resetSdk, wipeEntireWallet)
|
HomeTopAppBar(isDebugMenuEnabled, resetSdk, wipeEntireWallet)
|
||||||
|
@ -88,7 +106,8 @@ fun Home(
|
||||||
goScan = goScan,
|
goScan = goScan,
|
||||||
goProfile = goProfile,
|
goProfile = goProfile,
|
||||||
goSend = goSend,
|
goSend = goSend,
|
||||||
goRequest = goRequest
|
goRequest = goRequest,
|
||||||
|
updateAvailable = updateAvailable
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,9 +182,10 @@ private fun HomeMainContent(
|
||||||
goScan: () -> Unit,
|
goScan: () -> Unit,
|
||||||
goProfile: () -> Unit,
|
goProfile: () -> Unit,
|
||||||
goSend: () -> Unit,
|
goSend: () -> Unit,
|
||||||
goRequest: () -> Unit
|
goRequest: () -> Unit,
|
||||||
|
updateAvailable: Boolean
|
||||||
) {
|
) {
|
||||||
Column {
|
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||||
Row(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.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))
|
PrimaryButton(onClick = goSend, text = stringResource(R.string.home_button_send))
|
||||||
|
|
||||||
TertiaryButton(onClick = goRequest, text = stringResource(R.string.home_button_request))
|
TertiaryButton(onClick = goRequest, text = stringResource(R.string.home_button_request))
|
||||||
|
|
||||||
History(transactionHistory)
|
History(transactionHistory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Status(walletSnapshot: WalletSnapshot) {
|
@Suppress("LongMethod", "MagicNumber")
|
||||||
Column(Modifier.fillMaxWidth()) {
|
private fun Status(
|
||||||
Header(text = walletSnapshot.totalBalance().toZecString())
|
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(
|
Body(
|
||||||
text = stringResource(
|
text = walletDisplayValues.statusText,
|
||||||
id = R.string.home_status_shielding_format,
|
modifier = Modifier.testTag(HomeTag.SINGLE_LINE_TEXT)
|
||||||
walletSnapshot.saplingBalance.total.toZecString()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@Suppress("MagicNumber")
|
||||||
private fun History(transactionHistory: List<Transaction>) {
|
private fun History(transactionHistory: List<Transaction>) {
|
||||||
Column(Modifier.fillMaxWidth()) {
|
if (transactionHistory.isEmpty()) {
|
||||||
LazyColumn {
|
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) {
|
items(transactionHistory) {
|
||||||
Text(it.toString())
|
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.WalletBalance
|
||||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||||
import cash.z.ecc.android.sdk.tool.DerivationTool
|
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.PersistableWallet
|
||||||
import cash.z.ecc.sdk.model.WalletAddresses
|
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.common.ANDROID_STATE_FLOW_TIMEOUT
|
||||||
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceKeys
|
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceKeys
|
||||||
import co.electriccoin.zcash.ui.preference.EncryptedPreferenceSingleton
|
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 co.electriccoin.zcash.work.WorkIds
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.WhileSubscribed
|
import kotlinx.coroutines.flow.WhileSubscribed
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.emitAll
|
import kotlinx.coroutines.flow.emitAll
|
||||||
|
@ -63,6 +69,18 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
|
||||||
null
|
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.
|
* 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()
|
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
|
// No good way around needing magic numbers for the indices
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
private fun Synchronizer.toWalletSnapshot() =
|
private fun Synchronizer.toWalletSnapshot() =
|
||||||
|
@ -250,7 +330,9 @@ private fun Synchronizer.toWalletSnapshot() =
|
||||||
orchardBalances, // 2
|
orchardBalances, // 2
|
||||||
saplingBalances, // 3
|
saplingBalances, // 3
|
||||||
transparentBalances, // 4
|
transparentBalances, // 4
|
||||||
pendingTransactions.distinctUntilChanged() // 5
|
pendingTransactions.distinctUntilChanged(), // 5
|
||||||
|
progress, // 6
|
||||||
|
toCommonError() // 7
|
||||||
) { flows ->
|
) { flows ->
|
||||||
val pendingCount = (flows[5] as List<*>)
|
val pendingCount = (flows[5] as List<*>)
|
||||||
.filterIsInstance(PendingTransaction::class.java)
|
.filterIsInstance(PendingTransaction::class.java)
|
||||||
|
@ -261,13 +343,22 @@ private fun Synchronizer.toWalletSnapshot() =
|
||||||
val saplingBalance = flows[3] as WalletBalance?
|
val saplingBalance = flows[3] as WalletBalance?
|
||||||
val transparentBalance = flows[4] 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(
|
WalletSnapshot(
|
||||||
flows[0] as Synchronizer.Status,
|
flows[0] as Synchronizer.Status,
|
||||||
flows[1] as CompactBlockProcessor.ProcessorInfo,
|
flows[1] as CompactBlockProcessor.ProcessorInfo,
|
||||||
orchardBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0)),
|
orchardBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0)),
|
||||||
saplingBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0)),
|
saplingBalance ?: WalletBalance(Zatoshi(0), Zatoshi(0)),
|
||||||
transparentBalance ?: 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.Reference
|
||||||
import co.electriccoin.zcash.ui.design.component.TertiaryButton
|
import co.electriccoin.zcash.ui.design.component.TertiaryButton
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
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.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.UpdateInfo
|
||||||
import co.electriccoin.zcash.ui.screen.update.model.UpdateState
|
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_scan_content_description">Scan</string>
|
||||||
<string name="home_profile_content_description">Profile</string>
|
<string name="home_profile_content_description">Profile</string>
|
||||||
<string name="home_button_send">Send</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_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>
|
</resources>
|
||||||
|
|
Loading…
Reference in New Issue