[#905] Add copy-to-clipboard for addresses

* [#905] Add copy-to-clipboard for addresses

* Unify copy-to-clipboard across screens and APIs

* Fix ripple effect on address item
This commit is contained in:
Honza Rychnovský 2023-07-17 12:33:14 +02:00 committed by GitHub
parent 8931cf0d4a
commit 6b5359119c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 162 additions and 52 deletions

View File

@ -0,0 +1,12 @@
@file:Suppress("ktlint:filename")
package co.electriccoin.zcash.spackle
import android.content.ClipData
import android.content.ClipboardManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend fun ClipboardManager.setPrimaryClipSuspend(data: ClipData) = withContext(Dispatchers.IO) {
setPrimaryClip(data)
}

View File

@ -0,0 +1,35 @@
package co.electriccoin.zcash.spackle
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.widget.Toast
import kotlinx.coroutines.runBlocking
object ClipboardManagerUtil {
fun copyToClipboard(
context: Context,
label: String,
value: String
) {
Twig.info { "Copied to clipboard: label: $label, value: $value" }
val clipboardManager = context.getSystemService(ClipboardManager::class.java)
val data = ClipData.newPlainText(
label,
value
)
if (AndroidApiVersion.isAtLeastT) {
// API 33 and later implement their system Toast UI.
clipboardManager.setPrimaryClip(data)
} else {
// Blocking call is fine here, as we just moved to the IO thread to satisfy theStrictMode on an older API
runBlocking { clipboardManager.setPrimaryClipSuspend(data) }
Toast.makeText(
context,
value,
Toast.LENGTH_SHORT
).show()
}
}
}

View File

@ -3,6 +3,7 @@ package co.electriccoin.zcash.ui.screen.address.view
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.filters.MediumTest
@ -11,15 +12,14 @@ import cash.z.ecc.android.sdk.model.WalletAddresses
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.screen.address.WalletAddressesTag
import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger
@OptIn(ExperimentalCoroutinesApi::class)
class WalletAddressViewTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
@ -114,7 +114,7 @@ class WalletAddressViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun back() = runTest {
fun back_clicked() = runTest {
val testSetup = newTestSetup(WalletAddressesFixture.new())
assertEquals(0, testSetup.getOnBackCount())
@ -128,22 +128,47 @@ class WalletAddressViewTest : UiTestPrerequisites() {
assertEquals(1, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun copy_to_clipboard_clicked() = runTest {
val testSetup = newTestSetup(WalletAddressesFixture.new())
assertEquals(0, testSetup.getOnCopyToClipboardCount())
composeTestRule.onNodeWithTag(
WalletAddressesTag.WALLET_ADDRESS
).also {
it.performClick()
}
assertEquals(1, testSetup.getOnCopyToClipboardCount())
}
private fun newTestSetup(initialState: WalletAddresses) = TestSetup(composeTestRule, initialState)
private class TestSetup(private val composeTestRule: ComposeContentTestRule, initialState: WalletAddresses) {
private val onBackCount = AtomicInteger(0)
private val onCopyToClipboardCount = AtomicInteger(0)
fun getOnBackCount(): Int {
composeTestRule.waitForIdle()
return onBackCount.get()
}
fun getOnCopyToClipboardCount(): Int {
composeTestRule.waitForIdle()
return onCopyToClipboardCount.get()
}
init {
composeTestRule.setContent {
ZcashTheme {
WalletAddresses(
initialState,
walletAddresses = initialState,
onCopyToClipboard = {
onCopyToClipboardCount.incrementAndGet()
},
onBack = {
onBackCount.incrementAndGet()
}

View File

@ -6,7 +6,9 @@ import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.spackle.ClipboardManagerUtil
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.screen.address.view.WalletAddresses
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
@ -31,7 +33,14 @@ private fun WrapWalletAddresses(
} else {
WalletAddresses(
walletAddresses,
goBack
goBack,
onCopyToClipboard = { address ->
ClipboardManagerUtil.copyToClipboard(
activity.applicationContext,
activity.getString(R.string.wallet_address_clipboard_tag),
address
)
},
)
}
}

View File

@ -0,0 +1,8 @@
package co.electriccoin.zcash.ui.screen.address
/**
* These are only used for automated testing.
*/
object WalletAddressesTag {
const val WALLET_ADDRESS = "wallet_address_tag"
}

View File

@ -39,6 +39,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -51,6 +52,7 @@ import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.ListHeader
import co.electriccoin.zcash.ui.design.component.ListItem
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.address.WalletAddressesTag
import kotlinx.coroutines.runBlocking
@Preview("WalletAddresses")
@ -60,19 +62,25 @@ private fun ComposablePreview() {
GradientSurface {
WalletAddresses(
runBlocking { WalletAddressesFixture.new() },
onBack = {}
onBack = {},
onCopyToClipboard = {}
)
}
}
}
@Composable
fun WalletAddresses(walletAddresses: WalletAddresses, onBack: () -> Unit) {
fun WalletAddresses(
walletAddresses: WalletAddresses,
onBack: () -> Unit,
onCopyToClipboard: (String) -> Unit
) {
Column {
WalletDetailTopAppBar(onBack)
WalletDetailAddresses(
walletAddresses,
Modifier
walletAddresses = walletAddresses,
onCopyToClipboard = onCopyToClipboard,
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
)
@ -107,6 +115,7 @@ private val SMALL_INDICATOR_WIDTH = 16.dp
@Composable
private fun WalletDetailAddresses(
walletAddresses: WalletAddresses,
onCopyToClipboard: (String) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier) {
@ -128,12 +137,7 @@ private fun WalletDetailAddresses(
title = stringResource(R.string.wallet_address_unified),
content = walletAddresses.unified.address,
isInitiallyExpanded = true,
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = ZcashTheme.dimens.spacingDefault,
vertical = ZcashTheme.dimens.spacingTiny
)
onCopyToClipboard = onCopyToClipboard
)
Box(Modifier.height(IntrinsicSize.Min)) {
@ -146,6 +150,7 @@ private fun WalletDetailAddresses(
SaplingAddress(
saplingAddress = walletAddresses.sapling.address,
onCopyToClipboard = onCopyToClipboard,
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
@ -153,6 +158,7 @@ private fun WalletDetailAddresses(
TransparentAddress(
transparentAddress = walletAddresses.transparent.address,
onCopyToClipboard = onCopyToClipboard,
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min)
@ -169,6 +175,7 @@ private fun WalletDetailAddresses(
@Composable
private fun SaplingAddress(
saplingAddress: String,
onCopyToClipboard: (String) -> Unit,
modifier: Modifier = Modifier
) {
Row(modifier) {
@ -178,12 +185,7 @@ private fun SaplingAddress(
title = stringResource(R.string.wallet_address_sapling),
content = saplingAddress,
isInitiallyExpanded = false,
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = ZcashTheme.dimens.spacingDefault,
vertical = ZcashTheme.dimens.spacingTiny
)
onCopyToClipboard = onCopyToClipboard
)
}
}
@ -191,6 +193,7 @@ private fun SaplingAddress(
@Composable
private fun TransparentAddress(
transparentAddress: String,
onCopyToClipboard: (String) -> Unit,
modifier: Modifier = Modifier
) {
Row(modifier) {
@ -199,12 +202,7 @@ private fun TransparentAddress(
title = stringResource(R.string.wallet_address_transparent),
content = transparentAddress,
isInitiallyExpanded = false,
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = ZcashTheme.dimens.spacingDefault,
vertical = ZcashTheme.dimens.spacingTiny
)
onCopyToClipboard = onCopyToClipboard
)
}
}
@ -214,16 +212,21 @@ private fun ExpandableRow(
title: String,
content: String,
isInitiallyExpanded: Boolean,
modifier: Modifier = Modifier
onCopyToClipboard: (String) -> Unit
) {
var expandedState by rememberSaveable { mutableStateOf(isInitiallyExpanded) }
Column(
Modifier
.clickable { expandedState = !expandedState }
.then(modifier) // To have proper ripple effect
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.defaultMinSize(minHeight = 48.dp)) {
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.defaultMinSize(minHeight = 48.dp)
.clickable { expandedState = !expandedState }
.padding(
horizontal = ZcashTheme.dimens.spacingDefault,
vertical = ZcashTheme.dimens.spacingTiny
)
) {
ListItem(text = title)
Spacer(
modifier = Modifier
@ -233,7 +236,16 @@ private fun ExpandableRow(
ExpandableArrow(expandedState)
}
if (expandedState) {
Body(content)
Body(
content,
modifier = Modifier
.clickable { onCopyToClipboard(content) }
.padding(
horizontal = ZcashTheme.dimens.spacingDefault,
vertical = ZcashTheme.dimens.spacingTiny
)
.testTag(WalletAddressesTag.WALLET_ADDRESS)
)
}
}
}
@ -241,6 +253,7 @@ private fun ExpandableRow(
@Composable
private fun SmallIndicator(color: Color) {
// TODO [#160]: Border is not the right implementation here, as it causes double thickness for the middle item
// TODO [#160]: https://github.com/zcash/secant-android-wallet/issues/160
Image(
modifier = Modifier
.fillMaxHeight()

View File

@ -1,14 +1,12 @@
package co.electriccoin.zcash.ui.screen.backup
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import cash.z.ecc.android.sdk.model.PersistableWallet
import co.electriccoin.zcash.spackle.ClipboardManagerUtil
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
@ -54,7 +52,13 @@ internal fun WrapLongNewWallet(
) {
WrapLongNewWallet(
persistableWallet,
onCopyToClipboard = { copyToClipboard(activity.applicationContext, persistableWallet) },
onCopyToClipboard = {
ClipboardManagerUtil.copyToClipboard(
activity.applicationContext,
activity.getString(R.string.new_wallet_clipboard_tag),
persistableWallet.seedPhrase.joinToString()
)
},
onBackupComplete = onBackupComplete
)
}
@ -95,7 +99,13 @@ private fun WrapShortNewWallet(
) {
WrapShortNewWallet(
persistableWallet,
onCopyToClipboard = { copyToClipboard(activity.applicationContext, persistableWallet) },
onCopyToClipboard = {
ClipboardManagerUtil.copyToClipboard(
activity.applicationContext,
activity.getString(R.string.new_wallet_clipboard_tag),
persistableWallet.seedPhrase.joinToString()
)
},
onNewWalletComplete = onBackupComplete
)
}
@ -112,12 +122,3 @@ private fun WrapShortNewWallet(
onComplete = onNewWalletComplete,
)
}
internal fun copyToClipboard(context: Context, persistableWallet: PersistableWallet) {
val clipboardManager = context.getSystemService(ClipboardManager::class.java)
val data = ClipData.newPlainText(
context.getString(R.string.new_wallet_clipboard_tag),
persistableWallet.seedPhrase.joinToString()
)
clipboardManager.setPrimaryClip(data)
}

View File

@ -6,8 +6,9 @@ import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.spackle.ClipboardManagerUtil
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.screen.backup.copyToClipboard
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState
import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.seed.view.Seed
@ -43,8 +44,12 @@ private fun WrapSeed(
persistableWallet = persistableWallet,
onBack = goBack,
onCopyToClipboard = {
copyToClipboard(activity.applicationContext, persistableWallet)
}
ClipboardManagerUtil.copyToClipboard(
activity.applicationContext,
activity.getString(R.string.new_wallet_clipboard_tag),
persistableWallet.seedPhrase.joinToString()
)
},
)
}
}

View File

@ -9,4 +9,6 @@
<string name="wallet_address_transparent">Transparent</string>
<string name="wallet_address_show">Show address</string>
<string name="wallet_address_hide">Hide address</string>
<string name="wallet_address_clipboard_tag">Zcash Wallet Address</string>
</resources>