[#584 ]Adopt Twitter's Compose Detekt rules

Twitter had a set of Compose rules, which have been forked into Detekt, Ktlint, and Android Lint implementations.

I see ktlint as mostly being formatting, Detekt being more for multiplatform static analysis, and Android Lint for Android-specific issues.  Android lint is slow, so I didn’t want to use it as the first choice.

I went with the Detekt implementation which can be useful if we leverage multiplatform for our Composables in the future.
This commit is contained in:
Carter Jernigan 2023-03-01 07:58:47 -05:00 committed by GitHub
parent 6d01f210fe
commit 4acd5d3593
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 242 additions and 191 deletions

View File

@ -5,6 +5,10 @@ plugins {
id("io.gitlab.arturbosch.detekt")
}
dependencies {
detektPlugins("io.nlopez.compose.rules:detekt:${project.property("DETEKT_COMPOSE_RULES_VERSION")}")
}
tasks {
register("detektAll", Detekt::class) {
parallel = true

View File

@ -3,6 +3,7 @@ package co.electriccoin.zcash.configuration.api
import co.electriccoin.zcash.configuration.model.entry.ConfigKey
import co.electriccoin.zcash.configuration.model.map.Configuration
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@ -16,7 +17,7 @@ class MergingConfigurationProvider(private val configurationProviders: Persisten
override fun getConfigurationFlow(): Flow<Configuration> {
return if (configurationProviders.isEmpty()) {
flowOf(MergingConfiguration(emptyList<Configuration>().toPersistentList()))
flowOf(MergingConfiguration(persistentListOf<Configuration>()))
} else {
combine(configurationProviders.map { it.getConfigurationFlow() }) { configurations ->
MergingConfiguration(configurations.toList().toPersistentList())

View File

@ -102,6 +102,7 @@ ANDROID_NDK_VERSION=23.0.7599858
ANDROID_GRADLE_PLUGIN_VERSION=7.4.0
DETEKT_VERSION=1.22.0
DETEKT_COMPOSE_RULES_VERSION=0.1.2
EMULATOR_WTF_GRADLE_PLUGIN_VERSION=0.0.15
FIREBASE_CRASHLYTICS_BUILD_TOOLS_VERSION=2.9.2
FLANK_VERSION=23.01.0

View File

@ -29,3 +29,7 @@ style:
excludes: [ '**/*.kts' ]
WildcardImport:
active: false
Compose:
ModifierMissing:
active: false

View File

@ -35,6 +35,7 @@ dependencies {
implementation(libs.kotlin.stdlib)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.core)
api(libs.kotlinx.immutable)
implementation(projects.spackleAndroidLib)
androidTestImplementation(libs.bundles.androidx.test)

View File

@ -162,12 +162,12 @@ 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,
onClick: () -> Unit,
content: @Composable RowScope.() -> Unit
coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
LaunchedEffect(interactionSource) {
var action: Job? = null

View File

@ -8,12 +8,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import co.electriccoin.zcash.spackle.model.Index
import kotlinx.collections.immutable.ImmutableList
// Note: Row size should probably change for landscape layouts
const val CHIP_GRID_ROW_SIZE = 3
@Composable
fun ChipGrid(wordList: List<String>) {
fun ChipGrid(wordList: ImmutableList<String>) {
Column(Modifier.testTag(CommonTag.CHIP_LAYOUT)) {
wordList.chunked(CHIP_GRID_ROW_SIZE).forEachIndexed { chunkIndex, chunk ->
Row(Modifier.fillMaxWidth()) {

View File

@ -46,8 +46,8 @@ fun Body(
@Composable
fun Small(
text: String,
modifier: Modifier = Modifier,
textAlign: TextAlign
textAlign: TextAlign,
modifier: Modifier = Modifier
) {
Text(
text = text,

View File

@ -173,6 +173,7 @@ internal val LightExtendedColorPalette = ExtendedColors(
reference = Light.reference
)
@Suppress("CompositionLocalAllowlist")
internal val LocalExtendedColors = staticCompositionLocalOf {
ExtendedColors(
surfaceEnd = Color.Unspecified,

View File

@ -70,6 +70,7 @@ data class ExtendedTypography(
val zecBalance: TextStyle
)
@Suppress("CompositionLocalAllowlist")
val LocalExtendedTypography = staticCompositionLocalOf {
ExtendedTypography(
chipIndex = Typography.bodyLarge.copy(

View File

@ -34,7 +34,7 @@ class ScanViewIntegrationTest : UiTestPrerequisites() {
val restorationTester = StateRestorationTester(composeTestRule)
restorationTester.setContent {
testSetup.getDefaultContent()
testSetup.DefaultContent()
}
assertEquals(testSetup.getScanState(), ScanState.Permission)
@ -54,7 +54,7 @@ class ScanViewIntegrationTest : UiTestPrerequisites() {
val restorationTester = StateRestorationTester(composeTestRule)
restorationTester.setContent {
testSetup.getDefaultContent()
testSetup.DefaultContent()
}
val permissionPositiveButtonUiObject = getPermissionPositiveButtonUiObject()
@ -78,7 +78,7 @@ class ScanViewIntegrationTest : UiTestPrerequisites() {
val restorationTester = StateRestorationTester(composeTestRule)
restorationTester.setContent {
testSetup.getDefaultContent()
testSetup.DefaultContent()
}
testSetup.grantPermission()

View File

@ -46,7 +46,7 @@ class ScanViewTestSetup(
}
@Composable
fun getDefaultContent() {
fun DefaultContent() {
Scan(
snackbarHostState = SnackbarHostState(),
onBack = {},
@ -63,7 +63,7 @@ class ScanViewTestSetup(
fun setDefaultContent() {
composeTestRule.setContent {
ZcashTheme {
getDefaultContent()
DefaultContent()
}
}
}

View File

@ -67,6 +67,7 @@ dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.guava)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.immutable)
implementation(libs.zcash.sdk)
implementation(libs.zcash.sdk.incubator)
implementation(libs.zcash.bip39)

View File

@ -50,7 +50,7 @@ class BackupIntegrationTest : UiTestPrerequisites() {
val testSetup = newTestSetup(BackupStage.EducationOverview)
restorationTester.setContent {
testSetup.getDefaultContent()
testSetup.DefaultContent()
}
assertEquals(BackupStage.EducationOverview, testSetup.getStage())
@ -73,7 +73,7 @@ class BackupIntegrationTest : UiTestPrerequisites() {
val testSetup = newTestSetup(BackupStage.Test)
restorationTester.setContent {
testSetup.getDefaultContent()
testSetup.DefaultContent()
}
assertEquals(BackupStage.Test, testSetup.getStage())
@ -102,7 +102,7 @@ class BackupIntegrationTest : UiTestPrerequisites() {
val testSetup = newTestSetup(BackupStage.Test)
restorationTester.setContent {
testSetup.getDefaultContent()
testSetup.DefaultContent()
}
assertEquals(0, testSetup.getOnChoicesCallbackCount())

View File

@ -1,6 +1,5 @@
package co.electriccoin.zcash.ui.screen.backup.view
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import cash.z.ecc.sdk.fixture.PersistableWalletFixture
@ -50,9 +49,9 @@ class BackupTestSetup(
return state.current.value
}
@SuppressLint("ComposableNaming")
@Composable
fun getDefaultContent() {
@Suppress("TestFunctionName")
fun DefaultContent() {
ZcashTheme {
BackupWallet(
PersistableWalletFixture.new(),
@ -70,7 +69,7 @@ class BackupTestSetup(
fun setDefaultContent() {
composeTestRule.setContent {
getDefaultContent()
DefaultContent()
}
}
}

View File

@ -5,6 +5,7 @@ import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
import co.electriccoin.zcash.ui.screen.home.view.Home
import kotlinx.collections.immutable.persistentListOf
import java.util.concurrent.atomic.AtomicInteger
class HomeTestSetup(
@ -54,10 +55,11 @@ class HomeTestSetup(
}
@Composable
fun getDefaultContent() {
@Suppress("TestFunctionName")
fun DefaultContent() {
Home(
walletSnapshot,
transactionHistory = emptyList(),
transactionHistory = persistentListOf(),
isKeepScreenOnDuringSync = false,
isUpdateAvailable = false,
goSettings = {
@ -86,7 +88,7 @@ class HomeTestSetup(
fun setDefaultContent() {
composeTestRule.setContent {
ZcashTheme {
getDefaultContent()
DefaultContent()
}
}
}

View File

@ -41,7 +41,7 @@ class HomeViewIntegrationTest : UiTestPrerequisites() {
val testSetup = newTestSetup(walletSnapshot)
restorationTester.setContent {
testSetup.getDefaultContent()
testSetup.DefaultContent()
}
assertNotEquals(WalletSnapshotFixture.STATUS, testSetup.getWalletSnapshot().status)

View File

@ -1,6 +1,5 @@
package co.electriccoin.zcash.ui.screen.onboarding
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@ -42,9 +41,9 @@ class OnboardingTestSetup(
return onboardingState.current.value
}
@SuppressLint("ComposableNaming")
@Composable
fun getDefaultContent() {
@Suppress("TestFunctionName")
fun DefaultContent() {
ZcashTheme {
Onboarding(
isFullOnboardingEnabled,
@ -60,7 +59,7 @@ class OnboardingTestSetup(
fun setDefaultContent() {
composeTestRule.setContent {
getDefaultContent()
DefaultContent()
}
}
}

View File

@ -37,7 +37,7 @@ class OnboardingIntegrationTest : UiTestPrerequisites() {
val testSetup = newTestSetup(OnboardingStage.UnifiedAddresses)
restorationTester.setContent {
testSetup.getDefaultContent()
testSetup.DefaultContent()
}
assertEquals(OnboardingStage.UnifiedAddresses, testSetup.getOnboardingStage())

View File

@ -10,6 +10,7 @@ import co.electriccoin.zcash.ui.common.LocalScreenSecurity
import co.electriccoin.zcash.ui.common.ScreenSecurity
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.restore.state.WordList
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Rule
@ -40,7 +41,7 @@ class RestoreViewSecuredScreenTest : UiTestPrerequisites() {
CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) {
ZcashTheme {
RestoreWallet(
Mnemonics.getCachedWords(Locale.ENGLISH.language).toSortedSet(),
Mnemonics.getCachedWords(Locale.ENGLISH.language).toPersistentSet(),
WordList(emptyList()),
onBack = { },
paste = { "" },

View File

@ -26,6 +26,7 @@ import co.electriccoin.zcash.ui.screen.restore.RestoreTag
import co.electriccoin.zcash.ui.screen.restore.state.WordList
import co.electriccoin.zcash.ui.test.getAppContext
import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.collections.immutable.toPersistentSet
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
@ -219,7 +220,7 @@ class RestoreViewTest : UiTestPrerequisites() {
composeTestRule.setContent {
ZcashTheme {
RestoreWallet(
Mnemonics.getCachedWords(Locale.ENGLISH.language).toSortedSet(),
Mnemonics.getCachedWords(Locale.ENGLISH.language).toPersistentSet(),
state,
onBack = {
onBackCount.incrementAndGet()

View File

@ -25,7 +25,8 @@ class ScanViewBasicTestSetup(
}
@Composable
fun getDefaultContent() {
@Suppress("TestFunctionName")
fun DefaultContent() {
Scan(
snackbarHostState = SnackbarHostState(),
onBack = {
@ -42,7 +43,7 @@ class ScanViewBasicTestSetup(
fun setDefaultContent() {
composeTestRule.setContent {
ZcashTheme {
getDefaultContent()
DefaultContent()
}
}
}

View File

@ -26,7 +26,7 @@ class SupportViewIntegrationTest : UiTestPrerequisites() {
val testSetup = newTestSetup()
restorationTester.setContent {
testSetup.getDefaultContent()
testSetup.DefaultContent()
}
composeTestRule.onNodeWithText("I can haz cheezburger?").also {
@ -55,7 +55,7 @@ class SupportViewIntegrationTest : UiTestPrerequisites() {
val testSetup = newTestSetup()
restorationTester.setContent {
testSetup.getDefaultContent()
testSetup.DefaultContent()
}
composeTestRule.onNodeWithText("I can haz cheezburger?").also {

View File

@ -31,7 +31,8 @@ class SupportViewTestSetup(private val composeTestRule: ComposeContentTestRule)
}
@Composable
fun getDefaultContent() {
@Suppress("TestFunctionName")
fun DefaultContent() {
Support(
SnackbarHostState(),
onBack = {
@ -47,7 +48,7 @@ class SupportViewTestSetup(private val composeTestRule: ComposeContentTestRule)
fun setDefaultContent() {
composeTestRule.setContent {
ZcashTheme {
getDefaultContent()
DefaultContent()
}
}
}

View File

@ -36,7 +36,7 @@ class UpdateViewIntegrationTest {
)
restorationTester.setContent {
testSetup.getDefaultContent()
testSetup.DefaultContent()
}
assertEquals(testSetup.getUpdateInfo().priority, AppUpdateChecker.Priority.HIGH)

View File

@ -44,7 +44,8 @@ class UpdateViewTestSetup(
}
@Composable
fun getDefaultContent() {
@Suppress("TestFunctionName")
fun DefaultContent() {
Update(
snackbarHostState = SnackbarHostState(),
updateInfo = updateInfo,
@ -64,7 +65,7 @@ class UpdateViewTestSetup(
fun setDefaultContent() {
composeTestRule.setContent {
ZcashTheme {
getDefaultContent()
DefaultContent()
}
}
}

View File

@ -26,6 +26,7 @@ class ScreenBrightness {
}
}
@Suppress("CompositionLocalAllowlist")
val LocalScreenBrightness = compositionLocalOf { ScreenBrightness() }
@Composable

View File

@ -26,6 +26,7 @@ class ScreenSecurity {
}
}
@Suppress("CompositionLocalAllowlist")
val LocalScreenSecurity = compositionLocalOf { ScreenSecurity() }
@Composable

View File

@ -26,6 +26,7 @@ class ScreenTimeout {
}
}
@Suppress("CompositionLocalAllowlist")
val LocalScreenTimeout = compositionLocalOf { ScreenTimeout() }
@Composable

View File

@ -5,4 +5,5 @@ import co.electriccoin.zcash.configuration.model.map.Configuration
import co.electriccoin.zcash.configuration.model.map.StringConfiguration
import kotlinx.collections.immutable.persistentMapOf
@Suppress("CompositionLocalAllowlist")
val RemoteConfig = compositionLocalOf<Configuration> { StringConfiguration(persistentMapOf(), null) }

View File

@ -1,16 +1,18 @@
package co.electriccoin.zcash.ui.screen.backup.state
import co.electriccoin.zcash.spackle.model.Index
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toPersistentMap
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class TestChoices(initial: Map<Index, String?> = emptyMap()) {
private val mutableState = MutableStateFlow<Map<Index, String?>>(HashMap(initial))
private val mutableState = MutableStateFlow<ImmutableMap<Index, String?>>(initial.toPersistentMap())
val current: StateFlow<Map<Index, String?>> = mutableState
val current: StateFlow<ImmutableMap<Index, String?>> = mutableState
fun set(map: Map<Index, String?>) {
mutableState.value = HashMap(map)
mutableState.value = map.toPersistentMap()
}
companion object

View File

@ -57,6 +57,8 @@ import co.electriccoin.zcash.ui.screen.backup.BackupTag
import co.electriccoin.zcash.ui.screen.backup.model.BackupStage
import co.electriccoin.zcash.ui.screen.backup.state.BackupState
import co.electriccoin.zcash.ui.screen.backup.state.TestChoices
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
@Preview(device = Devices.PIXEL_4)
@Composable
@ -145,7 +147,7 @@ fun BackupMainContent(
is BackupStage.Test -> TestInProgress(
selectedTestChoices = choices,
onChoicesChanged = onChoicesChanged,
splitSeedPhrase = wallet.seedPhrase.split,
splitSeedPhrase = wallet.seedPhrase.split.toPersistentList(),
backupState = backupState
)
is BackupStage.Failure -> TestFailure()
@ -202,7 +204,7 @@ private fun SeedPhrase(persistableWallet: PersistableWallet) {
.padding(vertical = ZcashTheme.paddings.padding)
) {
Body(stringResource(R.string.new_wallet_3_body_1))
ChipGrid(persistableWallet.seedPhrase.split)
ChipGrid(persistableWallet.seedPhrase.split.toPersistentList())
}
}
@ -219,7 +221,7 @@ private data class TestChoice(val originalIndex: Index, val word: String)
@Composable
private fun TestInProgress(
splitSeedPhrase: List<String>,
splitSeedPhrase: ImmutableList<String>,
selectedTestChoices: TestChoices,
onChoicesChanged: ((choicesCount: Int) -> Unit)?,
backupState: BackupState
@ -258,20 +260,21 @@ private fun TestInProgress(
currentIndex,
dropdownText = currentSelectedTestChoice[currentIndex]
?: "",
choices = testChoices.map { it.word },
choices = testChoices.map { it.word }.toPersistentList(),
{
selectedTestChoices.set(
HashMap(currentSelectedTestChoice).apply {
this[currentIndex] = testChoices[it.value].word
}
)
if (onChoicesChanged != null) {
onChoicesChanged(selectedTestChoices.current.value.size)
}
},
modifier = Modifier
.weight(MINIMAL_WEIGHT)
.testTag(BackupTag.DROPDOWN_CHIP)
) {
selectedTestChoices.set(
HashMap(currentSelectedTestChoice).apply {
this[currentIndex] = testChoices[it.value].word
}
)
if (onChoicesChanged != null) {
onChoicesChanged(selectedTestChoices.current.value.size)
}
}
)
} else {
Chip(
index = Index(chunkIndex * CHIP_GRID_ROW_SIZE + subIndex),
@ -388,25 +391,27 @@ private fun BackupTopAppBar(
@Composable
private fun CopySeedMenu(onCopyToClipboard: () -> Unit) {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(
Icons.Default.MoreVert,
contentDescription = stringResource(R.string.new_wallet_toolbar_more_button_content_description)
)
}
Column {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(
Icons.Default.MoreVert,
contentDescription = stringResource(R.string.new_wallet_toolbar_more_button_content_description)
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.new_wallet_3_button_copy)) },
onClick = {
expanded = false
onCopyToClipboard()
}
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.new_wallet_3_button_copy)) },
onClick = {
expanded = false
onCopyToClipboard()
}
)
}
}
}

View File

@ -29,6 +29,7 @@ import co.electriccoin.zcash.spackle.model.Index
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.backup.BackupTag
import kotlinx.collections.immutable.ImmutableList
/**
* @param chipIndex The index of the chip, which is displayed to the user.
@ -36,17 +37,15 @@ import co.electriccoin.zcash.ui.screen.backup.BackupTag
* @param choices Item choices to display in the open drop down menu. Positional index is important.
* @param onChoiceSelected Callback with the positional index of the item the user selected from [choices].
*/
@Composable
fun ChipDropDown(
chipIndex: Index,
dropdownText: String,
choices: List<String>,
modifier: Modifier = Modifier,
onChoiceSelected: (Index) -> Unit
choices: ImmutableList<String>,
onChoiceSelected: (Index) -> Unit,
modifier: Modifier = Modifier
) {
var expanded by remember { mutableStateOf(false) }
Surface(
modifier = modifier.then(
Modifier
@ -71,7 +70,7 @@ fun ChipDropDown(
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSecondary
)
Spacer(modifier = modifier.fillMaxWidth(MINIMAL_WEIGHT))
Spacer(modifier = Modifier.fillMaxWidth(MINIMAL_WEIGHT))
Icon(
imageVector = Icons.Filled.ArrowDropDown,
contentDescription = null,

View File

@ -73,6 +73,8 @@ import co.electriccoin.zcash.ui.screen.home.HomeTag
import co.electriccoin.zcash.ui.screen.home.model.CommonTransaction
import co.electriccoin.zcash.ui.screen.home.model.WalletDisplayValues
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
@Preview
@ -82,7 +84,7 @@ fun ComposablePreview() {
GradientSurface {
Home(
walletSnapshot = WalletSnapshotFixture.new(),
transactionHistory = emptyList(),
transactionHistory = persistentListOf(),
isUpdateAvailable = false,
isKeepScreenOnDuringSync = false,
isDebugMenuEnabled = false,
@ -103,7 +105,7 @@ fun ComposablePreview() {
@Composable
fun Home(
walletSnapshot: WalletSnapshot,
transactionHistory: List<CommonTransaction>,
transactionHistory: ImmutableList<CommonTransaction>,
isUpdateAvailable: Boolean,
isKeepScreenOnDuringSync: Boolean?,
isDebugMenuEnabled: Boolean,
@ -181,39 +183,41 @@ private fun HomeTopAppBar(
private fun DebugMenu(
resetSdk: () -> Unit
) {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = null)
}
Column {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = null)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text("Throw Uncaught Exception") },
onClick = {
// Supposed to be generic, for manual debugging only
@Suppress("TooGenericExceptionThrown")
throw RuntimeException("Manually crashed from debug menu")
}
)
DropdownMenuItem(
text = { Text("Report Caught Exception") },
onClick = {
// Eventually this shouldn't rely on the Android implementation, but rather an expect/actual
// should be used at the crash API level.
GlobalCrashReporter.reportCaughtException(RuntimeException("Manually caught exception from debug menu"))
expanded = false
}
)
DropdownMenuItem(
text = { Text("Reset SDK") },
onClick = {
resetSdk()
expanded = false
}
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text("Throw Uncaught Exception") },
onClick = {
// Supposed to be generic, for manual debugging only
@Suppress("TooGenericExceptionThrown")
throw RuntimeException("Manually crashed from debug menu")
}
)
DropdownMenuItem(
text = { Text("Report Caught Exception") },
onClick = {
// Eventually this shouldn't rely on the Android implementation, but rather an expect/actual
// should be used at the crash API level.
GlobalCrashReporter.reportCaughtException(RuntimeException("Manually caught exception from debug menu"))
expanded = false
}
)
DropdownMenuItem(
text = { Text("Reset SDK") },
onClick = {
resetSdk()
expanded = false
}
)
}
}
}
@ -275,7 +279,7 @@ private fun HomeDrawer(
private fun HomeMainContent(
paddingValues: PaddingValues,
walletSnapshot: WalletSnapshot,
transactionHistory: List<CommonTransaction>,
transactionHistory: ImmutableList<CommonTransaction>,
isUpdateAvailable: Boolean,
isKeepScreenOnDuringSync: Boolean?,
goReceive: () -> Unit,
@ -406,7 +410,7 @@ private fun Status(
@Composable
@Suppress("MagicNumber")
private fun History(transactionHistory: List<CommonTransaction>) {
private fun History(transactionHistory: ImmutableList<CommonTransaction>) {
if (transactionHistory.isEmpty()) {
return
}

View File

@ -32,6 +32,9 @@ import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton
import co.electriccoin.zcash.ui.screen.home.model.CommonTransaction
import co.electriccoin.zcash.ui.screen.home.model.WalletSnapshot
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
@ -151,10 +154,10 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
// This is not the right API, because the transaction list could be very long and might need UI filtering
@OptIn(ExperimentalCoroutinesApi::class)
val transactionSnapshot: StateFlow<List<CommonTransaction>> = synchronizer
val transactionSnapshot: StateFlow<ImmutableList<CommonTransaction>> = synchronizer
.flatMapLatest {
if (null == it) {
flowOf(emptyList())
flowOf(persistentListOf())
} else {
it.toTransactions()
}
@ -162,7 +165,7 @@ class WalletViewModel(application: Application) : AndroidViewModel(application)
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
emptyList()
persistentListOf()
)
val addresses: StateFlow<WalletAddresses?> = synchronizer
@ -360,7 +363,7 @@ private fun Synchronizer.toWalletSnapshot() =
)
}
private fun Synchronizer.toTransactions() =
private fun Synchronizer.toTransactions(): Flow<ImmutableList<CommonTransaction>> =
combine(
clearedTransactions.distinctUntilChanged(),
pendingTransactions.distinctUntilChanged()
@ -372,5 +375,5 @@ private fun Synchronizer.toTransactions() =
buildList {
addAll(cleared.map { CommonTransaction.Overview(it) })
addAll(pending.map { CommonTransaction.Pending(it) })
}
}.toPersistentList()
}

View File

@ -137,19 +137,21 @@ private fun OnboardingTopAppBar(
@Composable
private fun DebugMenu(onFixtureWallet: () -> Unit) {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = null)
}
Column {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = null)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text("Import wallet with fixture seed phrase") },
onClick = onFixtureWallet
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text("Import wallet with fixture seed phrase") },
onClick = onFixtureWallet
)
}
}
}

View File

@ -109,23 +109,24 @@ private fun ReceiveContents(
}
@Composable
private fun QrCode(data: String, size: Dp, modifier: Modifier) {
BrightenScreen()
DisableScreenTimeout()
val sizePixels = with(LocalDensity.current) { size.toPx() }.roundToInt()
private fun QrCode(data: String, size: Dp, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
BrightenScreen()
DisableScreenTimeout()
val sizePixels = with(LocalDensity.current) { size.toPx() }.roundToInt()
// In the future, use actual/expect to switch QR code generator implementations for multiplatform
// In the future, use actual/expect to switch QR code generator implementations for multiplatform
// Note that our implementation has an extra array copy to BooleanArray, which is a cross-platform
// representation. This should have minimal performance impact since the QR code is relatively
// small and we only generate QR codes infrequently.
// Note that our implementation has an extra array copy to BooleanArray, which is a cross-platform
// representation. This should have minimal performance impact since the QR code is relatively
// small and we only generate QR codes infrequently.
val qrCodePixelArray = JvmQrCodeGenerator.generate(data, sizePixels)
val qrCodeImage = AndroidQrCodeImageGenerator.generate(qrCodePixelArray, sizePixels)
val qrCodePixelArray = JvmQrCodeGenerator.generate(data, sizePixels)
val qrCodeImage = AndroidQrCodeImageGenerator.generate(qrCodePixelArray, sizePixels)
Image(
bitmap = qrCodeImage,
contentDescription = stringResource(R.string.receive_qr_code_content_description),
modifier = modifier
)
Image(
bitmap = qrCodeImage,
contentDescription = stringResource(R.string.receive_qr_code_content_description)
)
}
}

View File

@ -1,21 +1,23 @@
package co.electriccoin.zcash.ui.screen.restore.state
import cash.z.ecc.sdk.model.SeedPhraseValidation
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
class WordList(initial: List<String> = emptyList()) {
private val mutableState = MutableStateFlow(initial)
private val mutableState: MutableStateFlow<ImmutableList<String>> = MutableStateFlow(initial.toPersistentList())
val current: StateFlow<List<String>> = mutableState
val current: StateFlow<ImmutableList<String>> = mutableState
fun set(list: List<String>) {
mutableState.value = ArrayList(list)
mutableState.value = list.toPersistentList()
}
fun append(words: List<String>) {
mutableState.value = ArrayList(current.value) + words
mutableState.value = (current.value + words).toPersistentList()
}
// Custom toString to prevent leaking word list

View File

@ -70,6 +70,9 @@ import co.electriccoin.zcash.ui.screen.restore.RestoreTag
import co.electriccoin.zcash.ui.screen.restore.model.ParseResult
import co.electriccoin.zcash.ui.screen.restore.state.WordList
import co.electriccoin.zcash.ui.screen.restore.state.wordValidation
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentHashSetOf
import kotlinx.coroutines.launch
@Preview("Restore Wallet")
@ -78,7 +81,7 @@ fun PreviewRestore() {
ZcashTheme(darkTheme = true) {
GradientSurface {
RestoreWallet(
completeWordList = setOf(
completeWordList = persistentHashSetOf(
"abandon",
"ability",
"able",
@ -113,7 +116,7 @@ fun PreviewRestoreComplete() {
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun RestoreWallet(
completeWordList: Set<String>,
completeWordList: ImmutableSet<String>,
userWordList: WordList,
onBack: () -> Unit,
paste: () -> String?,
@ -131,16 +134,16 @@ fun RestoreWallet(
}, bottomBar = {
Column(Modifier.verticalScroll(rememberScrollState())) {
Warn(parseResult)
Autocomplete(parseResult = parseResult) {
Autocomplete(parseResult = parseResult, {
textState = ""
userWordList.append(listOf(it))
focusRequester.requestFocus()
}
})
NextWordTextField(
modifier = Modifier.focusRequester(focusRequester),
parseResult = parseResult,
text = textState,
setText = { textState = it }
setText = { textState = it },
modifier = Modifier.focusRequester(focusRequester)
)
}
}) { paddingValues ->
@ -236,7 +239,7 @@ private fun RestoreMainContent(
@Composable
private fun ChipGridWithText(
userWordList: List<String>
userWordList: ImmutableList<String>
) {
Column(
Modifier
@ -268,10 +271,10 @@ private fun ChipGridWithText(
@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun NextWordTextField(
modifier: Modifier = Modifier,
parseResult: ParseResult,
text: String,
setText: (String) -> Unit
setText: (String) -> Unit,
modifier: Modifier = Modifier
) {
/*
* Treat the user input as a password, but disable the transformation to obscure input.
@ -285,7 +288,7 @@ private fun NextWordTextField(
shadowElevation = 8.dp
) {
TextField(
modifier = modifier
modifier = Modifier
.fillMaxWidth()
.padding(4.dp)
.testTag(RestoreTag.SEED_WORD_TEXT_FIELD),
@ -312,9 +315,9 @@ private fun NextWordTextField(
@Composable
private fun Autocomplete(
modifier: Modifier = Modifier,
parseResult: ParseResult,
onSuggestionSelected: (String) -> Unit
onSuggestionSelected: (String) -> Unit,
modifier: Modifier = Modifier
) {
val (isHighlight, suggestions) = when (parseResult) {
is ParseResult.Autocomplete -> {
@ -336,6 +339,7 @@ private fun Autocomplete(
modifier
}
@Suppress("ModifierReused")
LazyRow(highlightModifier.testTag(RestoreTag.AUTOCOMPLETE_LAYOUT)) {
items(it) {
Chip(

View File

@ -8,6 +8,8 @@ import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.sdk.ext.collectWith
import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.ui.screen.restore.state.WordList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.WhileSubscribed
@ -15,7 +17,6 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import java.util.Locale
import java.util.TreeSet
class RestoreViewModel(application: Application, savedStateHandle: SavedStateHandle) : AndroidViewModel(application) {
@ -29,7 +30,7 @@ class RestoreViewModel(application: Application, savedStateHandle: SavedStateHan
Mnemonics.getCachedWords(Locale.ENGLISH.language)
}
emit(CompleteWordSetState.Loaded(TreeSet(completeWordList)))
emit(CompleteWordSetState.Loaded(completeWordList.toPersistentSet()))
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
@ -54,7 +55,7 @@ class RestoreViewModel(application: Application, savedStateHandle: SavedStateHan
// viewModelScope is constructed with Dispatchers.Main.immediate, so this will
// update the save state as soon as a change occurs.
userWordList.current.collectWith(viewModelScope) {
savedStateHandle.set(KEY_WORD_LIST, it)
savedStateHandle[KEY_WORD_LIST] = ArrayList(it)
}
}
@ -65,5 +66,5 @@ class RestoreViewModel(application: Application, savedStateHandle: SavedStateHan
sealed class CompleteWordSetState {
object Loading : CompleteWordSetState()
data class Loaded(val list: Set<String>) : CompleteWordSetState()
data class Loaded(val list: ImmutableSet<String>) : CompleteWordSetState()
}

View File

@ -26,6 +26,7 @@ import co.electriccoin.zcash.ui.design.component.ChipGrid
import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.TertiaryButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import kotlinx.collections.immutable.toPersistentList
@Preview("Seed")
@Composable
@ -95,7 +96,7 @@ private fun SeedMainContent(
) {
Body(stringResource(R.string.seed_body))
ChipGrid(persistableWallet.seedPhrase.split)
ChipGrid(persistableWallet.seedPhrase.split.toPersistentList())
TertiaryButton(onClick = onCopyToClipboard, text = stringResource(R.string.seed_copy))
}

View File

@ -72,9 +72,9 @@ fun PreviewSend() {
@Composable
fun Send(
mySpendableBalance: Zatoshi,
pressAndHoldInteractionSource: MutableInteractionSource = remember { MutableInteractionSource() },
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
// change once deep-linking support is added. It depends on whether deep linking should do one of:
@ -272,9 +272,10 @@ private fun Confirmation(
TimedButton(
onClick = onConfirmation,
{
Text(text = stringResource(id = R.string.send_confirm))
},
interactionSource = pressAndHoldInteractionSource
) {
Text(text = stringResource(id = R.string.send_confirm))
}
)
}
}

View File

@ -122,22 +122,24 @@ private fun SettingsTopAppBar(
private fun TroubleshootingMenu(
onRescanWallet: () -> Unit
) {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = stringResource(id = R.string.settings_overflow_content_description))
}
Column {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(Icons.Default.MoreVert, contentDescription = stringResource(id = R.string.settings_overflow_content_description))
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(id = R.string.settings_rescan)) },
onClick = {
onRescanWallet()
expanded = false
}
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(id = R.string.settings_rescan)) },
onClick = {
onRescanWallet()
expanded = false
}
)
}
}
}

View File

@ -48,9 +48,9 @@ fun NotEnoughSpaceView(storageSpaceRequiredGigabytes: Int, spaceRequiredToContin
)
Spacer(Modifier.height(64.dp))
Small(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.space_required_to_continue, spaceRequiredToContinueMegabytes),
textAlign = TextAlign.Center
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}
}