[#785] Remove press-and-hold from Send

* [#785] Remove press-and-hold for send confirmation

- Timed button replaced by standard behaviour button
- Related SendView test updated

* Remove unused API

If we need this back, we can restore it from the Git history.

* Remove unused imports

---------

Co-authored-by: Carter Jernigan <git@carterjernigan.com>
This commit is contained in:
Honza Rychnovsky 2023-03-20 10:17:22 +01:00 committed by GitHub
parent d37310a935
commit 0c0bf8cb34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 11 additions and 177 deletions

View File

@ -1,93 +0,0 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.material3.Text
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.testTimeSource
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.ExperimentalTime
class ButtonTest {
@get:Rule
val composeTestRule = createComposeRule()
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
@Test
@MediumTest
fun timedButtonTest(): Unit = runTest {
val testDispatcher = StandardTestDispatcher(testScheduler)
val testSetup = newTestSetup(testDispatcher, 2.seconds)
val mark = testTimeSource.markNow()
launch(Dispatchers.Main) {
testSetup.interactionSource.emit(PressInteraction.Press(Offset.Zero))
advanceTimeBy(3.seconds.inWholeMilliseconds)
}
launch {
testSetup.mutableActionExecuted.collect {
if (!it) return@collect
assertTrue { mark.elapsedNow() >= 2.seconds }
this.cancel()
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
@MediumTest
fun buttonClickTest() = runTest {
val testDispatcher = StandardTestDispatcher(testScheduler)
val testSetup = newTestSetup(testDispatcher, 2.seconds)
composeTestRule.onNodeWithText("button").also {
it.performClick()
}
advanceTimeBy(3.seconds.inWholeMilliseconds)
assertFalse { testSetup.mutableActionExecuted.value }
}
private fun newTestSetup(testDispatcher: CoroutineDispatcher, duration: Duration) = TestSetup(testDispatcher, composeTestRule, duration)
private class TestSetup(coroutineDispatcher: CoroutineDispatcher, composeTestRule: ComposeContentTestRule, duration: Duration) {
val mutableActionExecuted = MutableStateFlow(false)
val interactionSource = MutableInteractionSource()
init {
composeTestRule.setContent {
ZcashTheme {
TimedButton(
duration = duration,
onClick = { mutableActionExecuted.update { true } },
coroutineDispatcher = coroutineDispatcher,
content = { Text(text = "button") },
interactionSource = interactionSource
)
}
}
}
}
}

View File

@ -1,9 +1,6 @@
package co.electriccoin.zcash.ui.design.component package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button import androidx.compose.material3.Button
@ -12,20 +9,10 @@ import androidx.compose.material3.ButtonDefaults.buttonColors
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@Preview @Preview
@Composable @Composable
@ -158,41 +145,3 @@ fun DangerousButton(
) )
} }
} }
@Suppress("LongParameterList")
@Composable
fun TimedButton(
onClick: () -> Unit,
content: @Composable (RowScope.() -> Unit),
modifier: Modifier = Modifier,
duration: Duration = 5.seconds,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
LaunchedEffect(interactionSource) {
var action: Job? = null
interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> {
action = launch(coroutineDispatcher) {
delay(duration)
withContext(Dispatchers.Main) {
onClick()
}
}
}
is PressInteraction.Release -> {
action?.cancel()
}
}
}
}
Button(
modifier = modifier,
onClick = {},
interactionSource = interactionSource,
content = content
)
}

View File

@ -1,8 +1,5 @@
package co.electriccoin.zcash.ui.screen.send.view package co.electriccoin.zcash.ui.screen.send.view
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule
@ -26,13 +23,11 @@ import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.test.getStringResource import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -70,7 +65,7 @@ class SendViewTest : UiTestPrerequisites() {
composeTestRule.setValidAddress() composeTestRule.setValidAddress()
composeTestRule.clickCreateAndSend() composeTestRule.clickCreateAndSend()
composeTestRule.assertOnConfirmation() composeTestRule.assertOnConfirmation()
clickConfirmation(testSetup.interactionSource) composeTestRule.clickConfirmation()
launch { launch {
testSetup.mutableActionExecuted.collectWith(this) { testSetup.mutableActionExecuted.collectWith(this) {
@ -106,7 +101,7 @@ class SendViewTest : UiTestPrerequisites() {
composeTestRule.clickCreateAndSend() composeTestRule.clickCreateAndSend()
composeTestRule.assertOnConfirmation() composeTestRule.assertOnConfirmation()
clickConfirmation(testSetup.interactionSource) composeTestRule.clickConfirmation()
launch { launch {
testSetup.mutableActionExecuted.collectWith(this) { testSetup.mutableActionExecuted.collectWith(this) {
@ -164,7 +159,7 @@ class SendViewTest : UiTestPrerequisites() {
composeTestRule.clickCreateAndSend() composeTestRule.clickCreateAndSend()
composeTestRule.assertOnConfirmation() composeTestRule.assertOnConfirmation()
clickConfirmation(testSetup.interactionSource) composeTestRule.clickConfirmation()
launch { launch {
testSetup.mutableActionExecuted.collectWith(this) { testSetup.mutableActionExecuted.collectWith(this) {
@ -242,7 +237,7 @@ class SendViewTest : UiTestPrerequisites() {
composeTestRule.clickCreateAndSend() composeTestRule.clickCreateAndSend()
composeTestRule.assertOnConfirmation() composeTestRule.assertOnConfirmation()
clickConfirmation(testSetup.interactionSource) composeTestRule.clickConfirmation()
launch { launch {
testSetup.mutableActionExecuted.collectWith(this) { testSetup.mutableActionExecuted.collectWith(this) {
@ -296,7 +291,6 @@ class SendViewTest : UiTestPrerequisites() {
private val onBackCount = AtomicInteger(0) private val onBackCount = AtomicInteger(0)
private val onCreateCount = AtomicInteger(0) private val onCreateCount = AtomicInteger(0)
val interactionSource = MutableInteractionSource()
val mutableActionExecuted = MutableStateFlow(false) val mutableActionExecuted = MutableStateFlow(false)
@Volatile @Volatile
@ -322,7 +316,6 @@ class SendViewTest : UiTestPrerequisites() {
ZcashTheme { ZcashTheme {
Send( Send(
mySpendableBalance = ZatoshiFixture.new(), mySpendableBalance = ZatoshiFixture.new(),
pressAndHoldInteractionSource = interactionSource,
goBack = { goBack = {
onBackCount.incrementAndGet() onBackCount.incrementAndGet()
}, },
@ -386,10 +379,9 @@ private fun ComposeContentTestRule.clickCreateAndSend() {
} }
} }
@OptIn(ExperimentalCoroutinesApi::class) private fun ComposeContentTestRule.clickConfirmation() {
private fun TestScope.clickConfirmation(interactionSource: MutableInteractionSource) { onNodeWithText(getStringResource(R.string.send_confirm)).also {
launch(Dispatchers.Main) { it.performClick()
interactionSource.emit(PressInteraction.Press(Offset.Zero))
} }
} }

View File

@ -1,6 +1,5 @@
package co.electriccoin.zcash.ui.screen.send.view package co.electriccoin.zcash.ui.screen.send.view
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@ -22,7 +21,6 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -47,7 +45,6 @@ import co.electriccoin.zcash.ui.design.component.FormTextField
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.Header import co.electriccoin.zcash.ui.design.component.Header
import co.electriccoin.zcash.ui.design.component.PrimaryButton import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.TimedButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.ZcashTheme.dimens 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.ABBREVIATION_INDEX
@ -69,16 +66,12 @@ fun PreviewSend() {
} }
} }
/**
* @param pressAndHoldInteractionSource This is an argument that can be injected for automated testing.
*/
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun Send( fun Send(
mySpendableBalance: Zatoshi, mySpendableBalance: Zatoshi,
goBack: () -> Unit, goBack: () -> Unit,
onCreateAndSend: (ZecSend) -> Unit, onCreateAndSend: (ZecSend) -> Unit
pressAndHoldInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) { ) {
// For now, we're avoiding sub-navigation to keep the navigation logic simple. But this might // For now, we're avoiding sub-navigation to keep the navigation logic simple. But this might
// change once deep-linking support is added. It depends on whether deep linking should do one of: // change once deep-linking support is added. It depends on whether deep linking should do one of:
@ -98,7 +91,6 @@ fun Send(
SendMainContent( SendMainContent(
myBalance = mySpendableBalance, myBalance = mySpendableBalance,
sendStage = sendStage, sendStage = sendStage,
pressAndHoldInteractionSource = pressAndHoldInteractionSource,
setSendStage = setSendStage, setSendStage = setSendStage,
onCreateAndSend = onCreateAndSend, onCreateAndSend = onCreateAndSend,
modifier = Modifier modifier = Modifier
@ -138,7 +130,6 @@ private fun SendTopAppBar(onBack: () -> Unit) {
private fun SendMainContent( private fun SendMainContent(
myBalance: Zatoshi, myBalance: Zatoshi,
sendStage: SendStage, sendStage: SendStage,
pressAndHoldInteractionSource: MutableInteractionSource,
setSendStage: (SendStage) -> Unit, setSendStage: (SendStage) -> Unit,
onCreateAndSend: (ZecSend) -> Unit, onCreateAndSend: (ZecSend) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@ -158,7 +149,6 @@ private fun SendMainContent(
} else { } else {
Confirmation( Confirmation(
zecSend = zecSend, zecSend = zecSend,
pressAndHoldInteractionSource = pressAndHoldInteractionSource,
onConfirmation = { onConfirmation = {
onCreateAndSend(zecSend) onCreateAndSend(zecSend)
}, },
@ -293,7 +283,6 @@ private fun SendForm(
@Composable @Composable
private fun Confirmation( private fun Confirmation(
zecSend: ZecSend, zecSend: ZecSend,
pressAndHoldInteractionSource: MutableInteractionSource,
onConfirmation: () -> Unit, onConfirmation: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@ -306,12 +295,9 @@ private fun Confirmation(
) )
) )
TimedButton( PrimaryButton(
onClick = onConfirmation, onClick = onConfirmation,
{ text = stringResource(id = R.string.send_confirm)
Text(text = stringResource(id = R.string.send_confirm))
},
interactionSource = pressAndHoldInteractionSource
) )
} }
} }

View File

@ -12,6 +12,6 @@
<string name="send_amount_and_address_format" formatted="true">Send <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to <xliff:g id="address" example="zs1g7cqw … mvyzgm">%2$s</xliff:g>?</string> <string name="send_amount_and_address_format" formatted="true">Send <xliff:g id="amount" example="12.345">%1$s</xliff:g> ZEC to <xliff:g id="address" example="zs1g7cqw … mvyzgm">%2$s</xliff:g>?</string>
<string name="send_abbreviated_address_format" formatted="true"><xliff:g id="first_five" example="zs1g7">%1$s</xliff:g><xliff:g id="last_five" example="mvyzg">%2$s</xliff:g></string> <string name="send_abbreviated_address_format" formatted="true"><xliff:g id="first_five" example="zs1g7">%1$s</xliff:g><xliff:g id="last_five" example="mvyzg">%2$s</xliff:g></string>
<string name="send_confirm">Press and hold to send ZEC</string> <string name="send_confirm">Press to send ZEC</string>
</resources> </resources>