[#135] Request ZEC scaffold
This commit is contained in:
parent
52588e6bd3
commit
a1199e706e
|
@ -26,11 +26,11 @@ If you wish to report a security issue, please follow our Responsible Disclosure
|
|||
2.
|
||||
3.
|
||||
|
||||
### Expected behaviour
|
||||
### Expected behavior
|
||||
<!-- Tell us what should happen -->
|
||||
|
||||
### Actual behaviour + errors
|
||||
<!-- Tell us what happens instead including any noticable error output (any messages
|
||||
### Actual behavior + errors
|
||||
<!-- Tell us what happens instead including any noticeable error output (any messages
|
||||
displayed on-screen when e.g. a crash occurred) -->
|
||||
<!-- Note: please do not include sensitive information. blur, scratch or annotate any
|
||||
information like addresses, usernames, amounts or anything other that you might consider sensitive and it's not relevant to the problem you are reporting. -->
|
||||
|
|
|
@ -152,6 +152,26 @@ class ScreenshotTest {
|
|||
it.performClick()
|
||||
}
|
||||
addressDetailsScreenshots(composeTestRule)
|
||||
|
||||
// Back to profile
|
||||
composeTestRule.onNode(hasContentDescription(getStringResource(R.string.wallet_address_back_content_description))).also {
|
||||
it.assertExists()
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
// Back to home screen
|
||||
composeTestRule.onNode(hasContentDescription(getStringResource(R.string.settings_back_content_description))).also {
|
||||
it.assertExists()
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.Ready }
|
||||
composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.walletSnapshot.value != null }
|
||||
composeTestRule.onNode(hasText(getStringResource(R.string.home_button_request))).also {
|
||||
it.assertExists()
|
||||
it.performClick()
|
||||
}
|
||||
requestZecScreenshots(composeTestRule)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -322,3 +342,11 @@ private fun addressDetailsScreenshots(composeTestRule: ComposeTestRule) {
|
|||
|
||||
ScreenshotTest.takeScreenshot("Addresses 1")
|
||||
}
|
||||
|
||||
private fun requestZecScreenshots(composeTestRule: ComposeTestRule) {
|
||||
composeTestRule.onNode(hasText(getStringResource(R.string.request_title))).also {
|
||||
it.assertExists()
|
||||
}
|
||||
|
||||
ScreenshotTest.takeScreenshot("Request 1")
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ dependencies {
|
|||
|
||||
androidTestImplementation(libs.bundles.androidx.test)
|
||||
androidTestImplementation(libs.kotlinx.coroutines.test)
|
||||
androidTestImplementation(libs.kotlin.test)
|
||||
|
||||
if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) {
|
||||
androidTestUtil(libs.androidx.test.orchestrator) {
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package cash.z.ecc.sdk.model
|
||||
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class ZatoshiTest {
|
||||
@kotlin.test.Test
|
||||
fun minValue() {
|
||||
assertFailsWith<IllegalArgumentException> {
|
||||
Zatoshi(-1)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package cash.z.ecc.sdk.model
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class ZecStringTest {
|
||||
|
||||
companion object {
|
||||
private val EN_US_MONETARY_SEPARATORS = MonetarySeparators(',', '.')
|
||||
}
|
||||
|
||||
@Test
|
||||
fun empty_string() {
|
||||
val actual = Zatoshi.fromZecString("", EN_US_MONETARY_SEPARATORS)
|
||||
val expected = null
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decimal_monetary_separator() {
|
||||
val actual = Zatoshi.fromZecString("1.13", EN_US_MONETARY_SEPARATORS)
|
||||
val expected = Zatoshi(113000000L)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun comma_grouping_separator() {
|
||||
val actual = Zatoshi.fromZecString("1,130", EN_US_MONETARY_SEPARATORS)
|
||||
val expected = Zatoshi(113000000000L)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decimal_monetary_and() {
|
||||
val actual = Zatoshi.fromZecString("1,130", EN_US_MONETARY_SEPARATORS)
|
||||
val expected = Zatoshi(113000000000L)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun toZecString() {
|
||||
val expected = "1.13000000"
|
||||
val actual = Zatoshi(113000000).toZecString()
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("https://github.com/zcash/zcash-android-wallet-sdk/issues/412")
|
||||
fun round_trip() {
|
||||
val expected = Zatoshi(113000000L)
|
||||
val actual = Zatoshi.fromZecString(expected.toZecString(), EN_US_MONETARY_SEPARATORS)
|
||||
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("https://github.com/zcash/secant-android-wallet/issues/223")
|
||||
fun parse_bad_string() {
|
||||
val actual = Zatoshi.fromZecString("asdf", EN_US_MONETARY_SEPARATORS)
|
||||
|
||||
assertNull(actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("https://github.com/zcash/secant-android-wallet/issues/223")
|
||||
fun parse_bad_number() {
|
||||
val actual = Zatoshi.fromZecString("1.2,3,4", EN_US_MONETARY_SEPARATORS)
|
||||
|
||||
assertNull(actual)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package cash.z.ecc.sdk.fixture
|
||||
|
||||
import cash.z.ecc.sdk.model.WalletAddress
|
||||
import cash.z.ecc.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.model.ZecRequest
|
||||
import cash.z.ecc.sdk.model.ZecRequestMessage
|
||||
|
||||
object ZecRequestFixture {
|
||||
const val ADDRESS: String = WalletAddressFixture.UNIFIED_ADDRESS_STRING
|
||||
@Suppress("MagicNumber")
|
||||
val AMOUNT = Zatoshi(123)
|
||||
val MESSAGE = ZecRequestMessage("Thanks for lunch")
|
||||
|
||||
suspend fun new(
|
||||
address: String = ADDRESS,
|
||||
amount: Zatoshi = AMOUNT,
|
||||
message: ZecRequestMessage = MESSAGE
|
||||
) = ZecRequest(WalletAddress.Unified.new(address), amount, message)
|
||||
}
|
|
@ -1,17 +1,13 @@
|
|||
package cash.z.ecc.sdk.model
|
||||
|
||||
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
|
||||
|
||||
// Eventually, this could move into the SDK and provide a stronger API for amounts
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@JvmInline
|
||||
value class Zatoshi(val amount: Long) {
|
||||
init {
|
||||
require(amount >= 0)
|
||||
}
|
||||
|
||||
override fun toString() = amount.convertZatoshiToZecString(DECIMALS, DECIMALS)
|
||||
|
||||
companion object {
|
||||
private const val DECIMALS = 8
|
||||
}
|
||||
companion object
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package cash.z.ecc.sdk.model
|
||||
|
||||
import cash.z.ecc.sdk.fixture.ZecRequestFixture
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
data class ZecRequest(val address: WalletAddress.Unified, val amount: Zatoshi, val message: ZecRequestMessage) {
|
||||
|
||||
// https://github.com/zcash/zcash-android-wallet-sdk/issues/397
|
||||
// TODO [#397]: There's an issue in the SDK to implement the parser
|
||||
@Suppress("FunctionOnlyReturningConstant")
|
||||
fun toUri(): String = ""
|
||||
|
||||
companion object {
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
suspend fun fromUri(uriString: String) {
|
||||
// TODO [#397]: Use URI parser
|
||||
runBlocking { ZecRequestFixture.new() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
value class ZecRequestMessage(val message: String) {
|
||||
init {
|
||||
require(message.length <= MAX_MESSAGE_LENGTH)
|
||||
}
|
||||
|
||||
companion object {
|
||||
// TODO [#219]: Define a maximum message length
|
||||
// Also note that the length varies from what the user types in versus the encoded version
|
||||
// that is actually sent.
|
||||
const val MAX_MESSAGE_LENGTH = 320
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package cash.z.ecc.sdk.model
|
||||
|
||||
import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString
|
||||
import cash.z.ecc.android.sdk.ext.convertZecToZatoshi
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.text.ParseException
|
||||
import java.util.Locale
|
||||
|
||||
object ZecString {
|
||||
|
||||
fun allowedCharacters(monetarySeparators: MonetarySeparators) = buildSet<Char> {
|
||||
add('0')
|
||||
add('1')
|
||||
add('2')
|
||||
add('3')
|
||||
add('4')
|
||||
add('5')
|
||||
add('6')
|
||||
add('7')
|
||||
add('8')
|
||||
add('9')
|
||||
add(monetarySeparators.decimal)
|
||||
add(monetarySeparators.grouping)
|
||||
}
|
||||
}
|
||||
|
||||
data class MonetarySeparators(val grouping: Char, val decimal: Char) {
|
||||
init {
|
||||
require(grouping != decimal) { "Grouping and decimal separator cannot be the same character" }
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* @return The current localized monetary separators. Do not cache this value, as it
|
||||
* can change if the system Locale changes.
|
||||
*/
|
||||
fun current(): MonetarySeparators {
|
||||
val decimalFormatSymbols = DecimalFormatSymbols.getInstance()
|
||||
|
||||
return MonetarySeparators(
|
||||
decimalFormatSymbols.groupingSeparator,
|
||||
decimalFormatSymbols.monetaryDecimalSeparator
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val DECIMALS = 8
|
||||
|
||||
// TODO [#412]: https://github.com/zcash/zcash-android-wallet-sdk/issues/412
|
||||
// The SDK needs to fix the API for currency conversion
|
||||
fun Zatoshi.toZecString() = amount.convertZatoshiToZecString(DECIMALS, DECIMALS)
|
||||
|
||||
/*
|
||||
* ZEC is our own currency, so there's not going to be an existing localization that matches it perfectly.
|
||||
*
|
||||
* To ensure consistent behavior regardless of user Locale, use US localization except that we swap out the
|
||||
* separator characters based on the user's current Locale. This should avoid unexpected surprises
|
||||
* while also localizing the separator format.
|
||||
*/
|
||||
/**
|
||||
* @return [zecString] parsed into Zatoshi or null if parsing failed.
|
||||
*/
|
||||
fun Zatoshi.Companion.fromZecString(zecString: String, monetarySeparators: MonetarySeparators): Zatoshi? {
|
||||
if (zecString.isBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val symbols = DecimalFormatSymbols.getInstance(Locale.US).apply {
|
||||
this.groupingSeparator = monetarySeparators.grouping
|
||||
this.decimalSeparator = monetarySeparators.decimal
|
||||
}
|
||||
val localizedPattern = "#${monetarySeparators.grouping}##0${monetarySeparators.decimal}0#"
|
||||
|
||||
val decimalFormat = DecimalFormat(localizedPattern, symbols).apply {
|
||||
isParseBigDecimal = true
|
||||
roundingMode = RoundingMode.HALF_EVEN // aka Bankers rounding
|
||||
}
|
||||
|
||||
val bigDecimal = try {
|
||||
decimalFormat.parse(zecString) as BigDecimal
|
||||
} catch (e: NumberFormatException) {
|
||||
null
|
||||
} catch (e: ParseException) {
|
||||
null
|
||||
}
|
||||
|
||||
return Zatoshi(bigDecimal.convertZecToZatoshi())
|
||||
}
|
|
@ -137,6 +137,7 @@ dependencyResolutionManagement {
|
|||
alias("google-material").to("com.google.android.material:material:$googleMaterialVersion")
|
||||
alias("kotlin-stdlib").to("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")
|
||||
alias("kotlin-reflect").to("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
|
||||
alias("kotlin-test").to("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
|
||||
alias("kotlinx-coroutines-android").to("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinxCoroutinesVersion")
|
||||
alias("kotlinx-coroutines-core").to("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
|
||||
alias("zcash-sdk").to("cash.z.ecc.android:zcash-android-sdk:$zcashSdkVersion")
|
||||
|
|
|
@ -32,6 +32,7 @@ android {
|
|||
"src/main/res/ui/onboarding",
|
||||
"src/main/res/ui/profile",
|
||||
"src/main/res/ui/restore",
|
||||
"src/main/res/ui/request",
|
||||
"src/main/res/ui/seed",
|
||||
"src/main/res/ui/settings",
|
||||
"src/main/res/ui/wallet_address"
|
||||
|
@ -65,6 +66,7 @@ dependencies {
|
|||
androidTestImplementation(libs.androidx.compose.test.junit)
|
||||
androidTestImplementation(libs.androidx.compose.test.manifest)
|
||||
androidTestImplementation(libs.kotlin.reflect)
|
||||
androidTestImplementation(libs.kotlin.test)
|
||||
|
||||
if (project.property("IS_USE_TEST_ORCHESTRATOR").toString().toBoolean()) {
|
||||
androidTestUtil(libs.androidx.test.orchestrator) {
|
||||
|
|
|
@ -0,0 +1,219 @@
|
|||
package cash.z.ecc.ui.screen.request.view
|
||||
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
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.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.sdk.fixture.ZecRequestFixture
|
||||
import cash.z.ecc.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.model.ZecRequest
|
||||
import cash.z.ecc.sdk.model.ZecRequestMessage
|
||||
import cash.z.ecc.ui.R
|
||||
import cash.z.ecc.ui.test.getStringResource
|
||||
import cash.z.ecc.ui.theme.ZcashTheme
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class RequestViewTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun create_button_disabled() {
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_create)).also {
|
||||
it.assertIsNotEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun create_request_no_message() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
assertEquals(null, testSetup.getLastCreateZecRequest())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_amount)).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
|
||||
it.performTextInput("{${separators.decimal}}123")
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_create)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
|
||||
testSetup.getLastCreateZecRequest().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.address)
|
||||
assertEquals(Zatoshi(12300000), it.amount)
|
||||
assertTrue(it.message.message.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun create_request_with_message() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
assertEquals(null, testSetup.getLastCreateZecRequest())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_amount)).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
|
||||
it.performTextInput("{${separators.decimal}}123")
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_message)).also {
|
||||
it.performTextInput(ZecRequestFixture.MESSAGE.message)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_create)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
|
||||
testSetup.getLastCreateZecRequest().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.address)
|
||||
assertEquals(Zatoshi(12300000), it.amount)
|
||||
assertEquals(ZecRequestFixture.MESSAGE.message, it.message.message)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
@Ignore("https://github.com/zcash/secant-android-wallet/issues/218")
|
||||
fun create_request_illegal_input() {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
assertEquals(null, testSetup.getLastCreateZecRequest())
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_amount)).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
|
||||
it.performTextInput("{${separators.decimal}}1{${separators.decimal}}2{${separators.decimal}}3{${separators.decimal}}4")
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_create)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(0, testSetup.getOnCreateCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun max_message_length() = runTest {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_amount)).also {
|
||||
val separators = MonetarySeparators.current()
|
||||
|
||||
it.performTextInput("{${separators.decimal}}123")
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_message)).also {
|
||||
val input = buildString {
|
||||
repeat(ZecRequestMessage.MAX_MESSAGE_LENGTH + 1) { _ ->
|
||||
append("$it")
|
||||
}
|
||||
}
|
||||
|
||||
it.performTextInput(input)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText(getStringResource(R.string.request_create)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
|
||||
testSetup.getLastCreateZecRequest().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.address)
|
||||
assertEquals(Zatoshi(12300000), it.amount)
|
||||
assertTrue(it.message.message.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@MediumTest
|
||||
fun back() {
|
||||
val testSetup = TestSetup(composeTestRule)
|
||||
|
||||
assertEquals(0, testSetup.getOnBackCount())
|
||||
|
||||
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.request_back_content_description)).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
assertEquals(1, testSetup.getOnBackCount())
|
||||
}
|
||||
|
||||
private class TestSetup(private val composeTestRule: ComposeContentTestRule) {
|
||||
|
||||
private val onBackCount = AtomicInteger(0)
|
||||
private val onCreateCount = AtomicInteger(0)
|
||||
@Volatile
|
||||
private var onCreateZecRequest: ZecRequest? = null
|
||||
|
||||
fun getOnBackCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onBackCount.get()
|
||||
}
|
||||
|
||||
fun getOnCreateCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
return onCreateCount.get()
|
||||
}
|
||||
|
||||
fun getLastCreateZecRequest(): ZecRequest? {
|
||||
composeTestRule.waitForIdle()
|
||||
return onCreateZecRequest
|
||||
}
|
||||
|
||||
init {
|
||||
composeTestRule.setContent {
|
||||
ZcashTheme {
|
||||
Request(
|
||||
myAddress = runBlocking { WalletAddressFixture.unified() },
|
||||
goBack = {
|
||||
onBackCount.incrementAndGet()
|
||||
},
|
||||
onCreateAndSend = {
|
||||
onCreateCount.incrementAndGet()
|
||||
onCreateZecRequest = it
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -54,8 +54,8 @@ class SeedViewTest {
|
|||
|
||||
private class TestSetup(private val composeTestRule: ComposeContentTestRule) {
|
||||
|
||||
private var onBackCount = AtomicInteger(0)
|
||||
private var onCopyToClipboardCount = AtomicInteger(0)
|
||||
private val onBackCount = AtomicInteger(0)
|
||||
private val onCopyToClipboardCount = AtomicInteger(0)
|
||||
|
||||
fun getOnBackCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
|
|
|
@ -79,10 +79,10 @@ class SettingsViewTest {
|
|||
|
||||
private class TestSetup(private val composeTestRule: ComposeContentTestRule) {
|
||||
|
||||
private var onBackCount = AtomicInteger(0)
|
||||
private var onBackupCount = AtomicInteger(0)
|
||||
private var onRescanCount = AtomicInteger(0)
|
||||
private var onWipeCount = AtomicInteger(0)
|
||||
private val onBackCount = AtomicInteger(0)
|
||||
private val onBackupCount = AtomicInteger(0)
|
||||
private val onRescanCount = AtomicInteger(0)
|
||||
private val onWipeCount = AtomicInteger(0)
|
||||
|
||||
fun getOnBackCount(): Int {
|
||||
composeTestRule.waitForIdle()
|
||||
|
|
|
@ -3,6 +3,7 @@ package cash.z.ecc.ui
|
|||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import androidx.activity.ComponentActivity
|
||||
|
@ -26,6 +27,7 @@ import cash.z.ecc.android.sdk.type.WalletBirthday
|
|||
import cash.z.ecc.android.sdk.type.ZcashNetwork
|
||||
import cash.z.ecc.sdk.model.PersistableWallet
|
||||
import cash.z.ecc.sdk.model.SeedPhrase
|
||||
import cash.z.ecc.sdk.model.ZecRequest
|
||||
import cash.z.ecc.sdk.type.fromResources
|
||||
import cash.z.ecc.ui.screen.backup.view.BackupWallet
|
||||
import cash.z.ecc.ui.screen.backup.viewmodel.BackupViewModel
|
||||
|
@ -36,6 +38,7 @@ import cash.z.ecc.ui.screen.home.viewmodel.WalletViewModel
|
|||
import cash.z.ecc.ui.screen.onboarding.view.Onboarding
|
||||
import cash.z.ecc.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
||||
import cash.z.ecc.ui.screen.profile.view.Profile
|
||||
import cash.z.ecc.ui.screen.request.view.Request
|
||||
import cash.z.ecc.ui.screen.restore.view.RestoreWallet
|
||||
import cash.z.ecc.ui.screen.restore.viewmodel.CompleteWordSetState
|
||||
import cash.z.ecc.ui.screen.restore.viewmodel.RestoreViewModel
|
||||
|
@ -215,7 +218,7 @@ class MainActivity : ComponentActivity() {
|
|||
goScan = {},
|
||||
goProfile = { navController.navigate(NAV_PROFILE) },
|
||||
goSend = {},
|
||||
goRequest = {}
|
||||
goRequest = { navController.navigate(NAV_REQUEST) }
|
||||
)
|
||||
}
|
||||
composable(NAV_PROFILE) {
|
||||
|
@ -252,6 +255,9 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
)
|
||||
}
|
||||
composable(NAV_REQUEST) {
|
||||
WrapRequest(goBack = { navController.popBackStack() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -373,6 +379,28 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WrapRequest(
|
||||
goBack: () -> Unit
|
||||
) {
|
||||
val walletAddresses = walletViewModel.addresses.collectAsState().value
|
||||
if (null == walletAddresses) {
|
||||
// Display loading indicator
|
||||
} else {
|
||||
Request(
|
||||
walletAddresses.unified,
|
||||
goBack = goBack,
|
||||
onCreateAndSend = {
|
||||
val chooserIntent = Intent.createChooser(it.newShareIntent(applicationContext), null)
|
||||
|
||||
startActivity(chooserIntent)
|
||||
|
||||
goBack()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting
|
||||
internal val SPLASH_SCREEN_DELAY = 0.seconds
|
||||
|
@ -391,6 +419,9 @@ class MainActivity : ComponentActivity() {
|
|||
|
||||
@VisibleForTesting
|
||||
const val NAV_SEED = "seed"
|
||||
|
||||
@VisibleForTesting
|
||||
const val NAV_REQUEST = "request"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -416,3 +447,9 @@ private suspend fun prefetchFontLegacy(context: Context, @FontRes fontRes: Int)
|
|||
withContext(Dispatchers.IO) {
|
||||
ResourcesCompat.getFont(context, fontRes)
|
||||
}
|
||||
|
||||
private fun ZecRequest.newShareIntent(context: Context) = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_TEXT, context.getString(R.string.request_template_format, toUri()))
|
||||
type = "text/plain"
|
||||
}
|
||||
|
|
|
@ -33,7 +33,8 @@ fun ButtonComposablePreview() {
|
|||
fun PrimaryButton(
|
||||
onClick: () -> Unit,
|
||||
text: String,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
Button(
|
||||
onClick = onClick,
|
||||
|
@ -42,6 +43,7 @@ fun PrimaryButton(
|
|||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
),
|
||||
enabled = enabled,
|
||||
colors = buttonColors(backgroundColor = MaterialTheme.colors.primary)
|
||||
) {
|
||||
Text(style = MaterialTheme.typography.button, text = text, color = MaterialTheme.colors.onPrimary)
|
||||
|
|
|
@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import cash.z.ecc.android.sdk.db.entity.Transaction
|
||||
import cash.z.ecc.sdk.model.toZecString
|
||||
import cash.z.ecc.sdk.model.total
|
||||
import cash.z.ecc.ui.R
|
||||
import cash.z.ecc.ui.fixture.WalletSnapshotFixture
|
||||
|
@ -126,11 +127,11 @@ private fun HomeMainContent(
|
|||
@Composable
|
||||
private fun Status(walletSnapshot: WalletSnapshot) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Header(text = walletSnapshot.totalBalance().toString())
|
||||
Header(text = walletSnapshot.totalBalance().toZecString())
|
||||
Body(
|
||||
text = stringResource(
|
||||
id = R.string.home_status_shielding_format,
|
||||
walletSnapshot.saplingBalance.total.toString()
|
||||
walletSnapshot.saplingBalance.total.toZecString()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
package cash.z.ecc.ui.screen.request.view
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.sdk.model.WalletAddress
|
||||
import cash.z.ecc.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.model.ZecRequest
|
||||
import cash.z.ecc.sdk.model.ZecRequestMessage
|
||||
import cash.z.ecc.sdk.model.ZecString
|
||||
import cash.z.ecc.sdk.model.fromZecString
|
||||
import cash.z.ecc.ui.R
|
||||
import cash.z.ecc.ui.screen.common.GradientSurface
|
||||
import cash.z.ecc.ui.screen.common.PrimaryButton
|
||||
import cash.z.ecc.ui.theme.MINIMAL_WEIGHT
|
||||
import cash.z.ecc.ui.theme.ZcashTheme
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
@Composable
|
||||
fun PreviewRequest() {
|
||||
ZcashTheme(darkTheme = true) {
|
||||
GradientSurface {
|
||||
Request(
|
||||
myAddress = runBlocking { WalletAddressFixture.unified() },
|
||||
goBack = {},
|
||||
onCreateAndSend = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param myAddress The address that ZEC should be sent to.
|
||||
*/
|
||||
@Composable
|
||||
fun Request(
|
||||
myAddress: WalletAddress.Unified,
|
||||
goBack: () -> Unit,
|
||||
onCreateAndSend: (ZecRequest) -> Unit,
|
||||
) {
|
||||
Scaffold(topBar = {
|
||||
RequestTopAppBar(onBack = goBack)
|
||||
}) {
|
||||
RequestMainContent(
|
||||
myAddress,
|
||||
onCreateAndSend = onCreateAndSend
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RequestTopAppBar(onBack: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringResource(id = R.string.request_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBack
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.request_back_content_description)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// TODO [#215]: Need to add some UI to explain to the user if a request is invalid
|
||||
// TODO [#217]: Need to handle changing of Locale after user input, but before submitting the button.
|
||||
@Composable
|
||||
private fun RequestMainContent(
|
||||
myAddress: WalletAddress.Unified,
|
||||
onCreateAndSend: (ZecRequest) -> Unit
|
||||
) {
|
||||
val monetarySeparators = MonetarySeparators.current()
|
||||
val allowedCharacters = ZecString.allowedCharacters(monetarySeparators)
|
||||
|
||||
var amountZecString by rememberSaveable { mutableStateOf("") }
|
||||
var message by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
TextField(
|
||||
value = amountZecString,
|
||||
onValueChange = { newValue ->
|
||||
// TODO [#218]: this doesn't prevent illegal input. So users could still type `1.2.3.4`
|
||||
amountZecString = newValue.filter { allowedCharacters.contains(it) }
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
label = { Text(stringResource(id = R.string.request_amount)) }
|
||||
)
|
||||
|
||||
Spacer(Modifier.size(8.dp))
|
||||
|
||||
TextField(value = message, onValueChange = {
|
||||
if (it.length <= ZecRequestMessage.MAX_MESSAGE_LENGTH) {
|
||||
message = it
|
||||
}
|
||||
}, label = { Text(stringResource(id = R.string.request_message)) })
|
||||
|
||||
Spacer(Modifier.fillMaxHeight(MINIMAL_WEIGHT))
|
||||
|
||||
val zatoshi = Zatoshi.fromZecString(amountZecString, monetarySeparators)
|
||||
|
||||
PrimaryButton(
|
||||
onClick = {
|
||||
onCreateAndSend(ZecRequest(myAddress, zatoshi!!, ZecRequestMessage(message)))
|
||||
},
|
||||
text = stringResource(id = R.string.request_create),
|
||||
enabled = null != zatoshi
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="request_title">Request ZEC</string>
|
||||
<string name="request_back_content_description">Back</string>
|
||||
<string name="request_from">Who would you like to request ZEC from?</string>
|
||||
<string name="request_amount">How much?</string>
|
||||
<string name="request_message">Message</string>
|
||||
|
||||
<string name="request_create">Create and Send</string>
|
||||
|
||||
<string name="request_template_format" formatted="true">Someone is requesting ZEC from you. Please click the link: <xliff:g id="link" example="zcash:asdf">%1$s</xliff:g></string>
|
||||
|
||||
</resources>
|
Loading…
Reference in New Issue