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] ## [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 ## [1.5.2 (932)] - 2025-04-23
### Added ### Added

View File

@ -25,6 +25,12 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </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> </activity-alias>
<!-- Enable profiling by benchmark --> <!-- 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.repositoryModule
import co.electriccoin.zcash.di.useCaseModule import co.electriccoin.zcash.di.useCaseModule
import co.electriccoin.zcash.di.viewModelModule import co.electriccoin.zcash.di.viewModelModule
import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.spackle.StrictModeCompat import co.electriccoin.zcash.spackle.StrictModeCompat
import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider 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.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 kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
@ -27,10 +28,12 @@ import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
class ZcashApplication : CoroutineApplication() { class ZcashApplication : CoroutineApplication() {
private val standardPreferenceProvider by inject<StandardPreferenceProvider>()
private val flexaRepository by inject<FlexaRepository>() private val flexaRepository by inject<FlexaRepository>()
private val applicationStateProvider: ApplicationStateProvider by inject() private val applicationStateProvider: ApplicationStateProvider by inject()
private val getAvailableCrashReporters: CrashReportersProvider 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() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -68,6 +71,8 @@ class ZcashApplication : CoroutineApplication() {
configureAnalytics() configureAnalytics()
flexaRepository.init() flexaRepository.init()
homeMessageCacheRepository.init()
walletSnapshotRepository.init()
} }
private fun configureLogging() { private fun configureLogging() {
@ -89,9 +94,9 @@ class ZcashApplication : CoroutineApplication() {
private fun configureAnalytics() { private fun configureAnalytics() {
if (GlobalCrashReporter.register(this, getAvailableCrashReporters())) { if (GlobalCrashReporter.register(this, getAvailableCrashReporters())) {
applicationScope.launch { applicationScope.launch {
StandardPreferenceKeys.IS_ANALYTICS_ENABLED.observe(standardPreferenceProvider()).collect { crashReportingStorageProvider.observe().collect {
Twig.debug { "Is crashlytics enabled: $it" } Twig.debug { "Is crashlytics enabled: $it" }
if (it) { if (it == true) {
GlobalCrashReporter.enable() GlobalCrashReporter.enable()
} else { } else {
GlobalCrashReporter.disableAndDelete() GlobalCrashReporter.disableAndDelete()

View File

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

View File

@ -12,6 +12,20 @@ directly impact users rather than highlighting other key architectural updates.*
## [Unreleased] ## [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 ## [1.5.2 (932)] - 2025-04-23
### Added: ### Added:

View File

@ -12,6 +12,19 @@ directly impact users rather than highlighting other key architectural updates.*
## [Unreleased] ## [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 ## [1.5.2 (932)] - 2025-04-23
### Añadido: ### 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. # 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. # If not using automated Google Play deployment, then these serve as the actual version numbers.
ZCASH_VERSION_CODE=1 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") # 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 # 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 # WARNING: Ensure a non-snapshot version is used before releasing to production
ZCASH_BIP39_VERSION=1.0.9 ZCASH_BIP39_VERSION=1.0.9
# WARNING: Ensure a non-snapshot version is used before releasing to production # 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 # Toolchain is the Java version used to build the application, which is separate from the
# Java version used to run the application. # Java version used to run the application.

View File

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

View File

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

View File

@ -39,6 +39,10 @@ interface PreferenceDefault<T> {
newValue: T newValue: T
) )
suspend fun clear(preferenceProvider: PreferenceProvider) {
preferenceProvider.remove(key)
}
/** /**
* @param preferenceProvider Provides actual preference values. * @param preferenceProvider Provides actual preference values.
* @return Flow that emits preference changes. Note that implementations should emit an initial value * @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 // For the mock implementation, does not support observability of changes
override fun observe(key: PreferenceKey): Flow<String?> = flow { emit(getString(key)) } 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 { override suspend fun clearPreferences(): Boolean {
map.clear() map.clear()
return true return true

View File

@ -11,9 +11,11 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -32,6 +34,8 @@ class AndroidPreferenceProvider private constructor(
private val sharedPreferences: SharedPreferences, private val sharedPreferences: SharedPreferences,
private val dispatcher: CoroutineDispatcher private val dispatcher: CoroutineDispatcher
) : PreferenceProvider { ) : PreferenceProvider {
private val clearPipeline = MutableSharedFlow<Unit>()
private val mutex = Mutex() private val mutex = Mutex()
/* /*
* Implementation note: EncryptedSharedPreferences are not thread-safe, so this implementation * Implementation note: EncryptedSharedPreferences are not thread-safe, so this implementation
@ -119,6 +123,8 @@ class AndroidPreferenceProvider private constructor(
editor.clear() editor.clear()
clearPipeline.emit(Unit)
return@withContext editor.commit() return@withContext editor.commit()
} }
@ -131,6 +137,12 @@ class AndroidPreferenceProvider private constructor(
} }
sharedPreferences.registerOnSharedPreferenceChangeListener(listener) sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
this.launch {
clearPipeline.collect {
send(Unit)
}
}
// Kickstart the emissions // Kickstart the emissions
trySend(Unit) trySend(Unit)
@ -140,6 +152,17 @@ class AndroidPreferenceProvider private constructor(
}.flowOn(dispatcher) }.flowOn(dispatcher)
.map { getString(key) } .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 { companion object {
suspend fun newStandard( suspend fun newStandard(
context: Context, 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( data class ZecAmountPair(
val main: String, val main: String,
val suffix: 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 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.basicMarquee
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable 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 textColor Optional color to modify the default font color from [textStyle]
* @param modifier Modifier to modify the Text UI element as needed * @param modifier Modifier to modify the Text UI element as needed
*/ */
@OptIn(ExperimentalFoundationApi::class)
@Suppress("LongParameterList") @Suppress("LongParameterList")
@Composable @Composable
fun StyledBalance( fun StyledBalance(
balanceParts: ZecAmountTriple, balanceParts: ZecAmountTriple,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
isHideBalances: Boolean = false, isHideBalances: Boolean = false,
showDust: Boolean = true,
hiddenBalancePlaceholder: String = stringResource(id = R.string.hide_balance_placeholder), hiddenBalancePlaceholder: String = stringResource(id = R.string.hide_balance_placeholder),
textColor: Color = Color.Unspecified, textColor: Color = Color.Unspecified,
textStyle: BalanceTextStyle = StyledBalanceDefaults.textStyles(), textStyle: BalanceTextStyle = StyledBalanceDefaults.textStyles(),
@ -91,6 +90,7 @@ fun StyledBalance(
) { ) {
append(balanceSplit.first) append(balanceSplit.first)
} }
if (showDust) {
withStyle( withStyle(
style = textStyle.leastSignificantPart.toSpanStyle() style = textStyle.leastSignificantPart.toSpanStyle()
) { ) {
@ -98,13 +98,14 @@ fun StyledBalance(
} }
} }
} }
}
val resultModifier = val resultModifier =
Modifier Modifier
.basicMarquee() .basicMarquee()
.animateContentSize()
.then(modifier) .then(modifier)
SelectionContainer {
Text( Text(
text = content, text = content,
color = textColor, color = textColor,
@ -112,6 +113,7 @@ fun StyledBalance(
modifier = resultModifier modifier = resultModifier
) )
} }
}
private const val CUT_POSITION_OFFSET = 4 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
@ -66,7 +68,9 @@ fun LabeledCheckBox(
text: String, text: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
checked: Boolean = false, 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) } val (checkedState, setCheckedState) = rememberSaveable { mutableStateOf(checked) }
@ -113,8 +117,8 @@ fun LabeledCheckBox(
) )
Text( Text(
text = AnnotatedString(text), text = AnnotatedString(text),
color = ZcashTheme.colors.textPrimary, color = color,
style = ZcashTheme.extendedTypography.checkboxText 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.Image
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@ -21,17 +20,19 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp 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.colors.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.scaffoldPadding import co.electriccoin.zcash.ui.design.util.scaffoldPadding
@Suppress("LongMethod") @Suppress("LongMethod")
@Composable @Composable
internal fun BaseExchangeRateOptIn( fun ZashiBaseSettingsOptIn(
header: String,
@DrawableRes image: Int,
info: String?,
onDismiss: () -> Unit, onDismiss: () -> Unit,
footer: @Composable ColumnScope.() -> Unit, footer: @Composable ColumnScope.() -> Unit,
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
@ -54,7 +55,7 @@ internal fun BaseExchangeRateOptIn(
) )
) { ) {
Image( Image(
painter = painterResource(id = R.drawable.ic_exchange_rate_close), painter = painterResource(id = R.drawable.ic_settings_opt_int_close),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(ZashiColors.Btns.Tertiary.btnTertiaryFg) colorFilter = ColorFilter.tint(ZashiColors.Btns.Tertiary.btnTertiaryFg)
) )
@ -68,31 +69,21 @@ internal fun BaseExchangeRateOptIn(
.weight(1f) .weight(1f)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
Image(painter = painterResource(R.drawable.exchange_rate), contentDescription = null) Image(painter = painterResource(image), contentDescription = null)
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = stringResource(id = R.string.exchange_rate_opt_in_subtitle), text = header,
color = ZashiColors.Text.textPrimary, color = ZashiColors.Text.textPrimary,
style = ZashiTypography.header6, style = ZashiTypography.header6,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
) )
content() content()
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Row { if (info != null) {
Image( Spacer(modifier = Modifier.height(24.dp))
painter = painterResource(R.drawable.ic_exchange_rate_info), ZashiInfoText(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
)
} }
} }
Spacer(modifier = Modifier.height(20.dp)) 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter 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.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme 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.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.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.StringResource import co.electriccoin.zcash.ui.design.util.StringResource
import co.electriccoin.zcash.ui.design.util.getValue import co.electriccoin.zcash.ui.design.util.getValue
@ -42,7 +44,7 @@ fun ZashiButton(
style: TextStyle = ZashiButtonDefaults.style, style: TextStyle = ZashiButtonDefaults.style,
shape: Shape = ZashiButtonDefaults.shape, shape: Shape = ZashiButtonDefaults.shape,
contentPadding: PaddingValues = ZashiButtonDefaults.contentPadding, contentPadding: PaddingValues = ZashiButtonDefaults.contentPadding,
colors: ZashiButtonColors = ZashiButtonDefaults.primaryColors(), colors: ZashiButtonColors = LocalZashiButtonColors.current ?: ZashiButtonDefaults.primaryColors(),
content: @Composable RowScope.(ZashiButtonScope) -> Unit = ZashiButtonDefaults.content content: @Composable RowScope.(ZashiButtonScope) -> Unit = ZashiButtonDefaults.content
) { ) {
ZashiButton( ZashiButton(
@ -67,14 +69,14 @@ fun ZashiButton(
text: String, text: String,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
style: TextStyle = ZashiButtonDefaults.style,
shape: Shape = ZashiButtonDefaults.shape,
contentPadding: PaddingValues = ZashiButtonDefaults.contentPadding,
@DrawableRes icon: Int? = null, @DrawableRes icon: Int? = null,
@DrawableRes trailingIcon: Int? = null, @DrawableRes trailingIcon: Int? = null,
enabled: Boolean = true, enabled: Boolean = true,
isLoading: Boolean = false, 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 content: @Composable RowScope.(ZashiButtonScope) -> Unit = ZashiButtonDefaults.content
) { ) {
val scope = val scope =
@ -180,10 +182,11 @@ object ZashiButtonDefaults {
@Composable @Composable
fun primaryColors( fun primaryColors(
containerColor: Color = ZashiColors.Btns.Primary.btnPrimaryBg, source: ZashiColorsInternal = ZashiColors,
contentColor: Color = ZashiColors.Btns.Primary.btnPrimaryFg, containerColor: Color = source.Btns.Primary.btnPrimaryBg,
disabledContainerColor: Color = ZashiColors.Btns.Primary.btnPrimaryBgDisabled, contentColor: Color = source.Btns.Primary.btnPrimaryFg,
disabledContentColor: Color = ZashiColors.Btns.Primary.btnBoldFgDisabled, disabledContainerColor: Color = source.Btns.Primary.btnPrimaryBgDisabled,
disabledContentColor: Color = source.Btns.Primary.btnBoldFgDisabled,
) = ZashiButtonColors( ) = ZashiButtonColors(
containerColor = containerColor, containerColor = containerColor,
contentColor = contentColor, contentColor = contentColor,
@ -195,11 +198,12 @@ object ZashiButtonDefaults {
@Composable @Composable
fun secondaryColors( fun secondaryColors(
containerColor: Color = ZashiColors.Btns.Secondary.btnSecondaryBg, source: ZashiColorsInternal = ZashiColors,
contentColor: Color = ZashiColors.Btns.Secondary.btnSecondaryFg, containerColor: Color = source.Btns.Secondary.btnSecondaryBg,
contentColor: Color = source.Btns.Secondary.btnSecondaryFg,
borderColor: Color = Color.Unspecified, borderColor: Color = Color.Unspecified,
disabledContainerColor: Color = ZashiColors.Btns.Secondary.btnSecondaryBgDisabled, disabledContainerColor: Color = source.Btns.Secondary.btnSecondaryBgDisabled,
disabledContentColor: Color = ZashiColors.Btns.Secondary.btnSecondaryFg, disabledContentColor: Color = source.Btns.Secondary.btnSecondaryFgDisabled,
) = ZashiButtonColors( ) = ZashiButtonColors(
containerColor = containerColor, containerColor = containerColor,
contentColor = contentColor, contentColor = contentColor,
@ -211,10 +215,11 @@ object ZashiButtonDefaults {
@Composable @Composable
fun tertiaryColors( fun tertiaryColors(
containerColor: Color = ZashiColors.Btns.Tertiary.btnTertiaryBg, source: ZashiColorsInternal = ZashiColors,
contentColor: Color = ZashiColors.Btns.Tertiary.btnTertiaryFg, containerColor: Color = source.Btns.Tertiary.btnTertiaryBg,
disabledContainerColor: Color = ZashiColors.Btns.Tertiary.btnTertiaryBgDisabled, contentColor: Color = source.Btns.Tertiary.btnTertiaryFg,
disabledContentColor: Color = ZashiColors.Btns.Tertiary.btnTertiaryFgDisabled, disabledContainerColor: Color = source.Btns.Tertiary.btnTertiaryBgDisabled,
disabledContentColor: Color = source.Btns.Tertiary.btnTertiaryFgDisabled,
) = ZashiButtonColors( ) = ZashiButtonColors(
containerColor = containerColor, containerColor = containerColor,
contentColor = contentColor, contentColor = contentColor,
@ -226,11 +231,12 @@ object ZashiButtonDefaults {
@Composable @Composable
fun destructive1Colors( fun destructive1Colors(
containerColor: Color = ZashiColors.Btns.Destructive1.btnDestroy1Bg, source: ZashiColorsInternal = ZashiColors,
contentColor: Color = ZashiColors.Btns.Destructive1.btnDestroy1Fg, containerColor: Color = source.Btns.Destructive1.btnDestroy1Bg,
borderColor: Color = ZashiColors.Btns.Destructive1.btnDestroy1Border, contentColor: Color = source.Btns.Destructive1.btnDestroy1Fg,
disabledContainerColor: Color = ZashiColors.Btns.Destructive1.btnDestroy1BgDisabled, borderColor: Color = source.Btns.Destructive1.btnDestroy1Border,
disabledContentColor: Color = ZashiColors.Btns.Destructive1.btnDestroy1FgDisabled, disabledContainerColor: Color = source.Btns.Destructive1.btnDestroy1BgDisabled,
disabledContentColor: Color = source.Btns.Destructive1.btnDestroy1FgDisabled,
) = ZashiButtonColors( ) = ZashiButtonColors(
containerColor = containerColor, containerColor = containerColor,
contentColor = contentColor, contentColor = contentColor,
@ -242,11 +248,12 @@ object ZashiButtonDefaults {
@Composable @Composable
fun destructive2Colors( fun destructive2Colors(
containerColor: Color = ZashiColors.Btns.Destructive2.btnDestroy2Bg, source: ZashiColorsInternal = ZashiColors,
contentColor: Color = ZashiColors.Btns.Destructive2.btnDestroy2Fg, containerColor: Color = source.Btns.Destructive2.btnDestroy2Bg,
contentColor: Color = source.Btns.Destructive2.btnDestroy2Fg,
borderColor: Color = Color.Unspecified, borderColor: Color = Color.Unspecified,
disabledContainerColor: Color = ZashiColors.Btns.Destructive2.btnDestroy2BgDisabled, disabledContainerColor: Color = source.Btns.Destructive2.btnDestroy2BgDisabled,
disabledContentColor: Color = ZashiColors.Btns.Destructive2.btnDestroy2FgDisabled, disabledContentColor: Color = source.Btns.Destructive2.btnDestroy2FgDisabled,
) = ZashiButtonColors( ) = ZashiButtonColors(
containerColor = containerColor, containerColor = containerColor,
contentColor = contentColor, contentColor = contentColor,
@ -278,7 +285,7 @@ data class ButtonState(
) )
@Composable @Composable
private fun ZashiButtonColors.toButtonColors() = fun ZashiButtonColors.toButtonColors() =
ButtonDefaults.buttonColors( ButtonDefaults.buttonColors(
containerColor = containerColor, containerColor = containerColor,
contentColor = contentColor, contentColor = contentColor,
@ -286,6 +293,12 @@ private fun ZashiButtonColors.toButtonColors() =
disabledContentColor = disabledContentColor, disabledContentColor = disabledContentColor,
) )
@Suppress("CompositionLocalAllowlist")
val LocalZashiButtonColors =
compositionLocalOf<ZashiButtonColors?> {
null
}
@PreviewScreens @PreviewScreens
@Composable @Composable
private fun PrimaryPreview() = private fun PrimaryPreview() =

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import androidx.annotation.DrawableRes
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -41,30 +39,17 @@ fun ZashiChipButton(
border: BorderStroke? = ZashiChipButtonDefaults.border, border: BorderStroke? = ZashiChipButtonDefaults.border,
color: Color = ZashiChipButtonDefaults.color, color: Color = ZashiChipButtonDefaults.color,
contentPadding: PaddingValues = ZashiChipButtonDefaults.contentPadding, contentPadding: PaddingValues = ZashiChipButtonDefaults.contentPadding,
hasRippleEffect: Boolean = true,
textStyle: TextStyle = ZashiChipButtonDefaults.textStyle, textStyle: TextStyle = ZashiChipButtonDefaults.textStyle,
endIconSpacer: Dp = ZashiChipButtonDefaults.endIconSpacer, 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( Surface(
modifier = clickableModifier, modifier = modifier,
shape = shape, shape = shape,
border = border, border = border,
color = color, color = color,
) { ) {
Row( Row(
modifier = Modifier.padding(contentPadding), modifier = Modifier.clickable(onClick = state.onClick) then Modifier.padding(contentPadding),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
if (state.startIcon != null) { 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 @Composable
@ExperimentalMaterial3Api @ExperimentalMaterial3Api
private fun rememberSheetState( fun rememberSheetState(
skipPartiallyExpanded: Boolean, skipPartiallyExpanded: Boolean,
confirmValueChange: (SheetValue) -> Boolean, confirmValueChange: (SheetValue) -> Boolean,
initialValue: SheetValue, 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.border
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@ -22,23 +23,32 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse 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.TextStyle
import androidx.compose.ui.text.font.FontWeight 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.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme 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.ZashiColors
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography 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.getValue
import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.design.util.stringRes
@ -48,9 +58,18 @@ fun ZashiTextField(
value: String, value: String,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
innerModifier: Modifier = Modifier, innerModifier: Modifier = ZashiTextFieldDefaults.innerModifier,
error: String? = null, error: String? = null,
isEnabled: Boolean = true, isEnabled: Boolean = true,
handle: ZashiTextFieldHandle =
rememberZashiTextFieldHandle(
TextFieldState(
value = stringRes(value),
error = error?.let { stringRes(it) },
isEnabled = isEnabled,
onValueChange = onValueChange,
)
),
readOnly: Boolean = false, readOnly: Boolean = false,
textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium), textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium),
label: @Composable (() -> Unit)? = null, label: @Composable (() -> Unit)? = null,
@ -97,7 +116,8 @@ fun ZashiTextField(
interactionSource = interactionSource, interactionSource = interactionSource,
shape = shape, shape = shape,
colors = colors, colors = colors,
innerModifier = innerModifier innerModifier = innerModifier,
handle = handle,
) )
} }
@ -106,7 +126,8 @@ fun ZashiTextField(
fun ZashiTextField( fun ZashiTextField(
state: TextFieldState, state: TextFieldState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
innerModifier: Modifier = Modifier, innerModifier: Modifier = ZashiTextFieldDefaults.innerModifier,
handle: ZashiTextFieldHandle = rememberZashiTextFieldHandle(state),
readOnly: Boolean = false, readOnly: Boolean = false,
textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium), textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium),
label: @Composable (() -> Unit)? = null, label: @Composable (() -> Unit)? = null,
@ -124,6 +145,13 @@ fun ZashiTextField(
minLines: Int = 1, minLines: Int = 1,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = ZashiTextFieldDefaults.shape, 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() colors: ZashiTextFieldColors = ZashiTextFieldDefaults.defaultColors()
) { ) {
TextFieldInternal( TextFieldInternal(
@ -147,10 +175,41 @@ fun ZashiTextField(
interactionSource = interactionSource, interactionSource = interactionSource,
shape = shape, shape = shape,
colors = colors, 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") @Suppress("LongParameterList", "LongMethod")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -174,10 +233,32 @@ private fun TextFieldInternal(
interactionSource: MutableInteractionSource, interactionSource: MutableInteractionSource,
shape: Shape, shape: Shape,
colors: ZashiTextFieldColors, colors: ZashiTextFieldColors,
contentPadding: PaddingValues,
handle: ZashiTextFieldHandle,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
innerModifier: 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() val androidColors = colors.toTextFieldColors()
// If color is not provided via the text style, use content color as a default // If color is not provided via the text style, use content color as a default
val textColor = val textColor =
@ -186,24 +267,35 @@ private fun TextFieldInternal(
} }
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
var lastTextValue by remember(value) { mutableStateOf(value) }
CompositionLocalProvider(LocalTextSelectionColors provides androidColors.selectionColors) { CompositionLocalProvider(LocalTextSelectionColors provides androidColors.selectionColors) {
Column( Column(
modifier = modifier, modifier = modifier,
) { ) {
BasicTextField( BasicTextField(
value = state.value.getValue(), value = textFieldValue,
modifier = modifier =
innerModifier.fillMaxWidth() then innerModifier then
if (borderColor == Color.Unspecified) { if (borderColor == Color.Unspecified) {
Modifier Modifier
} else { } else {
Modifier.border( Modifier.border(
width = 1.dp, width = 1.dp,
color = borderColor, 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, enabled = state.isEnabled,
readOnly = readOnly, readOnly = readOnly,
textStyle = mergedTextStyle, textStyle = mergedTextStyle,
@ -215,7 +307,7 @@ private fun TextFieldInternal(
singleLine = singleLine, singleLine = singleLine,
maxLines = maxLines, maxLines = maxLines,
minLines = minLines, minLines = minLines,
decorationBox = @Composable { innerTextField -> ) { innerTextField: @Composable () -> Unit ->
// places leading icon, text field with label and placeholder, trailing icon // places leading icon, text field with label and placeholder, trailing icon
TextFieldDefaults.DecorationBox( TextFieldDefaults.DecorationBox(
value = state.value.getValue(), value = state.value.getValue(),
@ -243,16 +335,9 @@ private fun TextFieldInternal(
isError = state.isError, isError = state.isError,
interactionSource = interactionSource, interactionSource = interactionSource,
colors = androidColors, colors = androidColors,
contentPadding = contentPadding = 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),
)
) )
} }
)
if (state.error != null && state.error.getValue().isNotEmpty()) { if (state.error != null && state.error.getValue().isNotEmpty()) {
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
@ -303,7 +388,9 @@ data class ZashiTextFieldColors(
val textColor: Color, val textColor: Color,
val hintColor: Color, val hintColor: Color,
val borderColor: Color, val borderColor: Color,
val focusedBorderColor: Color,
val containerColor: Color, val containerColor: Color,
val focusedContainerColor: Color,
val placeholderColor: Color, val placeholderColor: Color,
val disabledTextColor: Color, val disabledTextColor: Color,
val disabledHintColor: Color, val disabledHintColor: Color,
@ -317,11 +404,15 @@ data class ZashiTextFieldColors(
val errorPlaceholderColor: Color, val errorPlaceholderColor: Color,
) { ) {
@Composable @Composable
internal fun borderColor(state: TextFieldState): State<Color> { internal fun borderColor(
state: TextFieldState,
isFocused: Boolean
): State<Color> {
val targetValue = val targetValue =
when { when {
!state.isEnabled -> disabledBorderColor !state.isEnabled -> disabledBorderColor
state.isError -> errorBorderColor state.isError -> errorBorderColor
isFocused -> focusedBorderColor.takeOrElse { borderColor }
else -> borderColor else -> borderColor
} }
return rememberUpdatedState(targetValue) return rememberUpdatedState(targetValue)
@ -345,7 +436,7 @@ data class ZashiTextFieldColors(
unfocusedTextColor = textColor, unfocusedTextColor = textColor,
disabledTextColor = disabledTextColor, disabledTextColor = disabledTextColor,
errorTextColor = errorTextColor, errorTextColor = errorTextColor,
focusedContainerColor = containerColor, focusedContainerColor = focusedContainerColor.takeOrElse { containerColor },
unfocusedContainerColor = containerColor, unfocusedContainerColor = containerColor,
disabledContainerColor = disabledContainerColor, disabledContainerColor = disabledContainerColor,
errorContainerColor = errorContainerColor, errorContainerColor = errorContainerColor,
@ -391,13 +482,21 @@ object ZashiTextFieldDefaults {
val shape: Shape val shape: Shape
get() = RoundedCornerShape(8.dp) get() = RoundedCornerShape(8.dp)
val innerModifier: Modifier
get() =
Modifier
.defaultMinSize(minWidth = TextFieldDefaults.MinWidth)
.fillMaxWidth()
@Suppress("LongParameterList") @Suppress("LongParameterList")
@Composable @Composable
fun defaultColors( fun defaultColors(
textColor: Color = ZashiColors.Inputs.Filled.text, textColor: Color = ZashiColors.Inputs.Filled.text,
hintColor: Color = ZashiColors.Inputs.Default.hint, hintColor: Color = ZashiColors.Inputs.Default.hint,
borderColor: Color = Color.Unspecified, borderColor: Color = Color.Unspecified,
focusedBorderColor: Color = ZashiColors.Inputs.Focused.stroke,
containerColor: Color = ZashiColors.Inputs.Default.bg, containerColor: Color = ZashiColors.Inputs.Default.bg,
focusedContainerColor: Color = ZashiColors.Inputs.Focused.bg,
placeholderColor: Color = ZashiColors.Inputs.Default.text, placeholderColor: Color = ZashiColors.Inputs.Default.text,
disabledTextColor: Color = ZashiColors.Inputs.Disabled.text, disabledTextColor: Color = ZashiColors.Inputs.Disabled.text,
disabledHintColor: Color = ZashiColors.Inputs.Disabled.hint, disabledHintColor: Color = ZashiColors.Inputs.Disabled.hint,
@ -413,7 +512,9 @@ object ZashiTextFieldDefaults {
textColor = textColor, textColor = textColor,
hintColor = hintColor, hintColor = hintColor,
borderColor = borderColor, borderColor = borderColor,
focusedBorderColor = focusedBorderColor,
containerColor = containerColor, containerColor = containerColor,
focusedContainerColor = focusedContainerColor,
placeholderColor = placeholderColor, placeholderColor = placeholderColor,
disabledTextColor = disabledTextColor, disabledTextColor = disabledTextColor,
disabledHintColor = disabledHintColor, 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 @PreviewScreens
@Composable @Composable
private fun DefaultPreview() = 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, textAlign = TextAlign.Center,
fontWeight = FontWeight.SemiBold, 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.material3.RippleDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider 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.balances.LocalBalancesAvailable
import co.electriccoin.zcash.ui.design.theme.colors.DarkZashiColorsInternal import co.electriccoin.zcash.ui.design.theme.colors.DarkZashiColorsInternal
import co.electriccoin.zcash.ui.design.theme.colors.LightZashiColorsInternal import co.electriccoin.zcash.ui.design.theme.colors.LightZashiColorsInternal
@ -49,7 +53,9 @@ fun ZcashTheme(
LocalZashiColors provides zashiColors, LocalZashiColors provides zashiColors,
LocalZashiTypography provides ZashiTypographyInternal, LocalZashiTypography provides ZashiTypographyInternal,
LocalRippleConfiguration provides MaterialRippleConfig, LocalRippleConfiguration provides MaterialRippleConfig,
LocalBalancesAvailable provides balancesAvailable LocalBalancesAvailable provides balancesAvailable,
LocalKeyboardManager provides rememberKeyboardManager(),
LocalSheetStateManager provides rememberSheetStateManager()
) { ) {
ProvideDimens { ProvideDimens {
MaterialTheme( MaterialTheme(

View File

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

View File

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

View File

@ -8,5 +8,17 @@ import androidx.compose.runtime.staticCompositionLocalOf
val ZashiColors: ZashiColorsInternal val ZashiColors: ZashiColorsInternal
@Composable get() = LocalZashiColors.current @Composable get() = LocalZashiColors.current
val ZashiLightColors: ZashiColorsInternal
@Composable get() = LocalLightZashiColors.current
val ZashiDarkColors: ZashiColorsInternal
@Composable get() = LocalDarkZashiColors.current
@Suppress("CompositionLocalAllowlist") @Suppress("CompositionLocalAllowlist")
internal val LocalZashiColors = staticCompositionLocalOf<ZashiColorsInternal> { error("no colors specified") } 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 utilityPurple50: Color,
val utilityPurple100: Color, val utilityPurple100: Color,
val utilityPurple400: Color, val utilityPurple400: Color,
val utilityPurple300: Color val utilityPurple300: Color,
val utilityPurple900: Color
) )
@Immutable @Immutable
@ -661,6 +662,7 @@ data class UtilityEspresso(
val utilityEspresso400: Color, val utilityEspresso400: Color,
val utilityEspresso300: Color, val utilityEspresso300: Color,
val utilityEspresso900: Color, val utilityEspresso900: Color,
val utilityEspresso950: Color,
val utilityEspresso800: 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.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine

View File

@ -17,6 +17,9 @@ sealed interface ImageResource {
value class DisplayString( value class DisplayString(
val value: String val value: String
) : ImageResource ) : ImageResource
@Immutable
data object Loading : ImageResource
} }
@Stable @Stable
@ -26,3 +29,6 @@ fun imageRes(
@Stable @Stable
fun imageRes(value: String): ImageResource = ImageResource.DisplayString(value) 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 address: String,
val abbreviated: Boolean val abbreviated: Boolean
) : StringResource ) : 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 @Stable
fun stringRes( fun stringRes(
@StringRes resource: Int, @StringRes resource: Int,
@ -109,18 +118,15 @@ fun StringResource.getValue(
convertYearMonth: (YearMonth) -> String = StringResourceDefaults::convertYearMonth, convertYearMonth: (YearMonth) -> String = StringResourceDefaults::convertYearMonth,
convertAddress: (StringResource.ByAddress) -> String = StringResourceDefaults::convertAddress, convertAddress: (StringResource.ByAddress) -> String = StringResourceDefaults::convertAddress,
convertTransactionId: (StringResource.ByTransactionId) -> String = StringResourceDefaults::convertTransactionId convertTransactionId: (StringResource.ByTransactionId) -> String = StringResourceDefaults::convertTransactionId
) = when (this) { ): String =
is StringResource.ByResource -> { getString(
val context = LocalContext.current context = LocalContext.current,
context.getString(resource, *args.normalize(context).toTypedArray()) convertZatoshi = convertZatoshi,
} convertDateTime = convertDateTime,
is StringResource.ByString -> value convertYearMonth = convertYearMonth,
is StringResource.ByZatoshi -> convertZatoshi(zatoshi) convertAddress = convertAddress,
is StringResource.ByDateTime -> convertDateTime(this) convertTransactionId = convertTransactionId
is StringResource.ByYearMonth -> convertYearMonth(yearMonth) )
is StringResource.ByAddress -> convertAddress(this)
is StringResource.ByTransactionId -> convertTransactionId(this)
}
@Suppress("SpreadOperator") @Suppress("SpreadOperator")
fun StringResource.getString( fun StringResource.getString(
@ -130,7 +136,8 @@ fun StringResource.getString(
convertYearMonth: (YearMonth) -> String = StringResourceDefaults::convertYearMonth, convertYearMonth: (YearMonth) -> String = StringResourceDefaults::convertYearMonth,
convertAddress: (StringResource.ByAddress) -> String = StringResourceDefaults::convertAddress, convertAddress: (StringResource.ByAddress) -> String = StringResourceDefaults::convertAddress,
convertTransactionId: (StringResource.ByTransactionId) -> String = StringResourceDefaults::convertTransactionId convertTransactionId: (StringResource.ByTransactionId) -> String = StringResourceDefaults::convertTransactionId
) = when (this) { ): String =
when (this) {
is StringResource.ByResource -> context.getString(resource, *args.normalize(context).toTypedArray()) is StringResource.ByResource -> context.getString(resource, *args.normalize(context).toTypedArray())
is StringResource.ByString -> value is StringResource.ByString -> value
is StringResource.ByZatoshi -> convertZatoshi(zatoshi) is StringResource.ByZatoshi -> convertZatoshi(zatoshi)
@ -138,6 +145,17 @@ fun StringResource.getString(
is StringResource.ByYearMonth -> convertYearMonth(yearMonth) is StringResource.ByYearMonth -> convertYearMonth(yearMonth)
is StringResource.ByAddress -> convertAddress(this) is StringResource.ByAddress -> convertAddress(this)
is StringResource.ByTransactionId -> convertTransactionId(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> = private fun List<Any>.normalize(context: Context): List<Any> =

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="hide_balance_placeholder">-----</string>
<string name="back_navigation_content_description">Atrás</string> <string name="back_navigation_content_description">Atrás</string>
<string name="triple_dots"></string> <string name="triple_dots"></string>
<string name="seed_recovery_reveal">Mostrar frase de recuperación</string>
</resources> </resources>

View File

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

View File

@ -8,8 +8,8 @@ import androidx.test.filters.LargeTest
import co.electriccoin.zcash.test.UiTestPrerequisites import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.integration.test.common.IntegrationTestingActivity import co.electriccoin.zcash.ui.integration.test.common.IntegrationTestingActivity
import co.electriccoin.zcash.ui.integration.test.common.getPermissionPositiveButtonUiObject 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.ScanTag
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
import org.junit.Assert import org.junit.Assert
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before 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.getStringResource
import co.electriccoin.zcash.ui.integration.test.common.getStringResourceWithArgs import co.electriccoin.zcash.ui.integration.test.common.getStringResourceWithArgs
import co.electriccoin.zcash.ui.integration.test.common.waitForDeviceIdle 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.ScanTag
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue 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.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule 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.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.integration.test.common.getPermissionNegativeButtonUiObject import co.electriccoin.zcash.ui.integration.test.common.getPermissionNegativeButtonUiObject
import co.electriccoin.zcash.ui.integration.test.common.getPermissionPositiveButtonUiObject 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.Scan
import co.electriccoin.zcash.ui.screen.scan.model.ScanValidationState import co.electriccoin.zcash.ui.screen.scan.ScanScreenState
import co.electriccoin.zcash.ui.screen.scan.view.Scan import co.electriccoin.zcash.ui.screen.scan.ScanValidationState
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@ -60,7 +59,6 @@ class ScanViewTestSetup(
onScanStateChange = { onScanStateChange = {
scanState.set(it) scanState.set(it)
}, },
topAppBarSubTitleState = TopAppBarSubTitleState.None,
validationResult = ScanValidationState.VALID validationResult = ScanValidationState.VALID
) )
} }

View File

@ -50,6 +50,7 @@ android {
"src/main/res/ui/crash_reporting_opt_in", "src/main/res/ui/crash_reporting_opt_in",
"src/main/res/ui/delete_wallet", "src/main/res/ui/delete_wallet",
"src/main/res/ui/export_data", "src/main/res/ui/export_data",
"src/main/res/ui/error",
"src/main/res/ui/home", "src/main/res/ui/home",
"src/main/res/ui/choose_server", "src/main/res/ui/choose_server",
"src/main/res/ui/integrations", "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.material3.SnackbarHostState
import androidx.compose.ui.test.junit4.ComposeContentTestRule 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.common.model.VersionInfo
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo
@ -28,7 +27,6 @@ class AboutViewTestSetup(
configInfo = configInfo, configInfo = configInfo,
onPrivacyPolicy = {}, onPrivacyPolicy = {},
snackbarHostState = SnackbarHostState(), snackbarHostState = SnackbarHostState(),
topAppBarSubTitleState = TopAppBarSubTitleState.None,
versionInfo = versionInfo, 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.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule 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.design.theme.ZcashTheme
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
@ -46,7 +45,6 @@ class ExportPrivateDataViewTestSetup(
onConfirm = { onConfirm = {
onConfirmCount.incrementAndGet() 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 { ZcashTheme {
Onboarding( Onboarding(
// Debug only UI state does not need to be tested // Debug only UI state does not need to be tested
isDebugMenuEnabled = false,
onImportWallet = { onImportWalletCallbackCount.incrementAndGet() }, onImportWallet = { onImportWalletCallbackCount.incrementAndGet() },
onCreateWallet = { onCreateWalletCallbackCount.incrementAndGet() }, onCreateWallet = { onCreateWalletCallbackCount.incrementAndGet() }
onFixtureWallet = {}
) )
} }
} }

View File

@ -46,8 +46,9 @@ class ReceiveViewTestSetup(
) )
), ),
isLoading = false, isLoading = false,
onBack = {}
), ),
zashiMainTopAppBarState = appBarState =
ZashiMainTopAppBarStateFixture.new( ZashiMainTopAppBarStateFixture.new(
settingsButton = settingsButton =
IconButtonState( 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 androidx.test.rule.GrantPermissionRule
import co.electriccoin.zcash.test.UiTestPrerequisites import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R 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.ScanTag
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
import co.electriccoin.zcash.ui.test.getStringResource import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Rule 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.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.ComposeContentTestRule 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.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState import co.electriccoin.zcash.ui.screen.scan.Scan
import co.electriccoin.zcash.ui.screen.scan.model.ScanValidationState 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.AtomicInteger
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
@ -30,7 +30,7 @@ class ScanViewBasicTestSetup(
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
fun DefaultContent() { fun DefaultContent() {
Scan( Scan(
validationResult = ScanValidationState.VALID, snackbarHostState = SnackbarHostState(),
onBack = { onBack = {
onBackCount.incrementAndGet() onBackCount.incrementAndGet()
}, },
@ -40,8 +40,7 @@ class ScanViewBasicTestSetup(
onScanStateChange = { onScanStateChange = {
scanState.set(it) scanState.set(it)
}, },
snackbarHostState = SnackbarHostState(), validationResult = ScanValidationState.VALID,
topAppBarSubTitleState = TopAppBarSubTitleState.None,
) )
} }

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@ package co.electriccoin.zcash.ui.screen.settings
import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.R 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.component.listitem.ZashiListItemState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.util.stringRes 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 android.graphics.ImageFormat
import androidx.camera.core.ImageProxy import androidx.camera.core.ImageProxy
import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.spackle.Twig
import co.electriccoin.zcash.ui.screen.scan.QrCodeAnalyzer
import co.electriccoin.zcash.ui.screen.scankeystone.view.FramePosition import co.electriccoin.zcash.ui.screen.scankeystone.view.FramePosition
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
import com.google.zxing.BinaryBitmap import com.google.zxing.BinaryBitmap

View File

@ -19,6 +19,7 @@
android:configChanges="orientation|locale|layoutDirection|screenLayout|uiMode|colorMode|keyboard|screenSize" android:configChanges="orientation|locale|layoutDirection|screenLayout|uiMode|colorMode|keyboard|screenSize"
android:exported="false" android:exported="false"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:launchMode="singleTask"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.App.Starting" /> 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.global.newInstance
import co.electriccoin.zcash.preference.EncryptedPreferenceProvider import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
import co.electriccoin.zcash.preference.StandardPreferenceProvider 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.NavigationRouter
import co.electriccoin.zcash.ui.NavigationRouterImpl import co.electriccoin.zcash.ui.NavigationRouterImpl
import co.electriccoin.zcash.ui.preference.PersistableWalletPreferenceDefault
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.bind import org.koin.dsl.bind
import org.koin.dsl.module import org.koin.dsl.module
@ -21,22 +17,12 @@ val coreModule =
single { single {
WalletCoordinator.newInstance( WalletCoordinator.newInstance(
context = get(), context = get(),
encryptedPreferenceProvider = get(), persistableWalletStorageProvider = get()
persistableWalletPreference = get(),
) )
} }
single {
PersistableWalletPreferenceDefault(PreferenceKey("persistable_wallet"))
}
singleOf(::StandardPreferenceProvider) singleOf(::StandardPreferenceProvider)
singleOf(::EncryptedPreferenceProvider) singleOf(::EncryptedPreferenceProvider)
single { BiometricManager.from(get()) } single { BiometricManager.from(get()) }
factory { AndroidConfigurationFactory.new() } factory { AndroidConfigurationFactory.new() }
singleOf(::NavigationRouterImpl) bind NavigationRouter::class 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.AccountDataSource
import co.electriccoin.zcash.ui.common.datasource.AccountDataSourceImpl 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.ProposalDataSource
import co.electriccoin.zcash.ui.common.datasource.ProposalDataSourceImpl import co.electriccoin.zcash.ui.common.datasource.ProposalDataSourceImpl
import co.electriccoin.zcash.ui.common.datasource.RestoreTimestampDataSource import co.electriccoin.zcash.ui.common.datasource.RestoreTimestampDataSource
import co.electriccoin.zcash.ui.common.datasource.RestoreTimestampDataSourceImpl 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.ZashiSpendingKeyDataSource
import co.electriccoin.zcash.ui.common.datasource.ZashiSpendingKeyDataSourceImpl import co.electriccoin.zcash.ui.common.datasource.ZashiSpendingKeyDataSourceImpl
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
@ -18,4 +24,7 @@ val dataSourceModule =
singleOf(::ZashiSpendingKeyDataSourceImpl) bind ZashiSpendingKeyDataSource::class singleOf(::ZashiSpendingKeyDataSourceImpl) bind ZashiSpendingKeyDataSource::class
singleOf(::ProposalDataSourceImpl) bind ProposalDataSource::class singleOf(::ProposalDataSourceImpl) bind ProposalDataSource::class
singleOf(::RestoreTimestampDataSourceImpl) bind RestoreTimestampDataSource::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.ApplicationStateProvider
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProviderImpl 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.GetDefaultServersProvider
import co.electriccoin.zcash.ui.common.provider.GetMonetarySeparatorProvider import co.electriccoin.zcash.ui.common.provider.GetMonetarySeparatorProvider
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
import co.electriccoin.zcash.ui.common.provider.GetZcashCurrencyProvider import co.electriccoin.zcash.ui.common.provider.GetZcashCurrencyProvider
import co.electriccoin.zcash.ui.common.provider.PersistableWalletProvider import co.electriccoin.zcash.ui.common.provider.PersistableWalletProvider
import co.electriccoin.zcash.ui.common.provider.PersistableWalletProviderImpl 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.RestoreTimestampStorageProvider
import co.electriccoin.zcash.ui.common.provider.RestoreTimestampStorageProviderImpl import co.electriccoin.zcash.ui.common.provider.RestoreTimestampStorageProviderImpl
import co.electriccoin.zcash.ui.common.provider.SelectedAccountUUIDProvider import co.electriccoin.zcash.ui.common.provider.SelectedAccountUUIDProvider
import co.electriccoin.zcash.ui.common.provider.SelectedAccountUUIDProviderImpl 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.SynchronizerProvider
import co.electriccoin.zcash.ui.common.provider.SynchronizerProviderImpl 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.core.module.dsl.singleOf
import org.koin.dsl.bind import org.koin.dsl.bind
import org.koin.dsl.module import org.koin.dsl.module
val providerModule = val providerModule =
module { module {
factoryOf(::GetDefaultServersProvider) singleOf(::GetDefaultServersProvider)
factoryOf(::GetVersionInfoProvider) singleOf(::GetVersionInfoProvider)
factoryOf(::GetZcashCurrencyProvider) singleOf(::GetZcashCurrencyProvider)
factoryOf(::GetMonetarySeparatorProvider) singleOf(::GetMonetarySeparatorProvider)
factoryOf(::SelectedAccountUUIDProviderImpl) bind SelectedAccountUUIDProvider::class singleOf(::SelectedAccountUUIDProviderImpl) bind SelectedAccountUUIDProvider::class
singleOf(::PersistableWalletProviderImpl) bind PersistableWalletProvider::class singleOf(::PersistableWalletProviderImpl) bind PersistableWalletProvider::class
singleOf(::SynchronizerProviderImpl) bind SynchronizerProvider::class singleOf(::SynchronizerProviderImpl) bind SynchronizerProvider::class
singleOf(::ApplicationStateProviderImpl) bind ApplicationStateProvider::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 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.BiometricRepository
import co.electriccoin.zcash.ui.common.repository.BiometricRepositoryImpl import co.electriccoin.zcash.ui.common.repository.BiometricRepositoryImpl
import co.electriccoin.zcash.ui.common.repository.ConfigurationRepository 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.ExchangeRateRepositoryImpl
import co.electriccoin.zcash.ui.common.repository.FlexaRepository import co.electriccoin.zcash.ui.common.repository.FlexaRepository
import co.electriccoin.zcash.ui.common.repository.FlexaRepositoryImpl 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.KeystoneProposalRepository
import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepositoryImpl 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.TransactionFilterRepository
import co.electriccoin.zcash.ui.common.repository.TransactionFilterRepositoryImpl import co.electriccoin.zcash.ui.common.repository.TransactionFilterRepositoryImpl
import co.electriccoin.zcash.ui.common.repository.TransactionRepository import co.electriccoin.zcash.ui.common.repository.TransactionRepository
import co.electriccoin.zcash.ui.common.repository.TransactionRepositoryImpl import co.electriccoin.zcash.ui.common.repository.TransactionRepositoryImpl
import co.electriccoin.zcash.ui.common.repository.WalletRepository import co.electriccoin.zcash.ui.common.repository.WalletRepository
import co.electriccoin.zcash.ui.common.repository.WalletRepositoryImpl 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.ZashiProposalRepository
import co.electriccoin.zcash.ui.common.repository.ZashiProposalRepositoryImpl import co.electriccoin.zcash.ui.common.repository.ZashiProposalRepositoryImpl
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
@ -29,11 +33,13 @@ val repositoryModule =
singleOf(::WalletRepositoryImpl) bind WalletRepository::class singleOf(::WalletRepositoryImpl) bind WalletRepository::class
singleOf(::ConfigurationRepositoryImpl) bind ConfigurationRepository::class singleOf(::ConfigurationRepositoryImpl) bind ConfigurationRepository::class
singleOf(::ExchangeRateRepositoryImpl) bind ExchangeRateRepository::class singleOf(::ExchangeRateRepositoryImpl) bind ExchangeRateRepository::class
singleOf(::BalanceRepositoryImpl) bind BalanceRepository::class
singleOf(::FlexaRepositoryImpl) bind FlexaRepository::class singleOf(::FlexaRepositoryImpl) bind FlexaRepository::class
singleOf(::BiometricRepositoryImpl) bind BiometricRepository::class singleOf(::BiometricRepositoryImpl) bind BiometricRepository::class
singleOf(::KeystoneProposalRepositoryImpl) bind KeystoneProposalRepository::class singleOf(::KeystoneProposalRepositoryImpl) bind KeystoneProposalRepository::class
singleOf(::TransactionRepositoryImpl) bind TransactionRepository::class singleOf(::TransactionRepositoryImpl) bind TransactionRepository::class
singleOf(::TransactionFilterRepositoryImpl) bind TransactionFilterRepository::class singleOf(::TransactionFilterRepositoryImpl) bind TransactionFilterRepository::class
singleOf(::ZashiProposalRepositoryImpl) bind ZashiProposalRepository::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.CancelProposalFlowUseCase
import co.electriccoin.zcash.ui.common.usecase.ConfirmProposalUseCase import co.electriccoin.zcash.ui.common.usecase.ConfirmProposalUseCase
import co.electriccoin.zcash.ui.common.usecase.CopyToClipboardUseCase 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.CreateKeystoneAccountUseCase
import co.electriccoin.zcash.ui.common.usecase.CreateKeystoneProposalPCZTEncoderUseCase 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.CreateOrUpdateTransactionNoteUseCase
import co.electriccoin.zcash.ui.common.usecase.CreateProposalUseCase 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.DeleteContactUseCase
import co.electriccoin.zcash.ui.common.usecase.DeleteTransactionNoteUseCase import co.electriccoin.zcash.ui.common.usecase.DeleteTransactionNoteUseCase
import co.electriccoin.zcash.ui.common.usecase.DeriveKeystoneAccountUnifiedAddressUseCase import co.electriccoin.zcash.ui.common.usecase.DeriveKeystoneAccountUnifiedAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.ExportTaxUseCase import co.electriccoin.zcash.ui.common.usecase.ExportTaxUseCase
import co.electriccoin.zcash.ui.common.usecase.FlipTransactionBookmarkUseCase 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.GetContactByAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.GetCurrentFilteredTransactionsUseCase 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.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.GetMetadataUseCase
import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase
import co.electriccoin.zcash.ui.common.usecase.GetProposalUseCase 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.GetTransactionDetailByIdUseCase
import co.electriccoin.zcash.ui.common.usecase.GetTransactionFiltersUseCase import co.electriccoin.zcash.ui.common.usecase.GetTransactionFiltersUseCase
import co.electriccoin.zcash.ui.common.usecase.GetTransactionMetadataUseCase 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.GetTransparentAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.GetWalletAccountsUseCase
import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase
import co.electriccoin.zcash.ui.common.usecase.GetZashiAccountUseCase 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.IsCoinbaseAvailableUseCase
import co.electriccoin.zcash.ui.common.usecase.IsFlexaAvailableUseCase 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.MarkTxMemoAsReadUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToAddressBookUseCase import co.electriccoin.zcash.ui.common.usecase.NavigateToAddressBookUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase 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.NavigateToTaxExportUseCase
import co.electriccoin.zcash.ui.common.usecase.NavigateToWalletBackupUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveClearSendUseCase 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.ObserveContactByAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveFastestServersUseCase 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.ObservePersistableWalletUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveProposalUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveProposalUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedEndpointUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedEndpointUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedWalletAccountUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedWalletAccountUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveTransactionSubmitStateUseCase 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.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.ParseKeystonePCZTUseCase
import co.electriccoin.zcash.ui.common.usecase.ParseKeystoneSignInRequestUseCase import co.electriccoin.zcash.ui.common.usecase.ParseKeystoneSignInRequestUseCase
import co.electriccoin.zcash.ui.common.usecase.ParseKeystoneUrToZashiAccountsUseCase import co.electriccoin.zcash.ui.common.usecase.ParseKeystoneUrToZashiAccountsUseCase
import co.electriccoin.zcash.ui.common.usecase.PersistEndpointUseCase import co.electriccoin.zcash.ui.common.usecase.PersistEndpointUseCase
import co.electriccoin.zcash.ui.common.usecase.PrefillSendUseCase import co.electriccoin.zcash.ui.common.usecase.PrefillSendUseCase
import co.electriccoin.zcash.ui.common.usecase.RefreshFastestServersUseCase 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.RescanBlockchainUseCase
import co.electriccoin.zcash.ui.common.usecase.RescanQrUseCase
import co.electriccoin.zcash.ui.common.usecase.ResetInMemoryDataUseCase import co.electriccoin.zcash.ui.common.usecase.ResetInMemoryDataUseCase
import co.electriccoin.zcash.ui.common.usecase.ResetSharedPrefsDataUseCase import co.electriccoin.zcash.ui.common.usecase.ResetSharedPrefsDataUseCase
import co.electriccoin.zcash.ui.common.usecase.ResetTransactionFiltersUseCase 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.SaveContactUseCase
import co.electriccoin.zcash.ui.common.usecase.SelectWalletAccountUseCase import co.electriccoin.zcash.ui.common.usecase.SelectWalletAccountUseCase
import co.electriccoin.zcash.ui.common.usecase.SendEmailUseCase 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.SendTransactionAgainUseCase
import co.electriccoin.zcash.ui.common.usecase.ShareImageUseCase import co.electriccoin.zcash.ui.common.usecase.ShareImageUseCase
import co.electriccoin.zcash.ui.common.usecase.SharePCZTUseCase 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.UpdateContactUseCase
import co.electriccoin.zcash.ui.common.usecase.ValidateContactAddressUseCase import co.electriccoin.zcash.ui.common.usecase.ValidateContactAddressUseCase
import co.electriccoin.zcash.ui.common.usecase.ValidateContactNameUseCase import co.electriccoin.zcash.ui.common.usecase.ValidateContactNameUseCase
import co.electriccoin.zcash.ui.common.usecase.ValidateEndpointUseCase 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.ViewTransactionDetailAfterSuccessfulProposalUseCase
import co.electriccoin.zcash.ui.common.usecase.ViewTransactionsAfterSuccessfulProposalUseCase import co.electriccoin.zcash.ui.common.usecase.ViewTransactionsAfterSuccessfulProposalUseCase
import co.electriccoin.zcash.ui.common.usecase.Zip321BuildUriUseCase import co.electriccoin.zcash.ui.common.usecase.Zip321BuildUriUseCase
@ -99,7 +110,7 @@ val useCaseModule =
factoryOf(::ValidateEndpointUseCase) factoryOf(::ValidateEndpointUseCase)
factoryOf(::GetPersistableWalletUseCase) factoryOf(::GetPersistableWalletUseCase)
factoryOf(::GetSelectedEndpointUseCase) factoryOf(::GetSelectedEndpointUseCase)
factoryOf(::ObserveConfigurationUseCase) factoryOf(::GetConfigurationUseCase)
factoryOf(::RescanBlockchainUseCase) factoryOf(::RescanBlockchainUseCase)
factoryOf(::GetTransparentAddressUseCase) factoryOf(::GetTransparentAddressUseCase)
factoryOf(::ValidateContactAddressUseCase) factoryOf(::ValidateContactAddressUseCase)
@ -114,16 +125,13 @@ val useCaseModule =
factoryOf(::ShareImageUseCase) factoryOf(::ShareImageUseCase)
factoryOf(::Zip321BuildUriUseCase) factoryOf(::Zip321BuildUriUseCase)
factoryOf(::Zip321ParseUriValidationUseCase) factoryOf(::Zip321ParseUriValidationUseCase)
factoryOf(::ObserveWalletStateUseCase)
factoryOf(::IsCoinbaseAvailableUseCase) factoryOf(::IsCoinbaseAvailableUseCase)
factoryOf(::GetZashiSpendingKeyUseCase)
factoryOf(::ObservePersistableWalletUseCase) factoryOf(::ObservePersistableWalletUseCase)
factoryOf(::GetBackupPersistableWalletUseCase)
factoryOf(::GetSupportUseCase) factoryOf(::GetSupportUseCase)
factoryOf(::SendEmailUseCase) factoryOf(::SendEmailUseCase)
factoryOf(::SendSupportEmailUseCase) factoryOf(::SendSupportEmailUseCase)
factoryOf(::IsFlexaAvailableUseCase) factoryOf(::IsFlexaAvailableUseCase)
factoryOf(::ObserveWalletAccountsUseCase) factoryOf(::GetWalletAccountsUseCase)
factoryOf(::SelectWalletAccountUseCase) factoryOf(::SelectWalletAccountUseCase)
factoryOf(::ObserveSelectedWalletAccountUseCase) factoryOf(::ObserveSelectedWalletAccountUseCase)
factoryOf(::ObserveZashiAccountUseCase) factoryOf(::ObserveZashiAccountUseCase)
@ -135,18 +143,17 @@ val useCaseModule =
factoryOf(::GetSelectedWalletAccountUseCase) factoryOf(::GetSelectedWalletAccountUseCase)
singleOf(::ObserveClearSendUseCase) singleOf(::ObserveClearSendUseCase)
singleOf(::PrefillSendUseCase) singleOf(::PrefillSendUseCase)
factoryOf(::GetCurrentTransactionsUseCase) factoryOf(::GetTransactionsUseCase)
factoryOf(::GetCurrentFilteredTransactionsUseCase) onClose ::closeableCallback factoryOf(::GetCurrentFilteredTransactionsUseCase) onClose ::closeableCallback
factoryOf(::CreateProposalUseCase) factoryOf(::CreateProposalUseCase)
factoryOf(::CreateZip321ProposalUseCase) factoryOf(::OnZip321ScannedUseCase)
factoryOf(::CreateKeystoneShieldProposalUseCase) factoryOf(::OnAddressScannedUseCase)
factoryOf(::ParseKeystonePCZTUseCase) factoryOf(::ParseKeystonePCZTUseCase)
factoryOf(::ParseKeystoneSignInRequestUseCase) factoryOf(::ParseKeystoneSignInRequestUseCase)
factoryOf(::CancelProposalFlowUseCase) factoryOf(::CancelProposalFlowUseCase)
factoryOf(::ObserveProposalUseCase) factoryOf(::ObserveProposalUseCase)
factoryOf(::SharePCZTUseCase) factoryOf(::SharePCZTUseCase)
factoryOf(::CreateKeystoneProposalPCZTEncoderUseCase) factoryOf(::CreateKeystoneProposalPCZTEncoderUseCase)
factoryOf(::ObserveOnAccountChangedUseCase)
factoryOf(::ViewTransactionsAfterSuccessfulProposalUseCase) factoryOf(::ViewTransactionsAfterSuccessfulProposalUseCase)
factoryOf(::ViewTransactionDetailAfterSuccessfulProposalUseCase) factoryOf(::ViewTransactionDetailAfterSuccessfulProposalUseCase)
factoryOf(::NavigateToCoinbaseUseCase) factoryOf(::NavigateToCoinbaseUseCase)
@ -172,4 +179,19 @@ val useCaseModule =
factoryOf(::GetMetadataUseCase) factoryOf(::GetMetadataUseCase)
factoryOf(::ExportTaxUseCase) factoryOf(::ExportTaxUseCase)
factoryOf(::NavigateToTaxExportUseCase) 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