[#791] Add Scan To Send

* [#791] Add Scan Button To Send Screen

- Switched Send.Form screen input fields ordering to match the MFP design
- Added Scan button to Send.Form
- Changed navigation to pass the scanned result to the Send.Form screen
- Added leading and trailing icons to FormTextField component
- Created SendArgumentsWrapper to pass any future Zcash URL parsed parameters

* Report invalid address scanned

- Merged composes to provide snackbar after invalid address scanned

* Add SendView tests

- Added one for scanner click and one for initial send arguments inserted
- This led to input and check amount and memo as part of the SendArgumentsWrapper too to have it prepare for future Zcash URI parsing

* Static analysis warnings fixes

* Don't make camera feature required

- And hide the scanner button on Send.Form when any camera is not supported by the device

* Let SendArgumentsWrapperFixture use SDK fixtures

* Tweak hint text

We need better text, but this is at least more correct than the previous iteration.

* Replace assertTextContains with assertTextEquals

- To compare inputs texts to an exact match to avoid potentially missing bugs if there is extra text in the field that we're not expecting

* Fix ktlint warning

---------

Co-authored-by: Carter Jernigan <git@carterjernigan.com>
This commit is contained in:
Honza Rychnovsky 2023-03-30 09:58:20 +02:00 committed by GitHub
parent 136e56ea9a
commit 5296dc5ea9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 321 additions and 82 deletions

View File

@ -57,5 +57,5 @@ This section is optional and is required only if you'd like to test on an Androi
1. Grant the Camera permission with one of the previous procedures
1. Create QR code from the valid Zcash wallet address. You can use, for example, the [QR Code Generator](https://www.qr-code-generator.com/) tool.
1. Scan the created QR code
1. The code should be scanned but not validated
1. The code should be scanned but not validated (error message is displayed).
1. The app UI should not be changed and the Camera view should be still available for scanning another codes

View File

@ -18,6 +18,8 @@ fun FormTextField(
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
label: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
colors: TextFieldColors = TextFieldDefaults.textFieldColors(
containerColor = Color.Transparent
@ -29,6 +31,8 @@ fun FormTextField(
label = label,
keyboardOptions = keyboardOptions,
colors = colors,
modifier = modifier
modifier = modifier,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon
)
}

View File

@ -0,0 +1,26 @@
package co.electriccoin.zcash.ui.fixture
import cash.z.ecc.android.sdk.fixture.WalletFixture
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.model.toZecString
import cash.z.ecc.sdk.fixture.MemoFixture
import cash.z.ecc.sdk.fixture.ZatoshiFixture
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
internal object SendArgumentsWrapperFixture {
val RECIPIENT_ADDRESS = WalletFixture.Alice.getAddresses(ZcashNetwork.Testnet).unified
val MEMO = MemoFixture.new("Thanks for lunch").value
val AMOUNT = ZatoshiFixture.new(1)
fun amountToFixtureZecString(amount: Zatoshi?) = amount?.toZecString()
fun new(
recipientAddress: String? = RECIPIENT_ADDRESS,
amount: Zatoshi? = AMOUNT,
memo: String? = MEMO
) = SendArgumentsWrapper(
recipientAddress = recipientAddress,
amount = amountToFixtureZecString(amount),
memo = memo
)
}

View File

@ -20,6 +20,12 @@ internal fun ComposeContentTestRule.clickBack() {
}
}
internal fun ComposeContentTestRule.clickScanner() {
onNodeWithContentDescription(getStringResource(R.string.send_scan_content_description)).also {
it.performClick()
}
}
internal fun ComposeContentTestRule.setValidAmount() {
onNodeWithText(getStringResource(R.string.send_amount)).also {
val separators = MonetarySeparators.current()

View File

@ -9,6 +9,7 @@ import cash.z.ecc.android.sdk.model.ZecSend
import cash.z.ecc.sdk.fixture.ZatoshiFixture
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.send.ext.Saver
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
import co.electriccoin.zcash.ui.screen.send.model.SendStage
import co.electriccoin.zcash.ui.screen.send.view.Send
import kotlinx.coroutines.flow.MutableStateFlow
@ -18,10 +19,13 @@ import java.util.concurrent.atomic.AtomicInteger
class SendViewTestSetup(
private val composeTestRule: ComposeContentTestRule,
private val initialState: SendStage,
private val initialZecSend: ZecSend?
private val initialZecSend: ZecSend?,
private val initialSendArgumentWrapper: SendArgumentsWrapper?,
private val hasCameraFeature: Boolean
) {
private val onBackCount = AtomicInteger(0)
private val onCreateCount = AtomicInteger(0)
private val onScannerCount = AtomicInteger(0)
val mutableActionExecuted = MutableStateFlow(false)
@Volatile
@ -40,6 +44,11 @@ class SendViewTestSetup(
return onCreateCount.get()
}
fun getOnScannerCount(): Int {
composeTestRule.waitForIdle()
return onScannerCount.get()
}
fun getLastZecSend(): ZecSend? {
composeTestRule.waitForIdle()
return lastZecSend
@ -82,6 +91,7 @@ class SendViewTestSetup(
Send(
mySpendableBalance = ZatoshiFixture.new(),
sendStage = sendStage,
sendArgumentsWrapper = initialSendArgumentWrapper,
onSendStageChange = setSendStage,
zecSend = zecSend,
onZecSendChange = setZecSend,
@ -90,7 +100,11 @@ class SendViewTestSetup(
onCreateCount.incrementAndGet()
lastZecSend = it
mutableActionExecuted.update { true }
}
},
onQrScannerOpen = {
onScannerCount.incrementAndGet()
},
hasCameraFeature = hasCameraFeature
)
}
}

View File

@ -1,6 +1,6 @@
package co.electriccoin.zcash.ui.screen.send.integration
import androidx.compose.ui.test.assertTextContains
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
@ -51,10 +51,13 @@ class SendViewIntegrationTest {
restorationTester.setContent {
WrapSend(
sendArgumentsWrapper = null,
synchronizer = synchronizer,
spendableBalance = balance,
spendingKey = spendingKey,
goBack = {}
goToQrScanner = {},
goBack = {},
hasCameraFeature = true
)
}
@ -77,14 +80,24 @@ class SendViewIntegrationTest {
composeTestRule.clickBack()
composeTestRule.assertOnForm()
// And check recreated form values too
// We use that the assertTextContains searches in SemanticsProperties.EditableText too
// Note also that we don't check the amount field value, as it's changed by validation mechanisms
// And check recreated form values too. Note also that we don't check the amount field value, as it's changed
// by validation mechanisms
// We use that the assertTextEquals searches in SemanticsProperties.EditableText too, although to be able to
// compare its editable value to an exact match we need to pass all its texts
composeTestRule.onNodeWithText(getStringResource(R.string.send_to)).also {
it.assertTextContains(ZecSendFixture.ADDRESS)
it.assertTextEquals(
getStringResource(R.string.send_to),
ZecSendFixture.ADDRESS,
includeEditableText = true
)
}
composeTestRule.onNodeWithText(getStringResource(R.string.send_memo)).also {
it.assertTextContains(ZecSendFixture.MEMO.value)
it.assertTextEquals(
getStringResource(R.string.send_memo),
ZecSendFixture.MEMO.value,
includeEditableText = true
)
}
}
}

View File

@ -31,7 +31,9 @@ class SendViewAndroidTest : UiTestPrerequisites() {
) = SendViewTestSetup(
composeTestRule,
sendStage,
zecSend
zecSend,
null,
true
).apply {
setDefaultContent()
}

View File

@ -1,6 +1,7 @@
package co.electriccoin.zcash.ui.screen.send.view
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
@ -16,6 +17,7 @@ import cash.z.ecc.sdk.fixture.ZecRequestFixture
import cash.z.ecc.sdk.fixture.ZecSendFixture
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.fixture.SendArgumentsWrapperFixture
import co.electriccoin.zcash.ui.screen.send.SendViewTestSetup
import co.electriccoin.zcash.ui.screen.send.assertOnConfirmation
import co.electriccoin.zcash.ui.screen.send.assertOnForm
@ -27,6 +29,8 @@ import co.electriccoin.zcash.ui.screen.send.assertSendEnabled
import co.electriccoin.zcash.ui.screen.send.clickBack
import co.electriccoin.zcash.ui.screen.send.clickConfirmation
import co.electriccoin.zcash.ui.screen.send.clickCreateAndSend
import co.electriccoin.zcash.ui.screen.send.clickScanner
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
import co.electriccoin.zcash.ui.screen.send.model.SendStage
import co.electriccoin.zcash.ui.screen.send.setAmount
import co.electriccoin.zcash.ui.screen.send.setMemo
@ -51,11 +55,15 @@ class SendViewTest : UiTestPrerequisites() {
private fun newTestSetup(
sendStage: SendStage = SendStage.Form,
zecSend: ZecSend? = null
zecSend: ZecSend? = null,
sendArgumentsWrapper: SendArgumentsWrapper? = null,
hasCameraFeature: Boolean = true
) = SendViewTestSetup(
composeTestRule,
sendStage,
zecSend
zecSend,
sendArgumentsWrapper,
hasCameraFeature
).apply {
setDefaultContent()
}
@ -395,4 +403,86 @@ class SendViewTest : UiTestPrerequisites() {
assertEquals(1, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun scanner_button_on_form_hit() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnScannerCount())
composeTestRule.clickScanner()
assertEquals(1, testSetup.getOnScannerCount())
}
@Test
@MediumTest
fun input_arguments_to_form() {
newTestSetup(
sendStage = SendStage.Form,
sendArgumentsWrapper = SendArgumentsWrapperFixture.new(
recipientAddress = SendArgumentsWrapperFixture.RECIPIENT_ADDRESS,
amount = SendArgumentsWrapperFixture.AMOUNT,
memo = SendArgumentsWrapperFixture.MEMO
),
zecSend = null
)
composeTestRule.assertOnForm()
// We use that the assertTextEquals searches in SemanticsProperties.EditableText too, although to be able to
// compare its editable value to an exact match we need to pass all its texts
composeTestRule.onNodeWithText(getStringResource(R.string.send_to)).also {
it.assertTextEquals(
getStringResource(R.string.send_to),
SendArgumentsWrapperFixture.RECIPIENT_ADDRESS,
includeEditableText = true
)
}
composeTestRule.onNodeWithText(getStringResource(R.string.send_amount)).also {
it.assertTextEquals(
getStringResource(R.string.send_amount),
SendArgumentsWrapperFixture.amountToFixtureZecString(SendArgumentsWrapperFixture.AMOUNT)!!,
includeEditableText = true
)
}
composeTestRule.onNodeWithText(getStringResource(R.string.send_memo)).also {
it.assertTextEquals(
getStringResource(R.string.send_memo),
SendArgumentsWrapperFixture.MEMO,
includeEditableText = true
)
}
}
@Test
@MediumTest
fun device_has_camera_feature() {
newTestSetup(
sendStage = SendStage.Form,
hasCameraFeature = true
)
composeTestRule.assertOnForm()
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.send_scan_content_description)).also {
it.assertExists()
}
}
@Test
@MediumTest
fun device_has_not_camera_feature() {
newTestSetup(
sendStage = SendStage.Form,
hasCameraFeature = false
)
composeTestRule.assertOnForm()
composeTestRule.onNodeWithContentDescription(getStringResource(R.string.send_scan_content_description)).also {
it.assertDoesNotExist()
}
}
}

View File

@ -3,6 +3,8 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
<application
android:icon="@mipmap/ic_launcher_square"
android:roundIcon="@mipmap/ic_launcher_round"

View File

@ -7,6 +7,10 @@ import androidx.navigation.NavOptionsBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.NavigationArguments.SEND_AMOUNT
import co.electriccoin.zcash.ui.NavigationArguments.SEND_MEMO
import co.electriccoin.zcash.ui.NavigationArguments.SEND_RECIPIENT_ADDRESS
import co.electriccoin.zcash.ui.NavigationTargets.ABOUT
import co.electriccoin.zcash.ui.NavigationTargets.HOME
import co.electriccoin.zcash.ui.NavigationTargets.RECEIVE
@ -27,6 +31,7 @@ import co.electriccoin.zcash.ui.screen.request.WrapRequest
import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator
import co.electriccoin.zcash.ui.screen.seed.WrapSeed
import co.electriccoin.zcash.ui.screen.send.WrapSend
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
import co.electriccoin.zcash.ui.screen.settings.WrapSettings
import co.electriccoin.zcash.ui.screen.support.WrapSupport
import co.electriccoin.zcash.ui.screen.update.WrapCheckForUpdate
@ -85,8 +90,22 @@ internal fun MainActivity.Navigation() {
composable(REQUEST) {
WrapRequest(goBack = { navController.popBackStackJustOnce(REQUEST) })
}
composable(SEND) {
WrapSend(goBack = { navController.popBackStackJustOnce(SEND) })
composable(SEND) { backStackEntry ->
WrapSend(
goToQrScanner = {
Twig.debug { "Opening Qr Scanner Screen" }
navController.navigateJustOnce(SCAN)
},
goBack = { navController.popBackStackJustOnce(SEND) },
sendArgumentsWrapper = SendArgumentsWrapper(
recipientAddress = backStackEntry.savedStateHandle[SEND_RECIPIENT_ADDRESS],
amount = backStackEntry.savedStateHandle[SEND_AMOUNT],
memo = backStackEntry.savedStateHandle[SEND_MEMO]
)
)
backStackEntry.savedStateHandle.remove<String>(SEND_RECIPIENT_ADDRESS)
backStackEntry.savedStateHandle.remove<String>(SEND_AMOUNT)
backStackEntry.savedStateHandle.remove<String>(SEND_MEMO)
}
composable(SUPPORT) {
// Pop back stack won't be right if we deep link into support
@ -97,11 +116,14 @@ internal fun MainActivity.Navigation() {
}
composable(SCAN) {
WrapScanValidator(
onScanValid = {
// TODO [#449] https://github.com/zcash/secant-android-wallet/issues/449
navController.navigateJustOnce(SEND) {
popUpTo(HOME) { inclusive = false }
onScanValid = { result ->
// At this point we only pass recipient address
navController.previousBackStackEntry?.savedStateHandle?.apply {
set(SEND_RECIPIENT_ADDRESS, result)
set(SEND_AMOUNT, null)
set(SEND_MEMO, null)
}
navController.popBackStackJustOnce(SCAN)
},
goBack = { navController.popBackStackJustOnce(SCAN) }
)
@ -137,6 +159,12 @@ private fun NavHostController.popBackStackJustOnce(currentRouteToBePopped: Strin
popBackStack()
}
object NavigationArguments {
const val SEND_RECIPIENT_ADDRESS = "send_recipient_address"
const val SEND_AMOUNT = "send_amount"
const val SEND_MEMO = "send_memo"
}
object NavigationTargets {
const val HOME = "home"

View File

@ -7,7 +7,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
@ -20,7 +19,7 @@ internal fun MainActivity.WrapScanValidator(
onScanValid: (address: String) -> Unit,
goBack: () -> Unit
) {
WrapScanValidator(
WrapScan(
this,
onScanValid = onScanValid,
goBack = goBack
@ -28,7 +27,7 @@ internal fun MainActivity.WrapScanValidator(
}
@Composable
private fun WrapScanValidator(
fun WrapScan(
activity: ComponentActivity,
onScanValid: (address: String) -> Unit,
goBack: () -> Unit
@ -37,50 +36,41 @@ private fun WrapScanValidator(
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
if (synchronizer == null) {
// Display loading indicator
} else {
WrapScan(
activity,
onScanned = { result ->
activity.lifecycleScope.launch {
val isAddressValid = !synchronizer.validateAddress(result).isNotValid
if (isAddressValid) {
onScanValid(result)
}
}
},
goBack = goBack
)
}
}
@Composable
fun WrapScan(
activity: ComponentActivity,
onScanned: (result: String) -> Unit,
goBack: () -> Unit
) {
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Scan(
snackbarHostState,
onBack = goBack,
onScanned = onScanned,
onOpenSettings = {
runCatching {
activity.startActivity(SettingsUtil.newSettingsIntent(activity.packageName))
}.onFailure {
// This case should not really happen, as the Settings app should be available on every
// Android device, but we need to handle it somehow.
if (synchronizer == null) {
// Display loading indicator
} else {
Scan(
snackbarHostState = snackbarHostState,
onBack = goBack,
onScanned = { result ->
scope.launch {
snackbarHostState.showSnackbar(
message = activity.getString(R.string.scan_settings_open_failed)
)
val isAddressValid = !synchronizer.validateAddress(result).isNotValid
if (isAddressValid) {
onScanValid(result)
} else {
snackbarHostState.showSnackbar(
message = activity.getString(R.string.scan_validation_invalid_address)
)
}
}
}
},
onScanStateChanged = {}
)
},
onOpenSettings = {
runCatching {
activity.startActivity(SettingsUtil.newSettingsIntent(activity.packageName))
}.onFailure {
// This case should not really happen, as the Settings app should be available on every
// Android device, but we need to handle it somehow.
scope.launch {
snackbarHostState.showSnackbar(
message = activity.getString(R.string.scan_settings_open_failed)
)
}
}
},
onScanStateChanged = {}
)
}
}

View File

@ -2,6 +2,7 @@
package co.electriccoin.zcash.ui.screen.send
import android.content.pm.PackageManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
@ -23,22 +24,29 @@ import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.screen.home.model.spendableBalance
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.send.ext.Saver
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
import co.electriccoin.zcash.ui.screen.send.model.SendStage
import co.electriccoin.zcash.ui.screen.send.view.Send
import kotlinx.coroutines.launch
@Composable
internal fun MainActivity.WrapSend(
sendArgumentsWrapper: SendArgumentsWrapper?,
goToQrScanner: () -> Unit,
goBack: () -> Unit
) {
WrapSend(this, goBack)
WrapSend(this, sendArgumentsWrapper, goToQrScanner, goBack)
}
@Composable
private fun WrapSend(
activity: ComponentActivity,
sendArgumentsWrapper: SendArgumentsWrapper?,
goToQrScanner: () -> Unit,
goBack: () -> Unit
) {
val hasCameraFeature = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
val walletViewModel by activity.viewModels<WalletViewModel>()
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
@ -47,16 +55,20 @@ private fun WrapSend(
val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value
WrapSend(synchronizer, spendableBalance, spendingKey, goBack)
WrapSend(sendArgumentsWrapper, synchronizer, spendableBalance, spendingKey, goToQrScanner, goBack, hasCameraFeature)
}
@Suppress("LongParameterList")
@VisibleForTesting
@Composable
internal fun WrapSend(
sendArgumentsWrapper: SendArgumentsWrapper?,
synchronizer: Synchronizer?,
spendableBalance: Zatoshi?,
spendingKey: UnifiedSpendingKey?,
goBack: () -> Unit
goToQrScanner: () -> Unit,
goBack: () -> Unit,
hasCameraFeature: Boolean
) {
val scope = rememberCoroutineScope()
@ -88,6 +100,7 @@ internal fun WrapSend(
} else {
Send(
mySpendableBalance = spendableBalance,
sendArgumentsWrapper = sendArgumentsWrapper,
sendStage = sendStage,
onSendStageChange = setSendStage,
zecSend = zecSend,
@ -111,7 +124,9 @@ internal fun WrapSend(
// All other states of Pending transaction mean waiting for one of the states above
}
}
}
},
onQrScannerOpen = goToQrScanner,
hasCameraFeature = hasCameraFeature
)
}
}

View File

@ -0,0 +1,7 @@
package co.electriccoin.zcash.ui.screen.send.model
data class SendArgumentsWrapper(
val recipientAddress: String? = null,
val amount: String? = null,
val memo: String? = null
)

View File

@ -12,6 +12,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.outlined.QrCodeScanner
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -50,6 +51,7 @@ import co.electriccoin.zcash.ui.design.theme.ZcashTheme.dimens
import co.electriccoin.zcash.ui.screen.send.ext.ABBREVIATION_INDEX
import co.electriccoin.zcash.ui.screen.send.ext.abbreviated
import co.electriccoin.zcash.ui.screen.send.ext.valueOrEmptyChar
import co.electriccoin.zcash.ui.screen.send.model.SendArgumentsWrapper
import co.electriccoin.zcash.ui.screen.send.model.SendStage
@Composable
@ -59,12 +61,15 @@ fun PreviewSend() {
GradientSurface {
Send(
mySpendableBalance = ZatoshiFixture.new(),
sendArgumentsWrapper = null,
sendStage = SendStage.Form,
onSendStageChange = {},
zecSend = null,
onZecSendChange = {},
onCreateAndSend = {},
onBack = {}
onQrScannerOpen = {},
onBack = {},
hasCameraFeature = true
)
}
}
@ -75,12 +80,15 @@ fun PreviewSend() {
@Composable
fun Send(
mySpendableBalance: Zatoshi,
sendArgumentsWrapper: SendArgumentsWrapper?,
sendStage: SendStage,
onSendStageChange: (SendStage) -> Unit,
zecSend: ZecSend?,
onZecSendChange: (ZecSend) -> Unit,
onBack: () -> Unit,
onCreateAndSend: (ZecSend) -> Unit
onCreateAndSend: (ZecSend) -> Unit,
onQrScannerOpen: () -> Unit,
hasCameraFeature: Boolean
) {
Scaffold(topBar = {
SendTopAppBar(
@ -90,12 +98,15 @@ fun Send(
}) { paddingValues ->
SendMainContent(
myBalance = mySpendableBalance,
sendArgumentsWrapper = sendArgumentsWrapper,
onBack = onBack,
sendStage = sendStage,
onSendStageChange = onSendStageChange,
zecSend = zecSend,
onZecSendChange = onZecSendChange,
onSendSubmit = onCreateAndSend,
onQrScannerOpen = onQrScannerOpen,
hasCameraFeature = hasCameraFeature,
modifier = Modifier
.padding(
top = paddingValues.calculateTopPadding() + dimens.spacingDefault,
@ -136,23 +147,29 @@ private fun SendTopAppBar(
@Composable
private fun SendMainContent(
myBalance: Zatoshi,
sendArgumentsWrapper: SendArgumentsWrapper?,
zecSend: ZecSend?,
onZecSendChange: (ZecSend) -> Unit,
onBack: () -> Unit,
sendStage: SendStage,
onSendStageChange: (SendStage) -> Unit,
onSendSubmit: (ZecSend) -> Unit,
onQrScannerOpen: () -> Unit,
hasCameraFeature: Boolean,
modifier: Modifier = Modifier
) {
when {
(sendStage == SendStage.Form || null == zecSend) -> {
SendForm(
myBalance = myBalance,
sendArgumentsWrapper = sendArgumentsWrapper,
previousZecSend = zecSend,
onCreateZecSend = {
onSendStageChange(SendStage.Confirmation)
onZecSendChange(it)
},
onQrScannerOpen = onQrScannerOpen,
hasCameraFeature = hasCameraFeature,
modifier = modifier
)
}
@ -192,12 +209,15 @@ private fun SendMainContent(
// TODO [#217]: Need to handle changing of Locale after user input, but before submitting the button.
// TODO [#288]: TextField component can't do long-press backspace.
// TODO [#294]: DetektAll failed LongMethod
@Suppress("LongMethod")
@Suppress("LongMethod", "LongParameterList")
@Composable
private fun SendForm(
myBalance: Zatoshi,
sendArgumentsWrapper: SendArgumentsWrapper?,
previousZecSend: ZecSend?,
onCreateZecSend: (ZecSend) -> Unit,
onQrScannerOpen: () -> Unit,
hasCameraFeature: Boolean,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
@ -218,6 +238,16 @@ private fun SendForm(
mutableStateOf<Set<ZecSendExt.ZecSendValidation.Invalid.ValidationError>>(emptySet())
}
if (sendArgumentsWrapper?.recipientAddress != null) {
recipientAddressString = sendArgumentsWrapper.recipientAddress
}
if (sendArgumentsWrapper?.amount != null) {
amountZecString = sendArgumentsWrapper.amount
}
if (sendArgumentsWrapper?.memo != null) {
memoString = sendArgumentsWrapper.memo
}
Column(
modifier
.fillMaxHeight()
@ -236,6 +266,26 @@ private fun SendForm(
Spacer(modifier = Modifier.height(dimens.spacingLarge))
FormTextField(
value = recipientAddressString,
onValueChange = { recipientAddressString = it },
label = { Text(stringResource(id = R.string.send_to)) },
modifier = Modifier.fillMaxWidth(),
trailingIcon = if (hasCameraFeature) { {
IconButton(
onClick = onQrScannerOpen,
content = {
Icon(
imageVector = Icons.Outlined.QrCodeScanner,
contentDescription = stringResource(R.string.send_scan_content_description)
)
}
)
} } else { null }
)
Spacer(Modifier.size(dimens.spacingSmall))
FormTextField(
value = amountZecString,
onValueChange = { newValue ->
@ -251,15 +301,6 @@ private fun SendForm(
Spacer(Modifier.size(dimens.spacingSmall))
FormTextField(
value = recipientAddressString,
onValueChange = { recipientAddressString = it },
label = { Text(stringResource(id = R.string.send_to)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.size(dimens.spacingSmall))
// TODO [#810]: Disable Memo UI field in case of Transparent address
// TODO [#810]: https://github.com/zcash/secant-android-wallet/issues/810
FormTextField(

View File

@ -8,7 +8,6 @@
<string name="home_menu_about">About</string>
<string name="home_menu_support">Contact support</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>

View File

@ -4,7 +4,7 @@
<string name="scan_back_content_description">Back</string>
<string name="scan_preview_content_description">Camera</string>
<string name="scan_hint">We will validate any Zcash URI and take you to the appropriate action.</string>
<string name="scan_hint">You can scan any Zcash QR code.</string>
<string name="scan_settings_button">Enable camera permission</string>
<string name="scan_settings_open_failed">Unable to launch Settings app.</string>
@ -15,4 +15,6 @@
<string name="scan_state_permission">Permission for camera is necessary.</string>
<string name="scan_state_scanning">Scanning…</string>
<string name="scan_state_failed">Scanning failed.</string>
<string name="scan_validation_invalid_address">Invalid Zcash address scanned.</string>
</resources>