diff --git a/docs/testing/manual_testing/QR Scan.md b/docs/testing/manual_testing/QR Scan.md index 6eeeb706..0be524a9 100644 --- a/docs/testing/manual_testing/QR Scan.md +++ b/docs/testing/manual_testing/QR Scan.md @@ -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 \ No newline at end of file diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/TextField.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/TextField.kt index 243b183d..de9d669e 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/TextField.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/TextField.kt @@ -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 ) } diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/fixture/SendArgumentsWrapperFixture.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/fixture/SendArgumentsWrapperFixture.kt new file mode 100644 index 00000000..f489e150 --- /dev/null +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/fixture/SendArgumentsWrapperFixture.kt @@ -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 + ) +} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/ComposeContentTestRuleExt.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/ComposeContentTestRuleExt.kt index 3ff867c6..03fe3902 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/ComposeContentTestRuleExt.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/ComposeContentTestRuleExt.kt @@ -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() diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/SendViewTestSetup.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/SendViewTestSetup.kt index 89374da2..03026607 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/SendViewTestSetup.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/SendViewTestSetup.kt @@ -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 ) } } diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/integration/SendViewIntegrationTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/integration/SendViewIntegrationTest.kt index 6c3daaf9..129d1926 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/integration/SendViewIntegrationTest.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/integration/SendViewIntegrationTest.kt @@ -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 + ) } } } diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/view/SendViewAndroidTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/view/SendViewAndroidTest.kt index 6d27cd39..15c8e007 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/view/SendViewAndroidTest.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/view/SendViewAndroidTest.kt @@ -31,7 +31,9 @@ class SendViewAndroidTest : UiTestPrerequisites() { ) = SendViewTestSetup( composeTestRule, sendStage, - zecSend + zecSend, + null, + true ).apply { setDefaultContent() } diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/view/SendViewTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/view/SendViewTest.kt index 46b24f78..8a2d602b 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/view/SendViewTest.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/view/SendViewTest.kt @@ -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() + } + } } diff --git a/ui-lib/src/main/AndroidManifest.xml b/ui-lib/src/main/AndroidManifest.xml index 28885cf7..b2100c91 100644 --- a/ui-lib/src/main/AndroidManifest.xml +++ b/ui-lib/src/main/AndroidManifest.xml @@ -3,6 +3,8 @@ + + + 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(SEND_RECIPIENT_ADDRESS) + backStackEntry.savedStateHandle.remove(SEND_AMOUNT) + backStackEntry.savedStateHandle.remove(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" diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/AndroidScan.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/AndroidScan.kt index 16d8c8bd..2aed1c4b 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/AndroidScan.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/AndroidScan.kt @@ -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 = {} + ) + } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/AndroidSend.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/AndroidSend.kt index 410fb113..3092d4cc 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/AndroidSend.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/AndroidSend.kt @@ -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() 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 ) } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/model/SendArgumentsWrapper.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/model/SendArgumentsWrapper.kt new file mode 100644 index 00000000..2a1471a2 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/model/SendArgumentsWrapper.kt @@ -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 +) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/view/SendView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/view/SendView.kt index 51f4b674..6ca4c0fa 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/view/SendView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/view/SendView.kt @@ -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>(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( diff --git a/ui-lib/src/main/res/ui/home/values/strings.xml b/ui-lib/src/main/res/ui/home/values/strings.xml index c2088987..5668a79a 100644 --- a/ui-lib/src/main/res/ui/home/values/strings.xml +++ b/ui-lib/src/main/res/ui/home/values/strings.xml @@ -8,7 +8,6 @@ About Contact support - Syncing - %1$d%% Syncing %1$s so far diff --git a/ui-lib/src/main/res/ui/scan/values/strings.xml b/ui-lib/src/main/res/ui/scan/values/strings.xml index 74fa91cd..0733b45b 100644 --- a/ui-lib/src/main/res/ui/scan/values/strings.xml +++ b/ui-lib/src/main/res/ui/scan/values/strings.xml @@ -4,7 +4,7 @@ Back Camera - We will validate any Zcash URI and take you to the appropriate action. + You can scan any Zcash QR code. Enable camera permission Unable to launch Settings app. @@ -15,4 +15,6 @@ Permission for camera is necessary. Scanning… Scanning failed. + + Invalid Zcash address scanned.