[#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:
parent
8931cf0d4a
commit
6b5359119c
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue