implementation of push and hold to send zec (#641)
* [#249] Implement Press-And-Hold To Confirm Sending ZEC * Remove default value for interaction source * Rename interaction source argument * removed dependency of ui-design-lib on sdkExtLib Co-authored-by: Carter Jernigan <git@carterjernigan.com>
This commit is contained in:
parent
6c6339fb41
commit
9150f80b9d
|
@ -6,8 +6,6 @@
|
|||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
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.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Button
|
||||
|
@ -9,10 +12,20 @@ import androidx.compose.material3.ButtonDefaults.buttonColors
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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
|
||||
@Composable
|
||||
|
@ -145,3 +158,41 @@ fun DangerousButton(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun TimedButton(
|
||||
modifier: Modifier = Modifier,
|
||||
duration: Duration = 5.seconds,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
onClick: () -> Unit,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
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.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
|
@ -10,6 +13,7 @@ import androidx.compose.ui.test.performClick
|
|||
import androidx.compose.ui.test.performTextClearance
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.sdk.ext.collectWith
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.ext.ui.model.MonetarySeparators
|
||||
import cash.z.ecc.sdk.fixture.MemoFixture
|
||||
|
@ -22,7 +26,13 @@ import co.electriccoin.zcash.test.UiTestPrerequisites
|
|||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
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.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
@ -60,15 +70,24 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
composeTestRule.setValidAddress()
|
||||
composeTestRule.clickCreateAndSend()
|
||||
composeTestRule.assertOnConfirmation()
|
||||
composeTestRule.clickConfirmation()
|
||||
clickConfirmation(testSetup.interactionSource)
|
||||
|
||||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
launch {
|
||||
testSetup.mutableActionExecuted.collectWith(this) {
|
||||
if (!it) return@collectWith
|
||||
|
||||
testSetup.getLastSend().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.destination)
|
||||
assertEquals(Zatoshi(12345600000), it.amount)
|
||||
assertTrue(it.memo.value.isEmpty())
|
||||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
|
||||
launch {
|
||||
testSetup.getLastSend().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.destination)
|
||||
assertEquals(Zatoshi(12345678900000), it.amount)
|
||||
assertEquals(ZecRequestFixture.MESSAGE.value, it.memo.value)
|
||||
}
|
||||
}
|
||||
this.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,15 +106,24 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
|
||||
composeTestRule.clickCreateAndSend()
|
||||
composeTestRule.assertOnConfirmation()
|
||||
composeTestRule.clickConfirmation()
|
||||
clickConfirmation(testSetup.interactionSource)
|
||||
|
||||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
launch {
|
||||
testSetup.mutableActionExecuted.collectWith(this) {
|
||||
if (!it) return@collectWith
|
||||
|
||||
testSetup.getLastSend().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.destination)
|
||||
assertEquals(Zatoshi(12345600000), it.amount)
|
||||
assertEquals(ZecRequestFixture.MESSAGE.value, it.memo.value)
|
||||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
|
||||
launch {
|
||||
testSetup.getLastSend().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.destination)
|
||||
assertEquals(Zatoshi(12345678900000), it.amount)
|
||||
assertEquals(ZecRequestFixture.MESSAGE.value, it.memo.value)
|
||||
}
|
||||
}
|
||||
this.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,15 +164,24 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
|
||||
composeTestRule.clickCreateAndSend()
|
||||
composeTestRule.assertOnConfirmation()
|
||||
composeTestRule.clickConfirmation()
|
||||
clickConfirmation(testSetup.interactionSource)
|
||||
|
||||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
launch {
|
||||
testSetup.mutableActionExecuted.collectWith(this) {
|
||||
if (!it) return@collectWith
|
||||
|
||||
testSetup.getLastSend().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.destination)
|
||||
assertEquals(Zatoshi(12345678900000), it.amount)
|
||||
assertEquals(ZecRequestFixture.MESSAGE.value, it.memo.value)
|
||||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
|
||||
launch {
|
||||
testSetup.getLastSend().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.destination)
|
||||
assertEquals(Zatoshi(12345678900000), it.amount)
|
||||
assertEquals(ZecRequestFixture.MESSAGE.value, it.memo.value)
|
||||
}
|
||||
}
|
||||
this.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -205,15 +242,24 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
|
||||
composeTestRule.clickCreateAndSend()
|
||||
composeTestRule.assertOnConfirmation()
|
||||
composeTestRule.clickConfirmation()
|
||||
clickConfirmation(testSetup.interactionSource)
|
||||
|
||||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
launch {
|
||||
testSetup.mutableActionExecuted.collectWith(this) {
|
||||
if (!it) return@collectWith
|
||||
|
||||
testSetup.getLastSend().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.destination)
|
||||
assertEquals(Zatoshi(12345600000), it.amount)
|
||||
assertTrue(it.memo.value.isEmpty())
|
||||
assertEquals(1, testSetup.getOnCreateCount())
|
||||
|
||||
launch {
|
||||
testSetup.getLastSend().also {
|
||||
assertNotNull(it)
|
||||
assertEquals(WalletAddressFixture.unified(), it.destination)
|
||||
assertEquals(Zatoshi(12345600000), it.amount)
|
||||
assertTrue(it.memo.value.isEmpty())
|
||||
}
|
||||
}
|
||||
this.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -250,6 +296,8 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
|
||||
private val onBackCount = AtomicInteger(0)
|
||||
private val onCreateCount = AtomicInteger(0)
|
||||
val interactionSource = MutableInteractionSource()
|
||||
val mutableActionExecuted = MutableStateFlow(false)
|
||||
|
||||
@Volatile
|
||||
private var onSendZecRequest: ZecSend? = null
|
||||
|
@ -274,12 +322,14 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
ZcashTheme {
|
||||
Send(
|
||||
mySpendableBalance = ZatoshiFixture.new(),
|
||||
pressAndHoldInteractionSource = interactionSource,
|
||||
goBack = {
|
||||
onBackCount.incrementAndGet()
|
||||
},
|
||||
onCreateAndSend = {
|
||||
onCreateCount.incrementAndGet()
|
||||
onSendZecRequest = it
|
||||
mutableActionExecuted.update { true }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -336,9 +386,10 @@ private fun ComposeContentTestRule.clickCreateAndSend() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun ComposeContentTestRule.clickConfirmation() {
|
||||
onNodeWithText(getStringResource(R.string.send_confirm)).also {
|
||||
it.performClick()
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun TestScope.clickConfirmation(interactionSource: MutableInteractionSource) {
|
||||
launch(Dispatchers.Main) {
|
||||
interactionSource.emit(PressInteraction.Press(Offset.Zero))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
@ -11,7 +12,6 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
|
@ -22,6 +22,7 @@ import androidx.compose.material3.TopAppBar
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -43,6 +44,7 @@ import co.electriccoin.zcash.ui.R
|
|||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||
import co.electriccoin.zcash.ui.design.component.GradientSurface
|
||||
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.screen.send.ext.ABBREVIATION_INDEX
|
||||
import co.electriccoin.zcash.ui.screen.send.ext.Saver
|
||||
|
@ -63,10 +65,14 @@ fun PreviewSend() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pressAndHoldInteractionSource This is an argument that can be injected for automated testing.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Send(
|
||||
mySpendableBalance: Zatoshi,
|
||||
pressAndHoldInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
goBack: () -> Unit,
|
||||
onCreateAndSend: (ZecSend) -> Unit
|
||||
) {
|
||||
|
@ -89,6 +95,7 @@ fun Send(
|
|||
paddingValues,
|
||||
mySpendableBalance,
|
||||
sendStage,
|
||||
pressAndHoldInteractionSource,
|
||||
setSendStage,
|
||||
onCreateAndSend = onCreateAndSend
|
||||
)
|
||||
|
@ -113,11 +120,13 @@ private fun SendTopAppBar(onBack: () -> Unit) {
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun SendMainContent(
|
||||
paddingValues: PaddingValues,
|
||||
myBalance: Zatoshi,
|
||||
sendStage: SendStage,
|
||||
pressAndHoldInteractionSource: MutableInteractionSource,
|
||||
setSendStage: (SendStage) -> Unit,
|
||||
onCreateAndSend: (ZecSend) -> Unit
|
||||
) {
|
||||
|
@ -135,7 +144,8 @@ private fun SendMainContent(
|
|||
} else {
|
||||
Confirmation(
|
||||
paddingValues,
|
||||
zecSend
|
||||
zecSend,
|
||||
pressAndHoldInteractionSource
|
||||
) {
|
||||
onCreateAndSend(zecSend)
|
||||
}
|
||||
|
@ -245,6 +255,7 @@ private fun SendForm(
|
|||
private fun Confirmation(
|
||||
paddingValues: PaddingValues,
|
||||
zecSend: ZecSend,
|
||||
pressAndHoldInteractionSource: MutableInteractionSource,
|
||||
onConfirmation: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
|
@ -259,8 +270,10 @@ private fun Confirmation(
|
|||
)
|
||||
)
|
||||
|
||||
// TODO [#249]: Implement press-and-hold
|
||||
Button(onClick = onConfirmation) {
|
||||
TimedButton(
|
||||
onClick = onConfirmation,
|
||||
interactionSource = pressAndHoldInteractionSource
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.send_confirm))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue