2.0.0 App redesign (#1842)

* Home redesign

* Home redesign

* Test hotfixes

* Test hotfixes

* Code cleanup

* Restore redesign

* Year-month date picker implementation

* Restore estimation design added

* Homepage performance updates

* Seed suggestions design implementation

* Seed suggestions optimization

* Seed suggestions optimization

* Code cleanup

* [#1812] Create wallet update

Closes #1812

* Test hotfixes

* Recovery seed screen redesigned

* App hotfixes

* Keyboard handling hotfix

* Automatic keyboard and bottom sheet handling during navigation

* Wallet backup screen implemented

* WIP home messages

* Wallet backup message

* Bottom sheet code cleanup

* Strings code cleanup

* Home messages and dialogs UI

* Home messages business logic, wallet info removed from status bar and general refactoring

* Message persistence

* Message visibility based on foreground/background

* Error handling

* Balances UI implementation

* Design updates

* Strings update

* Crash report message implemented

* Balance actions bussiness logic

* Balance actions bugfixes

* Balance actions bugfixes

* Restoration connected to sdk

* Design hotfixes

* Design hotfixes

* Sdk changes regarding sync progress adopted

* Shielded transaction immediately hidden after shield clicked

* Code cleanup

* Messages update

* Home message bugfixes

* Strings update

* Transaction detail hotfix for pending transaction

* Messages and balances hotfixes

* Balances hotfix

* Hotfix for foss

* Third party scan state

* Clearing shared prefs fixed

* Store crash reporting bugfix

* Store bugfix

* Spanish translations

* Shielding info update

* Shielding info update

* Bugfixes

* Bugfixes

* Sdk version bump

* Backup message shows only with zashi account

* Ktlint format

* Ktlint format

* Code cleanup

* Code cleanup

* Strings update

* Release 2.0.0 (934)

Closes ##1859

* Changelog update

Closes ##1859
This commit is contained in:
Milan 2025-04-25 20:08:09 +02:00 committed by GitHub
parent 3e4a57979f
commit 000697ba63
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
534 changed files with 14877 additions and 11003 deletions

View File

@ -6,6 +6,20 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
## [Unreleased]
## [2.0.0 (934)] - 2025-04-25
### Added:
- Zashi 2.0 is here!
- New Wallet Status Widget helps you navigate Zashi with ease and get more info upon tap.
### Changed:
- Redesigned Home Screen and streamlined app navigation.
- Balances redesigned into a new Spendable component on the Send screen.
- Revamped Restore flow.
- Create Wallet with a tap! New Wallet Backup flow moved to when your wallet receives first funds.
- Firebase Crashlytics are fully opt-in. Help us improve Zashi, or dont, your choice.
- Scanning a ZIP 321 QR code now opens Zashi!
## [1.5.2 (932)] - 2025-04-23
### Added

View File

@ -25,6 +25,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="zcash" />
</intent-filter>
</activity-alias>
<!-- Enable profiling by benchmark -->

View File

@ -14,12 +14,13 @@ import co.electriccoin.zcash.di.providerModule
import co.electriccoin.zcash.di.repositoryModule
import co.electriccoin.zcash.di.useCaseModule
import co.electriccoin.zcash.di.viewModelModule
import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.spackle.StrictModeCompat
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider
import co.electriccoin.zcash.ui.common.provider.CrashReportingStorageProvider
import co.electriccoin.zcash.ui.common.repository.FlexaRepository
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.common.repository.HomeMessageCacheRepository
import co.electriccoin.zcash.ui.common.repository.WalletSnapshotRepository
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
@ -27,10 +28,12 @@ import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
class ZcashApplication : CoroutineApplication() {
private val standardPreferenceProvider by inject<StandardPreferenceProvider>()
private val flexaRepository by inject<FlexaRepository>()
private val applicationStateProvider: ApplicationStateProvider by inject()
private val getAvailableCrashReporters: CrashReportersProvider by inject()
private val homeMessageCacheRepository: HomeMessageCacheRepository by inject()
private val walletSnapshotRepository: WalletSnapshotRepository by inject()
private val crashReportingStorageProvider: CrashReportingStorageProvider by inject()
override fun onCreate() {
super.onCreate()
@ -68,6 +71,8 @@ class ZcashApplication : CoroutineApplication() {
configureAnalytics()
flexaRepository.init()
homeMessageCacheRepository.init()
walletSnapshotRepository.init()
}
private fun configureLogging() {
@ -89,9 +94,9 @@ class ZcashApplication : CoroutineApplication() {
private fun configureAnalytics() {
if (GlobalCrashReporter.register(this, getAvailableCrashReporters())) {
applicationScope.launch {
StandardPreferenceKeys.IS_ANALYTICS_ENABLED.observe(standardPreferenceProvider()).collect {
crashReportingStorageProvider.observe().collect {
Twig.debug { "Is crashlytics enabled: $it" }
if (it) {
if (it == true) {
GlobalCrashReporter.enable()
} else {
GlobalCrashReporter.disableAndDelete()

View File

@ -23,7 +23,7 @@ class MergingConfigurationProvider(
override fun getConfigurationFlow(): Flow<Configuration> =
if (configurationProviders.isEmpty()) {
flowOf(MergingConfiguration(persistentListOf<Configuration>()))
flowOf(MergingConfiguration(persistentListOf()))
} else {
combine(configurationProviders.map { it.getConfigurationFlow() }) { configurations ->
MergingConfiguration(configurations.toList().toPersistentList())

View File

@ -12,6 +12,20 @@ directly impact users rather than highlighting other key architectural updates.*
## [Unreleased]
## [2.0.0 (934)] - 2025-04-25
### Added:
- Zashi 2.0 is here!
- New Wallet Status Widget helps you navigate Zashi with ease and get more info upon tap.
### Changed:
- Redesigned Home Screen and streamlined app navigation.
- Balances redesigned into a new Spendable component on the Send screen.
- Revamped Restore flow.
- Create Wallet with a tap! New Wallet Backup flow moved to when your wallet receives first funds.
- Firebase Crashlytics are fully opt-in. Help us improve Zashi, or dont, your choice.
- Scanning a ZIP 321 QR code now opens Zashi!
## [1.5.2 (932)] - 2025-04-23
### Added:

View File

@ -12,6 +12,19 @@ directly impact users rather than highlighting other key architectural updates.*
## [Unreleased]
## [2.0.0 (934)] - 2025-04-25
### Añadido:
- Un widget de estado de la billetera te ayuda a navegar por Zashi y a obtener información con un click.
### Cambiado:
- Pantalla de inicio rediseñada y navegación optimizada.
- Saldos rediseñados con un nuevo componente Gastable en la pantalla de envío.
- Flujo de restauración renovado.
- ¡Crea tu billetera facil! Un nuevo proceso de backup que se traslado a cuando recibes los primeros fondos.
- Firebase Crashlytics es totalmente opcional.
- ¡Escanear un código QR ZIP 321 abre Zashi!
## [1.5.2 (932)] - 2025-04-23
### Añadido:

View File

@ -0,0 +1,11 @@
Added:
Zashi 2.0 is here!
- New Wallet Status Widget helps you navigate Zashi with ease and get more info upon tap.
Changed:
- Redesigned Home Screen and streamlined app navigation.
- Balances redesigned into a new Spendable component on the Send screen.
- Revamped Restore flow.
- Create Wallet with a tap! New Wallet Backup flow moved to when your wallet receives first funds.
- Firebase Crashlytics are fully opt-in. Help us improve Zashi, or dont, your choice.
- Scanning a ZIP 321 QR code now opens Zashi!

View File

@ -0,0 +1,10 @@
Añadido:
- Un widget de estado de la billetera te ayuda a navegar por Zashi y a obtener información con un click.
Cambiado:
- Pantalla de inicio rediseñada y navegación optimizada.
- Saldos rediseñados con un nuevo componente Gastable en la pantalla de envío.
- Flujo de restauración renovado.
- ¡Crea tu billetera facil! Un nuevo proceso de backup que se traslado a cuando recibes los primeros fondos.
- Firebase Crashlytics es totalmente opcional.
- ¡Escanear un código QR ZIP 321 abre Zashi!

View File

@ -61,7 +61,7 @@ NDK_DEBUG_SYMBOL_LEVEL=symbol_table
# VERSION_CODE is effectively ignored. VERSION_NAME is suffixed with the version code.
# If not using automated Google Play deployment, then these serve as the actual version numbers.
ZCASH_VERSION_CODE=1
ZCASH_VERSION_NAME=1.5.2
ZCASH_VERSION_NAME=2.0.0
# Set these fields, as you need them (e.g. with values "Zcash X" and "co.electriccoin.zcash.x")
# to distinguish a different release build that can be installed alongside the official version
@ -211,7 +211,7 @@ ZIP_321_VERSION = 0.0.6
# WARNING: Ensure a non-snapshot version is used before releasing to production
ZCASH_BIP39_VERSION=1.0.9
# WARNING: Ensure a non-snapshot version is used before releasing to production
ZCASH_SDK_VERSION=2.2.11
ZCASH_SDK_VERSION=2.2.12-SNAPSHOT
# Toolchain is the Java version used to build the application, which is separate from the
# Java version used to run the application.

View File

@ -29,5 +29,7 @@ interface PreferenceProvider {
fun observe(key: PreferenceKey): Flow<String?>
suspend fun remove(key: PreferenceKey)
suspend fun clearPreferences(): Boolean
}

View File

@ -22,6 +22,6 @@ data class NullableBooleanPreferenceDefault(
preferenceProvider: PreferenceProvider,
newValue: Boolean?
) {
preferenceProvider.putString(key, newValue.toString())
preferenceProvider.putString(key, newValue?.toString())
}
}

View File

@ -39,6 +39,10 @@ interface PreferenceDefault<T> {
newValue: T
)
suspend fun clear(preferenceProvider: PreferenceProvider) {
preferenceProvider.remove(key)
}
/**
* @param preferenceProvider Provides actual preference values.
* @return Flow that emits preference changes. Note that implementations should emit an initial value

View File

@ -0,0 +1,16 @@
package co.electriccoin.zcash.preference.model.entry
import co.electriccoin.zcash.preference.api.PreferenceProvider
import java.time.Instant
class TimestampPreferenceDefault(
override val key: PreferenceKey
) : PreferenceDefault<Instant?> {
override suspend fun getValue(preferenceProvider: PreferenceProvider) =
preferenceProvider.getLong(key)?.let { Instant.ofEpochMilli(it) }
override suspend fun putValue(
preferenceProvider: PreferenceProvider,
newValue: Instant?
) = preferenceProvider.putLong(key, newValue?.toEpochMilli())
}

View File

@ -22,6 +22,10 @@ class MockPreferenceProvider(
// For the mock implementation, does not support observability of changes
override fun observe(key: PreferenceKey): Flow<String?> = flow { emit(getString(key)) }
override suspend fun remove(key: PreferenceKey) {
map.remove(key.key)
}
override suspend fun clearPreferences(): Boolean {
map.clear()
return true

View File

@ -11,9 +11,11 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
@ -32,6 +34,8 @@ class AndroidPreferenceProvider private constructor(
private val sharedPreferences: SharedPreferences,
private val dispatcher: CoroutineDispatcher
) : PreferenceProvider {
private val clearPipeline = MutableSharedFlow<Unit>()
private val mutex = Mutex()
/*
* Implementation note: EncryptedSharedPreferences are not thread-safe, so this implementation
@ -119,6 +123,8 @@ class AndroidPreferenceProvider private constructor(
editor.clear()
clearPipeline.emit(Unit)
return@withContext editor.commit()
}
@ -131,6 +137,12 @@ class AndroidPreferenceProvider private constructor(
}
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
this.launch {
clearPipeline.collect {
send(Unit)
}
}
// Kickstart the emissions
trySend(Unit)
@ -140,6 +152,17 @@ class AndroidPreferenceProvider private constructor(
}.flowOn(dispatcher)
.map { getString(key) }
@SuppressLint("ApplySharedPref")
override suspend fun remove(key: PreferenceKey) {
withContext(dispatcher) {
val editor = sharedPreferences.edit()
editor.remove(key.key)
editor.commit()
}
}
companion object {
suspend fun newStandard(
context: Context,

View File

@ -34,12 +34,12 @@ fun Zatoshi.toZecStringAbbreviated(suffix: String): ZecAmountPair {
}
}
private const val DEFAULT_LESS_THAN_FEE = 100_000L
val DEFAULT_FEE: String
get() = Zatoshi(DEFAULT_LESS_THAN_FEE).toZecStringFull()
data class ZecAmountPair(
val main: String,
val suffix: String
)
val Zatoshi.Companion.typicalFee: Zatoshi
get() = Zatoshi(TYPICAL_FEE)
private const val TYPICAL_FEE = 100000L

View File

@ -1,43 +0,0 @@
package cash.z.ecc.sdk.model
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.sdk.model.SeedPhrase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.Locale
// This is a stopgap; would like to see improvements to the SeedPhrase class to have validation moved
// there as part of creating the object
sealed class SeedPhraseValidation {
object BadCount : SeedPhraseValidation()
object BadWord : SeedPhraseValidation()
object FailedChecksum : SeedPhraseValidation()
class Valid(
val seedPhrase: SeedPhrase
) : SeedPhraseValidation()
companion object {
suspend fun new(list: List<String>): SeedPhraseValidation {
if (list.size != SeedPhrase.SEED_PHRASE_SIZE) {
return BadCount
}
@Suppress("SwallowedException")
return try {
val stringified = list.joinToString(SeedPhrase.DEFAULT_DELIMITER)
withContext(Dispatchers.Default) {
Mnemonics.MnemonicCode(stringified, Locale.ENGLISH.language).validate()
}
Valid(SeedPhrase.new(stringified))
} catch (e: Mnemonics.InvalidWordException) {
BadWord
} catch (e: Mnemonics.ChecksumException) {
FailedChecksum
}
}
}
}

View File

@ -0,0 +1,80 @@
package co.electriccoin.zcash.ui.design
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.ime
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withTimeoutOrNull
import kotlin.time.Duration.Companion.seconds
@Stable
class KeyboardManager(
isOpen: Boolean,
private val softwareKeyboardController: SoftwareKeyboardController?
) {
private var targetState = MutableStateFlow(isOpen)
var isOpen by mutableStateOf(isOpen)
private set
suspend fun close() {
if (targetState.value) {
withTimeoutOrNull(.5.seconds) {
softwareKeyboardController?.hide()
targetState.filter { !it }.first()
}
}
}
fun onKeyboardOpened() {
targetState.update { true }
isOpen = true
}
fun onKeyboardClosed() {
targetState.update { false }
isOpen = false
}
}
@Composable
fun rememberKeyboardManager(): KeyboardManager {
val isKeyboardOpen by rememberKeyboardState()
val softwareKeyboardController = LocalSoftwareKeyboardController.current
val keyboardManager = remember { KeyboardManager(isKeyboardOpen, softwareKeyboardController) }
LaunchedEffect(isKeyboardOpen) {
if (isKeyboardOpen) {
keyboardManager.onKeyboardOpened()
} else {
keyboardManager.onKeyboardClosed()
}
}
return keyboardManager
}
@Composable
private fun rememberKeyboardState(): State<Boolean> {
val isImeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0
return rememberUpdatedState(isImeVisible)
}
@Suppress("CompositionLocalAllowlist")
val LocalKeyboardManager =
compositionLocalOf<KeyboardManager> {
error("Keyboard manager not provided")
}

View File

@ -0,0 +1,41 @@
package co.electriccoin.zcash.ui.design
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import kotlinx.coroutines.withTimeoutOrNull
import kotlin.time.Duration.Companion.seconds
@OptIn(ExperimentalMaterial3Api::class)
@Stable
class SheetStateManager {
private var sheetState: SheetState? = null
fun onSheetOpened(sheetState: SheetState) {
this.sheetState = sheetState
}
fun onSheetDisposed(sheetState: SheetState) {
if (this.sheetState == sheetState) {
this.sheetState = null
}
}
suspend fun hide() {
withTimeoutOrNull(.5.seconds) {
sheetState?.hide()
}
}
}
@Composable
fun rememberSheetStateManager() = remember { SheetStateManager() }
@Suppress("CompositionLocalAllowlist")
val LocalSheetStateManager =
compositionLocalOf<SheetStateManager> {
error("Sheet state manager not provided")
}

View File

@ -1,9 +1,8 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
@ -62,13 +61,13 @@ private fun HiddenStyledBalancePreview() =
* @param textColor Optional color to modify the default font color from [textStyle]
* @param modifier Modifier to modify the Text UI element as needed
*/
@OptIn(ExperimentalFoundationApi::class)
@Suppress("LongParameterList")
@Composable
fun StyledBalance(
balanceParts: ZecAmountTriple,
modifier: Modifier = Modifier,
isHideBalances: Boolean = false,
showDust: Boolean = true,
hiddenBalancePlaceholder: String = stringResource(id = R.string.hide_balance_placeholder),
textColor: Color = Color.Unspecified,
textStyle: BalanceTextStyle = StyledBalanceDefaults.textStyles(),
@ -91,10 +90,12 @@ fun StyledBalance(
) {
append(balanceSplit.first)
}
withStyle(
style = textStyle.leastSignificantPart.toSpanStyle()
) {
append(balanceSplit.second)
if (showDust) {
withStyle(
style = textStyle.leastSignificantPart.toSpanStyle()
) {
append(balanceSplit.second)
}
}
}
}
@ -102,15 +103,16 @@ fun StyledBalance(
val resultModifier =
Modifier
.basicMarquee()
.animateContentSize()
.then(modifier)
Text(
text = content,
color = textColor,
maxLines = 1,
modifier = resultModifier
)
SelectionContainer {
Text(
text = content,
color = textColor,
maxLines = 1,
modifier = resultModifier
)
}
}
private const val CUT_POSITION_OFFSET = 4

View File

@ -0,0 +1,19 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.blur
import androidx.compose.ui.unit.Dp
import co.electriccoin.zcash.spackle.AndroidApiVersion
fun Modifier.blurCompat(
radius: Dp,
max: Dp
): Modifier =
if (AndroidApiVersion.isAtLeastS) {
this.blur(radius)
} else {
val progression = 1 - (radius.value / max.value)
this
.alpha(progression)
}

View File

@ -13,8 +13,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@ -66,7 +68,9 @@ fun LabeledCheckBox(
text: String,
modifier: Modifier = Modifier,
checked: Boolean = false,
checkBoxTestTag: String? = null
checkBoxTestTag: String? = null,
color: Color = ZcashTheme.colors.textPrimary,
style: TextStyle = ZcashTheme.extendedTypography.checkboxText
) {
val (checkedState, setCheckedState) = rememberSaveable { mutableStateOf(checked) }
@ -113,8 +117,8 @@ fun LabeledCheckBox(
)
Text(
text = AnnotatedString(text),
color = ZcashTheme.colors.textPrimary,
style = ZcashTheme.extendedTypography.checkboxText
color = color,
style = style
)
}
}

View File

@ -1,142 +0,0 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextOverflow
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.util.StringResource
import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Suppress("LongParameterList", "LongMethod")
@Composable
fun FormTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
error: String? = null,
enabled: Boolean = true,
textStyle: TextStyle = ZcashTheme.extendedTypography.textFieldValue,
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
colors: TextFieldColors =
TextFieldDefaults.colors(
cursorColor = ZcashTheme.colors.textPrimary,
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
disabledContainerColor = Color.Transparent,
errorContainerColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent
),
keyboardActions: KeyboardActions = KeyboardActions.Default,
shape: Shape = TextFieldDefaults.shape,
// To enable border around the TextField
withBorder: Boolean = true,
bringIntoViewRequester: BringIntoViewRequester? = null,
minHeight: Dp = ZcashTheme.dimens.textFieldDefaultHeight,
testTag: String? = null
) {
val coroutineScope = rememberCoroutineScope()
Column(modifier = Modifier.then(modifier)) {
TextField(
value = value,
onValueChange = onValueChange,
placeholder =
if (enabled) {
placeholder
} else {
null
},
textStyle = textStyle,
keyboardOptions = keyboardOptions,
colors = colors,
modifier =
Modifier
.fillMaxWidth()
.defaultMinSize(minHeight = minHeight)
.onFocusEvent { focusState ->
bringIntoViewRequester?.run {
if (focusState.isFocused) {
coroutineScope.launch {
bringIntoView()
}
}
}
}.then(
if (withBorder) {
Modifier.border(
width = 1.dp,
color =
if (enabled) {
ZcashTheme.colors.textFieldFrame
} else {
ZcashTheme.colors.textDisabled
}
)
} else {
Modifier
}
).then(
if (testTag.isNullOrEmpty()) {
Modifier
} else {
Modifier.testTag(testTag)
}
),
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
keyboardActions = keyboardActions,
shape = shape,
enabled = enabled
)
if (!error.isNullOrEmpty()) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
BodySmall(
text = error,
color = ZcashTheme.colors.textFieldWarning,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
@Immutable
data class TextFieldState(
val value: StringResource,
val error: StringResource? = null,
val isEnabled: Boolean = true,
val onValueChange: (String) -> Unit,
) {
val isError = error != null
}

View File

@ -1,15 +1,14 @@
package co.electriccoin.zcash.ui.screen.exchangerate
package co.electriccoin.zcash.ui.design.component
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
@ -21,17 +20,19 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.scaffoldPadding
@Suppress("LongMethod")
@Composable
internal fun BaseExchangeRateOptIn(
fun ZashiBaseSettingsOptIn(
header: String,
@DrawableRes image: Int,
info: String?,
onDismiss: () -> Unit,
footer: @Composable ColumnScope.() -> Unit,
content: @Composable ColumnScope.() -> Unit,
@ -54,7 +55,7 @@ internal fun BaseExchangeRateOptIn(
)
) {
Image(
painter = painterResource(id = R.drawable.ic_exchange_rate_close),
painter = painterResource(id = R.drawable.ic_settings_opt_int_close),
contentDescription = null,
colorFilter = ColorFilter.tint(ZashiColors.Btns.Tertiary.btnTertiaryFg)
)
@ -68,31 +69,21 @@ internal fun BaseExchangeRateOptIn(
.weight(1f)
.verticalScroll(rememberScrollState())
) {
Image(painter = painterResource(R.drawable.exchange_rate), contentDescription = null)
Image(painter = painterResource(image), contentDescription = null)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = R.string.exchange_rate_opt_in_subtitle),
text = header,
color = ZashiColors.Text.textPrimary,
style = ZashiTypography.header6,
fontWeight = FontWeight.SemiBold
)
content()
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.weight(1f))
Row {
Image(
painter = painterResource(R.drawable.ic_exchange_rate_info),
contentDescription = null,
colorFilter = ColorFilter.tint(ZashiColors.Text.textTertiary)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(R.string.exchange_rate_opt_in_note),
color = ZashiColors.Text.textTertiary,
style = ZashiTypography.textXs
)
if (info != null) {
Spacer(modifier = Modifier.height(24.dp))
ZashiInfoText(info)
}
}
Spacer(modifier = Modifier.height(20.dp))

View File

@ -0,0 +1,132 @@
package co.electriccoin.zcash.ui.design.component
import androidx.annotation.DrawableRes
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.pointer.changedToDown
import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.StringResource
import co.electriccoin.zcash.ui.design.util.getValue
import co.electriccoin.zcash.ui.design.util.orDark
import co.electriccoin.zcash.ui.design.util.stringRes
@Suppress("MagicNumber")
@Composable
fun ZashiBigIconButton(
state: BigIconButtonState,
modifier: Modifier = Modifier,
) {
var isPressed by remember { mutableStateOf(false) }
val shadowElevation by animateDpAsState(if (isPressed) 0.dp else (2.dp orDark 4.dp))
val darkBgGradient =
Brush.verticalGradient(
0f to ZashiColors.Surfaces.strokeSecondary,
.66f to ZashiColors.Surfaces.strokeSecondary.copy(alpha = 0.5f),
1f to ZashiColors.Surfaces.strokeSecondary.copy(alpha = 0.25f),
)
val darkBorderGradient =
Brush.verticalGradient(
0f to ZashiColors.Surfaces.strokePrimary,
1f to ZashiColors.Surfaces.strokePrimary.copy(alpha = 0f),
)
val backgroundModifier =
Modifier.background(ZashiColors.Surfaces.bgPrimary) orDark
Modifier.background(darkBgGradient)
Surface(
modifier =
modifier
.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent()
event.changes.forEach { change ->
if (change.changedToDown()) {
isPressed = true
}
if (change.changedToUp()) {
isPressed = false
}
}
}
}
},
onClick = state.onClick,
color = ZashiColors.Surfaces.bgPrimary,
shape = RoundedCornerShape(22.dp),
border =
BorderStroke(.5.dp, ZashiColors.Utility.Gray.utilityGray100) orDark
BorderStroke(.5.dp, darkBorderGradient),
shadowElevation = shadowElevation
) {
Column(
modifier = backgroundModifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(state.icon),
contentDescription = state.text.getValue(),
colorFilter = ColorFilter.tint(ZashiColors.Text.textPrimary)
)
Spacer(Modifier.height(4.dp))
Text(
text = state.text.getValue(),
style = ZashiTypography.textXs,
fontWeight = FontWeight.Medium,
color = ZashiColors.Text.textPrimary
)
}
}
}
data class BigIconButtonState(
val text: StringResource,
@DrawableRes val icon: Int,
val onClick: () -> Unit,
)
@PreviewScreens
@Composable
private fun Preview() =
ZcashTheme {
ZashiBigIconButton(
state =
BigIconButtonState(
text = stringRes("Text"),
icon = R.drawable.ic_reveal,
onClick = {}
)
)
}

View File

@ -0,0 +1,54 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.ParagraphStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextIndent
import androidx.compose.ui.text.withStyle
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
@Composable
fun ZashiBulletText(
vararg bulletText: String,
modifier: Modifier = Modifier,
style: TextStyle = ZashiTypography.textSm,
fontWeight: FontWeight = FontWeight.Normal,
color: Color = ZashiColors.Text.textPrimary,
) {
val normalizedStyle = style.copy(fontWeight = fontWeight)
val bulletString = remember { "\u2022 " }
val bulletTextMeasurer = rememberTextMeasurer()
val bulletStringWidth =
remember(normalizedStyle, bulletTextMeasurer) {
bulletTextMeasurer.measure(text = bulletString, style = normalizedStyle).size.width
}
val bulletRestLine = with(LocalDensity.current) { bulletStringWidth.toSp() }
val bulletParagraphStyle = ParagraphStyle(textIndent = TextIndent(restLine = bulletRestLine))
Text(
modifier = modifier,
text =
buildAnnotatedString {
withStyle(style = bulletParagraphStyle) {
bulletText.forEachIndexed { index, string ->
if (index != 0) {
appendLine()
}
append(bulletString)
append(string)
}
}
},
style = style,
fontWeight = fontWeight,
color = color,
)
}

View File

@ -18,6 +18,7 @@ import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
@ -31,6 +32,7 @@ import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColorsInternal
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.StringResource
import co.electriccoin.zcash.ui.design.util.getValue
@ -42,7 +44,7 @@ fun ZashiButton(
style: TextStyle = ZashiButtonDefaults.style,
shape: Shape = ZashiButtonDefaults.shape,
contentPadding: PaddingValues = ZashiButtonDefaults.contentPadding,
colors: ZashiButtonColors = ZashiButtonDefaults.primaryColors(),
colors: ZashiButtonColors = LocalZashiButtonColors.current ?: ZashiButtonDefaults.primaryColors(),
content: @Composable RowScope.(ZashiButtonScope) -> Unit = ZashiButtonDefaults.content
) {
ZashiButton(
@ -67,14 +69,14 @@ fun ZashiButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
style: TextStyle = ZashiButtonDefaults.style,
shape: Shape = ZashiButtonDefaults.shape,
contentPadding: PaddingValues = ZashiButtonDefaults.contentPadding,
@DrawableRes icon: Int? = null,
@DrawableRes trailingIcon: Int? = null,
enabled: Boolean = true,
isLoading: Boolean = false,
colors: ZashiButtonColors = ZashiButtonDefaults.primaryColors(),
style: TextStyle = ZashiButtonDefaults.style,
shape: Shape = ZashiButtonDefaults.shape,
contentPadding: PaddingValues = ZashiButtonDefaults.contentPadding,
colors: ZashiButtonColors = LocalZashiButtonColors.current ?: ZashiButtonDefaults.primaryColors(),
content: @Composable RowScope.(ZashiButtonScope) -> Unit = ZashiButtonDefaults.content
) {
val scope =
@ -180,10 +182,11 @@ object ZashiButtonDefaults {
@Composable
fun primaryColors(
containerColor: Color = ZashiColors.Btns.Primary.btnPrimaryBg,
contentColor: Color = ZashiColors.Btns.Primary.btnPrimaryFg,
disabledContainerColor: Color = ZashiColors.Btns.Primary.btnPrimaryBgDisabled,
disabledContentColor: Color = ZashiColors.Btns.Primary.btnBoldFgDisabled,
source: ZashiColorsInternal = ZashiColors,
containerColor: Color = source.Btns.Primary.btnPrimaryBg,
contentColor: Color = source.Btns.Primary.btnPrimaryFg,
disabledContainerColor: Color = source.Btns.Primary.btnPrimaryBgDisabled,
disabledContentColor: Color = source.Btns.Primary.btnBoldFgDisabled,
) = ZashiButtonColors(
containerColor = containerColor,
contentColor = contentColor,
@ -195,11 +198,12 @@ object ZashiButtonDefaults {
@Composable
fun secondaryColors(
containerColor: Color = ZashiColors.Btns.Secondary.btnSecondaryBg,
contentColor: Color = ZashiColors.Btns.Secondary.btnSecondaryFg,
source: ZashiColorsInternal = ZashiColors,
containerColor: Color = source.Btns.Secondary.btnSecondaryBg,
contentColor: Color = source.Btns.Secondary.btnSecondaryFg,
borderColor: Color = Color.Unspecified,
disabledContainerColor: Color = ZashiColors.Btns.Secondary.btnSecondaryBgDisabled,
disabledContentColor: Color = ZashiColors.Btns.Secondary.btnSecondaryFg,
disabledContainerColor: Color = source.Btns.Secondary.btnSecondaryBgDisabled,
disabledContentColor: Color = source.Btns.Secondary.btnSecondaryFgDisabled,
) = ZashiButtonColors(
containerColor = containerColor,
contentColor = contentColor,
@ -211,10 +215,11 @@ object ZashiButtonDefaults {
@Composable
fun tertiaryColors(
containerColor: Color = ZashiColors.Btns.Tertiary.btnTertiaryBg,
contentColor: Color = ZashiColors.Btns.Tertiary.btnTertiaryFg,
disabledContainerColor: Color = ZashiColors.Btns.Tertiary.btnTertiaryBgDisabled,
disabledContentColor: Color = ZashiColors.Btns.Tertiary.btnTertiaryFgDisabled,
source: ZashiColorsInternal = ZashiColors,
containerColor: Color = source.Btns.Tertiary.btnTertiaryBg,
contentColor: Color = source.Btns.Tertiary.btnTertiaryFg,
disabledContainerColor: Color = source.Btns.Tertiary.btnTertiaryBgDisabled,
disabledContentColor: Color = source.Btns.Tertiary.btnTertiaryFgDisabled,
) = ZashiButtonColors(
containerColor = containerColor,
contentColor = contentColor,
@ -226,11 +231,12 @@ object ZashiButtonDefaults {
@Composable
fun destructive1Colors(
containerColor: Color = ZashiColors.Btns.Destructive1.btnDestroy1Bg,
contentColor: Color = ZashiColors.Btns.Destructive1.btnDestroy1Fg,
borderColor: Color = ZashiColors.Btns.Destructive1.btnDestroy1Border,
disabledContainerColor: Color = ZashiColors.Btns.Destructive1.btnDestroy1BgDisabled,
disabledContentColor: Color = ZashiColors.Btns.Destructive1.btnDestroy1FgDisabled,
source: ZashiColorsInternal = ZashiColors,
containerColor: Color = source.Btns.Destructive1.btnDestroy1Bg,
contentColor: Color = source.Btns.Destructive1.btnDestroy1Fg,
borderColor: Color = source.Btns.Destructive1.btnDestroy1Border,
disabledContainerColor: Color = source.Btns.Destructive1.btnDestroy1BgDisabled,
disabledContentColor: Color = source.Btns.Destructive1.btnDestroy1FgDisabled,
) = ZashiButtonColors(
containerColor = containerColor,
contentColor = contentColor,
@ -242,11 +248,12 @@ object ZashiButtonDefaults {
@Composable
fun destructive2Colors(
containerColor: Color = ZashiColors.Btns.Destructive2.btnDestroy2Bg,
contentColor: Color = ZashiColors.Btns.Destructive2.btnDestroy2Fg,
source: ZashiColorsInternal = ZashiColors,
containerColor: Color = source.Btns.Destructive2.btnDestroy2Bg,
contentColor: Color = source.Btns.Destructive2.btnDestroy2Fg,
borderColor: Color = Color.Unspecified,
disabledContainerColor: Color = ZashiColors.Btns.Destructive2.btnDestroy2BgDisabled,
disabledContentColor: Color = ZashiColors.Btns.Destructive2.btnDestroy2FgDisabled,
disabledContainerColor: Color = source.Btns.Destructive2.btnDestroy2BgDisabled,
disabledContentColor: Color = source.Btns.Destructive2.btnDestroy2FgDisabled,
) = ZashiButtonColors(
containerColor = containerColor,
contentColor = contentColor,
@ -278,7 +285,7 @@ data class ButtonState(
)
@Composable
private fun ZashiButtonColors.toButtonColors() =
fun ZashiButtonColors.toButtonColors() =
ButtonDefaults.buttonColors(
containerColor = containerColor,
contentColor = contentColor,
@ -286,6 +293,12 @@ private fun ZashiButtonColors.toButtonColors() =
disabledContentColor = disabledContentColor,
)
@Suppress("CompositionLocalAllowlist")
val LocalZashiButtonColors =
compositionLocalOf<ZashiButtonColors?> {
null
}
@PreviewScreens
@Composable
private fun PrimaryPreview() =

View File

@ -1,18 +1,24 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
@Composable
fun ZashiCard(
modifier: Modifier = Modifier,
borderColor: Color = Color.Unspecified,
contentPadding: PaddingValues = PaddingValues(24.dp),
content: @Composable ColumnScope.() -> Unit,
) {
Card(
@ -22,9 +28,15 @@ fun ZashiCard(
containerColor = ZashiColors.Surfaces.bgSecondary,
contentColor = ZashiColors.Text.textTertiary
),
border =
if (borderColor.isSpecified) {
BorderStroke(1.dp, borderColor)
} else {
null
}
) {
Column(
Modifier.padding(24.dp)
Modifier.padding(contentPadding)
) {
content()
}

View File

@ -21,7 +21,9 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.R
@ -40,6 +42,9 @@ fun ZashiCheckbox(
isChecked: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
style: TextStyle = ZashiTypography.textSm,
fontWeight: FontWeight = FontWeight.Medium,
color: Color = ZashiColors.Text.textPrimary,
) {
ZashiCheckbox(
state =
@ -49,6 +54,9 @@ fun ZashiCheckbox(
onClick = onClick,
),
modifier = modifier,
style = style,
fontWeight = fontWeight,
color = color,
)
}
@ -56,6 +64,9 @@ fun ZashiCheckbox(
fun ZashiCheckbox(
state: CheckboxState,
modifier: Modifier = Modifier,
style: TextStyle = ZashiTypography.textSm,
fontWeight: FontWeight = FontWeight.Medium,
color: Color = ZashiColors.Text.textPrimary,
) {
Row(
modifier =
@ -70,9 +81,9 @@ fun ZashiCheckbox(
Text(
text = state.text.getValue(),
style = ZashiTypography.textSm,
fontWeight = FontWeight.Medium,
color = ZashiColors.Text.textPrimary,
style = style,
fontWeight = fontWeight,
color = color,
)
}
}

View File

@ -4,7 +4,6 @@ import androidx.annotation.DrawableRes
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -14,7 +13,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -41,30 +39,17 @@ fun ZashiChipButton(
border: BorderStroke? = ZashiChipButtonDefaults.border,
color: Color = ZashiChipButtonDefaults.color,
contentPadding: PaddingValues = ZashiChipButtonDefaults.contentPadding,
hasRippleEffect: Boolean = true,
textStyle: TextStyle = ZashiChipButtonDefaults.textStyle,
endIconSpacer: Dp = ZashiChipButtonDefaults.endIconSpacer,
) {
val clickableModifier =
if (hasRippleEffect) {
modifier.clickable(onClick = state.onClick)
} else {
val interactionSource = remember { MutableInteractionSource() }
modifier.clickable(
onClick = state.onClick,
indication = null,
interactionSource = interactionSource
)
}
Surface(
modifier = clickableModifier,
modifier = modifier,
shape = shape,
border = border,
color = color,
) {
Row(
modifier = Modifier.padding(contentPadding),
modifier = Modifier.clickable(onClick = state.onClick) then Modifier.padding(contentPadding),
verticalAlignment = Alignment.CenterVertically
) {
if (state.startIcon != null) {

View File

@ -0,0 +1,70 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
@Composable
fun ZashiCircularProgressIndicator(
progress: Float,
modifier: Modifier = Modifier,
colors: ZashiCircularProgressIndicatorColors =
LocalZashiCircularProgressIndicatorColors.current
?: ZashiCircularProgressIndicatorDefaults.colors()
) {
val animatedProgress by animateFloatAsState(
progress,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
)
CircularProgressIndicator(
modifier = modifier,
color = colors.progressColor,
trackColor = colors.trackColor,
progress = { animatedProgress },
gapSize = 0.dp,
strokeWidth = 3.dp
)
}
@Composable
fun ZashiCircularProgressIndicatorByPercent(
progressPercent: Float,
modifier: Modifier = Modifier,
colors: ZashiCircularProgressIndicatorColors =
LocalZashiCircularProgressIndicatorColors.current
?: ZashiCircularProgressIndicatorDefaults.colors()
) {
ZashiCircularProgressIndicator(
progress = progressPercent / 100f,
modifier = modifier,
colors = colors
)
}
@Immutable
data class ZashiCircularProgressIndicatorColors(
val progressColor: Color,
val trackColor: Color
)
@Suppress("CompositionLocalAllowlist")
val LocalZashiCircularProgressIndicatorColors = compositionLocalOf<ZashiCircularProgressIndicatorColors?> { null }
object ZashiCircularProgressIndicatorDefaults {
@Composable
fun colors(
progressColor: Color = ZashiColors.Utility.Purple.utilityPurple400,
trackColor: Color = ZashiColors.Utility.Purple.utilityPurple50
) = ZashiCircularProgressIndicatorColors(
progressColor = progressColor,
trackColor = trackColor
)
}

View File

@ -0,0 +1,43 @@
package co.electriccoin.zcash.ui.design.component
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
@Composable
fun ZashiInfoRow(
@DrawableRes icon: Int,
title: String,
subtitle: String,
) {
Row {
Image(
painterResource(icon),
contentDescription = null
)
Spacer(16.dp)
Column {
Spacer(2.dp)
Text(
text = title,
color = ZashiColors.Text.textPrimary,
style = ZashiTypography.textSm,
fontWeight = FontWeight.Medium
)
Spacer(4.dp)
Text(
text = subtitle,
color = ZashiColors.Text.textTertiary,
style = ZashiTypography.textSm
)
}
}
}

View File

@ -0,0 +1,46 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
@Composable
fun ZashiInfoText(
text: String,
modifier: Modifier = Modifier,
color: Color = ZashiColors.Text.textTertiary,
style: TextStyle = ZashiTypography.textXs,
textAlign: TextAlign = TextAlign.Start,
) {
Row(
modifier = modifier,
) {
Image(
modifier = Modifier,
painter = painterResource(R.drawable.ic_info),
contentDescription = null,
colorFilter = ColorFilter.tint(color)
)
Spacer(8.dp)
Text(
modifier =
Modifier
.weight(1f),
text = text,
textAlign = textAlign,
style = style,
color = color
)
}
}

View File

@ -1,38 +0,0 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
@Composable
fun ZashiModal(
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit,
) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(ZashiDimensions.Radius.radius2xl),
border = BorderStroke(1.dp, ZashiColors.Modals.surfaceStroke),
color = ZashiColors.Modals.surfacePrimary
) {
Box(
modifier =
Modifier
.padding(ZashiDimensions.Spacing.spacing3xl)
.wrapContentSize()
.animateContentSize()
) {
content()
}
}
}

View File

@ -90,7 +90,7 @@ fun rememberModalBottomSheetState(
@Composable
@ExperimentalMaterial3Api
private fun rememberSheetState(
fun rememberSheetState(
skipPartiallyExpanded: Boolean,
confirmValueChange: (SheetValue) -> Boolean,
initialValue: SheetValue,

View File

@ -0,0 +1,90 @@
package co.electriccoin.zcash.ui.design.component
import android.view.WindowManager
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.StringResource
import co.electriccoin.zcash.ui.design.util.getValue
@Composable
fun ZashiScreenDialog(
state: DialogState?,
properties: DialogProperties = DialogProperties()
) {
val parent = LocalView.current.parent
SideEffect {
(parent as? DialogWindowProvider)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
(parent as? DialogWindowProvider)?.window?.setDimAmount(0f)
}
state?.let {
Dialog(
positive = state.positive,
negative = state.negative,
onDismissRequest = state.onDismissRequest,
title = state.title,
message = state.message,
properties = properties,
)
}
}
@Composable
private fun Dialog(
positive: ButtonState,
negative: ButtonState,
title: StringResource,
message: StringResource,
onDismissRequest: (() -> Unit),
modifier: Modifier = Modifier,
properties: DialogProperties = DialogProperties()
) {
AlertDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
ZashiButton(state = positive)
},
dismissButton = {
ZashiButton(state = negative, colors = ZashiButtonDefaults.secondaryColors())
},
title = {
Text(
text = title.getValue(),
color = ZashiColors.Text.textPrimary,
style = ZashiTypography.textXl,
fontWeight = FontWeight.SemiBold
)
},
text = {
Text(
text = message.getValue(),
color = ZashiColors.Text.textTertiary,
style = ZashiTypography.textMd
)
},
properties = properties,
containerColor = ZashiColors.Surfaces.bgPrimary,
titleContentColor = ZashiColors.Text.textPrimary,
textContentColor = ZashiColors.Text.textPrimary,
modifier = modifier,
)
}
@Immutable
data class DialogState(
val positive: ButtonState,
val negative: ButtonState,
val onDismissRequest: (() -> Unit),
val title: StringResource,
val message: StringResource,
)

View File

@ -0,0 +1,107 @@
package co.electriccoin.zcash.ui.design.component
import android.view.WindowManager
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue
import androidx.compose.material3.SheetValue.Expanded
import androidx.compose.material3.SheetValue.Hidden
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogWindowProvider
import co.electriccoin.zcash.ui.design.LocalSheetStateManager
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T : ModalBottomSheetState> ZashiScreenModalBottomSheet(
state: T?,
sheetState: SheetState = rememberScreenModalBottomSheetState(),
content: @Composable ColumnScope.(state: T) -> Unit = {},
) {
val parent = LocalView.current.parent
SideEffect {
(parent as? DialogWindowProvider)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
(parent as? DialogWindowProvider)?.window?.setDimAmount(0f)
}
state?.let {
ZashiModalBottomSheet(
sheetState = sheetState,
content = {
BackHandler {
it.onBack()
}
content(it)
Spacer(24.dp)
androidx.compose.foundation.layout.Spacer(
modifier = Modifier.windowInsetsBottomHeight(WindowInsets.systemBars),
)
LaunchedEffect(Unit) {
sheetState.show()
}
},
onDismissRequest = it.onBack
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ZashiScreenModalBottomSheet(
onDismissRequest: () -> Unit,
sheetState: SheetState = rememberScreenModalBottomSheetState(),
content: @Composable () -> Unit = {},
) {
ZashiScreenModalBottomSheet(
state =
remember(onDismissRequest) {
object : ModalBottomSheetState {
override val onBack: () -> Unit = {
onDismissRequest()
}
}
},
sheetState = sheetState,
content = {
content()
},
)
}
@Composable
@ExperimentalMaterial3Api
fun rememberScreenModalBottomSheetState(
initialValue: SheetValue = if (LocalInspectionMode.current) Expanded else Hidden,
skipHiddenState: Boolean = LocalInspectionMode.current,
skipPartiallyExpanded: Boolean = true,
confirmValueChange: (SheetValue) -> Boolean = { true },
): SheetState {
val sheetManager = LocalSheetStateManager.current
val sheetState =
rememberSheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
confirmValueChange = confirmValueChange,
initialValue = initialValue,
skipHiddenState = skipHiddenState,
)
DisposableEffect(sheetState) {
sheetManager.onSheetOpened(sheetState)
onDispose {
sheetManager.onSheetDisposed(sheetState)
}
}
return sheetState
}

View File

@ -0,0 +1,178 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.spackle.AndroidApiVersion
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
@Suppress("MagicNumber")
@Composable
fun ZashiSeedText(
state: SeedTextState,
modifier: Modifier = Modifier
) {
val blur by animateDpAsState(if (state.isRevealed) 0.dp else 14.dp, label = "")
val color by animateColorAsState(
when {
AndroidApiVersion.isAtLeastS -> Color.Unspecified
state.isRevealed -> ZashiColors.Surfaces.bgPrimary
else -> ZashiColors.Surfaces.bgSecondary
},
label = ""
)
Box(
modifier = modifier.background(color, RoundedCornerShape(10.dp)),
) {
val rowItems =
remember(state) {
state.seed
.split(" ")
.withIndex()
.chunked(3)
}
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = spacedBy(4.dp)
) {
rowItems.forEach { row ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = spacedBy(4.dp)
) {
row.forEach { (index, string) ->
ZashiSeedWordText(
modifier = Modifier.weight(1f),
prefix = (index + 1).toString(),
state =
SeedWordTextState(
text = string,
),
content = { mod, text ->
ZashiSeedWordTextContent(
text = text,
modifier = mod.blurCompat(blur, 14.dp)
)
},
prefixContent = { mod, text ->
ZashiSeedWordPrefixContent(
text = text,
modifier =
mod then
if (!AndroidApiVersion.isAtLeastS) {
Modifier.blurCompat(blur, 14.dp)
} else {
Modifier
}
)
}
)
}
}
}
}
AnimatedVisibility(
modifier =
Modifier
.fillMaxWidth()
.align(Alignment.Center),
visible = !AndroidApiVersion.isAtLeastS && state.isRevealed.not(),
enter = fadeIn(),
exit = fadeOut(),
) {
Column(
modifier =
Modifier
.fillMaxWidth()
.padding(vertical = 18.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(R.drawable.ic_reveal),
contentDescription = null,
colorFilter = ColorFilter.tint(ZashiColors.Text.textPrimary)
)
Spacer(Modifier.height(ZashiDimensions.Spacing.spacingMd))
Text(
text = stringResource(R.string.seed_recovery_reveal),
style = ZashiTypography.textLg,
fontWeight = FontWeight.SemiBold,
color = ZashiColors.Text.textPrimary
)
}
}
}
}
@Immutable
data class SeedTextState(
val seed: String,
val isRevealed: Boolean
)
@PreviewScreens
@Composable
private fun Preview() =
ZcashTheme {
BlankSurface {
ZashiSeedText(
modifier = Modifier.fillMaxWidth(),
state =
SeedTextState(
seed = (1..24).joinToString(separator = " ") { "word" },
isRevealed = true,
)
)
}
}
@PreviewScreens
@Composable
private fun HiddenPreview() =
ZcashTheme {
BlankSurface {
ZashiSeedText(
modifier = Modifier.fillMaxWidth(),
state =
SeedTextState(
seed = (1..24).joinToString(separator = " ") { "word" },
isRevealed = false,
)
)
}
}

View File

@ -0,0 +1,291 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.FlowRowOverflow
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreenSizes
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.util.combineToFlow
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun ZashiSeedTextField(
state: SeedTextFieldState,
modifier: Modifier = Modifier,
wordModifier: (index: Int) -> Modifier = { Modifier },
handle: SeedTextFieldHandle = rememberSeedTextFieldHandle(),
) {
val focusManager = LocalFocusManager.current
LaunchedEffect(state.values.map { it.value }) {
val newValues = state.values.map { it.value }
handle.internalState =
handle.internalState.copy(
texts = newValues,
selectedText =
if (handle.internalState.selectedIndex <= -1) {
null
} else {
newValues[handle.internalState.selectedIndex]
}
)
}
LaunchedEffect(handle.selectedIndex) {
if (handle.selectedIndex >= 0) {
handle.focusRequesters[handle.selectedIndex].requestFocus()
} else {
focusManager.clearFocus(true)
}
}
LaunchedEffect(Unit) {
handle.interactions
.observeSelectedIndex()
.collect { index ->
handle.setSelectedIndex(index)
}
}
FlowRow(
modifier = modifier.fillMaxWidth(),
maxItemsInEachRow = 3,
horizontalArrangement = spacedBy(4.dp),
verticalArrangement = spacedBy(4.dp),
overflow = FlowRowOverflow.Visible,
) {
state.values.forEachIndexed { index, wordState ->
val focusRequester = remember { handle.focusRequesters[index] }
val interaction = remember { handle.interactions[index] }
val textFieldHandle = remember { handle.textFieldHandles[index] }
val previousHandle =
remember {
if (index > 0) handle.textFieldHandles[index - 1] else null
}
ZashiSeedWordTextField(
modifier =
Modifier
.weight(1f)
.focusRequester(focusRequester)
.onKeyEvent { event ->
when {
event.key == Key.Spacebar -> {
handle.requestNextFocus()
true
}
event.key == Key.Backspace && wordState.value.isEmpty() -> {
previousHandle?.moveCursorToEnd()
handle.requestPreviousFocus()
true
}
else -> {
false
}
}
},
handle = textFieldHandle,
innerModifier = wordModifier(index),
prefix = (index + 1).toString(),
state = wordState,
keyboardActions =
KeyboardActions(
onDone = {
handle.requestNextFocus()
},
onNext = {
handle.requestNextFocus()
},
),
keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Password,
autoCorrectEnabled = false,
imeAction = if (index == state.values.lastIndex) ImeAction.Done else ImeAction.Next
),
interactionSource = interaction
)
}
}
}
private fun List<MutableInteractionSource>.observeSelectedIndex() =
this
.map { interaction ->
interaction.isFocused()
}.combineToFlow()
.map {
it.indexOfFirst { isFocused -> isFocused }
}
private fun InteractionSource.isFocused(): Flow<Boolean> =
channelFlow {
val focusInteractions = mutableListOf<FocusInteraction.Focus>()
val isFocused = MutableStateFlow(false)
launch {
interactions.collect { interaction ->
when (interaction) {
is FocusInteraction.Focus -> focusInteractions.add(interaction)
is FocusInteraction.Unfocus -> focusInteractions.remove(interaction.focus)
}
isFocused.update { focusInteractions.isNotEmpty() }
}
}
launch {
isFocused.collect {
send(it)
}
}
awaitClose {
// do nothing
}
}
@Immutable
data class SeedTextFieldState(
val values: List<SeedWordTextFieldState>,
)
@Suppress("MagicNumber")
@Stable
class SeedTextFieldHandle(
seedTextFieldState: SeedTextFieldState,
selectedIndex: Int
) {
internal val textFieldHandles = seedTextFieldState.values.map { ZashiTextFieldHandle(it.value) }
internal val interactions = List(24) { MutableInteractionSource() }
internal val focusRequesters = List(24) { FocusRequester() }
internal var internalState by mutableStateOf(
SeedTextFieldInternalState(
selectedIndex = selectedIndex,
selectedText = null,
texts = seedTextFieldState.values.map { it.value }
)
)
val selectedText: String? by derivedStateOf { internalState.selectedText }
val selectedIndex by derivedStateOf { internalState.selectedIndex }
@Suppress("MagicNumber")
fun requestNextFocus() {
internalState =
if (internalState.selectedIndex == 23) {
internalState.copy(
selectedIndex = -1,
selectedText = null,
)
} else {
internalState.copy(
selectedIndex = internalState.selectedIndex + 1,
selectedText = internalState.texts[internalState.selectedIndex + 1],
)
}
}
fun requestPreviousFocus() {
internalState =
if (internalState.selectedIndex >= 1) {
internalState.copy(
selectedIndex = internalState.selectedIndex - 1,
selectedText = internalState.texts[internalState.selectedIndex - 1]
)
} else {
internalState.copy(
selectedIndex = -1,
selectedText = null,
)
}
}
fun setSelectedIndex(index: Int) {
internalState =
internalState.copy(
selectedIndex = index,
selectedText = if (index <= -1) null else internalState.texts[index]
)
}
}
internal data class SeedTextFieldInternalState(
val selectedIndex: Int,
val selectedText: String?,
val texts: List<String>
)
@Suppress("MagicNumber")
@Composable
fun rememberSeedTextFieldHandle(
seedTextFieldState: SeedTextFieldState =
SeedTextFieldState(
List(24) {
SeedWordTextFieldState(
value = "",
onValueChange = {},
isError = false
)
}
),
selectedIndex: Int = -1
): SeedTextFieldHandle = remember { SeedTextFieldHandle(seedTextFieldState, selectedIndex) }
@PreviewScreenSizes
@Composable
private fun Preview() =
ZcashTheme {
BlankSurface {
ZashiSeedTextField(
state =
SeedTextFieldState(
values =
(1..24).map {
SeedWordTextFieldState(
value = "Word",
onValueChange = { },
isError = false
)
}
)
)
}
}

View File

@ -0,0 +1,103 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
@Composable
fun ZashiSeedWordText(
prefix: String,
state: SeedWordTextState,
modifier: Modifier = Modifier,
prefixContent: @Composable (Modifier, String) -> Unit = { mod, text -> ZashiSeedWordPrefixContent(text, mod) },
content: @Composable (Modifier, String) -> Unit = { mod, text -> ZashiSeedWordTextContent(text, mod) }
) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(12.dp),
color = ZashiColors.Surfaces.bgSecondary,
) {
Box(
contentAlignment = Alignment.CenterStart
) {
prefixContent(Modifier, prefix)
Row(
verticalAlignment = Alignment.CenterVertically
) {
content(
Modifier.weight(1f),
state.text
)
}
}
}
}
@Composable
fun ZashiSeedWordPrefixContent(
text: String,
modifier: Modifier = Modifier
) {
Text(
modifier = modifier then Modifier.padding(start = 12.dp),
text = text,
color = ZashiColors.Text.textTertiary,
style = ZashiTypography.textXs,
fontWeight = FontWeight.Medium,
)
}
@Composable
fun ZashiSeedWordTextContent(
text: String,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier then Modifier.padding(start = 32.dp, top = 8.dp, bottom = 10.dp),
) {
Text(
modifier = Modifier,
text = text,
color = ZashiColors.Text.textPrimary,
style = ZashiTypography.textMd,
maxLines = 1,
overflow = TextOverflow.Clip
)
}
}
@Immutable
data class SeedWordTextState(
val text: String
)
@Composable
@PreviewScreens
private fun Preview() =
ZcashTheme {
BlankSurface {
ZashiSeedWordText(
modifier = Modifier.fillMaxWidth(),
prefix = "11",
state =
SeedWordTextState(
text = "asdasdasd",
)
)
}
}

View File

@ -0,0 +1,111 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.stringRes
@Composable
fun ZashiSeedWordTextField(
prefix: String,
state: SeedWordTextFieldState,
modifier: Modifier = Modifier,
innerModifier: Modifier = Modifier,
handle: ZashiTextFieldHandle =
rememberZashiTextFieldHandle(
TextFieldState(
value = stringRes(state.value),
onValueChange = state.onValueChange,
error = stringRes("").takeIf { state.isError }
)
),
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
ZashiTextField(
modifier = modifier,
innerModifier = innerModifier,
shape = RoundedCornerShape(12.dp),
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
singleLine = true,
maxLines = 1,
handle = handle,
interactionSource = interactionSource,
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
state =
TextFieldState(
value = stringRes(state.value),
onValueChange = state.onValueChange,
error = stringRes("").takeIf { state.isError }
),
textStyle = ZashiTypography.textMd,
prefix = {
Box(
modifier =
Modifier
.size(22.dp)
.background(ZashiColors.Tags.tcCountBg, CircleShape)
.padding(end = 1.dp),
contentAlignment = Alignment.Center
) {
Text(
text = prefix,
style = ZashiTypography.textSm,
color = ZashiColors.Tags.tcCountFg,
fontWeight = FontWeight.Medium
)
}
},
colors =
ZashiTextFieldDefaults.defaultColors(
containerColor = ZashiColors.Surfaces.bgSecondary,
focusedContainerColor = ZashiColors.Surfaces.bgPrimary,
focusedBorderColor = ZashiColors.Accordion.focusStroke
),
)
}
@Immutable
data class SeedWordTextFieldState(
val value: String,
val isError: Boolean,
val onValueChange: (String) -> Unit
)
@Composable
@PreviewScreens
private fun Preview() =
ZcashTheme {
BlankSurface {
ZashiSeedWordTextField(
prefix = "12",
state =
SeedWordTextFieldState(
value = "asd",
isError = false,
onValueChange = {},
)
)
}
}

View File

@ -0,0 +1,40 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
@Composable
fun ColumnScope.Spacer(height: Dp) {
Spacer(Modifier.height(height))
}
@Composable
fun ColumnScope.Spacer(weight: Float) {
Spacer(Modifier.weight(weight))
}
@Composable
fun RowScope.Spacer(weight: Float) {
Spacer(Modifier.weight(weight))
}
@Composable
fun RowScope.Spacer(width: Dp) {
Spacer(Modifier.width(width))
}
@Composable
fun HorizontalSpacer(width: Dp) {
Spacer(Modifier.width(width))
}
@Composable
fun VerticalSpacer(height: Dp) {
Spacer(Modifier.height(height))
}

View File

@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@ -22,23 +23,32 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.StringResource
import co.electriccoin.zcash.ui.design.util.getString
import co.electriccoin.zcash.ui.design.util.getValue
import co.electriccoin.zcash.ui.design.util.stringRes
@ -48,9 +58,18 @@ fun ZashiTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
innerModifier: Modifier = Modifier,
innerModifier: Modifier = ZashiTextFieldDefaults.innerModifier,
error: String? = null,
isEnabled: Boolean = true,
handle: ZashiTextFieldHandle =
rememberZashiTextFieldHandle(
TextFieldState(
value = stringRes(value),
error = error?.let { stringRes(it) },
isEnabled = isEnabled,
onValueChange = onValueChange,
)
),
readOnly: Boolean = false,
textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium),
label: @Composable (() -> Unit)? = null,
@ -97,7 +116,8 @@ fun ZashiTextField(
interactionSource = interactionSource,
shape = shape,
colors = colors,
innerModifier = innerModifier
innerModifier = innerModifier,
handle = handle,
)
}
@ -106,7 +126,8 @@ fun ZashiTextField(
fun ZashiTextField(
state: TextFieldState,
modifier: Modifier = Modifier,
innerModifier: Modifier = Modifier,
innerModifier: Modifier = ZashiTextFieldDefaults.innerModifier,
handle: ZashiTextFieldHandle = rememberZashiTextFieldHandle(state),
readOnly: Boolean = false,
textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium),
label: @Composable (() -> Unit)? = null,
@ -124,6 +145,13 @@ fun ZashiTextField(
minLines: Int = 1,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = ZashiTextFieldDefaults.shape,
contentPadding: PaddingValues =
PaddingValues(
start = if (leadingIcon != null) 8.dp else 14.dp,
end = if (suffix != null) 4.dp else 12.dp,
top = getVerticalPadding(trailingIcon, leadingIcon, suffix, prefix),
bottom = getVerticalPadding(trailingIcon, leadingIcon, suffix, prefix),
),
colors: ZashiTextFieldColors = ZashiTextFieldDefaults.defaultColors()
) {
TextFieldInternal(
@ -147,10 +175,41 @@ fun ZashiTextField(
interactionSource = interactionSource,
shape = shape,
colors = colors,
innerModifier = innerModifier
contentPadding = contentPadding,
innerModifier = innerModifier,
handle = handle
)
}
@Composable
fun ZashiTextFieldPlaceholder(res: StringResource) {
Text(
text = res.getValue(),
style = ZashiTypography.textMd,
color = ZashiColors.Inputs.Default.text
)
}
@Stable
class ZashiTextFieldHandle(
text: String
) {
var textFieldValueState by mutableStateOf(TextFieldValue(text = text))
fun moveCursorToEnd() {
textFieldValueState =
textFieldValueState.copy(
selection = TextRange(textFieldValueState.text.length),
)
}
}
@Composable
fun rememberZashiTextFieldHandle(state: TextFieldState): ZashiTextFieldHandle {
val context = LocalContext.current
return remember { ZashiTextFieldHandle(state.value.getString(context)) }
}
@Suppress("LongParameterList", "LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -174,10 +233,32 @@ private fun TextFieldInternal(
interactionSource: MutableInteractionSource,
shape: Shape,
colors: ZashiTextFieldColors,
contentPadding: PaddingValues,
handle: ZashiTextFieldHandle,
modifier: Modifier = Modifier,
innerModifier: Modifier = Modifier,
) {
val borderColor by colors.borderColor(state)
val context = LocalContext.current
val value = remember(state.value) { state.value.getString(context) }
// Holds the latest internal TextFieldValue state. We need to keep it to have the correct value
// of the composition.
val textFieldValueState = handle.textFieldValueState
// Holds the latest TextFieldValue that BasicTextField was recomposed with. We couldn't simply
// pass `TextFieldValue(text = value)` to the CoreTextField because we need to preserve the
// composition.
val textFieldValue = textFieldValueState.copy(text = value, selection = textFieldValueState.selection)
SideEffect {
if (textFieldValue.text != textFieldValueState.text ||
textFieldValue.selection != textFieldValueState.selection ||
textFieldValue.composition != textFieldValueState.composition
) {
handle.textFieldValueState = textFieldValue
}
}
val isFocused by interactionSource.collectIsFocusedAsState()
val borderColor by colors.borderColor(state, isFocused)
val androidColors = colors.toTextFieldColors()
// If color is not provided via the text style, use content color as a default
val textColor =
@ -186,24 +267,35 @@ private fun TextFieldInternal(
}
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
var lastTextValue by remember(value) { mutableStateOf(value) }
CompositionLocalProvider(LocalTextSelectionColors provides androidColors.selectionColors) {
Column(
modifier = modifier,
) {
BasicTextField(
value = state.value.getValue(),
value = textFieldValue,
modifier =
innerModifier.fillMaxWidth() then
innerModifier then
if (borderColor == Color.Unspecified) {
Modifier
} else {
Modifier.border(
width = 1.dp,
color = borderColor,
shape = ZashiTextFieldDefaults.shape
shape = shape
)
} then Modifier.defaultMinSize(minWidth = TextFieldDefaults.MinWidth),
onValueChange = state.onValueChange,
},
onValueChange = { newTextFieldValueState ->
handle.textFieldValueState = newTextFieldValueState
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
lastTextValue = newTextFieldValueState.text
if (stringChangedSinceLastInvocation) {
state.onValueChange(newTextFieldValueState.text)
}
},
enabled = state.isEnabled,
readOnly = readOnly,
textStyle = mergedTextStyle,
@ -215,44 +307,37 @@ private fun TextFieldInternal(
singleLine = singleLine,
maxLines = maxLines,
minLines = minLines,
decorationBox = @Composable { innerTextField ->
// places leading icon, text field with label and placeholder, trailing icon
TextFieldDefaults.DecorationBox(
value = state.value.getValue(),
visualTransformation = visualTransformation,
innerTextField = {
DecorationBox(prefix = prefix, suffix = suffix, content = innerTextField)
) { innerTextField: @Composable () -> Unit ->
// places leading icon, text field with label and placeholder, trailing icon
TextFieldDefaults.DecorationBox(
value = state.value.getValue(),
visualTransformation = visualTransformation,
innerTextField = {
DecorationBox(prefix = prefix, suffix = suffix, content = innerTextField)
},
placeholder =
if (placeholder != null) {
{
DecorationBox(prefix, suffix, placeholder)
}
} else {
null
},
placeholder =
if (placeholder != null) {
{
DecorationBox(prefix, suffix, placeholder)
}
} else {
null
},
label = label,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
prefix = prefix,
suffix = suffix,
supportingText = supportingText,
shape = shape,
singleLine = singleLine,
enabled = state.isEnabled,
isError = state.isError,
interactionSource = interactionSource,
colors = androidColors,
contentPadding =
PaddingValues(
start = if (leadingIcon != null) 8.dp else 14.dp,
end = if (suffix != null) 4.dp else 12.dp,
top = getVerticalPadding(trailingIcon, leadingIcon, suffix, prefix),
bottom = getVerticalPadding(trailingIcon, leadingIcon, suffix, prefix),
)
)
}
)
label = label,
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
prefix = prefix,
suffix = suffix,
supportingText = supportingText,
shape = shape,
singleLine = singleLine,
enabled = state.isEnabled,
isError = state.isError,
interactionSource = interactionSource,
colors = androidColors,
contentPadding = contentPadding
)
}
if (state.error != null && state.error.getValue().isNotEmpty()) {
Spacer(modifier = Modifier.height(6.dp))
@ -303,7 +388,9 @@ data class ZashiTextFieldColors(
val textColor: Color,
val hintColor: Color,
val borderColor: Color,
val focusedBorderColor: Color,
val containerColor: Color,
val focusedContainerColor: Color,
val placeholderColor: Color,
val disabledTextColor: Color,
val disabledHintColor: Color,
@ -317,11 +404,15 @@ data class ZashiTextFieldColors(
val errorPlaceholderColor: Color,
) {
@Composable
internal fun borderColor(state: TextFieldState): State<Color> {
internal fun borderColor(
state: TextFieldState,
isFocused: Boolean
): State<Color> {
val targetValue =
when {
!state.isEnabled -> disabledBorderColor
state.isError -> errorBorderColor
isFocused -> focusedBorderColor.takeOrElse { borderColor }
else -> borderColor
}
return rememberUpdatedState(targetValue)
@ -345,7 +436,7 @@ data class ZashiTextFieldColors(
unfocusedTextColor = textColor,
disabledTextColor = disabledTextColor,
errorTextColor = errorTextColor,
focusedContainerColor = containerColor,
focusedContainerColor = focusedContainerColor.takeOrElse { containerColor },
unfocusedContainerColor = containerColor,
disabledContainerColor = disabledContainerColor,
errorContainerColor = errorContainerColor,
@ -391,13 +482,21 @@ object ZashiTextFieldDefaults {
val shape: Shape
get() = RoundedCornerShape(8.dp)
val innerModifier: Modifier
get() =
Modifier
.defaultMinSize(minWidth = TextFieldDefaults.MinWidth)
.fillMaxWidth()
@Suppress("LongParameterList")
@Composable
fun defaultColors(
textColor: Color = ZashiColors.Inputs.Filled.text,
hintColor: Color = ZashiColors.Inputs.Default.hint,
borderColor: Color = Color.Unspecified,
focusedBorderColor: Color = ZashiColors.Inputs.Focused.stroke,
containerColor: Color = ZashiColors.Inputs.Default.bg,
focusedContainerColor: Color = ZashiColors.Inputs.Focused.bg,
placeholderColor: Color = ZashiColors.Inputs.Default.text,
disabledTextColor: Color = ZashiColors.Inputs.Disabled.text,
disabledHintColor: Color = ZashiColors.Inputs.Disabled.hint,
@ -413,7 +512,9 @@ object ZashiTextFieldDefaults {
textColor = textColor,
hintColor = hintColor,
borderColor = borderColor,
focusedBorderColor = focusedBorderColor,
containerColor = containerColor,
focusedContainerColor = focusedContainerColor,
placeholderColor = placeholderColor,
disabledTextColor = disabledTextColor,
disabledHintColor = disabledHintColor,
@ -428,6 +529,16 @@ object ZashiTextFieldDefaults {
)
}
@Immutable
data class TextFieldState(
val value: StringResource,
val error: StringResource? = null,
val isEnabled: Boolean = true,
val onValueChange: (String) -> Unit,
) {
val isError = error != null
}
@PreviewScreens
@Composable
private fun DefaultPreview() =

View File

@ -0,0 +1,416 @@
@file:Suppress("TooManyFunctions")
package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import kotlinx.coroutines.launch
import java.text.DateFormatSymbols
import java.time.Month
import java.time.Year
import java.time.YearMonth
import kotlin.math.absoluteValue
import kotlin.math.pow
@Suppress("MagicNumber")
@Composable
fun ZashiYearMonthWheelDatePicker(
selection: YearMonth,
onSelectionChange: (YearMonth) -> Unit,
modifier: Modifier = Modifier,
verticallyVisibleItems: Int = 3,
startInclusive: YearMonth = YearMonth.of(2018, 10),
endInclusive: YearMonth = YearMonth.now(),
) {
val latestOnSelectionChanged by rememberUpdatedState(onSelectionChange)
var state by remember {
mutableStateOf(
InternalState(
selectedDate = selection,
months = getMonthsForYear(Year.of(selection.year), startInclusive, endInclusive),
years = (startInclusive.year..endInclusive.year).map { Year.of(it) }.toList()
)
)
}
LaunchedEffect(state.selectedDate) {
Twig.debug { "Selection changed: ${state.selectedDate}" }
latestOnSelectionChanged(state.selectedDate)
}
Box(modifier = modifier) {
Row(
modifier =
Modifier
.fillMaxWidth()
.align(Alignment.Center),
) {
Box(
modifier =
Modifier
.weight(1f)
.height(34.dp)
.padding(top = 1.dp)
.background(ZashiColors.Surfaces.bgSecondary, RoundedCornerShape(6.dp))
)
Spacer(36.dp)
Box(
modifier =
Modifier
.weight(1f)
.height(34.dp)
.padding(top = 1.dp)
.background(ZashiColors.Surfaces.bgSecondary, RoundedCornerShape(6.dp))
)
}
Row(
horizontalArrangement = Arrangement.Center
) {
WheelLazyList(
modifier = Modifier.weight(1f),
selection = state.selectedMonthIndex,
itemCount = state.months.size,
itemVerticalOffset = verticallyVisibleItems,
isInfiniteScroll = false,
onFocusItem = {
state =
state.copy(
selectedDate = state.selectedDate.withMonth(state.months[it].value)
)
},
itemContent = {
Text(
text = DateFormatSymbols().months[state.months[it].value - 1],
textAlign = TextAlign.Center,
modifier = Modifier.fillParentMaxWidth(),
style = ZashiTypography.header6,
color = ZashiColors.Text.textPrimary,
maxLines = 1
)
}
)
Spacer(36.dp)
WheelLazyList(
modifier = Modifier.weight(1f),
selection = state.selectedYearIndex,
itemCount = state.years.size,
itemVerticalOffset = verticallyVisibleItems,
isInfiniteScroll = false,
onFocusItem = {
val year = state.years[it]
val normalizedSelectedMonth =
getSelectedMonthForYear(
year = year,
selectedMonth = state.selectedDate.month,
startYearMonth = startInclusive,
endYearMonth = endInclusive
)
val months = getMonthsForYear(year, startInclusive, endInclusive)
val selectedDate = state.selectedDate.withYear(year.value).withMonth(normalizedSelectedMonth.value)
state =
state.copy(
selectedDate = selectedDate,
months = months
)
},
itemContent = {
Text(
text = state.years[it].toString(),
textAlign = TextAlign.Center,
modifier = Modifier.fillParentMaxWidth(),
style = ZashiTypography.header6,
color = ZashiColors.Text.textPrimary,
maxLines = 1
)
}
)
}
}
}
private fun getMonthsForYear(year: Year, startYearMonth: YearMonth, endYearMonth: YearMonth): List<Month> =
when (year.value) {
startYearMonth.year -> {
(startYearMonth.month.value..Month.DECEMBER.value).map { index ->
Month.entries.first { it.value == index }
}
}
endYearMonth.year -> {
(Month.JANUARY.value..endYearMonth.month.value).map { index ->
Month.entries.first { it.value == index }
}
}
else -> {
listOf(
Month.JANUARY,
Month.FEBRUARY,
Month.MARCH,
Month.APRIL,
Month.MAY,
Month.JUNE,
Month.JULY,
Month.AUGUST,
Month.SEPTEMBER,
Month.OCTOBER,
Month.NOVEMBER,
Month.DECEMBER
)
}
}
private fun getSelectedMonthForYear(
year: Year,
selectedMonth: Month,
startYearMonth: YearMonth,
endYearMonth: YearMonth
): Month =
when (year.value) {
startYearMonth.year -> {
val months =
(startYearMonth.month.value..Month.DECEMBER.value).map { index ->
Month.entries.first { it.value == index }
}
if (selectedMonth in months) selectedMonth else months.findClosest(selectedMonth)
}
endYearMonth.year -> {
val months =
(Month.JANUARY.value..endYearMonth.month.value).map { index ->
Month.entries.first { it.value == index }
}
if (selectedMonth in months) selectedMonth else months.findClosest(selectedMonth)
}
else -> selectedMonth
}
private fun List<Month>.findClosest(target: Month): Month {
var closestNumber = this[0] // Initialize with the first element
var minDifference = (this[0].value - target.value).absoluteValue
for (number in this) {
val difference = (number.value - target.value).absoluteValue
if (difference < minDifference) {
minDifference = difference
closestNumber = number
}
}
return closestNumber
}
@Suppress("MagicNumber", "ContentSlotReused")
@Composable
private fun WheelLazyList(
itemCount: Int,
selection: Int,
itemVerticalOffset: Int,
onFocusItem: (Int) -> Unit,
isInfiniteScroll: Boolean,
modifier: Modifier = Modifier,
itemContent: @Composable LazyItemScope.(index: Int) -> Unit,
) {
val latestOnFocusItem by rememberUpdatedState(onFocusItem)
val coroutineScope = rememberCoroutineScope()
val haptic = LocalHapticFeedback.current
val count = if (isInfiniteScroll) itemCount else itemCount + 2 * itemVerticalOffset
val rowOffsetCount = maxOf(1, minOf(itemVerticalOffset, 4))
val rowCount = (rowOffsetCount * 2) + 1
val startIndex = if (isInfiniteScroll) selection + (itemCount * 1000) - itemVerticalOffset else selection
val state = rememberLazyListState(startIndex)
val itemHeightPx = with(LocalDensity.current) { 27.dp.toPx() }
val height = 32.dp * rowCount
val isScrollInProgress = state.isScrollInProgress
LaunchedEffect(itemCount) {
state.scrollToItem(startIndex)
}
LaunchedEffect(key1 = isScrollInProgress) {
if (!isScrollInProgress) {
calculateIndexToFocus(state, height).let {
val indexToFocus =
if (isInfiniteScroll) {
(it + rowOffsetCount) % itemCount
} else {
((it + rowOffsetCount) % count) - itemVerticalOffset
}
latestOnFocusItem(indexToFocus)
if (state.firstVisibleItemScrollOffset != 0) {
coroutineScope.launch {
state.animateScrollToItem(it, 0)
}
}
}
}
}
LaunchedEffect(state) {
snapshotFlow { state.firstVisibleItemIndex }
.collect {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}
}
Box(
modifier =
modifier
.height(height)
.fillMaxWidth(),
) {
LazyColumn(
modifier =
Modifier
.height(height)
.fillMaxWidth(),
state = state,
) {
items(if (isInfiniteScroll) Int.MAX_VALUE else count) { index ->
val (scale, alpha, translationY) =
remember {
derivedStateOf {
val info = state.layoutInfo
val middleOffset = info.viewportSize.height / 2
val item = info.visibleItemsInfo.firstOrNull { it.index == index }
val scrollOffset = if (item != null) item.offset + item.size / 2 else -1
val coefficient = calculateCoefficient(middleOffset = middleOffset, offset = scrollOffset)
val scale = calculateScale(coefficient)
val alpha = calculateAlpha(coefficient)
val translationY =
calculateTranslationY(
coefficient = coefficient,
itemHeightPx = itemHeightPx,
middleOffset = middleOffset,
offset = scrollOffset
)
Triple(scale, alpha, translationY)
}
}.value
Box(
modifier =
Modifier
.height(height / rowCount)
.fillMaxWidth()
.graphicsLayer {
this.alpha = alpha
this.scaleX = scale
this.scaleY = scale
this.translationY = translationY
},
contentAlignment = Alignment.Center,
) {
if (isInfiniteScroll) {
itemContent(index % itemCount)
} else if (index >= rowOffsetCount && index < itemCount + rowOffsetCount) {
itemContent((index - rowOffsetCount) % itemCount)
}
}
}
}
}
}
@Suppress("MagicNumber")
private fun calculateCoefficient(
middleOffset: Int,
offset: Int
): Float {
val diff = if (middleOffset > offset) middleOffset - offset else offset - middleOffset
return (1f - (diff.toFloat() / middleOffset.toFloat())).coerceAtLeast(0f)
}
@Suppress("MagicNumber")
private fun calculateScale(coefficient: Float): Float = coefficient.coerceAtLeast(.6f)
@Suppress("MagicNumber")
private fun calculateAlpha(coefficient: Float): Float = coefficient.pow(1.1f)
@Suppress("MagicNumber")
private fun calculateTranslationY(
coefficient: Float,
itemHeightPx: Float,
middleOffset: Int,
offset: Int
): Float {
// if (coefficient in 0.66f..1f) return 0f
val exponentialCoefficient = 1.2f - 5f.pow(-(coefficient))
val offsetBy = (1 - exponentialCoefficient) * itemHeightPx
return if (middleOffset > offset) offsetBy else -offsetBy
}
@Suppress("MagicNumber")
private fun calculateIndexToFocus(
listState: LazyListState,
height: Dp
): Int {
val currentItem = listState.layoutInfo.visibleItemsInfo.firstOrNull()
var index = currentItem?.index ?: 0
if (currentItem?.offset != 0 && currentItem != null && currentItem.offset <= -height.value * 3 / 10) {
index++
}
return index
}
@Immutable
private data class InternalState(
val selectedDate: YearMonth,
val months: List<Month>,
val years: List<Year>
) {
val selectedYearIndex = years.map { it.value }.indexOf(selectedDate.year)
val selectedMonthIndex = maxOf(months.indexOf(selectedDate.month), 0)
}
@PreviewScreens
@Composable
private fun Preview() =
ZcashTheme {
BlankSurface {
ZashiYearMonthWheelDatePicker(
selection = YearMonth.now(),
onSelectionChange = {}
)
}
}

View File

@ -103,6 +103,10 @@ private fun ContactItemLeading(
)
}
}
ImageResource.Loading -> {
// do nothing
}
}
}

View File

@ -66,6 +66,10 @@ fun ZashiCheckboxListItem(
textAlign = TextAlign.Center,
fontWeight = FontWeight.SemiBold,
)
ImageResource.Loading -> {
// do nothing
}
}
}
},

View File

@ -9,6 +9,10 @@ import androidx.compose.material3.RippleConfiguration
import androidx.compose.material3.RippleDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import co.electriccoin.zcash.ui.design.LocalKeyboardManager
import co.electriccoin.zcash.ui.design.LocalSheetStateManager
import co.electriccoin.zcash.ui.design.rememberKeyboardManager
import co.electriccoin.zcash.ui.design.rememberSheetStateManager
import co.electriccoin.zcash.ui.design.theme.balances.LocalBalancesAvailable
import co.electriccoin.zcash.ui.design.theme.colors.DarkZashiColorsInternal
import co.electriccoin.zcash.ui.design.theme.colors.LightZashiColorsInternal
@ -49,7 +53,9 @@ fun ZcashTheme(
LocalZashiColors provides zashiColors,
LocalZashiTypography provides ZashiTypographyInternal,
LocalRippleConfiguration provides MaterialRippleConfig,
LocalBalancesAvailable provides balancesAvailable
LocalBalancesAvailable provides balancesAvailable,
LocalKeyboardManager provides rememberKeyboardManager(),
LocalSheetStateManager provides rememberSheetStateManager()
) {
ProvideDimens {
MaterialTheme(

View File

@ -543,7 +543,8 @@ val DarkZashiColorsInternal =
utilityPurple50 = Purple.`950`,
utilityPurple100 = Purple.`900`,
utilityPurple400 = Purple.`600`,
utilityPurple300 = Purple.`700`
utilityPurple300 = Purple.`700`,
utilityPurple900 = Purple.`50`
),
Espresso =
UtilityEspresso(
@ -551,12 +552,13 @@ val DarkZashiColorsInternal =
utilityEspresso600 = Espresso.`300`,
utilityEspresso500 = Espresso.`400`,
utilityEspresso200 = Espresso.`700`,
utilityEspresso50 = Espresso.`900`,
utilityEspresso100 = Espresso.`800`,
utilityEspresso50 = Espresso.`950`,
utilityEspresso100 = Espresso.`900`,
utilityEspresso400 = Espresso.`500`,
utilityEspresso300 = Espresso.`600`,
utilityEspresso800 = Espresso.`100`,
utilityEspresso900 = Espresso.`50`,
utilityEspresso800 = Espresso.`100`
utilityEspresso950 = Espresso.`25`
)
),
Transparent =

View File

@ -543,7 +543,8 @@ val LightZashiColorsInternal =
utilityPurple50 = Purple.`50`,
utilityPurple100 = Purple.`100`,
utilityPurple400 = Purple.`400`,
utilityPurple300 = Purple.`300`
utilityPurple300 = Purple.`300`,
utilityPurple900 = Purple.`900`
),
Espresso =
UtilityEspresso(
@ -556,7 +557,8 @@ val LightZashiColorsInternal =
utilityEspresso400 = Espresso.`400`,
utilityEspresso300 = Espresso.`300`,
utilityEspresso900 = Espresso.`900`,
utilityEspresso800 = Espresso.`800`
utilityEspresso800 = Espresso.`800`,
utilityEspresso950 = Espresso.`950`
)
),
Transparent =

View File

@ -8,5 +8,17 @@ import androidx.compose.runtime.staticCompositionLocalOf
val ZashiColors: ZashiColorsInternal
@Composable get() = LocalZashiColors.current
val ZashiLightColors: ZashiColorsInternal
@Composable get() = LocalLightZashiColors.current
val ZashiDarkColors: ZashiColorsInternal
@Composable get() = LocalDarkZashiColors.current
@Suppress("CompositionLocalAllowlist")
internal val LocalZashiColors = staticCompositionLocalOf<ZashiColorsInternal> { error("no colors specified") }
@Suppress("CompositionLocalAllowlist")
internal val LocalLightZashiColors = staticCompositionLocalOf { LightZashiColorsInternal }
@Suppress("CompositionLocalAllowlist")
internal val LocalDarkZashiColors = staticCompositionLocalOf { DarkZashiColorsInternal }

View File

@ -647,7 +647,8 @@ data class UtilityPurple(
val utilityPurple50: Color,
val utilityPurple100: Color,
val utilityPurple400: Color,
val utilityPurple300: Color
val utilityPurple300: Color,
val utilityPurple900: Color
)
@Immutable
@ -661,6 +662,7 @@ data class UtilityEspresso(
val utilityEspresso400: Color,
val utilityEspresso300: Color,
val utilityEspresso900: Color,
val utilityEspresso950: Color,
val utilityEspresso800: Color
)

View File

@ -1,4 +1,4 @@
package co.electriccoin.zcash.ui.util
package co.electriccoin.zcash.ui.design.util
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine

View File

@ -17,6 +17,9 @@ sealed interface ImageResource {
value class DisplayString(
val value: String
) : ImageResource
@Immutable
data object Loading : ImageResource
}
@Stable
@ -26,3 +29,6 @@ fun imageRes(
@Stable
fun imageRes(value: String): ImageResource = ImageResource.DisplayString(value)
@Stable
fun loadingImageRes(): ImageResource = ImageResource.Loading

View File

@ -57,8 +57,17 @@ sealed interface StringResource {
val address: String,
val abbreviated: Boolean
) : StringResource
operator fun plus(other: StringResource): StringResource = CompositeStringResource(listOf(this, other))
operator fun plus(other: String): StringResource = CompositeStringResource(listOf(this, stringRes(other)))
}
@Immutable
private data class CompositeStringResource(
val resources: List<StringResource>
) : StringResource
@Stable
fun stringRes(
@StringRes resource: Int,
@ -109,18 +118,15 @@ fun StringResource.getValue(
convertYearMonth: (YearMonth) -> String = StringResourceDefaults::convertYearMonth,
convertAddress: (StringResource.ByAddress) -> String = StringResourceDefaults::convertAddress,
convertTransactionId: (StringResource.ByTransactionId) -> String = StringResourceDefaults::convertTransactionId
) = when (this) {
is StringResource.ByResource -> {
val context = LocalContext.current
context.getString(resource, *args.normalize(context).toTypedArray())
}
is StringResource.ByString -> value
is StringResource.ByZatoshi -> convertZatoshi(zatoshi)
is StringResource.ByDateTime -> convertDateTime(this)
is StringResource.ByYearMonth -> convertYearMonth(yearMonth)
is StringResource.ByAddress -> convertAddress(this)
is StringResource.ByTransactionId -> convertTransactionId(this)
}
): String =
getString(
context = LocalContext.current,
convertZatoshi = convertZatoshi,
convertDateTime = convertDateTime,
convertYearMonth = convertYearMonth,
convertAddress = convertAddress,
convertTransactionId = convertTransactionId
)
@Suppress("SpreadOperator")
fun StringResource.getString(
@ -130,15 +136,27 @@ fun StringResource.getString(
convertYearMonth: (YearMonth) -> String = StringResourceDefaults::convertYearMonth,
convertAddress: (StringResource.ByAddress) -> String = StringResourceDefaults::convertAddress,
convertTransactionId: (StringResource.ByTransactionId) -> String = StringResourceDefaults::convertTransactionId
) = when (this) {
is StringResource.ByResource -> context.getString(resource, *args.normalize(context).toTypedArray())
is StringResource.ByString -> value
is StringResource.ByZatoshi -> convertZatoshi(zatoshi)
is StringResource.ByDateTime -> convertDateTime(this)
is StringResource.ByYearMonth -> convertYearMonth(yearMonth)
is StringResource.ByAddress -> convertAddress(this)
is StringResource.ByTransactionId -> convertTransactionId(this)
}
): String =
when (this) {
is StringResource.ByResource -> context.getString(resource, *args.normalize(context).toTypedArray())
is StringResource.ByString -> value
is StringResource.ByZatoshi -> convertZatoshi(zatoshi)
is StringResource.ByDateTime -> convertDateTime(this)
is StringResource.ByYearMonth -> convertYearMonth(yearMonth)
is StringResource.ByAddress -> convertAddress(this)
is StringResource.ByTransactionId -> convertTransactionId(this)
is CompositeStringResource ->
this.resources.joinToString(separator = "") {
it.getString(
context = context,
convertZatoshi = convertZatoshi,
convertDateTime = convertDateTime,
convertYearMonth = convertYearMonth,
convertAddress = convertAddress,
convertTransactionId = convertTransactionId,
)
}
}
private fun List<Any>.normalize(context: Context): List<Any> =
this.map {

View File

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<group>
<clip-path
android:pathData="M0,0h20v20h-20z"/>
<path
android:pathData="M10,13.333V10M10,6.667H10.008M18.333,10C18.333,14.602 14.602,18.333 10,18.333C5.398,18.333 1.667,14.602 1.667,10C1.667,5.397 5.398,1.666 10,1.666C14.602,1.666 18.333,5.397 18.333,10Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<group>
<clip-path
android:pathData="M0,0h20v20h-20z"/>
<path
android:pathData="M10,13.333V10M10,6.667H10.008M18.333,10C18.333,14.602 14.602,18.333 10,18.333C5.398,18.333 1.667,14.602 1.667,10C1.667,5.398 5.398,1.667 10,1.667C14.602,1.667 18.333,5.398 18.333,10Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -3,4 +3,5 @@
<string name="hide_balance_placeholder">-----</string>
<string name="back_navigation_content_description">Atrás</string>
<string name="triple_dots"></string>
<string name="seed_recovery_reveal">Mostrar frase de recuperación</string>
</resources>

View File

@ -3,4 +3,5 @@
<string name="hide_balance_placeholder">-----</string>
<string name="back_navigation_content_description">Back</string>
<string name="triple_dots"></string>
<string name="seed_recovery_reveal">Reveal recovery phrase</string>
</resources>

View File

@ -8,8 +8,8 @@ import androidx.test.filters.LargeTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.integration.test.common.IntegrationTestingActivity
import co.electriccoin.zcash.ui.integration.test.common.getPermissionPositiveButtonUiObject
import co.electriccoin.zcash.ui.screen.scan.ScanScreenState
import co.electriccoin.zcash.ui.screen.scan.ScanTag
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Before

View File

@ -14,8 +14,8 @@ import co.electriccoin.zcash.ui.integration.test.common.getPermissionPositiveBut
import co.electriccoin.zcash.ui.integration.test.common.getStringResource
import co.electriccoin.zcash.ui.integration.test.common.getStringResourceWithArgs
import co.electriccoin.zcash.ui.integration.test.common.waitForDeviceIdle
import co.electriccoin.zcash.ui.screen.scan.ScanScreenState
import co.electriccoin.zcash.ui.screen.scan.ScanTag
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue

View File

@ -3,13 +3,12 @@ package co.electriccoin.zcash.ui.integration.test.screen.scan.view
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.integration.test.common.getPermissionNegativeButtonUiObject
import co.electriccoin.zcash.ui.integration.test.common.getPermissionPositiveButtonUiObject
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
import co.electriccoin.zcash.ui.screen.scan.model.ScanValidationState
import co.electriccoin.zcash.ui.screen.scan.view.Scan
import co.electriccoin.zcash.ui.screen.scan.Scan
import co.electriccoin.zcash.ui.screen.scan.ScanScreenState
import co.electriccoin.zcash.ui.screen.scan.ScanValidationState
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import java.util.concurrent.atomic.AtomicInteger
@ -60,7 +59,6 @@ class ScanViewTestSetup(
onScanStateChange = {
scanState.set(it)
},
topAppBarSubTitleState = TopAppBarSubTitleState.None,
validationResult = ScanValidationState.VALID
)
}

View File

@ -50,6 +50,7 @@ android {
"src/main/res/ui/crash_reporting_opt_in",
"src/main/res/ui/delete_wallet",
"src/main/res/ui/export_data",
"src/main/res/ui/error",
"src/main/res/ui/home",
"src/main/res/ui/choose_server",
"src/main/res/ui/integrations",

View File

@ -1,20 +0,0 @@
package co.electriccoin.zcash.ui.fixture
import cash.z.ecc.android.sdk.fixture.WalletFixture
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.type.AddressType
import co.electriccoin.zcash.ui.common.model.SerializableAddress
import co.electriccoin.zcash.ui.screen.send.model.SendArguments
internal object SendArgumentsWrapperFixture {
val RECIPIENT_ADDRESS =
SerializableAddress(
address = WalletFixture.Alice.getAddresses(ZcashNetwork.Testnet).unified,
type = AddressType.Unified
)
fun new(recipientAddress: SerializableAddress? = RECIPIENT_ADDRESS) =
SendArguments(
recipientAddress = recipientAddress?.toRecipient(),
)
}

View File

@ -2,7 +2,6 @@ package co.electriccoin.zcash.ui.screen.about.view
import androidx.compose.material3.SnackbarHostState
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo
@ -28,7 +27,6 @@ class AboutViewTestSetup(
configInfo = configInfo,
onPrivacyPolicy = {},
snackbarHostState = SnackbarHostState(),
topAppBarSubTitleState = TopAppBarSubTitleState.None,
versionInfo = versionInfo,
)
}

View File

@ -1,90 +0,0 @@
package co.electriccoin.zcash.ui.screen.account
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.component.IconButtonState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.fixture.BalanceStateFixture
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture
import co.electriccoin.zcash.ui.screen.account.view.Account
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetStateFixture
import java.util.concurrent.atomic.AtomicInteger
class AccountTestSetup(
private val composeTestRule: ComposeContentTestRule,
private val walletSnapshot: WalletSnapshot,
) {
// TODO [#1282]: Update AccountView Tests #1282
// TODO [#1282]: https://github.com/Electric-Coin-Company/zashi-android/issues/1282
private val onSettingsCount = AtomicInteger(0)
private val onHideBalancesCount = AtomicInteger(0)
fun getOnSettingsCount(): Int {
composeTestRule.waitForIdle()
return onSettingsCount.get()
}
fun getOnHideBalancesCount(): Int {
composeTestRule.waitForIdle()
return onHideBalancesCount.get()
}
fun getWalletSnapshot(): WalletSnapshot {
composeTestRule.waitForIdle()
return walletSnapshot
}
@Composable
@Suppress("TestFunctionName")
fun DefaultContent(isHideBalances: Boolean) {
Account(
balanceState = BalanceStateFixture.new(),
goBalances = {},
hideStatusDialog = {},
isHideBalances = isHideBalances,
onContactSupport = {},
showStatusDialog = null,
snackbarHostState = SnackbarHostState(),
zashiMainTopAppBarState =
ZashiMainTopAppBarStateFixture.new(
settingsButton =
IconButtonState(
icon = R.drawable.ic_app_bar_settings,
contentDescription =
stringRes(co.electriccoin.zcash.ui.R.string.settings_menu_content_description),
) {
onSettingsCount.incrementAndGet()
},
balanceVisibilityButton =
IconButtonState(
icon = R.drawable.ic_app_bar_balances_hide,
contentDescription =
stringRes(
co.electriccoin.zcash.ui.R.string.hide_balances_content_description
),
) {
onHideBalancesCount.incrementAndGet()
},
),
transactionHistoryWidgetState = TransactionHistoryWidgetStateFixture.new(),
isWalletRestoringState = WalletRestoringState.NONE,
walletSnapshot = WalletSnapshotFixture.new(),
onStatusClick = {},
)
}
fun setDefaultContent(isHideBalances: Boolean = false) {
composeTestRule.setContent {
ZcashTheme {
DefaultContent(isHideBalances)
}
}
}
}

View File

@ -1,64 +0,0 @@
package co.electriccoin.zcash.ui.screen.account.integration
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertWidthIsAtLeast
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.dp
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.account.AccountTag
import co.electriccoin.zcash.ui.screen.account.AccountTestSetup
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
class AccountViewIntegrationTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
private fun newTestSetup(walletSnapshot: WalletSnapshot) =
AccountTestSetup(
composeTestRule,
walletSnapshot,
)
// This is just basic sanity check that we still have UI set up as expected after the state restore
@Test
@MediumTest
fun wallet_snapshot_restoration() {
val restorationTester = StateRestorationTester(composeTestRule)
val walletSnapshot =
WalletSnapshotFixture.new(
saplingBalance = WalletSnapshotFixture.SAPLING_BALANCE,
orchardBalance = WalletSnapshotFixture.ORCHARD_BALANCE,
transparentBalance = WalletSnapshotFixture.TRANSPARENT_BALANCE
)
val testSetup = newTestSetup(walletSnapshot)
restorationTester.setContent {
ZcashTheme {
testSetup.DefaultContent(isHideBalances = false)
}
}
assertEquals(WalletSnapshotFixture.SAPLING_BALANCE, testSetup.getWalletSnapshot().saplingBalance)
assertEquals(WalletSnapshotFixture.ORCHARD_BALANCE, testSetup.getWalletSnapshot().orchardBalance)
assertEquals(WalletSnapshotFixture.TRANSPARENT_BALANCE, testSetup.getWalletSnapshot().transparentBalance)
restorationTester.emulateSavedInstanceStateRestore()
assertEquals(WalletSnapshotFixture.SAPLING_BALANCE, testSetup.getWalletSnapshot().saplingBalance)
assertEquals(WalletSnapshotFixture.ORCHARD_BALANCE, testSetup.getWalletSnapshot().orchardBalance)
assertEquals(WalletSnapshotFixture.TRANSPARENT_BALANCE, testSetup.getWalletSnapshot().transparentBalance)
composeTestRule.onNodeWithTag(AccountTag.BALANCE_VIEWS).also {
it.assertIsDisplayed()
it.assertWidthIsAtLeast(1.dp)
}
}
}

View File

@ -1,75 +0,0 @@
package co.electriccoin.zcash.ui.screen.account.view
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.design.component.CommonTag
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.account.AccountTag
import co.electriccoin.zcash.ui.screen.account.AccountTestSetup
import co.electriccoin.zcash.ui.screen.send.clickHideBalances
import co.electriccoin.zcash.ui.screen.send.clickSettingsTopAppBarMenu
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
// TODO [#1194]: Cover Current balances UI widget with tests
// TODO [#1194]: https://github.com/Electric-Coin-Company/zashi-android/issues/1194
class AccountViewTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
@Test
@MediumTest
fun check_all_elementary_ui_elements_displayed() {
newTestSetup()
composeTestRule
.onNodeWithTag(CommonTag.TOP_APP_BAR)
.also {
it.assertIsDisplayed()
}
composeTestRule.onNodeWithTag(AccountTag.BALANCE_VIEWS).also {
it.assertIsDisplayed()
}
}
@Test
@MediumTest
fun hamburger_settings_test() {
val testSetup = newTestSetup()
Assert.assertEquals(0, testSetup.getOnSettingsCount())
composeTestRule.clickSettingsTopAppBarMenu()
Assert.assertEquals(1, testSetup.getOnSettingsCount())
}
@Test
@MediumTest
fun hide_balances_btn_click_test() {
val testSetup = newTestSetup()
Assert.assertEquals(0, testSetup.getOnHideBalancesCount())
composeTestRule.clickHideBalances()
Assert.assertEquals(1, testSetup.getOnHideBalancesCount())
}
private fun newTestSetup(
walletSnapshot: WalletSnapshot = WalletSnapshotFixture.new(),
isHideBalances: Boolean = false
) = AccountTestSetup(
composeTestRule,
walletSnapshot = walletSnapshot,
).apply {
setDefaultContent(isHideBalances)
}
}

View File

@ -1,72 +0,0 @@
package co.electriccoin.zcash.ui.screen.balances
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.component.IconButtonState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.fixture.BalanceStateFixture
import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture
import co.electriccoin.zcash.ui.screen.balances.model.ShieldState
import co.electriccoin.zcash.ui.screen.balances.view.Balances
import java.util.concurrent.atomic.AtomicInteger
class BalancesTestSetup(
private val composeTestRule: ComposeContentTestRule,
private val walletSnapshot: WalletSnapshot,
) {
private val onSettingsCount = AtomicInteger(0)
fun getOnSettingsCount(): Int {
composeTestRule.waitForIdle()
return onSettingsCount.get()
}
fun getWalletSnapshot(): WalletSnapshot {
composeTestRule.waitForIdle()
return walletSnapshot
}
@Composable
@Suppress("TestFunctionName")
fun DefaultContent() {
Balances(
balanceState = BalanceStateFixture.new(),
hideStatusDialog = {},
isHideBalances = false,
showStatusDialog = null,
onStatusClick = {},
snackbarHostState = SnackbarHostState(),
isShowingErrorDialog = false,
setShowErrorDialog = {},
onContactSupport = {},
onShielding = {},
shieldState = ShieldState.Available,
walletSnapshot = walletSnapshot,
walletRestoringState = WalletRestoringState.NONE,
zashiMainTopAppBarState =
ZashiMainTopAppBarStateFixture.new(
settingsButton =
IconButtonState(
icon = R.drawable.ic_app_bar_settings,
contentDescription =
stringRes(co.electriccoin.zcash.ui.R.string.settings_menu_content_description),
) {
onSettingsCount.incrementAndGet()
}
)
)
}
fun setDefaultContent() {
composeTestRule.setContent {
ZcashTheme {
DefaultContent()
}
}
}
}

View File

@ -1,69 +0,0 @@
package co.electriccoin.zcash.ui.screen.balances.integration
import androidx.compose.ui.test.assertWidthIsAtLeast
import androidx.compose.ui.test.junit4.StateRestorationTester
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.unit.dp
import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.PercentDecimal
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.balances.BalancesTag
import co.electriccoin.zcash.ui.screen.balances.BalancesTestSetup
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Rule
import org.junit.Test
class BalancesViewIntegrationTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
private fun newTestSetup(walletSnapshot: WalletSnapshot) =
BalancesTestSetup(
composeTestRule,
walletSnapshot,
)
// This is just basic sanity check that we still have UI set up as expected after the state restore
@Test
@MediumTest
fun wallet_snapshot_restoration() {
val restorationTester = StateRestorationTester(composeTestRule)
val walletSnapshot =
WalletSnapshotFixture.new(
status = Synchronizer.Status.SYNCING,
progress = PercentDecimal(0.5f)
)
val testSetup = newTestSetup(walletSnapshot)
restorationTester.setContent {
ZcashTheme {
testSetup.DefaultContent()
}
}
assertNotEquals(WalletSnapshotFixture.STATUS, testSetup.getWalletSnapshot().status)
assertEquals(Synchronizer.Status.SYNCING, testSetup.getWalletSnapshot().status)
assertNotEquals(WalletSnapshotFixture.PROGRESS, testSetup.getWalletSnapshot().progress)
assertEquals(0.5f, testSetup.getWalletSnapshot().progress.decimal)
restorationTester.emulateSavedInstanceStateRestore()
assertNotEquals(WalletSnapshotFixture.STATUS, testSetup.getWalletSnapshot().status)
assertEquals(Synchronizer.Status.SYNCING, testSetup.getWalletSnapshot().status)
assertNotEquals(WalletSnapshotFixture.PROGRESS, testSetup.getWalletSnapshot().progress)
assertEquals(0.5f, testSetup.getWalletSnapshot().progress.decimal)
composeTestRule.onNodeWithTag(BalancesTag.STATUS).also {
it.assertExists()
it.assertWidthIsAtLeast(1.dp)
}
}
}

View File

@ -1,48 +0,0 @@
package co.electriccoin.zcash.ui.screen.balances.model
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.model.FiatCurrencyConversionRateState
import cash.z.ecc.android.sdk.model.PercentDecimal
import cash.z.ecc.android.sdk.model.toZecString
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.totalBalance
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.test.getAppContext
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Assert.assertEquals
import org.junit.Test
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class WalletDisplayValuesTest {
@Test
@SmallTest
fun download_running_test() {
val walletSnapshot =
WalletSnapshotFixture.new(
progress = PercentDecimal.ONE_HUNDRED_PERCENT,
status = Synchronizer.Status.SYNCING,
orchardBalance = WalletSnapshotFixture.ORCHARD_BALANCE,
saplingBalance = WalletSnapshotFixture.SAPLING_BALANCE,
transparentBalance = WalletSnapshotFixture.TRANSPARENT_BALANCE
)
val values =
WalletDisplayValues.getNextValues(
context = getAppContext(),
walletSnapshot = walletSnapshot,
)
assertNotNull(values)
assertEquals(1f, values.progress.decimal)
assertEquals(walletSnapshot.totalBalance().toZecString(), values.zecAmountText)
assertTrue(values.statusText.startsWith(getStringResource(R.string.balances_status_syncing)))
// TODO [#578]: Provide Zatoshi -> USD fiat currency formatting
// TODO [#578]: https://github.com/Electric-Coin-Company/zcash-android-wallet-sdk/issues/578
assertEquals(FiatCurrencyConversionRateState.Unavailable, values.fiatCurrencyAmountState)
assertEquals(
getStringResource(R.string.fiat_currency_conversion_rate_unavailable),
values.fiatCurrencyAmountText
)
}
}

View File

@ -1,56 +0,0 @@
package co.electriccoin.zcash.ui.screen.balances.view
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.model.WalletSnapshot
import co.electriccoin.zcash.ui.design.component.CommonTag
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.screen.balances.BalancesTestSetup
import co.electriccoin.zcash.ui.screen.send.clickSettingsTopAppBarMenu
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import kotlin.test.DefaultAsserter.assertEquals
// TODO [#1227]: Cover Balances UI and logic with tests
// TODO [#1227]: https://github.com/Electric-Coin-Company/zashi-android/issues/1227
class BalancesViewTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
private fun newTestSetup(walletSnapshot: WalletSnapshot = WalletSnapshotFixture.new()) =
BalancesTestSetup(
composeTestRule,
walletSnapshot = walletSnapshot,
).apply {
setDefaultContent()
}
@Test
@MediumTest
fun check_all_elementary_ui_elements_displayed() {
newTestSetup()
composeTestRule
.onNodeWithTag(CommonTag.TOP_APP_BAR)
.also {
it.assertIsDisplayed()
}
}
@Test
@MediumTest
fun hamburger_settings_test() {
val testSetup = newTestSetup()
assertEquals("Failed in comparison", 0, testSetup.getOnSettingsCount())
composeTestRule.clickSettingsTopAppBarMenu()
Assert.assertEquals(1, testSetup.getOnSettingsCount())
}
}

View File

@ -3,7 +3,6 @@ package co.electriccoin.zcash.ui.screen.exportdata.view
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
@ -46,7 +45,6 @@ class ExportPrivateDataViewTestSetup(
onConfirm = {
onConfirmCount.incrementAndGet()
},
topAppBarSubTitleState = TopAppBarSubTitleState.None,
)
}

View File

@ -1,55 +0,0 @@
package co.electriccoin.zcash.ui.screen.home
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import java.util.concurrent.atomic.AtomicInteger
class HomeTestSetup(
private val composeTestRule: ComposeContentTestRule,
) {
private val onAccountsCount = AtomicInteger(0)
private val onSendCount = AtomicInteger(0)
private val onReceiveCount = AtomicInteger(0)
private val onBalancesCount = AtomicInteger(0)
fun getOnAccountCount(): Int {
composeTestRule.waitForIdle()
return onAccountsCount.get()
}
fun getOnSendCount(): Int {
composeTestRule.waitForIdle()
return onSendCount.get()
}
fun getOnReceiveCount(): Int {
composeTestRule.waitForIdle()
return onReceiveCount.get()
}
fun getOnBalancesCount(): Int {
composeTestRule.waitForIdle()
return onBalancesCount.get()
}
// TODO [#1125]: Home screen navigation: Add integration test
// TODO [#1125]: https://github.com/Electric-Coin-Company/zashi-android/issues/1125
// TODO [#1126]: Home screen view: Add view test
// TODO [#1126]: https://github.com/Electric-Coin-Company/zashi-android/issues/1126
/*
@Composable
@Suppress("TestFunctionName")
fun DefaultContent() {
Home()
}
fun setDefaultContent() {
composeTestRule.setContent {
ZcashTheme {
DefaultContent()
}
}
}
*/
}

View File

@ -1,57 +0,0 @@
package co.electriccoin.zcash.ui.screen.home.integration
import androidx.compose.ui.test.junit4.createComposeRule
import co.electriccoin.zcash.test.UiTestPrerequisites
import org.junit.Rule
class HomeViewIntegrationTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
// TODO [#1125]: Home screen navigation: Add integration test
// TODO [#1125]: https://github.com/Electric-Coin-Company/zashi-android/issues/1125
/*
private fun newTestSetup(walletSnapshot: WalletSnapshot) =
HomeTestSetup(
composeTestRule,
walletSnapshot,
isShowFiatConversion = false
)
@Test
@MediumTest
fun wallet_snapshot_restoration() {
val restorationTester = StateRestorationTester(composeTestRule)
val walletSnapshot =
WalletSnapshotFixture.new(
status = Synchronizer.Status.SYNCING,
progress = PercentDecimal(0.5f)
)
val testSetup = newTestSetup(walletSnapshot)
restorationTester.setContent {
testSetup.DefaultContent()
}
assertNotEquals(WalletSnapshotFixture.STATUS, testSetup.getWalletSnapshot().status)
assertEquals(Synchronizer.Status.SYNCING, testSetup.getWalletSnapshot().status)
assertNotEquals(WalletSnapshotFixture.PROGRESS, testSetup.getWalletSnapshot().progress)
assertEquals(0.5f, testSetup.getWalletSnapshot().progress.decimal)
restorationTester.emulateSavedInstanceStateRestore()
assertNotEquals(WalletSnapshotFixture.STATUS, testSetup.getWalletSnapshot().status)
assertEquals(Synchronizer.Status.SYNCING, testSetup.getWalletSnapshot().status)
assertNotEquals(WalletSnapshotFixture.PROGRESS, testSetup.getWalletSnapshot().progress)
assertEquals(0.5f, testSetup.getWalletSnapshot().progress.decimal)
composeTestRule.onNodeWithTag(AccountTag.SINGLE_LINE_TEXT).also {
it.assertIsDisplayed()
it.assertWidthIsAtLeast(1.dp)
}
}
*/
}

View File

@ -1,40 +0,0 @@
@file:Suppress("UnusedPrivateMember")
package co.electriccoin.zcash.ui.screen.home.view
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.test.getStringResource
class HomeViewTest {
// TODO [#1126]: Home screen view: Add view test
// TODO [#1126]: https://github.com/Electric-Coin-Company/zashi-android/issues/1126
}
private fun ComposeContentTestRule.clickAccount() {
onNodeWithText(getStringResource(R.string.home_tab_account), ignoreCase = true).also {
it.performClick()
}
}
private fun ComposeContentTestRule.clickSend() {
onNodeWithText(getStringResource(R.string.home_tab_send), ignoreCase = true).also {
it.performScrollTo()
it.performClick()
}
}
private fun ComposeContentTestRule.clickReceive() {
onNodeWithText(getStringResource(R.string.home_tab_receive), ignoreCase = true).also {
it.performClick()
}
}
private fun ComposeContentTestRule.clickBalances() {
onNodeWithText(getStringResource(R.string.home_tab_balances), ignoreCase = true).also {
it.performClick()
}
}

View File

@ -28,10 +28,8 @@ class OnboardingTestSetup(
ZcashTheme {
Onboarding(
// Debug only UI state does not need to be tested
isDebugMenuEnabled = false,
onImportWallet = { onImportWalletCallbackCount.incrementAndGet() },
onCreateWallet = { onCreateWalletCallbackCount.incrementAndGet() },
onFixtureWallet = {}
onCreateWallet = { onCreateWalletCallbackCount.incrementAndGet() }
)
}
}

View File

@ -46,8 +46,9 @@ class ReceiveViewTestSetup(
)
),
isLoading = false,
onBack = {}
),
zashiMainTopAppBarState =
appBarState =
ZashiMainTopAppBarStateFixture.new(
settingsButton =
IconButtonState(

View File

@ -1,129 +0,0 @@
package co.electriccoin.zcash.ui.screen.restore.model
import androidx.test.filters.SmallTest
import cash.z.ecc.android.sdk.model.SeedPhrase
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ParseResultTest {
companion object {
private val SAMPLE_WORD_LIST = setOf("bar", "baz", "foo")
private val SAMPLE_WORD_LIST_EXT =
buildSet {
addAll(SAMPLE_WORD_LIST)
add("bazooka")
}
}
@Test
@SmallTest
fun continue_empty() {
val actual = ParseResult.new(SAMPLE_WORD_LIST, "")
assertEquals(ParseResult.Continue, actual)
}
@Test
@SmallTest
fun continue_blank() {
val actual = ParseResult.new(SAMPLE_WORD_LIST, " ")
assertEquals(ParseResult.Continue, actual)
}
@Test
@SmallTest
fun add_single() {
val actual = ParseResult.new(SAMPLE_WORD_LIST, "baz")
assertEquals(ParseResult.Add(listOf("baz")), actual)
}
@Test
@SmallTest
fun add_single_trimmed() {
val actual = ParseResult.new(SAMPLE_WORD_LIST, "foo ")
assertEquals(ParseResult.Add(listOf("foo")), actual)
}
@Test
@SmallTest
fun add_multiple() {
val actual = ParseResult.new(SAMPLE_WORD_LIST, SAMPLE_WORD_LIST.joinToString(SeedPhrase.DEFAULT_DELIMITER))
assertEquals(ParseResult.Add(listOf("bar", "baz", "foo")), actual)
}
@Test
@SmallTest
fun add_security() {
val actual = ParseResult.new(SAMPLE_WORD_LIST, "foo")
assertTrue(actual is ParseResult.Add)
assertFalse(actual.toString().contains("foo"))
}
@Test
@SmallTest
fun autocomplete_single() {
val actual = ParseResult.new(SAMPLE_WORD_LIST, "f")
assertEquals(ParseResult.Autocomplete(listOf("foo")), actual)
}
@Test
@SmallTest
fun autocomplete_multiple() {
val actual = ParseResult.new(SAMPLE_WORD_LIST, "ba")
assertEquals(ParseResult.Autocomplete(listOf("bar", "baz")), actual)
}
@Test
@SmallTest
fun autocomplete_multiple_same_base() {
ParseResult.new(SAMPLE_WORD_LIST_EXT, "baz").also {
assertTrue(it is ParseResult.Autocomplete)
assertTrue((it as ParseResult.Autocomplete).suggestions.size == 2)
assertTrue((it).suggestions.contains("baz"))
assertTrue((it).suggestions.contains("bazooka"))
}
ParseResult.new(SAMPLE_WORD_LIST_EXT, "bazo").also {
assertTrue(it is ParseResult.Autocomplete)
assertTrue((it as ParseResult.Autocomplete).suggestions.size == 1)
assertTrue((it).suggestions.contains("bazooka"))
}
ParseResult.new(SAMPLE_WORD_LIST_EXT, "bazooka").also {
assertTrue(it is ParseResult.Add)
assertTrue((it as ParseResult.Add).words.size == 1)
assertTrue(it.words.contains("bazooka"))
}
}
@Test
@SmallTest
fun autocomplete_security() {
val actual = ParseResult.new(SAMPLE_WORD_LIST, "f")
assertTrue(actual is ParseResult.Autocomplete)
assertFalse(actual.toString().contains("foo"))
}
@Test
@SmallTest
fun warn_backwards_recursion() {
val actual = ParseResult.new(SAMPLE_WORD_LIST, "bb")
assertEquals(ParseResult.Warn(listOf("bar", "baz")), actual)
}
@Test
@SmallTest
fun warn_backwards_recursion_2() {
val actual = ParseResult.new(SAMPLE_WORD_LIST, "bad")
assertEquals(ParseResult.Warn(listOf("bar", "baz")), actual)
}
@Test
@SmallTest
fun warn_security() {
val actual = ParseResult.new(SAMPLE_WORD_LIST, "foob")
assertTrue(actual is ParseResult.Warn)
assertFalse(actual.toString().contains("foo"))
}
}

View File

@ -1,30 +0,0 @@
package co.electriccoin.zcash.ui.screen.restore.model
import co.electriccoin.zcash.ui.screen.restore.state.WordList
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Test
class WordListTest {
@Test
fun append() {
val wordList = WordList(listOf("foo"))
val initialList = wordList.current.value
wordList.append(listOf("bar"))
assertEquals(listOf("foo", "bar"), wordList.current.value)
assertNotEquals(initialList, wordList.current.value)
}
@Test
fun set() {
val wordList = WordList(listOf("foo"))
val initialList = wordList.current.value
wordList.set(listOf("bar"))
assertEquals(listOf("bar"), wordList.current.value)
assertNotEquals(initialList, wordList.current.value)
}
}

View File

@ -1,170 +0,0 @@
package co.electriccoin.zcash.ui.screen.restore.view
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build.VERSION_CODES
import android.view.inputmethod.InputMethodManager
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsFocused
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performKeyInput
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.pressKey
import androidx.compose.ui.test.withKeyDown
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
import cash.z.ecc.android.sdk.model.SeedPhrase
import cash.z.ecc.sdk.fixture.SeedPhraseFixture
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.CommonTag
import co.electriccoin.zcash.ui.screen.restore.RestoreTag
import co.electriccoin.zcash.ui.screen.restore.model.RestoreStage
import co.electriccoin.zcash.ui.test.getAppContext
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import kotlin.test.Ignore
import kotlin.time.Duration.Companion.seconds
// Non-multiplatform tests that require interacting with the Android system (e.g. clipboard, Context)
// These don't have persistent state, so they are still unit tests.
class RestoreViewAndroidTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
@Before
fun setup() {
composeTestRule.mainClock.autoAdvance = true
}
@Test
@MediumTest
fun keyboard_appears_on_launch() {
newTestSetup()
composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
it.assertIsFocused()
}
val inputMethodManager = getAppContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
assertTrue(inputMethodManager.isAcceptingText)
}
@OptIn(ExperimentalTestApi::class)
@Test
@MediumTest
// Functionality should be compatible with Android 27+, but a bug in the Android framework causes a crash
// on Android 27. Further, copying to the clipboard seems to be broken in emulators until API 33 (even in
// other apps like the Contacts app). We haven't been able to test this on physical devices yet, but
// we're assuming that it works.
@SdkSuppress(minSdkVersion = VERSION_CODES.TIRAMISU)
// This started failing with the Compose 1.4 version bump, although the reason is not clear.
@Ignore
fun paste_too_many_words() {
val testSetup = newTestSetup()
copyToClipboard(
getAppContext(),
SeedPhraseFixture.SEED_PHRASE + " " + SeedPhraseFixture.SEED_PHRASE
)
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
it.performKeyInput {
withKeyDown(Key.CtrlLeft) {
pressKey(Key.V)
}
}
}
// There appears to be a bug introduced in Compose 1.4.0 which makes this necessary
composeTestRule.mainClock.autoAdvance = false
assertEquals(SeedPhrase.SEED_PHRASE_SIZE, testSetup.getUserInputWords().size)
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
it.assertDoesNotExist()
}
composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also {
it.assertDoesNotExist()
}
composeTestRule.onAllNodes(hasTestTag(CommonTag.CHIP), useUnmergedTree = true).also {
it.assertCountEquals(SeedPhrase.SEED_PHRASE_SIZE)
}
}
@Test
@MediumTest
@SdkSuppress(minSdkVersion = VERSION_CODES.TIRAMISU)
fun keyboard_disappears_after_correct_seed_inserted() {
newTestSetup()
composeTestRule.waitForIdle()
// Insert uncompleted seed words
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
it.performTextInput("test")
}
val imm =
getAppContext().getSystemService(Context.INPUT_METHOD_SERVICE)
as InputMethodManager
// Test that the input seed text field is still expecting an input, as the inserted seed words are not complete
composeTestRule.waitUntil(5.seconds.inWholeMilliseconds) {
imm.isAcceptingText
}
composeTestRule.waitForIdle()
// Clear test seed words
composeTestRule.onNodeWithText(getStringResource(R.string.restore_button_clear)).also {
it.performClick()
}
composeTestRule.waitForIdle()
// Insert complete seed words
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
it.performTextInput(SeedPhraseFixture.SEED_PHRASE)
}
// Test that the input seed text field is not expecting an input anymore, as the inserted seed words are
// complete
composeTestRule.waitUntil(5.seconds.inWholeMilliseconds) {
!imm.isAcceptingText
}
}
private fun newTestSetup(
initialStage: RestoreStage = RestoreStage.Seed,
initialWordsList: List<String> = emptyList()
) = RestoreViewTest.TestSetup(composeTestRule, initialStage, initialWordsList)
}
private fun copyToClipboard(
context: Context,
text: String
) {
val clipboardManager = context.getSystemService(ClipboardManager::class.java)
val data =
ClipData.newPlainText(
"TAG",
text
)
clipboardManager.setPrimaryClip(data)
}

View File

@ -1,64 +0,0 @@
package co.electriccoin.zcash.ui.screen.restore.view
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.filters.MediumTest
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.sdk.model.ZcashNetwork
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.common.compose.LocalScreenSecurity
import co.electriccoin.zcash.ui.common.compose.ScreenSecurity
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.restore.state.RestoreState
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
import org.junit.Test
import java.util.Locale
import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class)
class RestoreViewSecuredScreenTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
@Test
@MediumTest
fun acquireScreenSecurity() =
runTest {
val testSetup = TestSetup(composeTestRule)
assertEquals(1, testSetup.getSecureScreenCount())
}
private class TestSetup(
composeTestRule: ComposeContentTestRule
) {
private val screenSecurity = ScreenSecurity()
fun getSecureScreenCount() = screenSecurity.referenceCount.value
init {
composeTestRule.setContent {
CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) {
ZcashTheme {
RestoreWallet(
ZcashNetwork.Mainnet,
RestoreState(),
Mnemonics.getCachedWords(Locale.ENGLISH.language).toPersistentSet(),
WordList(emptyList()),
restoreHeight = null,
setRestoreHeight = {},
onBack = { },
paste = { "" },
onFinish = { }
)
}
}
}
}
}
}

View File

@ -1,434 +0,0 @@
package co.electriccoin.zcash.ui.screen.restore.view
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.assertTextContains
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
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.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextInput
import androidx.test.filters.MediumTest
import cash.z.ecc.android.bip39.Mnemonics
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.SeedPhrase
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.sdk.fixture.SeedPhraseFixture
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.CommonTag
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.restore.RestoreTag
import co.electriccoin.zcash.ui.screen.restore.model.RestoreStage
import co.electriccoin.zcash.ui.screen.restore.state.RestoreState
import co.electriccoin.zcash.ui.screen.restore.state.WordList
import co.electriccoin.zcash.ui.test.getStringResource
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.util.Locale
import java.util.concurrent.atomic.AtomicInteger
import kotlin.test.assertNull
class RestoreViewTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
@Before
fun setup() {
composeTestRule.mainClock.autoAdvance = true
}
@Test
@MediumTest
fun seed_autocomplete_suggestions_appear() {
newTestSetup()
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
it.performTextInput("ab")
// Make sure text isn't cleared
it.assertTextContains("ab")
}
composeTestRule
.onNode(
matcher = hasText("abandon", substring = true) and hasTestTag(RestoreTag.AUTOCOMPLETE_ITEM)
).also {
it.assertExists()
}
composeTestRule
.onNode(
matcher = hasText("able", substring = true) and hasTestTag(RestoreTag.AUTOCOMPLETE_ITEM)
).also {
it.assertExists()
}
}
@Test
@MediumTest
fun seed_choose_autocomplete() {
newTestSetup()
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
it.performTextInput("ab")
}
composeTestRule
.onNode(
matcher = hasText("abandon", substring = true) and hasTestTag(RestoreTag.AUTOCOMPLETE_ITEM)
).also {
it.performClick()
}
composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also {
it.assertDoesNotExist()
}
composeTestRule.onNode(matcher = hasText("abandon", substring = true)).also {
it.assertExists()
}
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
it.assertTextEquals("abandon ", includeEditableText = true)
}
}
@Test
@MediumTest
fun seed_type_full_word() {
newTestSetup()
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
it.performTextInput("abandon")
}
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
it.assertTextEquals("abandon ", includeEditableText = true)
}
composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also {
it.assertDoesNotExist()
}
composeTestRule
.onNode(matcher = hasText(text = "abandon", substring = true))
.also {
it.assertExists()
}
}
@Test
@MediumTest
fun seed_invalid_phrase_does_not_progress() {
newTestSetup(initialWordsList = generateSequence { "abandon" }.take(SeedPhrase.SEED_PHRASE_SIZE).toList())
composeTestRule
.onNodeWithText(
text = getStringResource(R.string.restore_seed_button_next),
ignoreCase = true
).also {
it.assertIsNotEnabled()
}
}
@Test
@MediumTest
fun seed_finish_appears_after_24_words() {
// There appears to be a bug introduced in Compose 1.4.0 which makes this necessary
composeTestRule.mainClock.autoAdvance = false
newTestSetup(initialWordsList = SeedPhraseFixture.new().split)
composeTestRule
.onNodeWithText(
text = getStringResource(R.string.restore_seed_button_next),
ignoreCase = true
).also {
it.assertExists()
}
}
@Test
@MediumTest
fun seed_clear() {
newTestSetup(initialWordsList = listOf("abandon"))
composeTestRule
.onNode(
matcher = hasText(text = "abandon", substring = true),
useUnmergedTree = true
).also {
it.assertExists()
}
composeTestRule.onNodeWithText(getStringResource(R.string.restore_button_clear)).also {
it.performClick()
}
composeTestRule
.onNode(
matcher = hasText("abandon", substring = true) and hasTestTag(CommonTag.CHIP),
useUnmergedTree = true
).also {
it.assertDoesNotExist()
}
}
@Test
@MediumTest
fun height_skip() {
val testSetup =
newTestSetup(
initialStage = RestoreStage.Birthday,
initialWordsList = SeedPhraseFixture.new().split
)
composeTestRule
.onNodeWithText(
text = getStringResource(R.string.restore_birthday_button_restore),
ignoreCase = true
).also {
it.performScrollTo()
it.assertIsEnabled()
it.performClick()
}
assertEquals(testSetup.getRestoreHeight(), null)
assertEquals(1, testSetup.getOnFinishedCount())
}
@Test
@MediumTest
fun height_set_valid() {
val testSetup =
newTestSetup(
initialStage = RestoreStage.Birthday,
initialWordsList = SeedPhraseFixture.new().split
)
composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also {
it.performTextInput(
ZcashNetwork.Mainnet.saplingActivationHeight.value
.toString()
)
}
composeTestRule
.onNodeWithText(
text = getStringResource(R.string.restore_birthday_button_restore),
ignoreCase = true
).also {
it.assertIsEnabled()
it.performClick()
}
assertEquals(testSetup.getRestoreHeight(), ZcashNetwork.Mainnet.saplingActivationHeight)
assertEquals(1, testSetup.getOnFinishedCount())
}
@Test
@MediumTest
fun height_set_invalid_too_small() {
val testSetup =
newTestSetup(
initialStage = RestoreStage.Birthday,
initialWordsList = SeedPhraseFixture.new().split
)
composeTestRule
.onNodeWithText(
text = getStringResource(R.string.restore_birthday_button_restore),
ignoreCase = true
).also {
it.assertIsEnabled()
}
composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also {
it.performTextInput((ZcashNetwork.Mainnet.saplingActivationHeight.value - 1L).toString())
}
composeTestRule
.onNodeWithText(
text = getStringResource(R.string.restore_birthday_button_restore),
ignoreCase = true
).also {
it.assertIsNotEnabled()
it.performClick()
}
assertNull(testSetup.getRestoreHeight())
assertEquals(0, testSetup.getOnFinishedCount())
}
@Test
@MediumTest
fun height_set_invalid_non_digit() {
val testSetup =
newTestSetup(
initialStage = RestoreStage.Birthday,
initialWordsList = SeedPhraseFixture.new().split
)
composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also {
it.performTextInput("1.2")
}
composeTestRule
.onNodeWithText(
text = getStringResource(R.string.restore_birthday_button_restore),
ignoreCase = true
).also {
it.assertIsNotEnabled()
it.performClick()
}
assertNull(testSetup.getRestoreHeight())
assertEquals(0, testSetup.getOnFinishedCount())
}
@Test
@MediumTest
fun complete_click_take_to_wallet() {
val testSetup =
newTestSetup(
initialStage = RestoreStage.Birthday,
initialWordsList = SeedPhraseFixture.new().split
)
assertEquals(0, testSetup.getOnFinishedCount())
composeTestRule
.onNodeWithText(
text = getStringResource(R.string.restore_birthday_button_restore),
ignoreCase = true
).also {
it.performClick()
}
assertEquals(1, testSetup.getOnFinishedCount())
}
@Test
@MediumTest
fun back_from_seed() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnBackCount())
composeTestRule
.onNodeWithContentDescription(
getStringResource(R.string.back_navigation_content_description)
).also {
it.performClick()
}
assertEquals(1, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun back_from_birthday() {
val testSetup =
newTestSetup(
initialStage = RestoreStage.Birthday,
initialWordsList = SeedPhraseFixture.new().split
)
assertEquals(0, testSetup.getOnBackCount())
composeTestRule
.onNodeWithContentDescription(
getStringResource(R.string.back_navigation_content_description)
).also {
it.performClick()
}
// There appears to be a bug introduced in Compose 1.4.0 which makes this necessary
composeTestRule.mainClock.autoAdvance = false
assertEquals(testSetup.getStage(), RestoreStage.Seed)
assertEquals(0, testSetup.getOnBackCount())
}
private fun newTestSetup(
initialStage: RestoreStage = RestoreStage.Seed,
initialWordsList: List<String> = emptyList()
) = TestSetup(composeTestRule, initialStage, initialWordsList)
internal class TestSetup(
private val composeTestRule: ComposeContentTestRule,
initialStage: RestoreStage,
initialWordsList: List<String>
) {
private val state = RestoreState(initialStage)
private val wordList = WordList(initialWordsList)
private val onBackCount = AtomicInteger(0)
private val onFinishedCount = AtomicInteger(0)
private val restoreHeight = MutableStateFlow<BlockHeight?>(null)
fun getUserInputWords(): List<String> {
composeTestRule.waitForIdle()
return wordList.current.value
}
fun getStage(): RestoreStage {
composeTestRule.waitForIdle()
return state.current.value
}
fun getRestoreHeight(): BlockHeight? {
composeTestRule.waitForIdle()
return restoreHeight.value
}
fun getOnBackCount(): Int {
composeTestRule.waitForIdle()
return onBackCount.get()
}
fun getOnFinishedCount(): Int {
composeTestRule.waitForIdle()
return onFinishedCount.get()
}
init {
composeTestRule.setContent {
ZcashTheme {
RestoreWallet(
ZcashNetwork.Mainnet,
state,
Mnemonics.getCachedWords(Locale.ENGLISH.language).toPersistentSet(),
wordList,
restoreHeight = restoreHeight.collectAsState().value,
setRestoreHeight = {
restoreHeight.value = it
},
onBack = {
onBackCount.incrementAndGet()
},
paste = { "" },
onFinish = {
onFinishedCount.incrementAndGet()
}
)
}
}
}
}
}

View File

@ -11,8 +11,8 @@ import androidx.test.filters.MediumTest
import androidx.test.rule.GrantPermissionRule
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.screen.scan.ScanScreenState
import co.electriccoin.zcash.ui.screen.scan.ScanTag
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Assert.assertEquals
import org.junit.Rule

View File

@ -3,10 +3,10 @@ package co.electriccoin.zcash.ui.screen.scan.view
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
import co.electriccoin.zcash.ui.screen.scan.model.ScanValidationState
import co.electriccoin.zcash.ui.screen.scan.Scan
import co.electriccoin.zcash.ui.screen.scan.ScanScreenState
import co.electriccoin.zcash.ui.screen.scan.ScanValidationState
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
@ -30,7 +30,7 @@ class ScanViewBasicTestSetup(
@Suppress("TestFunctionName")
fun DefaultContent() {
Scan(
validationResult = ScanValidationState.VALID,
snackbarHostState = SnackbarHostState(),
onBack = {
onBackCount.incrementAndGet()
},
@ -40,8 +40,7 @@ class ScanViewBasicTestSetup(
onScanStateChange = {
scanState.set(it)
},
snackbarHostState = SnackbarHostState(),
topAppBarSubTitleState = TopAppBarSubTitleState.None,
validationResult = ScanValidationState.VALID,
)
}

View File

@ -1,119 +0,0 @@
package co.electriccoin.zcash.ui.screen.securitywarning.view
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
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.compose.ui.test.performScrollTo
import androidx.test.filters.MediumTest
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Rule
import kotlin.test.Test
import kotlin.test.assertEquals
class SecurityWarningViewTest : UiTestPrerequisites() {
@get:Rule
val composeTestRule = createComposeRule()
@Test
@MediumTest
fun default_ui_state_test() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnBackCount())
assertEquals(false, testSetup.getOnAcknowledged())
assertEquals(0, testSetup.getOnConfirmCount())
composeTestRule.onNodeWithTag(SecurityScreenTag.ACKNOWLEDGE_CHECKBOX_TAG).also {
it.assertExists()
it.assertIsDisplayed()
it.assertHasClickAction()
it.assertIsEnabled()
}
composeTestRule.onNodeWithText(getStringResource(R.string.security_warning_confirm), ignoreCase = true).also {
it.performScrollTo()
it.assertExists()
it.assertIsDisplayed()
it.assertHasClickAction()
it.assertIsNotEnabled()
}
}
@Test
@MediumTest
fun back_test() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnBackCount())
composeTestRule.clickBack()
assertEquals(1, testSetup.getOnBackCount())
}
@Test
@MediumTest
fun click_disabled_confirm_button_test() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnConfirmCount())
assertEquals(false, testSetup.getOnAcknowledged())
composeTestRule.clickConfirm()
assertEquals(0, testSetup.getOnConfirmCount())
assertEquals(false, testSetup.getOnAcknowledged())
}
@Test
@MediumTest
fun click_enabled_confirm_button_test() {
val testSetup = newTestSetup()
assertEquals(0, testSetup.getOnConfirmCount())
assertEquals(false, testSetup.getOnAcknowledged())
composeTestRule.clickAcknowledge()
assertEquals(0, testSetup.getOnConfirmCount())
assertEquals(true, testSetup.getOnAcknowledged())
composeTestRule.clickConfirm()
assertEquals(1, testSetup.getOnConfirmCount())
assertEquals(true, testSetup.getOnAcknowledged())
}
private fun newTestSetup() =
SecurityWarningViewTestSetup(composeTestRule).apply {
setDefaultContent()
}
}
private fun ComposeContentTestRule.clickBack() {
onNodeWithContentDescription(getStringResource(R.string.back_navigation_content_description)).also {
it.performClick()
}
}
private fun ComposeContentTestRule.clickConfirm() {
onNodeWithText(getStringResource(R.string.security_warning_confirm), ignoreCase = true).also {
it.performScrollTo()
it.performClick()
}
}
private fun ComposeContentTestRule.clickAcknowledge() {
onNodeWithText(getStringResource(R.string.security_warning_acknowledge)).also {
it.performClick()
}
}

View File

@ -1,58 +0,0 @@
package co.electriccoin.zcash.ui.screen.securitywarning.view
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
class SecurityWarningViewTestSetup(
private val composeTestRule: ComposeContentTestRule
) {
private val onBackCount = AtomicInteger(0)
private val onAcknowledged = AtomicBoolean(false)
private val onConfirmCount = AtomicInteger(0)
fun getOnBackCount(): Int {
composeTestRule.waitForIdle()
return onBackCount.get()
}
fun getOnAcknowledged(): Boolean {
composeTestRule.waitForIdle()
return onAcknowledged.get()
}
fun getOnConfirmCount(): Int {
composeTestRule.waitForIdle()
return onConfirmCount.get()
}
@Composable
@Suppress("TestFunctionName")
fun DefaultContent() {
SecurityWarning(
onBack = {
onBackCount.incrementAndGet()
},
onAcknowledge = {
onAcknowledged.getAndSet(it)
},
onConfirm = {
onConfirmCount.incrementAndGet()
},
versionInfo = VersionInfoFixture.new()
)
}
fun setDefaultContent() {
composeTestRule.setContent {
ZcashTheme {
DefaultContent()
}
}
}
}

View File

@ -6,9 +6,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import cash.z.ecc.android.sdk.fixture.WalletBalanceFixture
import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZecSend
import cash.z.ecc.android.sdk.type.AddressType
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
@ -17,7 +15,6 @@ import co.electriccoin.zcash.ui.design.component.IconButtonState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.fixture.BalanceStateFixture
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture
import co.electriccoin.zcash.ui.screen.send.ext.Saver
import co.electriccoin.zcash.ui.screen.send.model.AmountState
@ -107,18 +104,13 @@ class SendViewTestSetup(
// TODO [#1260]: Cover Send.Form screen UI with tests
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
Send(
balanceState = BalanceStateFixture.new(),
balanceWidgetState = BalanceStateFixture.new(),
sendStage = sendStage,
onCreateZecSend = setZecSend,
onBack = onBackAction,
onQrScannerOpen = {
onScannerCount.incrementAndGet()
},
goBalances = {
// TODO [#1194]: Cover Current balances UI widget with tests
// TODO [#1194]: https://github.com/Electric-Coin-Company/zashi-android/issues/1194
},
isHideBalances = false,
hasCameraFeature = hasCameraFeature,
recipientAddressState = RecipientAddressState("", AddressType.Invalid()),
onRecipientAddressChange = {
@ -137,13 +129,7 @@ class SendViewTestSetup(
),
setMemoState = {},
memoState = MemoState.new(""),
walletSnapshot =
WalletSnapshotFixture.new(
saplingBalance =
WalletBalanceFixture.new(
available = Zatoshi(Zatoshi.MAX_INCLUSIVE.div(100))
)
),
selectedAccount = null,
exchangeRateState = ExchangeRateState.OptedOut,
sendAddressBookState =
SendAddressBookState(

View File

@ -5,6 +5,7 @@ import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.test.filters.MediumTest
import cash.z.ecc.sdk.fixture.ZecSendFixture
import co.electriccoin.zcash.ui.screen.send.Send
import co.electriccoin.zcash.ui.screen.send.WrapSend
import co.electriccoin.zcash.ui.screen.send.assertOnForm
import co.electriccoin.zcash.ui.screen.send.clickCreateAndSend
@ -33,10 +34,7 @@ class SendViewIntegrationTest {
restorationTester.setContent {
WrapSend(
sendArguments = null,
goToQrScanner = {},
goBack = {},
goBalances = {},
args = Send(),
)
}

View File

@ -9,15 +9,16 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.test.filters.MediumTest
import cash.z.ecc.android.sdk.ext.collectWith
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
import cash.z.ecc.android.sdk.fixture.WalletFixture
import cash.z.ecc.android.sdk.model.Memo
import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.Zatoshi
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.android.sdk.model.ZecSend
import cash.z.ecc.sdk.fixture.ZecRequestFixture
import cash.z.ecc.sdk.fixture.ZecSendFixture
import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.fixture.SendArgumentsWrapperFixture
import co.electriccoin.zcash.ui.screen.send.SendTag
import co.electriccoin.zcash.ui.screen.send.SendViewTestSetup
import co.electriccoin.zcash.ui.screen.send.assertOnForm
@ -379,7 +380,7 @@ class SendViewTest : UiTestPrerequisites() {
composeTestRule.onNodeWithText(getStringResource(R.string.send_address_hint)).also {
it.assertTextEquals(
getStringResource(R.string.send_address_hint),
SendArgumentsWrapperFixture.RECIPIENT_ADDRESS.address,
WalletFixture.Alice.getAddresses(ZcashNetwork.Testnet).unified,
includeEditableText = true
)
}

View File

@ -2,7 +2,6 @@ package co.electriccoin.zcash.ui.screen.settings
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.util.stringRes
@ -149,7 +148,6 @@ class SettingsViewTestSetup(
),
)
),
topAppBarSubTitleState = TopAppBarSubTitleState.None,
)
}
}

View File

@ -0,0 +1,18 @@
package co.electriccoin.zcash.ui.common.provider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
class CrashReportingStorageProviderImpl : CrashReportingStorageProvider {
override suspend fun get(): Boolean = false
override suspend fun store(amount: Boolean) {
// do nothing
}
override fun observe(): Flow<Boolean?> = flowOf(false)
override suspend fun clear() {
// do nothing
}
}

View File

@ -3,6 +3,7 @@ package co.electriccoin.zcash.ui.screen.scan.util
import android.graphics.ImageFormat
import androidx.camera.core.ImageProxy
import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.screen.scan.QrCodeAnalyzer
import co.electriccoin.zcash.ui.screen.scankeystone.view.FramePosition
import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap

View File

@ -19,6 +19,7 @@
android:configChanges="orientation|locale|layoutDirection|screenLayout|uiMode|colorMode|keyboard|screenSize"
android:exported="false"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTask"
android:label="@string/app_name"
android:theme="@style/Theme.App.Starting" />

View File

@ -6,12 +6,8 @@ import co.electriccoin.zcash.configuration.AndroidConfigurationFactory
import co.electriccoin.zcash.global.newInstance
import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
import co.electriccoin.zcash.ui.HomeTabNavigationRouter
import co.electriccoin.zcash.ui.HomeTabNavigationRouterImpl
import co.electriccoin.zcash.ui.NavigationRouter
import co.electriccoin.zcash.ui.NavigationRouterImpl
import co.electriccoin.zcash.ui.preference.PersistableWalletPreferenceDefault
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
@ -21,22 +17,12 @@ val coreModule =
single {
WalletCoordinator.newInstance(
context = get(),
encryptedPreferenceProvider = get(),
persistableWalletPreference = get(),
persistableWalletStorageProvider = get()
)
}
single {
PersistableWalletPreferenceDefault(PreferenceKey("persistable_wallet"))
}
singleOf(::StandardPreferenceProvider)
singleOf(::EncryptedPreferenceProvider)
single { BiometricManager.from(get()) }
factory { AndroidConfigurationFactory.new() }
singleOf(::NavigationRouterImpl) bind NavigationRouter::class
singleOf(::HomeTabNavigationRouterImpl) bind HomeTabNavigationRouter::class
}

View File

@ -2,10 +2,16 @@ package co.electriccoin.zcash.di
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
import co.electriccoin.zcash.ui.common.datasource.AccountDataSourceImpl
import co.electriccoin.zcash.ui.common.datasource.MessageAvailabilityDataSource
import co.electriccoin.zcash.ui.common.datasource.MessageAvailabilityDataSourceImpl
import co.electriccoin.zcash.ui.common.datasource.ProposalDataSource
import co.electriccoin.zcash.ui.common.datasource.ProposalDataSourceImpl
import co.electriccoin.zcash.ui.common.datasource.RestoreTimestampDataSource
import co.electriccoin.zcash.ui.common.datasource.RestoreTimestampDataSourceImpl
import co.electriccoin.zcash.ui.common.datasource.WalletBackupDataSource
import co.electriccoin.zcash.ui.common.datasource.WalletBackupDataSourceImpl
import co.electriccoin.zcash.ui.common.datasource.WalletSnapshotDataSource
import co.electriccoin.zcash.ui.common.datasource.WalletSnapshotDataSourceImpl
import co.electriccoin.zcash.ui.common.datasource.ZashiSpendingKeyDataSource
import co.electriccoin.zcash.ui.common.datasource.ZashiSpendingKeyDataSourceImpl
import org.koin.core.module.dsl.singleOf
@ -18,4 +24,7 @@ val dataSourceModule =
singleOf(::ZashiSpendingKeyDataSourceImpl) bind ZashiSpendingKeyDataSource::class
singleOf(::ProposalDataSourceImpl) bind ProposalDataSource::class
singleOf(::RestoreTimestampDataSourceImpl) bind RestoreTimestampDataSource::class
singleOf(::WalletBackupDataSourceImpl) bind WalletBackupDataSource::class
singleOf(::MessageAvailabilityDataSourceImpl) bind MessageAvailabilityDataSource::class
singleOf(::WalletSnapshotDataSourceImpl) bind WalletSnapshotDataSource::class
}

View File

@ -2,32 +2,57 @@ package co.electriccoin.zcash.di
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProviderImpl
import co.electriccoin.zcash.ui.common.provider.CrashReportingStorageProvider
import co.electriccoin.zcash.ui.common.provider.CrashReportingStorageProviderImpl
import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider
import co.electriccoin.zcash.ui.common.provider.GetMonetarySeparatorProvider
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
import co.electriccoin.zcash.ui.common.provider.GetZcashCurrencyProvider
import co.electriccoin.zcash.ui.common.provider.PersistableWalletProvider
import co.electriccoin.zcash.ui.common.provider.PersistableWalletProviderImpl
import co.electriccoin.zcash.ui.common.provider.PersistableWalletStorageProvider
import co.electriccoin.zcash.ui.common.provider.PersistableWalletStorageProviderImpl
import co.electriccoin.zcash.ui.common.provider.RestoreTimestampStorageProvider
import co.electriccoin.zcash.ui.common.provider.RestoreTimestampStorageProviderImpl
import co.electriccoin.zcash.ui.common.provider.SelectedAccountUUIDProvider
import co.electriccoin.zcash.ui.common.provider.SelectedAccountUUIDProviderImpl
import co.electriccoin.zcash.ui.common.provider.ShieldFundsInfoProvider
import co.electriccoin.zcash.ui.common.provider.ShieldFundsInfoProviderImpl
import co.electriccoin.zcash.ui.common.provider.SynchronizerProvider
import co.electriccoin.zcash.ui.common.provider.SynchronizerProviderImpl
import org.koin.core.module.dsl.factoryOf
import co.electriccoin.zcash.ui.common.provider.WalletBackupConsentStorageProvider
import co.electriccoin.zcash.ui.common.provider.WalletBackupConsentStorageProviderImpl
import co.electriccoin.zcash.ui.common.provider.WalletBackupFlagStorageProvider
import co.electriccoin.zcash.ui.common.provider.WalletBackupFlagStorageProviderImpl
import co.electriccoin.zcash.ui.common.provider.WalletBackupRemindMeCountStorageProvider
import co.electriccoin.zcash.ui.common.provider.WalletBackupRemindMeCountStorageProviderImpl
import co.electriccoin.zcash.ui.common.provider.WalletBackupRemindMeTimestampStorageProvider
import co.electriccoin.zcash.ui.common.provider.WalletBackupRemindMeTimestampStorageProviderImpl
import co.electriccoin.zcash.ui.common.provider.WalletRestoringStateProvider
import co.electriccoin.zcash.ui.common.provider.WalletRestoringStateProviderImpl
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind
import org.koin.dsl.module
val providerModule =
module {
factoryOf(::GetDefaultServersProvider)
factoryOf(::GetVersionInfoProvider)
factoryOf(::GetZcashCurrencyProvider)
factoryOf(::GetMonetarySeparatorProvider)
factoryOf(::SelectedAccountUUIDProviderImpl) bind SelectedAccountUUIDProvider::class
singleOf(::GetDefaultServersProvider)
singleOf(::GetVersionInfoProvider)
singleOf(::GetZcashCurrencyProvider)
singleOf(::GetMonetarySeparatorProvider)
singleOf(::SelectedAccountUUIDProviderImpl) bind SelectedAccountUUIDProvider::class
singleOf(::PersistableWalletProviderImpl) bind PersistableWalletProvider::class
singleOf(::SynchronizerProviderImpl) bind SynchronizerProvider::class
singleOf(::ApplicationStateProviderImpl) bind ApplicationStateProvider::class
factoryOf(::RestoreTimestampStorageProviderImpl) bind RestoreTimestampStorageProvider::class
singleOf(::RestoreTimestampStorageProviderImpl) bind RestoreTimestampStorageProvider::class
singleOf(::WalletBackupRemindMeCountStorageProviderImpl) bind
WalletBackupRemindMeCountStorageProvider::class
singleOf(::WalletBackupRemindMeTimestampStorageProviderImpl) bind
WalletBackupRemindMeTimestampStorageProvider::class
singleOf(::WalletBackupFlagStorageProviderImpl) bind WalletBackupFlagStorageProvider::class
singleOf(::WalletBackupConsentStorageProviderImpl) bind WalletBackupConsentStorageProvider::class
singleOf(::WalletRestoringStateProviderImpl) bind WalletRestoringStateProvider::class
singleOf(::CrashReportingStorageProviderImpl) bind CrashReportingStorageProvider::class
singleOf(::PersistableWalletStorageProviderImpl) bind PersistableWalletStorageProvider::class
singleOf(::ShieldFundsInfoProviderImpl) bind ShieldFundsInfoProvider::class
}

View File

@ -1,7 +1,5 @@
package co.electriccoin.zcash.di
import co.electriccoin.zcash.ui.common.repository.BalanceRepository
import co.electriccoin.zcash.ui.common.repository.BalanceRepositoryImpl
import co.electriccoin.zcash.ui.common.repository.BiometricRepository
import co.electriccoin.zcash.ui.common.repository.BiometricRepositoryImpl
import co.electriccoin.zcash.ui.common.repository.ConfigurationRepository
@ -10,14 +8,20 @@ import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepositoryImpl
import co.electriccoin.zcash.ui.common.repository.FlexaRepository
import co.electriccoin.zcash.ui.common.repository.FlexaRepositoryImpl
import co.electriccoin.zcash.ui.common.repository.HomeMessageCacheRepository
import co.electriccoin.zcash.ui.common.repository.HomeMessageCacheRepositoryImpl
import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepository
import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepositoryImpl
import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepository
import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepositoryImpl
import co.electriccoin.zcash.ui.common.repository.TransactionFilterRepository
import co.electriccoin.zcash.ui.common.repository.TransactionFilterRepositoryImpl
import co.electriccoin.zcash.ui.common.repository.TransactionRepository
import co.electriccoin.zcash.ui.common.repository.TransactionRepositoryImpl
import co.electriccoin.zcash.ui.common.repository.WalletRepository
import co.electriccoin.zcash.ui.common.repository.WalletRepositoryImpl
import co.electriccoin.zcash.ui.common.repository.WalletSnapshotRepository
import co.electriccoin.zcash.ui.common.repository.WalletSnapshotRepositoryImpl
import co.electriccoin.zcash.ui.common.repository.ZashiProposalRepository
import co.electriccoin.zcash.ui.common.repository.ZashiProposalRepositoryImpl
import org.koin.core.module.dsl.singleOf
@ -29,11 +33,13 @@ val repositoryModule =
singleOf(::WalletRepositoryImpl) bind WalletRepository::class
singleOf(::ConfigurationRepositoryImpl) bind ConfigurationRepository::class
singleOf(::ExchangeRateRepositoryImpl) bind ExchangeRateRepository::class
singleOf(::BalanceRepositoryImpl) bind BalanceRepository::class
singleOf(::FlexaRepositoryImpl) bind FlexaRepository::class
singleOf(::BiometricRepositoryImpl) bind BiometricRepository::class
singleOf(::KeystoneProposalRepositoryImpl) bind KeystoneProposalRepository::class
singleOf(::TransactionRepositoryImpl) bind TransactionRepository::class
singleOf(::TransactionFilterRepositoryImpl) bind TransactionFilterRepository::class
singleOf(::ZashiProposalRepositoryImpl) bind ZashiProposalRepository::class
singleOf(::ShieldFundsRepositoryImpl) bind ShieldFundsRepository::class
singleOf(::HomeMessageCacheRepositoryImpl) bind HomeMessageCacheRepository::class
singleOf(::WalletSnapshotRepositoryImpl) bind WalletSnapshotRepository::class
}

View File

@ -5,22 +5,24 @@ import co.electriccoin.zcash.ui.common.usecase.ApplyTransactionFulltextFiltersUs
import co.electriccoin.zcash.ui.common.usecase.CancelProposalFlowUseCase
import co.electriccoin.zcash.ui.common.usecase.ConfirmProposalUseCase
import co.electriccoin.zcash.ui.common.usecase.CopyToClipboardUseCase
import co.electriccoin.zcash.ui.common.usecase.CreateFlexaTransactionUseCase
import co.electriccoin.zcash.ui.common.usecase.CreateKeystoneAccountUseCase
import co.electriccoin.zcash.ui.common.usecase.CreateKeystoneProposalPCZTEncoderUseCase
import co.electriccoin.zcash.ui.common.usecase.CreateKeystoneShieldProposalUseCase
import co.electriccoin.zcash.ui.common.usecase.CreateOrUpdateTransactionNoteUseCase
import co.electriccoin.zcash.ui.common.usecase.CreateProposalUseCase
import co.electriccoin.zcash.ui.common.usecase.CreateZip321ProposalUseCase
import co.electriccoin.zcash.ui.common.usecase.DeleteContactUseCase
import co.electriccoin.zcash.ui.common.usecase.DeleteTransactionNoteUseCase
import co.electriccoin.zcash.ui.common.usecase.DeriveKeystoneAccountUnifiedAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.ExportTaxUseCase
import co.electriccoin.zcash.ui.common.usecase.FlipTransactionBookmarkUseCase
import co.electriccoin.zcash.ui.common.usecase.GetBackupPersistableWalletUseCase
import co.electriccoin.zcash.ui.common.usecase.GetCoinbaseStatusUseCase
import co.electriccoin.zcash.ui.common.usecase.GetConfigurationUseCase
import co.electriccoin.zcash.ui.common.usecase.GetContactByAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.GetCurrentFilteredTransactionsUseCase
import co.electriccoin.zcash.ui.common.usecase.GetCurrentTransactionsUseCase
import co.electriccoin.zcash.ui.common.usecase.GetExchangeRateUseCase
import co.electriccoin.zcash.ui.common.usecase.GetFlexaStatusUseCase
import co.electriccoin.zcash.ui.common.usecase.GetHomeMessageUseCase
import co.electriccoin.zcash.ui.common.usecase.GetKeystoneStatusUseCase
import co.electriccoin.zcash.ui.common.usecase.GetMetadataUseCase
import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase
import co.electriccoin.zcash.ui.common.usecase.GetProposalUseCase
@ -31,42 +33,48 @@ import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase
import co.electriccoin.zcash.ui.common.usecase.GetTransactionDetailByIdUseCase
import co.electriccoin.zcash.ui.common.usecase.GetTransactionFiltersUseCase
import co.electriccoin.zcash.ui.common.usecase.GetTransactionMetadataUseCase
import co.electriccoin.zcash.ui.common.usecase.GetTransactionsUseCase
import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.GetWalletAccountsUseCase
import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase
import co.electriccoin.zcash.ui.common.usecase.GetZashiAccountUseCase
import co.electriccoin.zcash.ui.common.usecase.GetZashiSpendingKeyUseCase
import co.electriccoin.zcash.ui.common.usecase.IsCoinbaseAvailableUseCase
import co.electriccoin.zcash.ui.common.usecase.IsFlexaAvailableUseCase
import co.electriccoin.zcash.ui.common.usecase.IsRestoreSuccessDialogVisibleUseCase
import co.electriccoin.zcash.ui.common.usecase.MarkTxMemoAsReadUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToAddressBookUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToErrorUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToTaxExportUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToWalletBackupUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveClearSendUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveContactByAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveFastestServersUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveOnAccountChangedUseCase
import co.electriccoin.zcash.ui.common.usecase.ObservePersistableWalletUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveProposalUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedEndpointUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedWalletAccountUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveTransactionSubmitStateUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveWalletAccountsUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveWalletStateUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveZashiAccountUseCase
import co.electriccoin.zcash.ui.common.usecase.OnAddressScannedUseCase
import co.electriccoin.zcash.ui.common.usecase.OnUserSavedWalletBackupUseCase
import co.electriccoin.zcash.ui.common.usecase.OnZip321ScannedUseCase
import co.electriccoin.zcash.ui.common.usecase.ParseKeystonePCZTUseCase
import co.electriccoin.zcash.ui.common.usecase.ParseKeystoneSignInRequestUseCase
import co.electriccoin.zcash.ui.common.usecase.ParseKeystoneUrToZashiAccountsUseCase
import co.electriccoin.zcash.ui.common.usecase.PersistEndpointUseCase
import co.electriccoin.zcash.ui.common.usecase.PrefillSendUseCase
import co.electriccoin.zcash.ui.common.usecase.RefreshFastestServersUseCase
import co.electriccoin.zcash.ui.common.usecase.RemindWalletBackupLaterUseCase
import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase
import co.electriccoin.zcash.ui.common.usecase.RescanQrUseCase
import co.electriccoin.zcash.ui.common.usecase.ResetInMemoryDataUseCase
import co.electriccoin.zcash.ui.common.usecase.ResetSharedPrefsDataUseCase
import co.electriccoin.zcash.ui.common.usecase.ResetTransactionFiltersUseCase
import co.electriccoin.zcash.ui.common.usecase.RestoreWalletUseCase
import co.electriccoin.zcash.ui.common.usecase.SaveContactUseCase
import co.electriccoin.zcash.ui.common.usecase.SelectWalletAccountUseCase
import co.electriccoin.zcash.ui.common.usecase.SendEmailUseCase
@ -74,10 +82,13 @@ import co.electriccoin.zcash.ui.common.usecase.SendSupportEmailUseCase
import co.electriccoin.zcash.ui.common.usecase.SendTransactionAgainUseCase
import co.electriccoin.zcash.ui.common.usecase.ShareImageUseCase
import co.electriccoin.zcash.ui.common.usecase.SharePCZTUseCase
import co.electriccoin.zcash.ui.common.usecase.ShieldFundsMessageUseCase
import co.electriccoin.zcash.ui.common.usecase.ShieldFundsUseCase
import co.electriccoin.zcash.ui.common.usecase.UpdateContactUseCase
import co.electriccoin.zcash.ui.common.usecase.ValidateContactAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.ValidateContactNameUseCase
import co.electriccoin.zcash.ui.common.usecase.ValidateEndpointUseCase
import co.electriccoin.zcash.ui.common.usecase.ValidateSeedUseCase
import co.electriccoin.zcash.ui.common.usecase.ViewTransactionDetailAfterSuccessfulProposalUseCase
import co.electriccoin.zcash.ui.common.usecase.ViewTransactionsAfterSuccessfulProposalUseCase
import co.electriccoin.zcash.ui.common.usecase.Zip321BuildUriUseCase
@ -99,7 +110,7 @@ val useCaseModule =
factoryOf(::ValidateEndpointUseCase)
factoryOf(::GetPersistableWalletUseCase)
factoryOf(::GetSelectedEndpointUseCase)
factoryOf(::ObserveConfigurationUseCase)
factoryOf(::GetConfigurationUseCase)
factoryOf(::RescanBlockchainUseCase)
factoryOf(::GetTransparentAddressUseCase)
factoryOf(::ValidateContactAddressUseCase)
@ -114,16 +125,13 @@ val useCaseModule =
factoryOf(::ShareImageUseCase)
factoryOf(::Zip321BuildUriUseCase)
factoryOf(::Zip321ParseUriValidationUseCase)
factoryOf(::ObserveWalletStateUseCase)
factoryOf(::IsCoinbaseAvailableUseCase)
factoryOf(::GetZashiSpendingKeyUseCase)
factoryOf(::ObservePersistableWalletUseCase)
factoryOf(::GetBackupPersistableWalletUseCase)
factoryOf(::GetSupportUseCase)
factoryOf(::SendEmailUseCase)
factoryOf(::SendSupportEmailUseCase)
factoryOf(::IsFlexaAvailableUseCase)
factoryOf(::ObserveWalletAccountsUseCase)
factoryOf(::GetWalletAccountsUseCase)
factoryOf(::SelectWalletAccountUseCase)
factoryOf(::ObserveSelectedWalletAccountUseCase)
factoryOf(::ObserveZashiAccountUseCase)
@ -135,18 +143,17 @@ val useCaseModule =
factoryOf(::GetSelectedWalletAccountUseCase)
singleOf(::ObserveClearSendUseCase)
singleOf(::PrefillSendUseCase)
factoryOf(::GetCurrentTransactionsUseCase)
factoryOf(::GetTransactionsUseCase)
factoryOf(::GetCurrentFilteredTransactionsUseCase) onClose ::closeableCallback
factoryOf(::CreateProposalUseCase)
factoryOf(::CreateZip321ProposalUseCase)
factoryOf(::CreateKeystoneShieldProposalUseCase)
factoryOf(::OnZip321ScannedUseCase)
factoryOf(::OnAddressScannedUseCase)
factoryOf(::ParseKeystonePCZTUseCase)
factoryOf(::ParseKeystoneSignInRequestUseCase)
factoryOf(::CancelProposalFlowUseCase)
factoryOf(::ObserveProposalUseCase)
factoryOf(::SharePCZTUseCase)
factoryOf(::CreateKeystoneProposalPCZTEncoderUseCase)
factoryOf(::ObserveOnAccountChangedUseCase)
factoryOf(::ViewTransactionsAfterSuccessfulProposalUseCase)
factoryOf(::ViewTransactionDetailAfterSuccessfulProposalUseCase)
factoryOf(::NavigateToCoinbaseUseCase)
@ -172,4 +179,19 @@ val useCaseModule =
factoryOf(::GetMetadataUseCase)
factoryOf(::ExportTaxUseCase)
factoryOf(::NavigateToTaxExportUseCase)
factoryOf(::CreateFlexaTransactionUseCase)
factoryOf(::IsRestoreSuccessDialogVisibleUseCase)
factoryOf(::ValidateSeedUseCase)
factoryOf(::RestoreWalletUseCase)
factoryOf(::NavigateToWalletBackupUseCase)
factoryOf(::GetKeystoneStatusUseCase)
factoryOf(::GetCoinbaseStatusUseCase)
factoryOf(::GetFlexaStatusUseCase)
singleOf(::GetHomeMessageUseCase)
factoryOf(::OnUserSavedWalletBackupUseCase)
factoryOf(::RemindWalletBackupLaterUseCase)
singleOf(::ShieldFundsUseCase)
singleOf(::NavigateToErrorUseCase)
factoryOf(::RescanQrUseCase)
factoryOf(::ShieldFundsMessageUseCase)
}

Some files were not shown because too many files have changed in this diff Show More