[#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:
parent
136e56ea9a
commit
5296dc5ea9
|
@ -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
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,9 @@ class SendViewAndroidTest : UiTestPrerequisites() {
|
|||
) = SendViewTestSetup(
|
||||
composeTestRule,
|
||||
sendStage,
|
||||
zecSend
|
||||
zecSend,
|
||||
null,
|
||||
true
|
||||
).apply {
|
||||
setDefaultContent()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue