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:
parent
3e4a57979f
commit
000697ba63
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -6,6 +6,20 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.0.0 (934)] - 2025-04-25
|
||||
|
||||
### Added:
|
||||
- Zashi 2.0 is here!
|
||||
- New Wallet Status Widget helps you navigate Zashi with ease and get more info upon tap.
|
||||
|
||||
### Changed:
|
||||
- Redesigned Home Screen and streamlined app navigation.
|
||||
- Balances redesigned into a new Spendable component on the Send screen.
|
||||
- Revamped Restore flow.
|
||||
- Create Wallet with a tap! New Wallet Backup flow moved to when your wallet receives first funds.
|
||||
- Firebase Crashlytics are fully opt-in. Help us improve Zashi, or don’t, your choice.
|
||||
- Scanning a ZIP 321 QR code now opens Zashi!
|
||||
|
||||
## [1.5.2 (932)] - 2025-04-23
|
||||
|
||||
### Added
|
||||
|
|
|
@ -25,6 +25,12 @@
|
|||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="zcash" />
|
||||
</intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<!-- Enable profiling by benchmark -->
|
||||
|
|
|
@ -14,12 +14,13 @@ import co.electriccoin.zcash.di.providerModule
|
|||
import co.electriccoin.zcash.di.repositoryModule
|
||||
import co.electriccoin.zcash.di.useCaseModule
|
||||
import co.electriccoin.zcash.di.viewModelModule
|
||||
import co.electriccoin.zcash.preference.StandardPreferenceProvider
|
||||
import co.electriccoin.zcash.spackle.StrictModeCompat
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.CrashReportingStorageProvider
|
||||
import co.electriccoin.zcash.ui.common.repository.FlexaRepository
|
||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
||||
import co.electriccoin.zcash.ui.common.repository.HomeMessageCacheRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletSnapshotRepository
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
|
@ -27,10 +28,12 @@ import org.koin.android.ext.koin.androidLogger
|
|||
import org.koin.core.context.startKoin
|
||||
|
||||
class ZcashApplication : CoroutineApplication() {
|
||||
private val standardPreferenceProvider by inject<StandardPreferenceProvider>()
|
||||
private val flexaRepository by inject<FlexaRepository>()
|
||||
private val applicationStateProvider: ApplicationStateProvider by inject()
|
||||
private val getAvailableCrashReporters: CrashReportersProvider by inject()
|
||||
private val homeMessageCacheRepository: HomeMessageCacheRepository by inject()
|
||||
private val walletSnapshotRepository: WalletSnapshotRepository by inject()
|
||||
private val crashReportingStorageProvider: CrashReportingStorageProvider by inject()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
@ -68,6 +71,8 @@ class ZcashApplication : CoroutineApplication() {
|
|||
configureAnalytics()
|
||||
|
||||
flexaRepository.init()
|
||||
homeMessageCacheRepository.init()
|
||||
walletSnapshotRepository.init()
|
||||
}
|
||||
|
||||
private fun configureLogging() {
|
||||
|
@ -89,9 +94,9 @@ class ZcashApplication : CoroutineApplication() {
|
|||
private fun configureAnalytics() {
|
||||
if (GlobalCrashReporter.register(this, getAvailableCrashReporters())) {
|
||||
applicationScope.launch {
|
||||
StandardPreferenceKeys.IS_ANALYTICS_ENABLED.observe(standardPreferenceProvider()).collect {
|
||||
crashReportingStorageProvider.observe().collect {
|
||||
Twig.debug { "Is crashlytics enabled: $it" }
|
||||
if (it) {
|
||||
if (it == true) {
|
||||
GlobalCrashReporter.enable()
|
||||
} else {
|
||||
GlobalCrashReporter.disableAndDelete()
|
||||
|
|
|
@ -23,7 +23,7 @@ class MergingConfigurationProvider(
|
|||
|
||||
override fun getConfigurationFlow(): Flow<Configuration> =
|
||||
if (configurationProviders.isEmpty()) {
|
||||
flowOf(MergingConfiguration(persistentListOf<Configuration>()))
|
||||
flowOf(MergingConfiguration(persistentListOf()))
|
||||
} else {
|
||||
combine(configurationProviders.map { it.getConfigurationFlow() }) { configurations ->
|
||||
MergingConfiguration(configurations.toList().toPersistentList())
|
||||
|
|
|
@ -12,6 +12,20 @@ directly impact users rather than highlighting other key architectural updates.*
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.0.0 (934)] - 2025-04-25
|
||||
|
||||
### Added:
|
||||
- Zashi 2.0 is here!
|
||||
- New Wallet Status Widget helps you navigate Zashi with ease and get more info upon tap.
|
||||
|
||||
### Changed:
|
||||
- Redesigned Home Screen and streamlined app navigation.
|
||||
- Balances redesigned into a new Spendable component on the Send screen.
|
||||
- Revamped Restore flow.
|
||||
- Create Wallet with a tap! New Wallet Backup flow moved to when your wallet receives first funds.
|
||||
- Firebase Crashlytics are fully opt-in. Help us improve Zashi, or don’t, your choice.
|
||||
- Scanning a ZIP 321 QR code now opens Zashi!
|
||||
|
||||
## [1.5.2 (932)] - 2025-04-23
|
||||
|
||||
### Added:
|
||||
|
|
|
@ -12,6 +12,19 @@ directly impact users rather than highlighting other key architectural updates.*
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.0.0 (934)] - 2025-04-25
|
||||
|
||||
### Añadido:
|
||||
- Un widget de estado de la billetera te ayuda a navegar por Zashi y a obtener información con un click.
|
||||
|
||||
### Cambiado:
|
||||
- Pantalla de inicio rediseñada y navegación optimizada.
|
||||
- Saldos rediseñados con un nuevo componente Gastable en la pantalla de envío.
|
||||
- Flujo de restauración renovado.
|
||||
- ¡Crea tu billetera facil! Un nuevo proceso de backup que se traslado a cuando recibes los primeros fondos.
|
||||
- Firebase Crashlytics es totalmente opcional.
|
||||
- ¡Escanear un código QR ZIP 321 abre Zashi!
|
||||
|
||||
## [1.5.2 (932)] - 2025-04-23
|
||||
|
||||
### Añadido:
|
||||
|
|
|
@ -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 don’t, your choice.
|
||||
- Scanning a ZIP 321 QR code now opens Zashi!
|
|
@ -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!
|
|
@ -61,7 +61,7 @@ NDK_DEBUG_SYMBOL_LEVEL=symbol_table
|
|||
# VERSION_CODE is effectively ignored. VERSION_NAME is suffixed with the version code.
|
||||
# If not using automated Google Play deployment, then these serve as the actual version numbers.
|
||||
ZCASH_VERSION_CODE=1
|
||||
ZCASH_VERSION_NAME=1.5.2
|
||||
ZCASH_VERSION_NAME=2.0.0
|
||||
|
||||
# Set these fields, as you need them (e.g. with values "Zcash X" and "co.electriccoin.zcash.x")
|
||||
# to distinguish a different release build that can be installed alongside the official version
|
||||
|
@ -211,7 +211,7 @@ ZIP_321_VERSION = 0.0.6
|
|||
# WARNING: Ensure a non-snapshot version is used before releasing to production
|
||||
ZCASH_BIP39_VERSION=1.0.9
|
||||
# WARNING: Ensure a non-snapshot version is used before releasing to production
|
||||
ZCASH_SDK_VERSION=2.2.11
|
||||
ZCASH_SDK_VERSION=2.2.12-SNAPSHOT
|
||||
|
||||
# Toolchain is the Java version used to build the application, which is separate from the
|
||||
# Java version used to run the application.
|
||||
|
|
|
@ -29,5 +29,7 @@ interface PreferenceProvider {
|
|||
|
||||
fun observe(key: PreferenceKey): Flow<String?>
|
||||
|
||||
suspend fun remove(key: PreferenceKey)
|
||||
|
||||
suspend fun clearPreferences(): Boolean
|
||||
}
|
||||
|
|
|
@ -22,6 +22,6 @@ data class NullableBooleanPreferenceDefault(
|
|||
preferenceProvider: PreferenceProvider,
|
||||
newValue: Boolean?
|
||||
) {
|
||||
preferenceProvider.putString(key, newValue.toString())
|
||||
preferenceProvider.putString(key, newValue?.toString())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,10 @@ interface PreferenceDefault<T> {
|
|||
newValue: T
|
||||
)
|
||||
|
||||
suspend fun clear(preferenceProvider: PreferenceProvider) {
|
||||
preferenceProvider.remove(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param preferenceProvider Provides actual preference values.
|
||||
* @return Flow that emits preference changes. Note that implementations should emit an initial value
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -22,6 +22,10 @@ class MockPreferenceProvider(
|
|||
// For the mock implementation, does not support observability of changes
|
||||
override fun observe(key: PreferenceKey): Flow<String?> = flow { emit(getString(key)) }
|
||||
|
||||
override suspend fun remove(key: PreferenceKey) {
|
||||
map.remove(key.key)
|
||||
}
|
||||
|
||||
override suspend fun clearPreferences(): Boolean {
|
||||
map.clear()
|
||||
return true
|
||||
|
|
|
@ -11,9 +11,11 @@ import kotlinx.coroutines.CoroutineDispatcher
|
|||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -32,6 +34,8 @@ class AndroidPreferenceProvider private constructor(
|
|||
private val sharedPreferences: SharedPreferences,
|
||||
private val dispatcher: CoroutineDispatcher
|
||||
) : PreferenceProvider {
|
||||
private val clearPipeline = MutableSharedFlow<Unit>()
|
||||
|
||||
private val mutex = Mutex()
|
||||
/*
|
||||
* Implementation note: EncryptedSharedPreferences are not thread-safe, so this implementation
|
||||
|
@ -119,6 +123,8 @@ class AndroidPreferenceProvider private constructor(
|
|||
|
||||
editor.clear()
|
||||
|
||||
clearPipeline.emit(Unit)
|
||||
|
||||
return@withContext editor.commit()
|
||||
}
|
||||
|
||||
|
@ -131,6 +137,12 @@ class AndroidPreferenceProvider private constructor(
|
|||
}
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
|
||||
|
||||
this.launch {
|
||||
clearPipeline.collect {
|
||||
send(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
// Kickstart the emissions
|
||||
trySend(Unit)
|
||||
|
||||
|
@ -140,6 +152,17 @@ class AndroidPreferenceProvider private constructor(
|
|||
}.flowOn(dispatcher)
|
||||
.map { getString(key) }
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
override suspend fun remove(key: PreferenceKey) {
|
||||
withContext(dispatcher) {
|
||||
val editor = sharedPreferences.edit()
|
||||
|
||||
editor.remove(key.key)
|
||||
|
||||
editor.commit()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
suspend fun newStandard(
|
||||
context: Context,
|
||||
|
|
|
@ -34,12 +34,12 @@ fun Zatoshi.toZecStringAbbreviated(suffix: String): ZecAmountPair {
|
|||
}
|
||||
}
|
||||
|
||||
private const val DEFAULT_LESS_THAN_FEE = 100_000L
|
||||
|
||||
val DEFAULT_FEE: String
|
||||
get() = Zatoshi(DEFAULT_LESS_THAN_FEE).toZecStringFull()
|
||||
|
||||
data class ZecAmountPair(
|
||||
val main: String,
|
||||
val suffix: String
|
||||
)
|
||||
|
||||
val Zatoshi.Companion.typicalFee: Zatoshi
|
||||
get() = Zatoshi(TYPICAL_FEE)
|
||||
|
||||
private const val TYPICAL_FEE = 100000L
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
package co.electriccoin.zcash.ui.design.component
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
@ -62,13 +61,13 @@ private fun HiddenStyledBalancePreview() =
|
|||
* @param textColor Optional color to modify the default font color from [textStyle]
|
||||
* @param modifier Modifier to modify the Text UI element as needed
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun StyledBalance(
|
||||
balanceParts: ZecAmountTriple,
|
||||
modifier: Modifier = Modifier,
|
||||
isHideBalances: Boolean = false,
|
||||
showDust: Boolean = true,
|
||||
hiddenBalancePlaceholder: String = stringResource(id = R.string.hide_balance_placeholder),
|
||||
textColor: Color = Color.Unspecified,
|
||||
textStyle: BalanceTextStyle = StyledBalanceDefaults.textStyles(),
|
||||
|
@ -91,10 +90,12 @@ fun StyledBalance(
|
|||
) {
|
||||
append(balanceSplit.first)
|
||||
}
|
||||
withStyle(
|
||||
style = textStyle.leastSignificantPart.toSpanStyle()
|
||||
) {
|
||||
append(balanceSplit.second)
|
||||
if (showDust) {
|
||||
withStyle(
|
||||
style = textStyle.leastSignificantPart.toSpanStyle()
|
||||
) {
|
||||
append(balanceSplit.second)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -102,15 +103,16 @@ fun StyledBalance(
|
|||
val resultModifier =
|
||||
Modifier
|
||||
.basicMarquee()
|
||||
.animateContentSize()
|
||||
.then(modifier)
|
||||
|
||||
Text(
|
||||
text = content,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
modifier = resultModifier
|
||||
)
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = content,
|
||||
color = textColor,
|
||||
maxLines = 1,
|
||||
modifier = resultModifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val CUT_POSITION_OFFSET = 4
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -13,8 +13,10 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
|
||||
|
@ -66,7 +68,9 @@ fun LabeledCheckBox(
|
|||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
checked: Boolean = false,
|
||||
checkBoxTestTag: String? = null
|
||||
checkBoxTestTag: String? = null,
|
||||
color: Color = ZcashTheme.colors.textPrimary,
|
||||
style: TextStyle = ZcashTheme.extendedTypography.checkboxText
|
||||
) {
|
||||
val (checkedState, setCheckedState) = rememberSaveable { mutableStateOf(checked) }
|
||||
|
||||
|
@ -113,8 +117,8 @@ fun LabeledCheckBox(
|
|||
)
|
||||
Text(
|
||||
text = AnnotatedString(text),
|
||||
color = ZcashTheme.colors.textPrimary,
|
||||
style = ZcashTheme.extendedTypography.checkboxText
|
||||
color = color,
|
||||
style = style
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -1,15 +1,14 @@
|
|||
package co.electriccoin.zcash.ui.screen.exchangerate
|
||||
package co.electriccoin.zcash.ui.design.component
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
|
@ -21,17 +20,19 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.R
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
||||
import co.electriccoin.zcash.ui.design.util.scaffoldPadding
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun BaseExchangeRateOptIn(
|
||||
fun ZashiBaseSettingsOptIn(
|
||||
header: String,
|
||||
@DrawableRes image: Int,
|
||||
info: String?,
|
||||
onDismiss: () -> Unit,
|
||||
footer: @Composable ColumnScope.() -> Unit,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
|
@ -54,7 +55,7 @@ internal fun BaseExchangeRateOptIn(
|
|||
)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_exchange_rate_close),
|
||||
painter = painterResource(id = R.drawable.ic_settings_opt_int_close),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(ZashiColors.Btns.Tertiary.btnTertiaryFg)
|
||||
)
|
||||
|
@ -68,31 +69,21 @@ internal fun BaseExchangeRateOptIn(
|
|||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Image(painter = painterResource(R.drawable.exchange_rate), contentDescription = null)
|
||||
Image(painter = painterResource(image), contentDescription = null)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.exchange_rate_opt_in_subtitle),
|
||||
text = header,
|
||||
color = ZashiColors.Text.textPrimary,
|
||||
style = ZashiTypography.header6,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
content()
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Row {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_exchange_rate_info),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(ZashiColors.Text.textTertiary)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.exchange_rate_opt_in_note),
|
||||
color = ZashiColors.Text.textTertiary,
|
||||
style = ZashiTypography.textXs
|
||||
)
|
||||
if (info != null) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
ZashiInfoText(info)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
|
@ -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 = {}
|
||||
)
|
||||
)
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -18,6 +18,7 @@ import androidx.compose.material3.LocalContentColor
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
|
@ -31,6 +32,7 @@ import co.electriccoin.zcash.ui.design.R
|
|||
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColorsInternal
|
||||
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
||||
import co.electriccoin.zcash.ui.design.util.StringResource
|
||||
import co.electriccoin.zcash.ui.design.util.getValue
|
||||
|
@ -42,7 +44,7 @@ fun ZashiButton(
|
|||
style: TextStyle = ZashiButtonDefaults.style,
|
||||
shape: Shape = ZashiButtonDefaults.shape,
|
||||
contentPadding: PaddingValues = ZashiButtonDefaults.contentPadding,
|
||||
colors: ZashiButtonColors = ZashiButtonDefaults.primaryColors(),
|
||||
colors: ZashiButtonColors = LocalZashiButtonColors.current ?: ZashiButtonDefaults.primaryColors(),
|
||||
content: @Composable RowScope.(ZashiButtonScope) -> Unit = ZashiButtonDefaults.content
|
||||
) {
|
||||
ZashiButton(
|
||||
|
@ -67,14 +69,14 @@ fun ZashiButton(
|
|||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
style: TextStyle = ZashiButtonDefaults.style,
|
||||
shape: Shape = ZashiButtonDefaults.shape,
|
||||
contentPadding: PaddingValues = ZashiButtonDefaults.contentPadding,
|
||||
@DrawableRes icon: Int? = null,
|
||||
@DrawableRes trailingIcon: Int? = null,
|
||||
enabled: Boolean = true,
|
||||
isLoading: Boolean = false,
|
||||
colors: ZashiButtonColors = ZashiButtonDefaults.primaryColors(),
|
||||
style: TextStyle = ZashiButtonDefaults.style,
|
||||
shape: Shape = ZashiButtonDefaults.shape,
|
||||
contentPadding: PaddingValues = ZashiButtonDefaults.contentPadding,
|
||||
colors: ZashiButtonColors = LocalZashiButtonColors.current ?: ZashiButtonDefaults.primaryColors(),
|
||||
content: @Composable RowScope.(ZashiButtonScope) -> Unit = ZashiButtonDefaults.content
|
||||
) {
|
||||
val scope =
|
||||
|
@ -180,10 +182,11 @@ object ZashiButtonDefaults {
|
|||
|
||||
@Composable
|
||||
fun primaryColors(
|
||||
containerColor: Color = ZashiColors.Btns.Primary.btnPrimaryBg,
|
||||
contentColor: Color = ZashiColors.Btns.Primary.btnPrimaryFg,
|
||||
disabledContainerColor: Color = ZashiColors.Btns.Primary.btnPrimaryBgDisabled,
|
||||
disabledContentColor: Color = ZashiColors.Btns.Primary.btnBoldFgDisabled,
|
||||
source: ZashiColorsInternal = ZashiColors,
|
||||
containerColor: Color = source.Btns.Primary.btnPrimaryBg,
|
||||
contentColor: Color = source.Btns.Primary.btnPrimaryFg,
|
||||
disabledContainerColor: Color = source.Btns.Primary.btnPrimaryBgDisabled,
|
||||
disabledContentColor: Color = source.Btns.Primary.btnBoldFgDisabled,
|
||||
) = ZashiButtonColors(
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
|
@ -195,11 +198,12 @@ object ZashiButtonDefaults {
|
|||
|
||||
@Composable
|
||||
fun secondaryColors(
|
||||
containerColor: Color = ZashiColors.Btns.Secondary.btnSecondaryBg,
|
||||
contentColor: Color = ZashiColors.Btns.Secondary.btnSecondaryFg,
|
||||
source: ZashiColorsInternal = ZashiColors,
|
||||
containerColor: Color = source.Btns.Secondary.btnSecondaryBg,
|
||||
contentColor: Color = source.Btns.Secondary.btnSecondaryFg,
|
||||
borderColor: Color = Color.Unspecified,
|
||||
disabledContainerColor: Color = ZashiColors.Btns.Secondary.btnSecondaryBgDisabled,
|
||||
disabledContentColor: Color = ZashiColors.Btns.Secondary.btnSecondaryFg,
|
||||
disabledContainerColor: Color = source.Btns.Secondary.btnSecondaryBgDisabled,
|
||||
disabledContentColor: Color = source.Btns.Secondary.btnSecondaryFgDisabled,
|
||||
) = ZashiButtonColors(
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
|
@ -211,10 +215,11 @@ object ZashiButtonDefaults {
|
|||
|
||||
@Composable
|
||||
fun tertiaryColors(
|
||||
containerColor: Color = ZashiColors.Btns.Tertiary.btnTertiaryBg,
|
||||
contentColor: Color = ZashiColors.Btns.Tertiary.btnTertiaryFg,
|
||||
disabledContainerColor: Color = ZashiColors.Btns.Tertiary.btnTertiaryBgDisabled,
|
||||
disabledContentColor: Color = ZashiColors.Btns.Tertiary.btnTertiaryFgDisabled,
|
||||
source: ZashiColorsInternal = ZashiColors,
|
||||
containerColor: Color = source.Btns.Tertiary.btnTertiaryBg,
|
||||
contentColor: Color = source.Btns.Tertiary.btnTertiaryFg,
|
||||
disabledContainerColor: Color = source.Btns.Tertiary.btnTertiaryBgDisabled,
|
||||
disabledContentColor: Color = source.Btns.Tertiary.btnTertiaryFgDisabled,
|
||||
) = ZashiButtonColors(
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
|
@ -226,11 +231,12 @@ object ZashiButtonDefaults {
|
|||
|
||||
@Composable
|
||||
fun destructive1Colors(
|
||||
containerColor: Color = ZashiColors.Btns.Destructive1.btnDestroy1Bg,
|
||||
contentColor: Color = ZashiColors.Btns.Destructive1.btnDestroy1Fg,
|
||||
borderColor: Color = ZashiColors.Btns.Destructive1.btnDestroy1Border,
|
||||
disabledContainerColor: Color = ZashiColors.Btns.Destructive1.btnDestroy1BgDisabled,
|
||||
disabledContentColor: Color = ZashiColors.Btns.Destructive1.btnDestroy1FgDisabled,
|
||||
source: ZashiColorsInternal = ZashiColors,
|
||||
containerColor: Color = source.Btns.Destructive1.btnDestroy1Bg,
|
||||
contentColor: Color = source.Btns.Destructive1.btnDestroy1Fg,
|
||||
borderColor: Color = source.Btns.Destructive1.btnDestroy1Border,
|
||||
disabledContainerColor: Color = source.Btns.Destructive1.btnDestroy1BgDisabled,
|
||||
disabledContentColor: Color = source.Btns.Destructive1.btnDestroy1FgDisabled,
|
||||
) = ZashiButtonColors(
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
|
@ -242,11 +248,12 @@ object ZashiButtonDefaults {
|
|||
|
||||
@Composable
|
||||
fun destructive2Colors(
|
||||
containerColor: Color = ZashiColors.Btns.Destructive2.btnDestroy2Bg,
|
||||
contentColor: Color = ZashiColors.Btns.Destructive2.btnDestroy2Fg,
|
||||
source: ZashiColorsInternal = ZashiColors,
|
||||
containerColor: Color = source.Btns.Destructive2.btnDestroy2Bg,
|
||||
contentColor: Color = source.Btns.Destructive2.btnDestroy2Fg,
|
||||
borderColor: Color = Color.Unspecified,
|
||||
disabledContainerColor: Color = ZashiColors.Btns.Destructive2.btnDestroy2BgDisabled,
|
||||
disabledContentColor: Color = ZashiColors.Btns.Destructive2.btnDestroy2FgDisabled,
|
||||
disabledContainerColor: Color = source.Btns.Destructive2.btnDestroy2BgDisabled,
|
||||
disabledContentColor: Color = source.Btns.Destructive2.btnDestroy2FgDisabled,
|
||||
) = ZashiButtonColors(
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
|
@ -278,7 +285,7 @@ data class ButtonState(
|
|||
)
|
||||
|
||||
@Composable
|
||||
private fun ZashiButtonColors.toButtonColors() =
|
||||
fun ZashiButtonColors.toButtonColors() =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
|
@ -286,6 +293,12 @@ private fun ZashiButtonColors.toButtonColors() =
|
|||
disabledContentColor = disabledContentColor,
|
||||
)
|
||||
|
||||
@Suppress("CompositionLocalAllowlist")
|
||||
val LocalZashiButtonColors =
|
||||
compositionLocalOf<ZashiButtonColors?> {
|
||||
null
|
||||
}
|
||||
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun PrimaryPreview() =
|
||||
|
|
|
@ -1,18 +1,24 @@
|
|||
package co.electriccoin.zcash.ui.design.component
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.isSpecified
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
|
||||
@Composable
|
||||
fun ZashiCard(
|
||||
modifier: Modifier = Modifier,
|
||||
borderColor: Color = Color.Unspecified,
|
||||
contentPadding: PaddingValues = PaddingValues(24.dp),
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Card(
|
||||
|
@ -22,9 +28,15 @@ fun ZashiCard(
|
|||
containerColor = ZashiColors.Surfaces.bgSecondary,
|
||||
contentColor = ZashiColors.Text.textTertiary
|
||||
),
|
||||
border =
|
||||
if (borderColor.isSpecified) {
|
||||
BorderStroke(1.dp, borderColor)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
Modifier.padding(24.dp)
|
||||
Modifier.padding(contentPadding)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
|
|
|
@ -21,7 +21,9 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.electriccoin.zcash.ui.design.R
|
||||
|
@ -40,6 +42,9 @@ fun ZashiCheckbox(
|
|||
isChecked: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
style: TextStyle = ZashiTypography.textSm,
|
||||
fontWeight: FontWeight = FontWeight.Medium,
|
||||
color: Color = ZashiColors.Text.textPrimary,
|
||||
) {
|
||||
ZashiCheckbox(
|
||||
state =
|
||||
|
@ -49,6 +54,9 @@ fun ZashiCheckbox(
|
|||
onClick = onClick,
|
||||
),
|
||||
modifier = modifier,
|
||||
style = style,
|
||||
fontWeight = fontWeight,
|
||||
color = color,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -56,6 +64,9 @@ fun ZashiCheckbox(
|
|||
fun ZashiCheckbox(
|
||||
state: CheckboxState,
|
||||
modifier: Modifier = Modifier,
|
||||
style: TextStyle = ZashiTypography.textSm,
|
||||
fontWeight: FontWeight = FontWeight.Medium,
|
||||
color: Color = ZashiColors.Text.textPrimary,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
|
@ -70,9 +81,9 @@ fun ZashiCheckbox(
|
|||
|
||||
Text(
|
||||
text = state.text.getValue(),
|
||||
style = ZashiTypography.textSm,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = ZashiColors.Text.textPrimary,
|
||||
style = style,
|
||||
fontWeight = fontWeight,
|
||||
color = color,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
@ -14,7 +13,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
@ -41,30 +39,17 @@ fun ZashiChipButton(
|
|||
border: BorderStroke? = ZashiChipButtonDefaults.border,
|
||||
color: Color = ZashiChipButtonDefaults.color,
|
||||
contentPadding: PaddingValues = ZashiChipButtonDefaults.contentPadding,
|
||||
hasRippleEffect: Boolean = true,
|
||||
textStyle: TextStyle = ZashiChipButtonDefaults.textStyle,
|
||||
endIconSpacer: Dp = ZashiChipButtonDefaults.endIconSpacer,
|
||||
) {
|
||||
val clickableModifier =
|
||||
if (hasRippleEffect) {
|
||||
modifier.clickable(onClick = state.onClick)
|
||||
} else {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
modifier.clickable(
|
||||
onClick = state.onClick,
|
||||
indication = null,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = clickableModifier,
|
||||
modifier = modifier,
|
||||
shape = shape,
|
||||
border = border,
|
||||
color = color,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(contentPadding),
|
||||
modifier = Modifier.clickable(onClick = state.onClick) then Modifier.padding(contentPadding),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (state.startIcon != null) {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -90,7 +90,7 @@ fun rememberModalBottomSheetState(
|
|||
|
||||
@Composable
|
||||
@ExperimentalMaterial3Api
|
||||
private fun rememberSheetState(
|
||||
fun rememberSheetState(
|
||||
skipPartiallyExpanded: Boolean,
|
||||
confirmValueChange: (SheetValue) -> Boolean,
|
||||
initialValue: SheetValue,
|
||||
|
|
|
@ -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,
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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 = {},
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.design.component
|
|||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
|
@ -22,23 +23,32 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
||||
import co.electriccoin.zcash.ui.design.util.StringResource
|
||||
import co.electriccoin.zcash.ui.design.util.getString
|
||||
import co.electriccoin.zcash.ui.design.util.getValue
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
|
||||
|
@ -48,9 +58,18 @@ fun ZashiTextField(
|
|||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
innerModifier: Modifier = Modifier,
|
||||
innerModifier: Modifier = ZashiTextFieldDefaults.innerModifier,
|
||||
error: String? = null,
|
||||
isEnabled: Boolean = true,
|
||||
handle: ZashiTextFieldHandle =
|
||||
rememberZashiTextFieldHandle(
|
||||
TextFieldState(
|
||||
value = stringRes(value),
|
||||
error = error?.let { stringRes(it) },
|
||||
isEnabled = isEnabled,
|
||||
onValueChange = onValueChange,
|
||||
)
|
||||
),
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium),
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
|
@ -97,7 +116,8 @@ fun ZashiTextField(
|
|||
interactionSource = interactionSource,
|
||||
shape = shape,
|
||||
colors = colors,
|
||||
innerModifier = innerModifier
|
||||
innerModifier = innerModifier,
|
||||
handle = handle,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -106,7 +126,8 @@ fun ZashiTextField(
|
|||
fun ZashiTextField(
|
||||
state: TextFieldState,
|
||||
modifier: Modifier = Modifier,
|
||||
innerModifier: Modifier = Modifier,
|
||||
innerModifier: Modifier = ZashiTextFieldDefaults.innerModifier,
|
||||
handle: ZashiTextFieldHandle = rememberZashiTextFieldHandle(state),
|
||||
readOnly: Boolean = false,
|
||||
textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium),
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
|
@ -124,6 +145,13 @@ fun ZashiTextField(
|
|||
minLines: Int = 1,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
shape: Shape = ZashiTextFieldDefaults.shape,
|
||||
contentPadding: PaddingValues =
|
||||
PaddingValues(
|
||||
start = if (leadingIcon != null) 8.dp else 14.dp,
|
||||
end = if (suffix != null) 4.dp else 12.dp,
|
||||
top = getVerticalPadding(trailingIcon, leadingIcon, suffix, prefix),
|
||||
bottom = getVerticalPadding(trailingIcon, leadingIcon, suffix, prefix),
|
||||
),
|
||||
colors: ZashiTextFieldColors = ZashiTextFieldDefaults.defaultColors()
|
||||
) {
|
||||
TextFieldInternal(
|
||||
|
@ -147,10 +175,41 @@ fun ZashiTextField(
|
|||
interactionSource = interactionSource,
|
||||
shape = shape,
|
||||
colors = colors,
|
||||
innerModifier = innerModifier
|
||||
contentPadding = contentPadding,
|
||||
innerModifier = innerModifier,
|
||||
handle = handle
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ZashiTextFieldPlaceholder(res: StringResource) {
|
||||
Text(
|
||||
text = res.getValue(),
|
||||
style = ZashiTypography.textMd,
|
||||
color = ZashiColors.Inputs.Default.text
|
||||
)
|
||||
}
|
||||
|
||||
@Stable
|
||||
class ZashiTextFieldHandle(
|
||||
text: String
|
||||
) {
|
||||
var textFieldValueState by mutableStateOf(TextFieldValue(text = text))
|
||||
|
||||
fun moveCursorToEnd() {
|
||||
textFieldValueState =
|
||||
textFieldValueState.copy(
|
||||
selection = TextRange(textFieldValueState.text.length),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberZashiTextFieldHandle(state: TextFieldState): ZashiTextFieldHandle {
|
||||
val context = LocalContext.current
|
||||
return remember { ZashiTextFieldHandle(state.value.getString(context)) }
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
@ -174,10 +233,32 @@ private fun TextFieldInternal(
|
|||
interactionSource: MutableInteractionSource,
|
||||
shape: Shape,
|
||||
colors: ZashiTextFieldColors,
|
||||
contentPadding: PaddingValues,
|
||||
handle: ZashiTextFieldHandle,
|
||||
modifier: Modifier = Modifier,
|
||||
innerModifier: Modifier = Modifier,
|
||||
) {
|
||||
val borderColor by colors.borderColor(state)
|
||||
val context = LocalContext.current
|
||||
val value = remember(state.value) { state.value.getString(context) }
|
||||
// Holds the latest internal TextFieldValue state. We need to keep it to have the correct value
|
||||
// of the composition.
|
||||
val textFieldValueState = handle.textFieldValueState
|
||||
// Holds the latest TextFieldValue that BasicTextField was recomposed with. We couldn't simply
|
||||
// pass `TextFieldValue(text = value)` to the CoreTextField because we need to preserve the
|
||||
// composition.
|
||||
val textFieldValue = textFieldValueState.copy(text = value, selection = textFieldValueState.selection)
|
||||
|
||||
SideEffect {
|
||||
if (textFieldValue.text != textFieldValueState.text ||
|
||||
textFieldValue.selection != textFieldValueState.selection ||
|
||||
textFieldValue.composition != textFieldValueState.composition
|
||||
) {
|
||||
handle.textFieldValueState = textFieldValue
|
||||
}
|
||||
}
|
||||
|
||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||
val borderColor by colors.borderColor(state, isFocused)
|
||||
val androidColors = colors.toTextFieldColors()
|
||||
// If color is not provided via the text style, use content color as a default
|
||||
val textColor =
|
||||
|
@ -186,24 +267,35 @@ private fun TextFieldInternal(
|
|||
}
|
||||
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
|
||||
|
||||
var lastTextValue by remember(value) { mutableStateOf(value) }
|
||||
|
||||
CompositionLocalProvider(LocalTextSelectionColors provides androidColors.selectionColors) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
BasicTextField(
|
||||
value = state.value.getValue(),
|
||||
value = textFieldValue,
|
||||
modifier =
|
||||
innerModifier.fillMaxWidth() then
|
||||
innerModifier then
|
||||
if (borderColor == Color.Unspecified) {
|
||||
Modifier
|
||||
} else {
|
||||
Modifier.border(
|
||||
width = 1.dp,
|
||||
color = borderColor,
|
||||
shape = ZashiTextFieldDefaults.shape
|
||||
shape = shape
|
||||
)
|
||||
} then Modifier.defaultMinSize(minWidth = TextFieldDefaults.MinWidth),
|
||||
onValueChange = state.onValueChange,
|
||||
},
|
||||
onValueChange = { newTextFieldValueState ->
|
||||
handle.textFieldValueState = newTextFieldValueState
|
||||
|
||||
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
|
||||
lastTextValue = newTextFieldValueState.text
|
||||
|
||||
if (stringChangedSinceLastInvocation) {
|
||||
state.onValueChange(newTextFieldValueState.text)
|
||||
}
|
||||
},
|
||||
enabled = state.isEnabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = mergedTextStyle,
|
||||
|
@ -215,44 +307,37 @@ private fun TextFieldInternal(
|
|||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
// places leading icon, text field with label and placeholder, trailing icon
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = state.value.getValue(),
|
||||
visualTransformation = visualTransformation,
|
||||
innerTextField = {
|
||||
DecorationBox(prefix = prefix, suffix = suffix, content = innerTextField)
|
||||
) { innerTextField: @Composable () -> Unit ->
|
||||
// places leading icon, text field with label and placeholder, trailing icon
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = state.value.getValue(),
|
||||
visualTransformation = visualTransformation,
|
||||
innerTextField = {
|
||||
DecorationBox(prefix = prefix, suffix = suffix, content = innerTextField)
|
||||
},
|
||||
placeholder =
|
||||
if (placeholder != null) {
|
||||
{
|
||||
DecorationBox(prefix, suffix, placeholder)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
placeholder =
|
||||
if (placeholder != null) {
|
||||
{
|
||||
DecorationBox(prefix, suffix, placeholder)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
label = label,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
prefix = prefix,
|
||||
suffix = suffix,
|
||||
supportingText = supportingText,
|
||||
shape = shape,
|
||||
singleLine = singleLine,
|
||||
enabled = state.isEnabled,
|
||||
isError = state.isError,
|
||||
interactionSource = interactionSource,
|
||||
colors = androidColors,
|
||||
contentPadding =
|
||||
PaddingValues(
|
||||
start = if (leadingIcon != null) 8.dp else 14.dp,
|
||||
end = if (suffix != null) 4.dp else 12.dp,
|
||||
top = getVerticalPadding(trailingIcon, leadingIcon, suffix, prefix),
|
||||
bottom = getVerticalPadding(trailingIcon, leadingIcon, suffix, prefix),
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
label = label,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
prefix = prefix,
|
||||
suffix = suffix,
|
||||
supportingText = supportingText,
|
||||
shape = shape,
|
||||
singleLine = singleLine,
|
||||
enabled = state.isEnabled,
|
||||
isError = state.isError,
|
||||
interactionSource = interactionSource,
|
||||
colors = androidColors,
|
||||
contentPadding = contentPadding
|
||||
)
|
||||
}
|
||||
|
||||
if (state.error != null && state.error.getValue().isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
|
@ -303,7 +388,9 @@ data class ZashiTextFieldColors(
|
|||
val textColor: Color,
|
||||
val hintColor: Color,
|
||||
val borderColor: Color,
|
||||
val focusedBorderColor: Color,
|
||||
val containerColor: Color,
|
||||
val focusedContainerColor: Color,
|
||||
val placeholderColor: Color,
|
||||
val disabledTextColor: Color,
|
||||
val disabledHintColor: Color,
|
||||
|
@ -317,11 +404,15 @@ data class ZashiTextFieldColors(
|
|||
val errorPlaceholderColor: Color,
|
||||
) {
|
||||
@Composable
|
||||
internal fun borderColor(state: TextFieldState): State<Color> {
|
||||
internal fun borderColor(
|
||||
state: TextFieldState,
|
||||
isFocused: Boolean
|
||||
): State<Color> {
|
||||
val targetValue =
|
||||
when {
|
||||
!state.isEnabled -> disabledBorderColor
|
||||
state.isError -> errorBorderColor
|
||||
isFocused -> focusedBorderColor.takeOrElse { borderColor }
|
||||
else -> borderColor
|
||||
}
|
||||
return rememberUpdatedState(targetValue)
|
||||
|
@ -345,7 +436,7 @@ data class ZashiTextFieldColors(
|
|||
unfocusedTextColor = textColor,
|
||||
disabledTextColor = disabledTextColor,
|
||||
errorTextColor = errorTextColor,
|
||||
focusedContainerColor = containerColor,
|
||||
focusedContainerColor = focusedContainerColor.takeOrElse { containerColor },
|
||||
unfocusedContainerColor = containerColor,
|
||||
disabledContainerColor = disabledContainerColor,
|
||||
errorContainerColor = errorContainerColor,
|
||||
|
@ -391,13 +482,21 @@ object ZashiTextFieldDefaults {
|
|||
val shape: Shape
|
||||
get() = RoundedCornerShape(8.dp)
|
||||
|
||||
val innerModifier: Modifier
|
||||
get() =
|
||||
Modifier
|
||||
.defaultMinSize(minWidth = TextFieldDefaults.MinWidth)
|
||||
.fillMaxWidth()
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun defaultColors(
|
||||
textColor: Color = ZashiColors.Inputs.Filled.text,
|
||||
hintColor: Color = ZashiColors.Inputs.Default.hint,
|
||||
borderColor: Color = Color.Unspecified,
|
||||
focusedBorderColor: Color = ZashiColors.Inputs.Focused.stroke,
|
||||
containerColor: Color = ZashiColors.Inputs.Default.bg,
|
||||
focusedContainerColor: Color = ZashiColors.Inputs.Focused.bg,
|
||||
placeholderColor: Color = ZashiColors.Inputs.Default.text,
|
||||
disabledTextColor: Color = ZashiColors.Inputs.Disabled.text,
|
||||
disabledHintColor: Color = ZashiColors.Inputs.Disabled.hint,
|
||||
|
@ -413,7 +512,9 @@ object ZashiTextFieldDefaults {
|
|||
textColor = textColor,
|
||||
hintColor = hintColor,
|
||||
borderColor = borderColor,
|
||||
focusedBorderColor = focusedBorderColor,
|
||||
containerColor = containerColor,
|
||||
focusedContainerColor = focusedContainerColor,
|
||||
placeholderColor = placeholderColor,
|
||||
disabledTextColor = disabledTextColor,
|
||||
disabledHintColor = disabledHintColor,
|
||||
|
@ -428,6 +529,16 @@ object ZashiTextFieldDefaults {
|
|||
)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class TextFieldState(
|
||||
val value: StringResource,
|
||||
val error: StringResource? = null,
|
||||
val isEnabled: Boolean = true,
|
||||
val onValueChange: (String) -> Unit,
|
||||
) {
|
||||
val isError = error != null
|
||||
}
|
||||
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun DefaultPreview() =
|
||||
|
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -103,6 +103,10 @@ private fun ContactItemLeading(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
ImageResource.Loading -> {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -66,6 +66,10 @@ fun ZashiCheckboxListItem(
|
|||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
|
||||
ImageResource.Loading -> {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -9,6 +9,10 @@ import androidx.compose.material3.RippleConfiguration
|
|||
import androidx.compose.material3.RippleDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import co.electriccoin.zcash.ui.design.LocalKeyboardManager
|
||||
import co.electriccoin.zcash.ui.design.LocalSheetStateManager
|
||||
import co.electriccoin.zcash.ui.design.rememberKeyboardManager
|
||||
import co.electriccoin.zcash.ui.design.rememberSheetStateManager
|
||||
import co.electriccoin.zcash.ui.design.theme.balances.LocalBalancesAvailable
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.DarkZashiColorsInternal
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.LightZashiColorsInternal
|
||||
|
@ -49,7 +53,9 @@ fun ZcashTheme(
|
|||
LocalZashiColors provides zashiColors,
|
||||
LocalZashiTypography provides ZashiTypographyInternal,
|
||||
LocalRippleConfiguration provides MaterialRippleConfig,
|
||||
LocalBalancesAvailable provides balancesAvailable
|
||||
LocalBalancesAvailable provides balancesAvailable,
|
||||
LocalKeyboardManager provides rememberKeyboardManager(),
|
||||
LocalSheetStateManager provides rememberSheetStateManager()
|
||||
) {
|
||||
ProvideDimens {
|
||||
MaterialTheme(
|
||||
|
|
|
@ -543,7 +543,8 @@ val DarkZashiColorsInternal =
|
|||
utilityPurple50 = Purple.`950`,
|
||||
utilityPurple100 = Purple.`900`,
|
||||
utilityPurple400 = Purple.`600`,
|
||||
utilityPurple300 = Purple.`700`
|
||||
utilityPurple300 = Purple.`700`,
|
||||
utilityPurple900 = Purple.`50`
|
||||
),
|
||||
Espresso =
|
||||
UtilityEspresso(
|
||||
|
@ -551,12 +552,13 @@ val DarkZashiColorsInternal =
|
|||
utilityEspresso600 = Espresso.`300`,
|
||||
utilityEspresso500 = Espresso.`400`,
|
||||
utilityEspresso200 = Espresso.`700`,
|
||||
utilityEspresso50 = Espresso.`900`,
|
||||
utilityEspresso100 = Espresso.`800`,
|
||||
utilityEspresso50 = Espresso.`950`,
|
||||
utilityEspresso100 = Espresso.`900`,
|
||||
utilityEspresso400 = Espresso.`500`,
|
||||
utilityEspresso300 = Espresso.`600`,
|
||||
utilityEspresso800 = Espresso.`100`,
|
||||
utilityEspresso900 = Espresso.`50`,
|
||||
utilityEspresso800 = Espresso.`100`
|
||||
utilityEspresso950 = Espresso.`25`
|
||||
)
|
||||
),
|
||||
Transparent =
|
||||
|
|
|
@ -543,7 +543,8 @@ val LightZashiColorsInternal =
|
|||
utilityPurple50 = Purple.`50`,
|
||||
utilityPurple100 = Purple.`100`,
|
||||
utilityPurple400 = Purple.`400`,
|
||||
utilityPurple300 = Purple.`300`
|
||||
utilityPurple300 = Purple.`300`,
|
||||
utilityPurple900 = Purple.`900`
|
||||
),
|
||||
Espresso =
|
||||
UtilityEspresso(
|
||||
|
@ -556,7 +557,8 @@ val LightZashiColorsInternal =
|
|||
utilityEspresso400 = Espresso.`400`,
|
||||
utilityEspresso300 = Espresso.`300`,
|
||||
utilityEspresso900 = Espresso.`900`,
|
||||
utilityEspresso800 = Espresso.`800`
|
||||
utilityEspresso800 = Espresso.`800`,
|
||||
utilityEspresso950 = Espresso.`950`
|
||||
)
|
||||
),
|
||||
Transparent =
|
||||
|
|
|
@ -8,5 +8,17 @@ import androidx.compose.runtime.staticCompositionLocalOf
|
|||
val ZashiColors: ZashiColorsInternal
|
||||
@Composable get() = LocalZashiColors.current
|
||||
|
||||
val ZashiLightColors: ZashiColorsInternal
|
||||
@Composable get() = LocalLightZashiColors.current
|
||||
|
||||
val ZashiDarkColors: ZashiColorsInternal
|
||||
@Composable get() = LocalDarkZashiColors.current
|
||||
|
||||
@Suppress("CompositionLocalAllowlist")
|
||||
internal val LocalZashiColors = staticCompositionLocalOf<ZashiColorsInternal> { error("no colors specified") }
|
||||
|
||||
@Suppress("CompositionLocalAllowlist")
|
||||
internal val LocalLightZashiColors = staticCompositionLocalOf { LightZashiColorsInternal }
|
||||
|
||||
@Suppress("CompositionLocalAllowlist")
|
||||
internal val LocalDarkZashiColors = staticCompositionLocalOf { DarkZashiColorsInternal }
|
||||
|
|
|
@ -647,7 +647,8 @@ data class UtilityPurple(
|
|||
val utilityPurple50: Color,
|
||||
val utilityPurple100: Color,
|
||||
val utilityPurple400: Color,
|
||||
val utilityPurple300: Color
|
||||
val utilityPurple300: Color,
|
||||
val utilityPurple900: Color
|
||||
)
|
||||
|
||||
@Immutable
|
||||
|
@ -661,6 +662,7 @@ data class UtilityEspresso(
|
|||
val utilityEspresso400: Color,
|
||||
val utilityEspresso300: Color,
|
||||
val utilityEspresso900: Color,
|
||||
val utilityEspresso950: Color,
|
||||
val utilityEspresso800: Color
|
||||
)
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.util
|
||||
package co.electriccoin.zcash.ui.design.util
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
|
@ -17,6 +17,9 @@ sealed interface ImageResource {
|
|||
value class DisplayString(
|
||||
val value: String
|
||||
) : ImageResource
|
||||
|
||||
@Immutable
|
||||
data object Loading : ImageResource
|
||||
}
|
||||
|
||||
@Stable
|
||||
|
@ -26,3 +29,6 @@ fun imageRes(
|
|||
|
||||
@Stable
|
||||
fun imageRes(value: String): ImageResource = ImageResource.DisplayString(value)
|
||||
|
||||
@Stable
|
||||
fun loadingImageRes(): ImageResource = ImageResource.Loading
|
||||
|
|
|
@ -57,8 +57,17 @@ sealed interface StringResource {
|
|||
val address: String,
|
||||
val abbreviated: Boolean
|
||||
) : StringResource
|
||||
|
||||
operator fun plus(other: StringResource): StringResource = CompositeStringResource(listOf(this, other))
|
||||
|
||||
operator fun plus(other: String): StringResource = CompositeStringResource(listOf(this, stringRes(other)))
|
||||
}
|
||||
|
||||
@Immutable
|
||||
private data class CompositeStringResource(
|
||||
val resources: List<StringResource>
|
||||
) : StringResource
|
||||
|
||||
@Stable
|
||||
fun stringRes(
|
||||
@StringRes resource: Int,
|
||||
|
@ -109,18 +118,15 @@ fun StringResource.getValue(
|
|||
convertYearMonth: (YearMonth) -> String = StringResourceDefaults::convertYearMonth,
|
||||
convertAddress: (StringResource.ByAddress) -> String = StringResourceDefaults::convertAddress,
|
||||
convertTransactionId: (StringResource.ByTransactionId) -> String = StringResourceDefaults::convertTransactionId
|
||||
) = when (this) {
|
||||
is StringResource.ByResource -> {
|
||||
val context = LocalContext.current
|
||||
context.getString(resource, *args.normalize(context).toTypedArray())
|
||||
}
|
||||
is StringResource.ByString -> value
|
||||
is StringResource.ByZatoshi -> convertZatoshi(zatoshi)
|
||||
is StringResource.ByDateTime -> convertDateTime(this)
|
||||
is StringResource.ByYearMonth -> convertYearMonth(yearMonth)
|
||||
is StringResource.ByAddress -> convertAddress(this)
|
||||
is StringResource.ByTransactionId -> convertTransactionId(this)
|
||||
}
|
||||
): String =
|
||||
getString(
|
||||
context = LocalContext.current,
|
||||
convertZatoshi = convertZatoshi,
|
||||
convertDateTime = convertDateTime,
|
||||
convertYearMonth = convertYearMonth,
|
||||
convertAddress = convertAddress,
|
||||
convertTransactionId = convertTransactionId
|
||||
)
|
||||
|
||||
@Suppress("SpreadOperator")
|
||||
fun StringResource.getString(
|
||||
|
@ -130,15 +136,27 @@ fun StringResource.getString(
|
|||
convertYearMonth: (YearMonth) -> String = StringResourceDefaults::convertYearMonth,
|
||||
convertAddress: (StringResource.ByAddress) -> String = StringResourceDefaults::convertAddress,
|
||||
convertTransactionId: (StringResource.ByTransactionId) -> String = StringResourceDefaults::convertTransactionId
|
||||
) = when (this) {
|
||||
is StringResource.ByResource -> context.getString(resource, *args.normalize(context).toTypedArray())
|
||||
is StringResource.ByString -> value
|
||||
is StringResource.ByZatoshi -> convertZatoshi(zatoshi)
|
||||
is StringResource.ByDateTime -> convertDateTime(this)
|
||||
is StringResource.ByYearMonth -> convertYearMonth(yearMonth)
|
||||
is StringResource.ByAddress -> convertAddress(this)
|
||||
is StringResource.ByTransactionId -> convertTransactionId(this)
|
||||
}
|
||||
): String =
|
||||
when (this) {
|
||||
is StringResource.ByResource -> context.getString(resource, *args.normalize(context).toTypedArray())
|
||||
is StringResource.ByString -> value
|
||||
is StringResource.ByZatoshi -> convertZatoshi(zatoshi)
|
||||
is StringResource.ByDateTime -> convertDateTime(this)
|
||||
is StringResource.ByYearMonth -> convertYearMonth(yearMonth)
|
||||
is StringResource.ByAddress -> convertAddress(this)
|
||||
is StringResource.ByTransactionId -> convertTransactionId(this)
|
||||
is CompositeStringResource ->
|
||||
this.resources.joinToString(separator = "") {
|
||||
it.getString(
|
||||
context = context,
|
||||
convertZatoshi = convertZatoshi,
|
||||
convertDateTime = convertDateTime,
|
||||
convertYearMonth = convertYearMonth,
|
||||
convertAddress = convertAddress,
|
||||
convertTransactionId = convertTransactionId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Any>.normalize(context: Context): List<Any> =
|
||||
this.map {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -3,4 +3,5 @@
|
|||
<string name="hide_balance_placeholder">-----</string>
|
||||
<string name="back_navigation_content_description">Atrás</string>
|
||||
<string name="triple_dots">…</string>
|
||||
<string name="seed_recovery_reveal">Mostrar frase de recuperación</string>
|
||||
</resources>
|
||||
|
|
|
@ -3,4 +3,5 @@
|
|||
<string name="hide_balance_placeholder">-----</string>
|
||||
<string name="back_navigation_content_description">Back</string>
|
||||
<string name="triple_dots">…</string>
|
||||
<string name="seed_recovery_reveal">Reveal recovery phrase</string>
|
||||
</resources>
|
||||
|
|
|
@ -8,8 +8,8 @@ import androidx.test.filters.LargeTest
|
|||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.integration.test.common.IntegrationTestingActivity
|
||||
import co.electriccoin.zcash.ui.integration.test.common.getPermissionPositiveButtonUiObject
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanScreenState
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
|
|
|
@ -14,8 +14,8 @@ import co.electriccoin.zcash.ui.integration.test.common.getPermissionPositiveBut
|
|||
import co.electriccoin.zcash.ui.integration.test.common.getStringResource
|
||||
import co.electriccoin.zcash.ui.integration.test.common.getStringResourceWithArgs
|
||||
import co.electriccoin.zcash.ui.integration.test.common.waitForDeviceIdle
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanScreenState
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
|
|
|
@ -3,13 +3,12 @@ package co.electriccoin.zcash.ui.integration.test.screen.scan.view
|
|||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.integration.test.common.getPermissionNegativeButtonUiObject
|
||||
import co.electriccoin.zcash.ui.integration.test.common.getPermissionPositiveButtonUiObject
|
||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
|
||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanValidationState
|
||||
import co.electriccoin.zcash.ui.screen.scan.view.Scan
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanScreenState
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanValidationState
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
@ -60,7 +59,6 @@ class ScanViewTestSetup(
|
|||
onScanStateChange = {
|
||||
scanState.set(it)
|
||||
},
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
validationResult = ScanValidationState.VALID
|
||||
)
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ android {
|
|||
"src/main/res/ui/crash_reporting_opt_in",
|
||||
"src/main/res/ui/delete_wallet",
|
||||
"src/main/res/ui/export_data",
|
||||
"src/main/res/ui/error",
|
||||
"src/main/res/ui/home",
|
||||
"src/main/res/ui/choose_server",
|
||||
"src/main/res/ui/integrations",
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
}
|
|
@ -2,7 +2,6 @@ package co.electriccoin.zcash.ui.screen.about.view
|
|||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||
import co.electriccoin.zcash.ui.common.model.VersionInfo
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo
|
||||
|
@ -28,7 +27,6 @@ class AboutViewTestSetup(
|
|||
configInfo = configInfo,
|
||||
onPrivacyPolicy = {},
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
versionInfo = versionInfo,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ package co.electriccoin.zcash.ui.screen.exportdata.view
|
|||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
@ -46,7 +45,6 @@ class ExportPrivateDataViewTestSetup(
|
|||
onConfirm = {
|
||||
onConfirmCount.incrementAndGet()
|
||||
},
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -28,10 +28,8 @@ class OnboardingTestSetup(
|
|||
ZcashTheme {
|
||||
Onboarding(
|
||||
// Debug only UI state does not need to be tested
|
||||
isDebugMenuEnabled = false,
|
||||
onImportWallet = { onImportWalletCallbackCount.incrementAndGet() },
|
||||
onCreateWallet = { onCreateWalletCallbackCount.incrementAndGet() },
|
||||
onFixtureWallet = {}
|
||||
onCreateWallet = { onCreateWalletCallbackCount.incrementAndGet() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,8 +46,9 @@ class ReceiveViewTestSetup(
|
|||
)
|
||||
),
|
||||
isLoading = false,
|
||||
onBack = {}
|
||||
),
|
||||
zashiMainTopAppBarState =
|
||||
appBarState =
|
||||
ZashiMainTopAppBarStateFixture.new(
|
||||
settingsButton =
|
||||
IconButtonState(
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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 = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,8 +11,8 @@ import androidx.test.filters.MediumTest
|
|||
import androidx.test.rule.GrantPermissionRule
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanScreenState
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
|
|
|
@ -3,10 +3,10 @@ package co.electriccoin.zcash.ui.screen.scan.view
|
|||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
|
||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanValidationState
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanScreenState
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanValidationState
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
|
@ -30,7 +30,7 @@ class ScanViewBasicTestSetup(
|
|||
@Suppress("TestFunctionName")
|
||||
fun DefaultContent() {
|
||||
Scan(
|
||||
validationResult = ScanValidationState.VALID,
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
onBack = {
|
||||
onBackCount.incrementAndGet()
|
||||
},
|
||||
|
@ -40,8 +40,7 @@ class ScanViewBasicTestSetup(
|
|||
onScanStateChange = {
|
||||
scanState.set(it)
|
||||
},
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
validationResult = ScanValidationState.VALID,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,9 +6,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import cash.z.ecc.android.sdk.fixture.WalletBalanceFixture
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.android.sdk.type.AddressType
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
|
@ -17,7 +15,6 @@ import co.electriccoin.zcash.ui.design.component.IconButtonState
|
|||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.fixture.BalanceStateFixture
|
||||
import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture
|
||||
import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture
|
||||
import co.electriccoin.zcash.ui.screen.send.ext.Saver
|
||||
import co.electriccoin.zcash.ui.screen.send.model.AmountState
|
||||
|
@ -107,18 +104,13 @@ class SendViewTestSetup(
|
|||
// TODO [#1260]: Cover Send.Form screen UI with tests
|
||||
// TODO [#1260]: https://github.com/Electric-Coin-Company/zashi-android/issues/1260
|
||||
Send(
|
||||
balanceState = BalanceStateFixture.new(),
|
||||
balanceWidgetState = BalanceStateFixture.new(),
|
||||
sendStage = sendStage,
|
||||
onCreateZecSend = setZecSend,
|
||||
onBack = onBackAction,
|
||||
onQrScannerOpen = {
|
||||
onScannerCount.incrementAndGet()
|
||||
},
|
||||
goBalances = {
|
||||
// TODO [#1194]: Cover Current balances UI widget with tests
|
||||
// TODO [#1194]: https://github.com/Electric-Coin-Company/zashi-android/issues/1194
|
||||
},
|
||||
isHideBalances = false,
|
||||
hasCameraFeature = hasCameraFeature,
|
||||
recipientAddressState = RecipientAddressState("", AddressType.Invalid()),
|
||||
onRecipientAddressChange = {
|
||||
|
@ -137,13 +129,7 @@ class SendViewTestSetup(
|
|||
),
|
||||
setMemoState = {},
|
||||
memoState = MemoState.new(""),
|
||||
walletSnapshot =
|
||||
WalletSnapshotFixture.new(
|
||||
saplingBalance =
|
||||
WalletBalanceFixture.new(
|
||||
available = Zatoshi(Zatoshi.MAX_INCLUSIVE.div(100))
|
||||
)
|
||||
),
|
||||
selectedAccount = null,
|
||||
exchangeRateState = ExchangeRateState.OptedOut,
|
||||
sendAddressBookState =
|
||||
SendAddressBookState(
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.compose.ui.test.junit4.createComposeRule
|
|||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.sdk.fixture.ZecSendFixture
|
||||
import co.electriccoin.zcash.ui.screen.send.Send
|
||||
import co.electriccoin.zcash.ui.screen.send.WrapSend
|
||||
import co.electriccoin.zcash.ui.screen.send.assertOnForm
|
||||
import co.electriccoin.zcash.ui.screen.send.clickCreateAndSend
|
||||
|
@ -33,10 +34,7 @@ class SendViewIntegrationTest {
|
|||
|
||||
restorationTester.setContent {
|
||||
WrapSend(
|
||||
sendArguments = null,
|
||||
goToQrScanner = {},
|
||||
goBack = {},
|
||||
goBalances = {},
|
||||
args = Send(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -9,15 +9,16 @@ import androidx.compose.ui.test.onNodeWithText
|
|||
import androidx.test.filters.MediumTest
|
||||
import cash.z.ecc.android.sdk.ext.collectWith
|
||||
import cash.z.ecc.android.sdk.fixture.WalletAddressFixture
|
||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||
import cash.z.ecc.android.sdk.model.Memo
|
||||
import cash.z.ecc.android.sdk.model.MonetarySeparators
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.android.sdk.model.ZecSend
|
||||
import cash.z.ecc.sdk.fixture.ZecRequestFixture
|
||||
import cash.z.ecc.sdk.fixture.ZecSendFixture
|
||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.fixture.SendArgumentsWrapperFixture
|
||||
import co.electriccoin.zcash.ui.screen.send.SendTag
|
||||
import co.electriccoin.zcash.ui.screen.send.SendViewTestSetup
|
||||
import co.electriccoin.zcash.ui.screen.send.assertOnForm
|
||||
|
@ -379,7 +380,7 @@ class SendViewTest : UiTestPrerequisites() {
|
|||
composeTestRule.onNodeWithText(getStringResource(R.string.send_address_hint)).also {
|
||||
it.assertTextEquals(
|
||||
getStringResource(R.string.send_address_hint),
|
||||
SendArgumentsWrapperFixture.RECIPIENT_ADDRESS.address,
|
||||
WalletFixture.Alice.getAddresses(ZcashNetwork.Testnet).unified,
|
||||
includeEditableText = true
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package co.electriccoin.zcash.ui.screen.settings
|
|||
|
||||
import androidx.compose.ui.test.junit4.ComposeContentTestRule
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
|
||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
|
@ -149,7 +148,6 @@ class SettingsViewTestSetup(
|
|||
),
|
||||
)
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package co.electriccoin.zcash.ui.screen.scan.util
|
|||
import android.graphics.ImageFormat
|
||||
import androidx.camera.core.ImageProxy
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.screen.scan.QrCodeAnalyzer
|
||||
import co.electriccoin.zcash.ui.screen.scankeystone.view.FramePosition
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.BinaryBitmap
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
android:configChanges="orientation|locale|layoutDirection|screenLayout|uiMode|colorMode|keyboard|screenSize"
|
||||
android:exported="false"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.App.Starting" />
|
||||
|
||||
|
|
|
@ -6,12 +6,8 @@ import co.electriccoin.zcash.configuration.AndroidConfigurationFactory
|
|||
import co.electriccoin.zcash.global.newInstance
|
||||
import co.electriccoin.zcash.preference.EncryptedPreferenceProvider
|
||||
import co.electriccoin.zcash.preference.StandardPreferenceProvider
|
||||
import co.electriccoin.zcash.preference.model.entry.PreferenceKey
|
||||
import co.electriccoin.zcash.ui.HomeTabNavigationRouter
|
||||
import co.electriccoin.zcash.ui.HomeTabNavigationRouterImpl
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.NavigationRouterImpl
|
||||
import co.electriccoin.zcash.ui.preference.PersistableWalletPreferenceDefault
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
@ -21,22 +17,12 @@ val coreModule =
|
|||
single {
|
||||
WalletCoordinator.newInstance(
|
||||
context = get(),
|
||||
encryptedPreferenceProvider = get(),
|
||||
persistableWalletPreference = get(),
|
||||
persistableWalletStorageProvider = get()
|
||||
)
|
||||
}
|
||||
|
||||
single {
|
||||
PersistableWalletPreferenceDefault(PreferenceKey("persistable_wallet"))
|
||||
}
|
||||
|
||||
singleOf(::StandardPreferenceProvider)
|
||||
singleOf(::EncryptedPreferenceProvider)
|
||||
|
||||
single { BiometricManager.from(get()) }
|
||||
|
||||
factory { AndroidConfigurationFactory.new() }
|
||||
|
||||
singleOf(::NavigationRouterImpl) bind NavigationRouter::class
|
||||
singleOf(::HomeTabNavigationRouterImpl) bind HomeTabNavigationRouter::class
|
||||
}
|
||||
|
|
|
@ -2,10 +2,16 @@ package co.electriccoin.zcash.di
|
|||
|
||||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
||||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSourceImpl
|
||||
import co.electriccoin.zcash.ui.common.datasource.MessageAvailabilityDataSource
|
||||
import co.electriccoin.zcash.ui.common.datasource.MessageAvailabilityDataSourceImpl
|
||||
import co.electriccoin.zcash.ui.common.datasource.ProposalDataSource
|
||||
import co.electriccoin.zcash.ui.common.datasource.ProposalDataSourceImpl
|
||||
import co.electriccoin.zcash.ui.common.datasource.RestoreTimestampDataSource
|
||||
import co.electriccoin.zcash.ui.common.datasource.RestoreTimestampDataSourceImpl
|
||||
import co.electriccoin.zcash.ui.common.datasource.WalletBackupDataSource
|
||||
import co.electriccoin.zcash.ui.common.datasource.WalletBackupDataSourceImpl
|
||||
import co.electriccoin.zcash.ui.common.datasource.WalletSnapshotDataSource
|
||||
import co.electriccoin.zcash.ui.common.datasource.WalletSnapshotDataSourceImpl
|
||||
import co.electriccoin.zcash.ui.common.datasource.ZashiSpendingKeyDataSource
|
||||
import co.electriccoin.zcash.ui.common.datasource.ZashiSpendingKeyDataSourceImpl
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
|
@ -18,4 +24,7 @@ val dataSourceModule =
|
|||
singleOf(::ZashiSpendingKeyDataSourceImpl) bind ZashiSpendingKeyDataSource::class
|
||||
singleOf(::ProposalDataSourceImpl) bind ProposalDataSource::class
|
||||
singleOf(::RestoreTimestampDataSourceImpl) bind RestoreTimestampDataSource::class
|
||||
singleOf(::WalletBackupDataSourceImpl) bind WalletBackupDataSource::class
|
||||
singleOf(::MessageAvailabilityDataSourceImpl) bind MessageAvailabilityDataSource::class
|
||||
singleOf(::WalletSnapshotDataSourceImpl) bind WalletSnapshotDataSource::class
|
||||
}
|
||||
|
|
|
@ -2,32 +2,57 @@ package co.electriccoin.zcash.di
|
|||
|
||||
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProviderImpl
|
||||
import co.electriccoin.zcash.ui.common.provider.CrashReportingStorageProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.CrashReportingStorageProviderImpl
|
||||
import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.GetMonetarySeparatorProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.GetZcashCurrencyProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.PersistableWalletProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.PersistableWalletProviderImpl
|
||||
import co.electriccoin.zcash.ui.common.provider.PersistableWalletStorageProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.PersistableWalletStorageProviderImpl
|
||||
import co.electriccoin.zcash.ui.common.provider.RestoreTimestampStorageProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.RestoreTimestampStorageProviderImpl
|
||||
import co.electriccoin.zcash.ui.common.provider.SelectedAccountUUIDProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.SelectedAccountUUIDProviderImpl
|
||||
import co.electriccoin.zcash.ui.common.provider.ShieldFundsInfoProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.ShieldFundsInfoProviderImpl
|
||||
import co.electriccoin.zcash.ui.common.provider.SynchronizerProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.SynchronizerProviderImpl
|
||||
import org.koin.core.module.dsl.factoryOf
|
||||
import co.electriccoin.zcash.ui.common.provider.WalletBackupConsentStorageProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.WalletBackupConsentStorageProviderImpl
|
||||
import co.electriccoin.zcash.ui.common.provider.WalletBackupFlagStorageProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.WalletBackupFlagStorageProviderImpl
|
||||
import co.electriccoin.zcash.ui.common.provider.WalletBackupRemindMeCountStorageProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.WalletBackupRemindMeCountStorageProviderImpl
|
||||
import co.electriccoin.zcash.ui.common.provider.WalletBackupRemindMeTimestampStorageProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.WalletBackupRemindMeTimestampStorageProviderImpl
|
||||
import co.electriccoin.zcash.ui.common.provider.WalletRestoringStateProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.WalletRestoringStateProviderImpl
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
|
||||
val providerModule =
|
||||
module {
|
||||
factoryOf(::GetDefaultServersProvider)
|
||||
factoryOf(::GetVersionInfoProvider)
|
||||
factoryOf(::GetZcashCurrencyProvider)
|
||||
factoryOf(::GetMonetarySeparatorProvider)
|
||||
factoryOf(::SelectedAccountUUIDProviderImpl) bind SelectedAccountUUIDProvider::class
|
||||
singleOf(::GetDefaultServersProvider)
|
||||
singleOf(::GetVersionInfoProvider)
|
||||
singleOf(::GetZcashCurrencyProvider)
|
||||
singleOf(::GetMonetarySeparatorProvider)
|
||||
singleOf(::SelectedAccountUUIDProviderImpl) bind SelectedAccountUUIDProvider::class
|
||||
singleOf(::PersistableWalletProviderImpl) bind PersistableWalletProvider::class
|
||||
singleOf(::SynchronizerProviderImpl) bind SynchronizerProvider::class
|
||||
singleOf(::ApplicationStateProviderImpl) bind ApplicationStateProvider::class
|
||||
factoryOf(::RestoreTimestampStorageProviderImpl) bind RestoreTimestampStorageProvider::class
|
||||
singleOf(::RestoreTimestampStorageProviderImpl) bind RestoreTimestampStorageProvider::class
|
||||
singleOf(::WalletBackupRemindMeCountStorageProviderImpl) bind
|
||||
WalletBackupRemindMeCountStorageProvider::class
|
||||
singleOf(::WalletBackupRemindMeTimestampStorageProviderImpl) bind
|
||||
WalletBackupRemindMeTimestampStorageProvider::class
|
||||
singleOf(::WalletBackupFlagStorageProviderImpl) bind WalletBackupFlagStorageProvider::class
|
||||
singleOf(::WalletBackupConsentStorageProviderImpl) bind WalletBackupConsentStorageProvider::class
|
||||
singleOf(::WalletRestoringStateProviderImpl) bind WalletRestoringStateProvider::class
|
||||
singleOf(::CrashReportingStorageProviderImpl) bind CrashReportingStorageProvider::class
|
||||
singleOf(::PersistableWalletStorageProviderImpl) bind PersistableWalletStorageProvider::class
|
||||
singleOf(::ShieldFundsInfoProviderImpl) bind ShieldFundsInfoProvider::class
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package co.electriccoin.zcash.di
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.BalanceRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.BalanceRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.BiometricRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.BiometricRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.ConfigurationRepository
|
||||
|
@ -10,14 +8,20 @@ import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
|
|||
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.FlexaRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.FlexaRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.HomeMessageCacheRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.HomeMessageCacheRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.KeystoneProposalRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.ShieldFundsRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.TransactionFilterRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.TransactionFilterRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.TransactionRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.TransactionRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletSnapshotRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletSnapshotRepositoryImpl
|
||||
import co.electriccoin.zcash.ui.common.repository.ZashiProposalRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.ZashiProposalRepositoryImpl
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
|
@ -29,11 +33,13 @@ val repositoryModule =
|
|||
singleOf(::WalletRepositoryImpl) bind WalletRepository::class
|
||||
singleOf(::ConfigurationRepositoryImpl) bind ConfigurationRepository::class
|
||||
singleOf(::ExchangeRateRepositoryImpl) bind ExchangeRateRepository::class
|
||||
singleOf(::BalanceRepositoryImpl) bind BalanceRepository::class
|
||||
singleOf(::FlexaRepositoryImpl) bind FlexaRepository::class
|
||||
singleOf(::BiometricRepositoryImpl) bind BiometricRepository::class
|
||||
singleOf(::KeystoneProposalRepositoryImpl) bind KeystoneProposalRepository::class
|
||||
singleOf(::TransactionRepositoryImpl) bind TransactionRepository::class
|
||||
singleOf(::TransactionFilterRepositoryImpl) bind TransactionFilterRepository::class
|
||||
singleOf(::ZashiProposalRepositoryImpl) bind ZashiProposalRepository::class
|
||||
singleOf(::ShieldFundsRepositoryImpl) bind ShieldFundsRepository::class
|
||||
singleOf(::HomeMessageCacheRepositoryImpl) bind HomeMessageCacheRepository::class
|
||||
singleOf(::WalletSnapshotRepositoryImpl) bind WalletSnapshotRepository::class
|
||||
}
|
||||
|
|
|
@ -5,22 +5,24 @@ import co.electriccoin.zcash.ui.common.usecase.ApplyTransactionFulltextFiltersUs
|
|||
import co.electriccoin.zcash.ui.common.usecase.CancelProposalFlowUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ConfirmProposalUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CopyToClipboardUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateFlexaTransactionUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateKeystoneAccountUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateKeystoneProposalPCZTEncoderUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateKeystoneShieldProposalUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateOrUpdateTransactionNoteUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateProposalUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.CreateZip321ProposalUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.DeleteContactUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.DeleteTransactionNoteUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.DeriveKeystoneAccountUnifiedAddressUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ExportTaxUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.FlipTransactionBookmarkUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetBackupPersistableWalletUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetCoinbaseStatusUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetConfigurationUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetContactByAddressUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetCurrentFilteredTransactionsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetCurrentTransactionsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetExchangeRateUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetFlexaStatusUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetHomeMessageUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetKeystoneStatusUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetMetadataUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetProposalUseCase
|
||||
|
@ -31,42 +33,48 @@ import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase
|
|||
import co.electriccoin.zcash.ui.common.usecase.GetTransactionDetailByIdUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetTransactionFiltersUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetTransactionMetadataUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetTransactionsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletAccountsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetZashiAccountUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetZashiSpendingKeyUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.IsCoinbaseAvailableUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.IsFlexaAvailableUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.IsRestoreSuccessDialogVisibleUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.MarkTxMemoAsReadUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToAddressBookUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToErrorUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToTaxExportUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToWalletBackupUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveClearSendUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveContactByAddressUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveFastestServersUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveOnAccountChangedUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObservePersistableWalletUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveProposalUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedEndpointUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedWalletAccountUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveTransactionSubmitStateUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveWalletAccountsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveWalletStateUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveZashiAccountUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.OnAddressScannedUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.OnUserSavedWalletBackupUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.OnZip321ScannedUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ParseKeystonePCZTUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ParseKeystoneSignInRequestUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ParseKeystoneUrToZashiAccountsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.PersistEndpointUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.PrefillSendUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.RefreshFastestServersUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.RemindWalletBackupLaterUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.RescanQrUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ResetInMemoryDataUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ResetSharedPrefsDataUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ResetTransactionFiltersUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.RestoreWalletUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.SaveContactUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.SelectWalletAccountUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.SendEmailUseCase
|
||||
|
@ -74,10 +82,13 @@ import co.electriccoin.zcash.ui.common.usecase.SendSupportEmailUseCase
|
|||
import co.electriccoin.zcash.ui.common.usecase.SendTransactionAgainUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ShareImageUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.SharePCZTUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ShieldFundsMessageUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ShieldFundsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.UpdateContactUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ValidateContactAddressUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ValidateContactNameUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ValidateEndpointUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ValidateSeedUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ViewTransactionDetailAfterSuccessfulProposalUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ViewTransactionsAfterSuccessfulProposalUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.Zip321BuildUriUseCase
|
||||
|
@ -99,7 +110,7 @@ val useCaseModule =
|
|||
factoryOf(::ValidateEndpointUseCase)
|
||||
factoryOf(::GetPersistableWalletUseCase)
|
||||
factoryOf(::GetSelectedEndpointUseCase)
|
||||
factoryOf(::ObserveConfigurationUseCase)
|
||||
factoryOf(::GetConfigurationUseCase)
|
||||
factoryOf(::RescanBlockchainUseCase)
|
||||
factoryOf(::GetTransparentAddressUseCase)
|
||||
factoryOf(::ValidateContactAddressUseCase)
|
||||
|
@ -114,16 +125,13 @@ val useCaseModule =
|
|||
factoryOf(::ShareImageUseCase)
|
||||
factoryOf(::Zip321BuildUriUseCase)
|
||||
factoryOf(::Zip321ParseUriValidationUseCase)
|
||||
factoryOf(::ObserveWalletStateUseCase)
|
||||
factoryOf(::IsCoinbaseAvailableUseCase)
|
||||
factoryOf(::GetZashiSpendingKeyUseCase)
|
||||
factoryOf(::ObservePersistableWalletUseCase)
|
||||
factoryOf(::GetBackupPersistableWalletUseCase)
|
||||
factoryOf(::GetSupportUseCase)
|
||||
factoryOf(::SendEmailUseCase)
|
||||
factoryOf(::SendSupportEmailUseCase)
|
||||
factoryOf(::IsFlexaAvailableUseCase)
|
||||
factoryOf(::ObserveWalletAccountsUseCase)
|
||||
factoryOf(::GetWalletAccountsUseCase)
|
||||
factoryOf(::SelectWalletAccountUseCase)
|
||||
factoryOf(::ObserveSelectedWalletAccountUseCase)
|
||||
factoryOf(::ObserveZashiAccountUseCase)
|
||||
|
@ -135,18 +143,17 @@ val useCaseModule =
|
|||
factoryOf(::GetSelectedWalletAccountUseCase)
|
||||
singleOf(::ObserveClearSendUseCase)
|
||||
singleOf(::PrefillSendUseCase)
|
||||
factoryOf(::GetCurrentTransactionsUseCase)
|
||||
factoryOf(::GetTransactionsUseCase)
|
||||
factoryOf(::GetCurrentFilteredTransactionsUseCase) onClose ::closeableCallback
|
||||
factoryOf(::CreateProposalUseCase)
|
||||
factoryOf(::CreateZip321ProposalUseCase)
|
||||
factoryOf(::CreateKeystoneShieldProposalUseCase)
|
||||
factoryOf(::OnZip321ScannedUseCase)
|
||||
factoryOf(::OnAddressScannedUseCase)
|
||||
factoryOf(::ParseKeystonePCZTUseCase)
|
||||
factoryOf(::ParseKeystoneSignInRequestUseCase)
|
||||
factoryOf(::CancelProposalFlowUseCase)
|
||||
factoryOf(::ObserveProposalUseCase)
|
||||
factoryOf(::SharePCZTUseCase)
|
||||
factoryOf(::CreateKeystoneProposalPCZTEncoderUseCase)
|
||||
factoryOf(::ObserveOnAccountChangedUseCase)
|
||||
factoryOf(::ViewTransactionsAfterSuccessfulProposalUseCase)
|
||||
factoryOf(::ViewTransactionDetailAfterSuccessfulProposalUseCase)
|
||||
factoryOf(::NavigateToCoinbaseUseCase)
|
||||
|
@ -172,4 +179,19 @@ val useCaseModule =
|
|||
factoryOf(::GetMetadataUseCase)
|
||||
factoryOf(::ExportTaxUseCase)
|
||||
factoryOf(::NavigateToTaxExportUseCase)
|
||||
factoryOf(::CreateFlexaTransactionUseCase)
|
||||
factoryOf(::IsRestoreSuccessDialogVisibleUseCase)
|
||||
factoryOf(::ValidateSeedUseCase)
|
||||
factoryOf(::RestoreWalletUseCase)
|
||||
factoryOf(::NavigateToWalletBackupUseCase)
|
||||
factoryOf(::GetKeystoneStatusUseCase)
|
||||
factoryOf(::GetCoinbaseStatusUseCase)
|
||||
factoryOf(::GetFlexaStatusUseCase)
|
||||
singleOf(::GetHomeMessageUseCase)
|
||||
factoryOf(::OnUserSavedWalletBackupUseCase)
|
||||
factoryOf(::RemindWalletBackupLaterUseCase)
|
||||
singleOf(::ShieldFundsUseCase)
|
||||
singleOf(::NavigateToErrorUseCase)
|
||||
factoryOf(::RescanQrUseCase)
|
||||
factoryOf(::ShieldFundsMessageUseCase)
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue