Merge pull request #1819 from Electric-Coin-Company/feature/restore-redesign
Restore redesign
This commit is contained in:
commit
8842ee10d2
|
@ -19,7 +19,7 @@ class MergingConfigurationProvider(
|
|||
|
||||
override fun getConfigurationFlow(): Flow<Configuration> {
|
||||
return if (configurationProviders.isEmpty()) {
|
||||
flowOf(MergingConfiguration(persistentListOf<Configuration>()))
|
||||
flowOf(MergingConfiguration(persistentListOf()))
|
||||
} else {
|
||||
combine(configurationProviders.map { it.getConfigurationFlow() }) { configurations ->
|
||||
MergingConfiguration(configurations.toList().toPersistentList())
|
||||
|
|
|
@ -1,41 +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")
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
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 {
|
||||
return 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) }
|
||||
|
||||
|
@ -114,8 +118,8 @@ fun LabeledCheckBox(
|
|||
)
|
||||
Text(
|
||||
text = AnnotatedString(text),
|
||||
color = ZcashTheme.colors.textPrimary,
|
||||
style = ZcashTheme.extendedTypography.checkboxText
|
||||
color = color,
|
||||
style = style
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,144 +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
|
||||
}
|
|
@ -67,13 +67,13 @@ 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,
|
||||
style: TextStyle = ZashiButtonDefaults.style,
|
||||
shape: Shape = ZashiButtonDefaults.shape,
|
||||
contentPadding: PaddingValues = ZashiButtonDefaults.contentPadding,
|
||||
colors: ZashiButtonColors = ZashiButtonDefaults.primaryColors(),
|
||||
content: @Composable RowScope.(ZashiButtonScope) -> Unit = ZashiButtonDefaults.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) {
|
||||
|
|
|
@ -90,7 +90,7 @@ fun rememberModalBottomSheetState(
|
|||
|
||||
@Composable
|
||||
@ExperimentalMaterial3Api
|
||||
private fun rememberSheetState(
|
||||
fun rememberSheetState(
|
||||
skipPartiallyExpanded: Boolean,
|
||||
confirmValueChange: (SheetValue) -> Boolean,
|
||||
initialValue: SheetValue,
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package co.electriccoin.zcash.ui.design.component
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.SheetValue.Hidden
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import co.electriccoin.zcash.ui.design.LocalSheetStateManager
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun <T : ModalBottomSheetState> ZashiScreenModalBottomSheet(
|
||||
state: T?,
|
||||
sheetState: SheetState = rememberScreenModalBottomSheetState(),
|
||||
content: @Composable () -> Unit = {},
|
||||
) {
|
||||
ZashiModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
content = {
|
||||
BackHandler(state != null) {
|
||||
state?.onBack?.invoke()
|
||||
}
|
||||
content()
|
||||
},
|
||||
onDismissRequest = { state?.onBack?.invoke() }
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
sheetState.show()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ExperimentalMaterial3Api
|
||||
fun rememberScreenModalBottomSheetState(
|
||||
initialValue: SheetValue = Hidden,
|
||||
skipHiddenState: Boolean = false,
|
||||
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,172 @@
|
|||
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,289 @@
|
|||
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,101 @@
|
|||
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,30 @@
|
|||
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 VerticalSpacer(height: Dp) {
|
||||
Spacer(Modifier.height(height))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ColumnScope.VerticalSpacer(weight: Float) {
|
||||
Spacer(Modifier.weight(weight))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RowScope.VerticalSpacer(weight: Float) {
|
||||
Spacer(Modifier.weight(weight))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HorizontalSpacer(width: Dp) {
|
||||
Spacer(Modifier.width(width))
|
||||
}
|
|
@ -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,39 @@ 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 +231,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 +265,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,7 +305,7 @@ private fun TextFieldInternal(
|
|||
singleLine = singleLine,
|
||||
maxLines = maxLines,
|
||||
minLines = minLines,
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
) { innerTextField: @Composable () -> Unit ->
|
||||
// places leading icon, text field with label and placeholder, trailing icon
|
||||
TextFieldDefaults.DecorationBox(
|
||||
value = state.value.getValue(),
|
||||
|
@ -243,16 +333,9 @@ private fun TextFieldInternal(
|
|||
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),
|
||||
)
|
||||
contentPadding = contentPadding
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (state.error != null && state.error.getValue().isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
|
@ -303,7 +386,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 +402,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 +434,7 @@ data class ZashiTextFieldColors(
|
|||
unfocusedTextColor = textColor,
|
||||
disabledTextColor = disabledTextColor,
|
||||
errorTextColor = errorTextColor,
|
||||
focusedContainerColor = containerColor,
|
||||
focusedContainerColor = focusedContainerColor.takeOrElse { containerColor },
|
||||
unfocusedContainerColor = containerColor,
|
||||
disabledContainerColor = disabledContainerColor,
|
||||
errorContainerColor = errorContainerColor,
|
||||
|
@ -391,13 +480,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 +510,9 @@ object ZashiTextFieldDefaults {
|
|||
textColor = textColor,
|
||||
hintColor = hintColor,
|
||||
borderColor = borderColor,
|
||||
focusedBorderColor = focusedBorderColor,
|
||||
containerColor = containerColor,
|
||||
focusedContainerColor = focusedContainerColor,
|
||||
placeholderColor = placeholderColor,
|
||||
disabledTextColor = disabledTextColor,
|
||||
disabledHintColor = disabledHintColor,
|
||||
|
@ -428,6 +527,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,292 @@
|
|||
package co.electriccoin.zcash.ui.design.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.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.pow
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
fun ZashiYearMonthWheelDatePicker(
|
||||
modifier: Modifier = Modifier,
|
||||
verticallyVisibleItems: Int = 3,
|
||||
startYear: Year = Year.of(2016),
|
||||
endYear: Year = Year.now(),
|
||||
selectedYear: YearMonth = YearMonth.now(),
|
||||
onSelectionChanged: (YearMonth) -> Unit,
|
||||
) {
|
||||
val latestOnSelectionChanged by rememberUpdatedState(onSelectionChanged)
|
||||
var selectedDate by remember { mutableStateOf(selectedYear) }
|
||||
val months =
|
||||
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
|
||||
)
|
||||
val years = (startYear.value..endYear.value).toList()
|
||||
|
||||
LaunchedEffect(selectedDate) {
|
||||
Twig.debug { "Selection changed: $selectedDate" }
|
||||
latestOnSelectionChanged(selectedDate)
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.Center),
|
||||
) {
|
||||
ZashiHorizontalDivider(color = ZashiColors.Surfaces.bgQuaternary, thickness = .5.dp)
|
||||
VerticalSpacer(31.dp)
|
||||
ZashiHorizontalDivider(color = ZashiColors.Surfaces.bgQuaternary, thickness = .5.dp)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Spacer(Modifier.weight(.5f))
|
||||
WheelLazyList(
|
||||
modifier = Modifier.weight(1f),
|
||||
selection = maxOf(months.indexOf(selectedDate.month), 0),
|
||||
itemCount = months.size,
|
||||
itemVerticalOffset = verticallyVisibleItems,
|
||||
isInfiniteScroll = true,
|
||||
onFocusItem = { selectedDate = selectedDate.withMonth(months[it].value) },
|
||||
itemContent = {
|
||||
Text(
|
||||
text = DateFormatSymbols().months[months[it].value - 1],
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillParentMaxWidth(),
|
||||
style = ZashiTypography.header6,
|
||||
color = ZashiColors.Text.textPrimary,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
)
|
||||
WheelLazyList(
|
||||
modifier = Modifier.weight(.75f),
|
||||
selection = years.indexOf(selectedDate.year),
|
||||
itemCount = years.size,
|
||||
itemVerticalOffset = verticallyVisibleItems,
|
||||
isInfiniteScroll = false,
|
||||
onFocusItem = { selectedDate = selectedDate.withYear(years[it]) },
|
||||
itemContent = {
|
||||
Text(
|
||||
text = years[it].toString(),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillParentMaxWidth(),
|
||||
style = ZashiTypography.header6,
|
||||
color = ZashiColors.Text.textPrimary,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
)
|
||||
Spacer(Modifier.weight(.5f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun WheelLazyList(
|
||||
itemCount: Int,
|
||||
selection: Int,
|
||||
itemVerticalOffset: Int,
|
||||
onFocusItem: (Int) -> Unit,
|
||||
isInfiniteScroll: Boolean,
|
||||
itemContent: @Composable LazyItemScope.(index: Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
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) {
|
||||
coroutineScope.launch {
|
||||
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 {
|
||||
return coefficient.coerceAtLeast(.6f)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun calculateAlpha(coefficient: Float): Float {
|
||||
return 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
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -551,8 +551,8 @@ 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`,
|
||||
utilityEspresso900 = Espresso.`50`,
|
||||
|
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ 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.screen.restore.seed.RestoreSeedTag
|
||||
import co.electriccoin.zcash.ui.test.getAppContext
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
import org.junit.Assert.assertEquals
|
||||
|
@ -55,7 +55,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
|
|||
|
||||
composeTestRule.waitForIdle()
|
||||
|
||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
||||
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||
it.assertIsFocused()
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
|
|||
SeedPhraseFixture.SEED_PHRASE + " " + SeedPhraseFixture.SEED_PHRASE
|
||||
)
|
||||
|
||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
||||
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||
it.performKeyInput {
|
||||
withKeyDown(Key.CtrlLeft) {
|
||||
pressKey(Key.V)
|
||||
|
@ -94,11 +94,11 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
|
|||
|
||||
assertEquals(SeedPhrase.SEED_PHRASE_SIZE, testSetup.getUserInputWords().size)
|
||||
|
||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
||||
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also {
|
||||
composeTestRule.onNodeWithTag(RestoreSeedTag.AUTOCOMPLETE_LAYOUT).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
|
||||
|
@ -116,7 +116,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
|
|||
composeTestRule.waitForIdle()
|
||||
|
||||
// Insert uncompleted seed words
|
||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
||||
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||
it.performTextInput("test")
|
||||
}
|
||||
|
||||
|
@ -139,7 +139,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
|
|||
composeTestRule.waitForIdle()
|
||||
|
||||
// Insert complete seed words
|
||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
||||
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||
it.performTextInput(SeedPhraseFixture.SEED_PHRASE)
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@ 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.seed.RestoreSeedState
|
||||
import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedView
|
||||
import co.electriccoin.zcash.ui.screen.restore.state.WordList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
@ -43,9 +44,9 @@ class RestoreViewSecuredScreenTest : UiTestPrerequisites() {
|
|||
composeTestRule.setContent {
|
||||
CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) {
|
||||
ZcashTheme {
|
||||
RestoreWallet(
|
||||
RestoreSeedView(
|
||||
ZcashNetwork.Mainnet,
|
||||
RestoreState(),
|
||||
RestoreSeedState(),
|
||||
Mnemonics.getCachedWords(Locale.ENGLISH.language).toPersistentSet(),
|
||||
WordList(emptyList()),
|
||||
restoreHeight = null,
|
||||
|
|
|
@ -25,9 +25,10 @@ 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.seed.RestoreSeedState
|
||||
import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedTag
|
||||
import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedView
|
||||
import co.electriccoin.zcash.ui.screen.restore.state.WordList
|
||||
import co.electriccoin.zcash.ui.test.getStringResource
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
|
@ -54,7 +55,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
|||
fun seed_autocomplete_suggestions_appear() {
|
||||
newTestSetup()
|
||||
|
||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
||||
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||
it.performTextInput("ab")
|
||||
|
||||
// Make sure text isn't cleared
|
||||
|
@ -62,13 +63,13 @@ class RestoreViewTest : UiTestPrerequisites() {
|
|||
}
|
||||
|
||||
composeTestRule.onNode(
|
||||
matcher = hasText("abandon", substring = true) and hasTestTag(RestoreTag.AUTOCOMPLETE_ITEM)
|
||||
matcher = hasText("abandon", substring = true) and hasTestTag(RestoreSeedTag.AUTOCOMPLETE_ITEM)
|
||||
).also {
|
||||
it.assertExists()
|
||||
}
|
||||
|
||||
composeTestRule.onNode(
|
||||
matcher = hasText("able", substring = true) and hasTestTag(RestoreTag.AUTOCOMPLETE_ITEM)
|
||||
matcher = hasText("able", substring = true) and hasTestTag(RestoreSeedTag.AUTOCOMPLETE_ITEM)
|
||||
).also {
|
||||
it.assertExists()
|
||||
}
|
||||
|
@ -79,17 +80,17 @@ class RestoreViewTest : UiTestPrerequisites() {
|
|||
fun seed_choose_autocomplete() {
|
||||
newTestSetup()
|
||||
|
||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
||||
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||
it.performTextInput("ab")
|
||||
}
|
||||
|
||||
composeTestRule.onNode(
|
||||
matcher = hasText("abandon", substring = true) and hasTestTag(RestoreTag.AUTOCOMPLETE_ITEM)
|
||||
matcher = hasText("abandon", substring = true) and hasTestTag(RestoreSeedTag.AUTOCOMPLETE_ITEM)
|
||||
).also {
|
||||
it.performClick()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also {
|
||||
composeTestRule.onNodeWithTag(RestoreSeedTag.AUTOCOMPLETE_LAYOUT).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
|
||||
|
@ -97,7 +98,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
|||
it.assertExists()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
||||
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||
it.assertTextEquals("abandon ", includeEditableText = true)
|
||||
}
|
||||
}
|
||||
|
@ -107,15 +108,15 @@ class RestoreViewTest : UiTestPrerequisites() {
|
|||
fun seed_type_full_word() {
|
||||
newTestSetup()
|
||||
|
||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
||||
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||
it.performTextInput("abandon")
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
||||
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||
it.assertTextEquals("abandon ", includeEditableText = true)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also {
|
||||
composeTestRule.onNodeWithTag(RestoreSeedTag.AUTOCOMPLETE_LAYOUT).also {
|
||||
it.assertDoesNotExist()
|
||||
}
|
||||
|
||||
|
@ -209,7 +210,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
|||
initialWordsList = SeedPhraseFixture.new().split
|
||||
)
|
||||
|
||||
composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also {
|
||||
composeTestRule.onNodeWithTag(RestoreSeedTag.BIRTHDAY_TEXT_FIELD).also {
|
||||
it.performTextInput(ZcashNetwork.Mainnet.saplingActivationHeight.value.toString())
|
||||
}
|
||||
|
||||
|
@ -241,7 +242,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
|||
it.assertIsEnabled()
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also {
|
||||
composeTestRule.onNodeWithTag(RestoreSeedTag.BIRTHDAY_TEXT_FIELD).also {
|
||||
it.performTextInput((ZcashNetwork.Mainnet.saplingActivationHeight.value - 1L).toString())
|
||||
}
|
||||
|
||||
|
@ -266,7 +267,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
|||
initialWordsList = SeedPhraseFixture.new().split
|
||||
)
|
||||
|
||||
composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also {
|
||||
composeTestRule.onNodeWithTag(RestoreSeedTag.BIRTHDAY_TEXT_FIELD).also {
|
||||
it.performTextInput("1.2")
|
||||
}
|
||||
|
||||
|
@ -353,7 +354,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
|||
initialStage: RestoreStage,
|
||||
initialWordsList: List<String>
|
||||
) {
|
||||
private val state = RestoreState(initialStage)
|
||||
private val state = RestoreSeedState(initialStage)
|
||||
|
||||
private val wordList = WordList(initialWordsList)
|
||||
|
||||
|
@ -391,7 +392,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
|||
init {
|
||||
composeTestRule.setContent {
|
||||
ZcashTheme {
|
||||
RestoreWallet(
|
||||
RestoreSeedView(
|
||||
ZcashNetwork.Mainnet,
|
||||
state,
|
||||
Mnemonics.getCachedWords(Locale.ENGLISH.language).toPersistentSet(),
|
||||
|
|
|
@ -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,56 +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()
|
||||
},
|
||||
onAcknowledged = {
|
||||
onAcknowledged.getAndSet(it)
|
||||
},
|
||||
onConfirm = {
|
||||
onConfirmCount.incrementAndGet()
|
||||
},
|
||||
versionInfo = VersionInfoFixture.new()
|
||||
)
|
||||
}
|
||||
|
||||
fun setDefaultContent() {
|
||||
composeTestRule.setContent {
|
||||
ZcashTheme {
|
||||
DefaultContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,11 +16,14 @@ 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.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
|
||||
|
@ -32,6 +35,7 @@ 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.GetTransparentAddressUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletAccountsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletStateInformationUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetZashiAccountUseCase
|
||||
|
@ -42,10 +46,10 @@ import co.electriccoin.zcash.ui.common.usecase.IsRestoreSuccessDialogVisibleUseC
|
|||
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.NavigateToSeedRecoveryUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToTaxExportUseCase
|
||||
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
|
||||
|
@ -56,7 +60,6 @@ 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.ObserveZashiAccountUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.OnAddressScannedUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.OnZip321ScannedUseCase
|
||||
|
@ -70,6 +73,7 @@ import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase
|
|||
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
|
||||
|
@ -81,6 +85,7 @@ 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
|
||||
|
@ -102,7 +107,7 @@ val useCaseModule =
|
|||
factoryOf(::ValidateEndpointUseCase)
|
||||
factoryOf(::GetPersistableWalletUseCase)
|
||||
factoryOf(::GetSelectedEndpointUseCase)
|
||||
factoryOf(::ObserveConfigurationUseCase)
|
||||
factoryOf(::GetConfigurationUseCase)
|
||||
factoryOf(::RescanBlockchainUseCase)
|
||||
factoryOf(::GetTransparentAddressUseCase)
|
||||
factoryOf(::ValidateContactAddressUseCase)
|
||||
|
@ -121,12 +126,11 @@ val useCaseModule =
|
|||
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)
|
||||
|
@ -178,4 +182,10 @@ val useCaseModule =
|
|||
factoryOf(::NavigateToTaxExportUseCase)
|
||||
factoryOf(::CreateFlexaTransactionUseCase)
|
||||
factoryOf(::IsRestoreSuccessDialogVisibleUseCase)
|
||||
factoryOf(::ValidateSeedUseCase)
|
||||
factoryOf(::RestoreWalletUseCase)
|
||||
factoryOf(::NavigateToSeedRecoveryUseCase)
|
||||
factoryOf(::GetKeystoneStatusUseCase)
|
||||
factoryOf(::GetCoinbaseStatusUseCase)
|
||||
factoryOf(::GetFlexaStatusUseCase)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
|||
import co.electriccoin.zcash.ui.screen.accountlist.viewmodel.AccountListViewModel
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.AddressBookViewModel
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.SelectRecipientViewModel
|
||||
import co.electriccoin.zcash.ui.screen.advancedsettings.viewmodel.AdvancedSettingsViewModel
|
||||
import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsViewModel
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalanceViewModel
|
||||
import co.electriccoin.zcash.ui.screen.chooseserver.ChooseServerViewModel
|
||||
import co.electriccoin.zcash.ui.screen.contact.viewmodel.AddContactViewModel
|
||||
|
@ -15,20 +15,21 @@ import co.electriccoin.zcash.ui.screen.contact.viewmodel.UpdateContactViewModel
|
|||
import co.electriccoin.zcash.ui.screen.feedback.viewmodel.FeedbackViewModel
|
||||
import co.electriccoin.zcash.ui.screen.flexa.FlexaViewModel
|
||||
import co.electriccoin.zcash.ui.screen.home.HomeViewModel
|
||||
import co.electriccoin.zcash.ui.screen.integrations.viewmodel.IntegrationsViewModel
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
||||
import co.electriccoin.zcash.ui.screen.integrations.IntegrationsViewModel
|
||||
import co.electriccoin.zcash.ui.screen.qrcode.viewmodel.QrCodeViewModel
|
||||
import co.electriccoin.zcash.ui.screen.receive.viewmodel.ReceiveViewModel
|
||||
import co.electriccoin.zcash.ui.screen.request.viewmodel.RequestViewModel
|
||||
import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
|
||||
import co.electriccoin.zcash.ui.screen.restore.date.RestoreBDDateViewModel
|
||||
import co.electriccoin.zcash.ui.screen.restore.estimation.RestoreBDEstimationViewModel
|
||||
import co.electriccoin.zcash.ui.screen.restore.height.RestoreBDHeightViewModel
|
||||
import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedViewModel
|
||||
import co.electriccoin.zcash.ui.screen.restoresuccess.viewmodel.RestoreSuccessViewModel
|
||||
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewTransactionViewModel
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import co.electriccoin.zcash.ui.screen.scan.viewmodel.ScanViewModel
|
||||
import co.electriccoin.zcash.ui.screen.scankeystone.viewmodel.ScanKeystonePCZTViewModel
|
||||
import co.electriccoin.zcash.ui.screen.scankeystone.viewmodel.ScanKeystoneSignInRequestViewModel
|
||||
import co.electriccoin.zcash.ui.screen.seed.SeedNavigationArgs
|
||||
import co.electriccoin.zcash.ui.screen.seed.viewmodel.SeedViewModel
|
||||
import co.electriccoin.zcash.ui.screen.seed.SeedRecoveryViewModel
|
||||
import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.SelectKeystoneAccount
|
||||
import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.viewmodel.SelectKeystoneAccountViewModel
|
||||
import co.electriccoin.zcash.ui.screen.send.SendViewModel
|
||||
|
@ -57,9 +58,8 @@ val viewModelModule =
|
|||
viewModelOf(::WalletViewModel)
|
||||
viewModelOf(::AuthenticationViewModel)
|
||||
viewModelOf(::OldHomeViewModel)
|
||||
viewModelOf(::OnboardingViewModel)
|
||||
viewModelOf(::StorageCheckViewModel)
|
||||
viewModelOf(::RestoreViewModel)
|
||||
viewModelOf(::RestoreSeedViewModel)
|
||||
viewModelOf(::ScreenBrightnessViewModel)
|
||||
viewModelOf(::SettingsViewModel)
|
||||
viewModelOf(::AdvancedSettingsViewModel)
|
||||
|
@ -92,27 +92,10 @@ val viewModelModule =
|
|||
}
|
||||
viewModelOf(::ScanKeystoneSignInRequestViewModel)
|
||||
viewModelOf(::ScanKeystonePCZTViewModel)
|
||||
viewModel { (isDialog: Boolean) ->
|
||||
IntegrationsViewModel(
|
||||
isDialog = isDialog,
|
||||
getZcashCurrency = get(),
|
||||
isFlexaAvailableUseCase = get(),
|
||||
isCoinbaseAvailable = get(),
|
||||
observeWalletAccounts = get(),
|
||||
navigationRouter = get(),
|
||||
navigateToCoinbase = get(),
|
||||
getWalletRestoringState = get()
|
||||
)
|
||||
}
|
||||
viewModelOf(::IntegrationsViewModel)
|
||||
viewModelOf(::FlexaViewModel)
|
||||
viewModelOf(::SendViewModel)
|
||||
viewModel { (args: SeedNavigationArgs) ->
|
||||
SeedViewModel(
|
||||
observePersistableWallet = get(),
|
||||
args = args,
|
||||
walletRepository = get(),
|
||||
)
|
||||
}
|
||||
viewModelOf(::SeedRecoveryViewModel)
|
||||
viewModelOf(::FeedbackViewModel)
|
||||
viewModelOf(::SignKeystoneTransactionViewModel)
|
||||
viewModelOf(::AccountListViewModel)
|
||||
|
@ -156,4 +139,7 @@ val viewModelModule =
|
|||
viewModelOf(::TaxExportViewModel)
|
||||
viewModelOf(::BalanceViewModel)
|
||||
viewModelOf(::HomeViewModel)
|
||||
viewModelOf(::RestoreBDHeightViewModel)
|
||||
viewModelOf(::RestoreBDDateViewModel)
|
||||
viewModelOf(::RestoreBDEstimationViewModel)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -23,22 +22,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||
import cash.z.ecc.android.sdk.model.ZcashNetwork
|
||||
import cash.z.ecc.sdk.type.fromResources
|
||||
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.common.compose.BindCompLocalProvider
|
||||
import co.electriccoin.zcash.ui.common.extension.setContentCompat
|
||||
import co.electriccoin.zcash.ui.common.model.OnboardingState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationUIState
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.OldHomeViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.SecretState
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.configuration.RemoteConfig
|
||||
import co.electriccoin.zcash.ui.design.component.BlankSurface
|
||||
import co.electriccoin.zcash.ui.design.component.ConfigurationOverride
|
||||
import co.electriccoin.zcash.ui.design.component.Override
|
||||
|
@ -48,11 +39,7 @@ import co.electriccoin.zcash.ui.screen.authentication.RETRY_TRIGGER_DELAY
|
|||
import co.electriccoin.zcash.ui.screen.authentication.WrapAuthentication
|
||||
import co.electriccoin.zcash.ui.screen.authentication.view.AnimationConstants
|
||||
import co.electriccoin.zcash.ui.screen.authentication.view.WelcomeAnimationAutostart
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.WrapOnboarding
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.persistExistingWalletWithSeedPhrase
|
||||
import co.electriccoin.zcash.ui.screen.securitywarning.WrapSecurityWarning
|
||||
import co.electriccoin.zcash.ui.screen.seed.SeedNavigationArgs
|
||||
import co.electriccoin.zcash.ui.screen.seed.WrapSeed
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.OnboardingNavigation
|
||||
import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel
|
||||
import co.electriccoin.zcash.work.WorkIds
|
||||
import kotlinx.coroutines.delay
|
||||
|
@ -131,8 +118,7 @@ class MainActivity : FragmentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
// Note this condition needs to be kept in sync with the condition in MainContent()
|
||||
oldHomeViewModel.configurationFlow.value == null || SecretState.Loading == walletViewModel.secretState.value
|
||||
SecretState.LOADING == walletViewModel.secretState.value
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -235,58 +221,20 @@ class MainActivity : FragmentActivity() {
|
|||
|
||||
@Composable
|
||||
private fun MainContent() {
|
||||
val configuration = oldHomeViewModel.configurationFlow.collectAsStateWithLifecycle().value
|
||||
val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value
|
||||
val secretState by walletViewModel.secretState.collectAsStateWithLifecycle()
|
||||
|
||||
// Note this condition needs to be kept in sync with the condition in setupSplashScreen()
|
||||
if (null == configuration || secretState == SecretState.Loading) {
|
||||
// For now, keep displaying splash screen using condition above.
|
||||
// In the future, we might consider displaying something different here.
|
||||
} else {
|
||||
// Note that the deeply nested child views will probably receive arguments derived from
|
||||
// the configuration. The CompositionLocalProvider is helpful for passing the configuration
|
||||
// to the "platform" layer, which is where the arguments will be derived from.
|
||||
CompositionLocalProvider(RemoteConfig provides configuration) {
|
||||
when (secretState) {
|
||||
SecretState.None -> {
|
||||
WrapOnboarding()
|
||||
SecretState.NONE -> {
|
||||
OnboardingNavigation()
|
||||
}
|
||||
|
||||
is SecretState.NeedsWarning -> {
|
||||
WrapSecurityWarning(
|
||||
onBack = { walletViewModel.persistOnboardingState(OnboardingState.NONE) },
|
||||
onConfirm = {
|
||||
walletViewModel.persistOnboardingState(OnboardingState.NEEDS_BACKUP)
|
||||
|
||||
if (FirebaseTestLabUtil.isFirebaseTestLab(applicationContext)) {
|
||||
persistExistingWalletWithSeedPhrase(
|
||||
applicationContext,
|
||||
walletViewModel,
|
||||
SeedPhrase.new(WalletFixture.Alice.seedPhrase),
|
||||
WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(applicationContext))
|
||||
)
|
||||
} else {
|
||||
walletViewModel.persistNewWalletAndRestoringState(WalletRestoringState.INITIATING)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is SecretState.NeedsBackup -> {
|
||||
WrapSeed(
|
||||
args = SeedNavigationArgs.NEW_WALLET,
|
||||
goBackOverride = null
|
||||
)
|
||||
}
|
||||
|
||||
is SecretState.Ready -> {
|
||||
SecretState.READY -> {
|
||||
Navigation()
|
||||
}
|
||||
|
||||
else -> {
|
||||
error("Unhandled secret state: $secretState")
|
||||
}
|
||||
}
|
||||
SecretState.LOADING -> {
|
||||
// For now, keep displaying splash screen using condition above.
|
||||
// In the future, we might consider displaying something different here.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -294,7 +242,7 @@ class MainActivity : FragmentActivity() {
|
|||
private fun monitorForBackgroundSync() {
|
||||
val isEnableBackgroundSyncFlow =
|
||||
run {
|
||||
val isSecretReadyFlow = walletViewModel.secretState.map { it is SecretState.Ready }
|
||||
val isSecretReadyFlow = walletViewModel.secretState.map { it == SecretState.READY }
|
||||
val isBackgroundSyncEnabledFlow = oldHomeViewModel.isBackgroundSyncEnabled.filterNotNull()
|
||||
|
||||
isSecretReadyFlow.combine(isBackgroundSyncEnabledFlow) { isSecretReady, isBackgroundSyncEnabled ->
|
||||
|
|
|
@ -31,7 +31,6 @@ import co.electriccoin.zcash.ui.NavigationTargets.EXPORT_PRIVATE_DATA
|
|||
import co.electriccoin.zcash.ui.NavigationTargets.NOT_ENOUGH_SPACE
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.QR_CODE
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.REQUEST
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.SEED_RECOVERY
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS_EXCHANGE_RATE_OPT_IN
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT
|
||||
|
@ -39,6 +38,8 @@ import co.electriccoin.zcash.ui.NavigationTargets.WHATS_NEW
|
|||
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
||||
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider
|
||||
import co.electriccoin.zcash.ui.common.provider.isInForeground
|
||||
import co.electriccoin.zcash.ui.design.LocalKeyboardManager
|
||||
import co.electriccoin.zcash.ui.design.LocalSheetStateManager
|
||||
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.enterTransition
|
||||
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.exitTransition
|
||||
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.popEnterTransition
|
||||
|
@ -76,6 +77,8 @@ import co.electriccoin.zcash.ui.screen.receive.AndroidReceive
|
|||
import co.electriccoin.zcash.ui.screen.receive.Receive
|
||||
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType
|
||||
import co.electriccoin.zcash.ui.screen.request.WrapRequest
|
||||
import co.electriccoin.zcash.ui.screen.restore.info.AndroidSeedInfo
|
||||
import co.electriccoin.zcash.ui.screen.restore.info.SeedInfo
|
||||
import co.electriccoin.zcash.ui.screen.reviewtransaction.AndroidReviewTransaction
|
||||
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewTransaction
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
|
@ -84,8 +87,10 @@ import co.electriccoin.zcash.ui.screen.scankeystone.ScanKeystonePCZTRequest
|
|||
import co.electriccoin.zcash.ui.screen.scankeystone.ScanKeystoneSignInRequest
|
||||
import co.electriccoin.zcash.ui.screen.scankeystone.WrapScanKeystonePCZTRequest
|
||||
import co.electriccoin.zcash.ui.screen.scankeystone.WrapScanKeystoneSignInRequest
|
||||
import co.electriccoin.zcash.ui.screen.seed.SeedNavigationArgs
|
||||
import co.electriccoin.zcash.ui.screen.seed.WrapSeed
|
||||
import co.electriccoin.zcash.ui.screen.seed.AndroidSeedRecovery
|
||||
import co.electriccoin.zcash.ui.screen.seed.SeedRecovery
|
||||
import co.electriccoin.zcash.ui.screen.seed.backup.AndroidSeedBackup
|
||||
import co.electriccoin.zcash.ui.screen.seed.backup.SeedBackup
|
||||
import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.AndroidSelectKeystoneAccount
|
||||
import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.SelectKeystoneAccount
|
||||
import co.electriccoin.zcash.ui.screen.send.Send
|
||||
|
@ -120,18 +125,32 @@ import org.koin.compose.koinInject
|
|||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
internal fun MainActivity.Navigation() {
|
||||
val navController = LocalNavController.current
|
||||
val keyboardManager = LocalKeyboardManager.current
|
||||
val flexaViewModel = koinViewModel<FlexaViewModel>()
|
||||
val navigationRouter = koinInject<NavigationRouter>()
|
||||
val sheetStateManager = LocalSheetStateManager.current
|
||||
|
||||
// Helper properties for triggering the system security UI from callbacks
|
||||
val (exportPrivateDataAuthentication, setExportPrivateDataAuthentication) =
|
||||
rememberSaveable { mutableStateOf(false) }
|
||||
val (seedRecoveryAuthentication, setSeedRecoveryAuthentication) =
|
||||
rememberSaveable { mutableStateOf(false) }
|
||||
val (deleteWalletAuthentication, setDeleteWalletAuthentication) =
|
||||
rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val navigator: Navigator = remember { NavigatorImpl(this@Navigation, navController, flexaViewModel) }
|
||||
val navigator: Navigator =
|
||||
remember(
|
||||
navController,
|
||||
flexaViewModel,
|
||||
keyboardManager,
|
||||
sheetStateManager
|
||||
) {
|
||||
NavigatorImpl(
|
||||
activity = this@Navigation,
|
||||
navController = navController,
|
||||
flexaViewModel = flexaViewModel,
|
||||
keyboardManager = keyboardManager,
|
||||
sheetStateManager = sheetStateManager
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
navigationRouter.observePipeline().collect {
|
||||
|
@ -163,14 +182,6 @@ internal fun MainActivity.Navigation() {
|
|||
unProtectedDestination = EXPORT_PRIVATE_DATA
|
||||
)
|
||||
},
|
||||
goSeedRecovery = {
|
||||
navController.checkProtectedDestination(
|
||||
scope = lifecycleScope,
|
||||
propertyToCheck = authenticationViewModel.isSeedAuthenticationRequired,
|
||||
setCheckedProperty = setSeedRecoveryAuthentication,
|
||||
unProtectedDestination = SEED_RECOVERY
|
||||
)
|
||||
},
|
||||
goDeleteWallet = {
|
||||
navController.checkProtectedDestination(
|
||||
scope = lifecycleScope,
|
||||
|
@ -199,27 +210,13 @@ internal fun MainActivity.Navigation() {
|
|||
setCheckedProperty = setExportPrivateDataAuthentication
|
||||
)
|
||||
}
|
||||
|
||||
seedRecoveryAuthentication -> {
|
||||
ShowSystemAuthentication(
|
||||
navHostController = navController,
|
||||
protectedDestination = SEED_RECOVERY,
|
||||
protectedUseCase = AuthenticationUseCase.SeedRecovery,
|
||||
setCheckedProperty = setSeedRecoveryAuthentication
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
composable(CHOOSE_SERVER) {
|
||||
WrapChooseServer()
|
||||
}
|
||||
composable(SEED_RECOVERY) {
|
||||
WrapSeed(
|
||||
args = SeedNavigationArgs.RECOVERY,
|
||||
goBackOverride = {
|
||||
setSeedRecoveryAuthentication(false)
|
||||
}
|
||||
)
|
||||
composable<SeedRecovery> {
|
||||
AndroidSeedRecovery()
|
||||
}
|
||||
composable(SUPPORT) {
|
||||
// Pop back stack won't be right if we deep link into support
|
||||
|
@ -275,21 +272,8 @@ internal fun MainActivity.Navigation() {
|
|||
) {
|
||||
AndroidAccountList()
|
||||
}
|
||||
composable(
|
||||
route = Scan.ROUTE,
|
||||
arguments =
|
||||
listOf(
|
||||
navArgument(Scan.KEY) {
|
||||
type = NavType.EnumType(Scan::class.java)
|
||||
defaultValue = Scan.SEND
|
||||
}
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val mode =
|
||||
backStackEntry.arguments
|
||||
?.getSerializableCompat<Scan>(Scan.KEY) ?: Scan.SEND
|
||||
|
||||
WrapScanValidator(args = mode)
|
||||
composable<Scan> {
|
||||
WrapScanValidator(it.toRoute())
|
||||
}
|
||||
composable(EXPORT_PRIVATE_DATA) {
|
||||
WrapExportPrivateData(
|
||||
|
@ -405,6 +389,18 @@ internal fun MainActivity.Navigation() {
|
|||
composable<Send> {
|
||||
WrapSend(it.toRoute())
|
||||
}
|
||||
dialog<SeedInfo>(
|
||||
dialogProperties =
|
||||
DialogProperties(
|
||||
dismissOnBackPress = false,
|
||||
dismissOnClickOutside = false,
|
||||
)
|
||||
) {
|
||||
AndroidSeedInfo()
|
||||
}
|
||||
composable<SeedBackup> {
|
||||
AndroidSeedBackup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -530,7 +526,6 @@ object NavigationTargets {
|
|||
const val NOT_ENOUGH_SPACE = "not_enough_space"
|
||||
const val QR_CODE = "qr_code"
|
||||
const val REQUEST = "request"
|
||||
const val SEED_RECOVERY = "seed_recovery"
|
||||
const val SETTINGS = "settings"
|
||||
const val SETTINGS_EXCHANGE_RATE_OPT_IN = "settings_exchange_rate_opt_in"
|
||||
const val SUPPORT = "support"
|
||||
|
|
|
@ -5,6 +5,8 @@ import androidx.activity.ComponentActivity
|
|||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavOptionsBuilder
|
||||
import androidx.navigation.serialization.generateHashCode
|
||||
import co.electriccoin.zcash.ui.design.KeyboardManager
|
||||
import co.electriccoin.zcash.ui.design.SheetStateManager
|
||||
import co.electriccoin.zcash.ui.screen.ExternalUrl
|
||||
import co.electriccoin.zcash.ui.screen.about.util.WebBrowserUtil
|
||||
import co.electriccoin.zcash.ui.screen.flexa.FlexaViewModel
|
||||
|
@ -14,15 +16,19 @@ import kotlinx.serialization.InternalSerializationApi
|
|||
import kotlinx.serialization.serializer
|
||||
|
||||
interface Navigator {
|
||||
fun executeCommand(command: NavigationCommand)
|
||||
suspend fun executeCommand(command: NavigationCommand)
|
||||
}
|
||||
|
||||
class NavigatorImpl(
|
||||
private val activity: ComponentActivity,
|
||||
private val navController: NavHostController,
|
||||
private val flexaViewModel: FlexaViewModel,
|
||||
private val keyboardManager: KeyboardManager,
|
||||
private val sheetStateManager: SheetStateManager,
|
||||
) : Navigator {
|
||||
override fun executeCommand(command: NavigationCommand) {
|
||||
override suspend fun executeCommand(command: NavigationCommand) {
|
||||
keyboardManager.close()
|
||||
sheetStateManager.hide()
|
||||
when (command) {
|
||||
is NavigationCommand.Forward -> forward(command)
|
||||
is NavigationCommand.Replace -> replace(command)
|
||||
|
|
|
@ -11,8 +11,9 @@ import co.electriccoin.zcash.ui.common.model.KeystoneAccount
|
|||
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||
import co.electriccoin.zcash.ui.common.model.ZashiAccount
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletAccountsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletStateInformationUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedWalletAccountUseCase
|
||||
import co.electriccoin.zcash.ui.design.R
|
||||
import co.electriccoin.zcash.ui.design.component.IconButtonState
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
|
@ -30,7 +31,8 @@ import kotlinx.coroutines.flow.stateIn
|
|||
import kotlinx.coroutines.launch
|
||||
|
||||
class ZashiTopAppBarViewModel(
|
||||
observeSelectedWalletAccount: ObserveSelectedWalletAccountUseCase,
|
||||
getWalletAccountUseCase: GetWalletAccountsUseCase,
|
||||
getSelectedWalletAccount: GetSelectedWalletAccountUseCase,
|
||||
getWalletStateInformation: GetWalletStateInformationUseCase,
|
||||
private val standardPreferenceProvider: StandardPreferenceProvider,
|
||||
private val navigationRouter: NavigationRouter,
|
||||
|
@ -39,7 +41,7 @@ class ZashiTopAppBarViewModel(
|
|||
|
||||
val state =
|
||||
combine(
|
||||
observeSelectedWalletAccount.require(),
|
||||
getSelectedWalletAccount.observe().filterNotNull(),
|
||||
isHideBalances,
|
||||
getWalletStateInformation.observe()
|
||||
) { currentAccount, isHideBalances, walletState ->
|
||||
|
@ -47,11 +49,16 @@ class ZashiTopAppBarViewModel(
|
|||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
initialValue = null
|
||||
initialValue =
|
||||
createState(
|
||||
currentAccount = getWalletAccountUseCase.observe().value?.firstOrNull { it.isSelected },
|
||||
isHideBalances = isHideBalances.value,
|
||||
topAppBarSubTitleState = getWalletStateInformation.observe().value
|
||||
)
|
||||
)
|
||||
|
||||
private fun createState(
|
||||
currentAccount: WalletAccount,
|
||||
currentAccount: WalletAccount?,
|
||||
isHideBalances: Boolean?,
|
||||
topAppBarSubTitleState: TopAppBarSubTitleState
|
||||
) = ZashiMainTopAppBarState(
|
||||
|
@ -61,6 +68,7 @@ class ZashiTopAppBarViewModel(
|
|||
when (currentAccount) {
|
||||
is KeystoneAccount -> ZashiMainTopAppBarState.AccountType.KEYSTONE
|
||||
is ZashiAccount -> ZashiMainTopAppBarState.AccountType.ZASHI
|
||||
else -> ZashiMainTopAppBarState.AccountType.ZASHI
|
||||
},
|
||||
onAccountTypeClick = ::onAccountTypeClicked,
|
||||
),
|
||||
|
|
|
@ -49,7 +49,7 @@ class TransactionHistoryMapper {
|
|||
false
|
||||
} else {
|
||||
val transactionMetadata = data.metadata
|
||||
hasMemo && (transactionMetadata == null || transactionMetadata.isRead.not())
|
||||
hasMemo && transactionMetadata.isRead.not()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,7 @@ class ConfigurationRepositoryImpl(
|
|||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
initialValue = null
|
||||
)
|
||||
|
||||
override val isCoinbaseAvailable: StateFlow<Boolean?> =
|
||||
flow {
|
||||
val versionInfo = getVersionInfo()
|
||||
|
|
|
@ -7,6 +7,7 @@ import co.electriccoin.zcash.preference.model.entry.NullableBooleanPreferenceDef
|
|||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.NavigationTargets.EXCHANGE_RATE_OPT_IN
|
||||
import co.electriccoin.zcash.ui.common.provider.SynchronizerProvider
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
import co.electriccoin.zcash.ui.common.wallet.RefreshLock
|
||||
import co.electriccoin.zcash.ui.common.wallet.StaleLock
|
||||
|
@ -24,7 +25,6 @@ import kotlinx.coroutines.flow.combine
|
|||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
@ -34,6 +34,7 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
interface ExchangeRateRepository {
|
||||
val isExchangeRateUsdOptedIn: StateFlow<Boolean?>
|
||||
|
@ -48,7 +49,7 @@ interface ExchangeRateRepository {
|
|||
}
|
||||
|
||||
class ExchangeRateRepositoryImpl(
|
||||
private val walletRepository: WalletRepository,
|
||||
private val synchronizerProvider: SynchronizerProvider,
|
||||
private val standardPreferenceProvider: StandardPreferenceProvider,
|
||||
private val navigationRouter: NavigationRouter,
|
||||
) : ExchangeRateRepository {
|
||||
|
@ -61,7 +62,8 @@ class ExchangeRateRepositoryImpl(
|
|||
private val exchangeRateUsdInternal =
|
||||
isExchangeRateUsdOptedIn.flatMapLatest { optedIn ->
|
||||
if (optedIn == true) {
|
||||
walletRepository.synchronizer
|
||||
synchronizerProvider
|
||||
.synchronizer
|
||||
.filterNotNull()
|
||||
.flatMapLatest { synchronizer ->
|
||||
synchronizer.exchangeRateUsd
|
||||
|
@ -105,6 +107,35 @@ class ExchangeRateRepositoryImpl(
|
|||
staleExchangeRateUsdLock.state,
|
||||
refreshExchangeRateUsdLock.state,
|
||||
) { isOptedIn, exchangeRate, isStale, isRefreshEnabled ->
|
||||
createState(isOptedIn, exchangeRate, isStale, isRefreshEnabled)
|
||||
}.distinctUntilChanged()
|
||||
.onEach {
|
||||
Twig.info { "[USD] $it" }
|
||||
send(it)
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
awaitClose {
|
||||
// do nothing
|
||||
}
|
||||
}.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.WhileSubscribed(5.seconds, 5.seconds),
|
||||
initialValue =
|
||||
createState(
|
||||
isOptedIn = isExchangeRateUsdOptedIn.value,
|
||||
exchangeRate = exchangeRateUsdInternal.value,
|
||||
isStale = false,
|
||||
isRefreshEnabled = false
|
||||
)
|
||||
)
|
||||
|
||||
private fun createState(
|
||||
isOptedIn: Boolean?,
|
||||
exchangeRate: ObserveFiatCurrencyResult,
|
||||
isStale: Boolean,
|
||||
isRefreshEnabled: Boolean
|
||||
): ExchangeRateState {
|
||||
lastExchangeRateUsdValue =
|
||||
when (isOptedIn) {
|
||||
true ->
|
||||
|
@ -144,22 +175,8 @@ class ExchangeRateRepositoryImpl(
|
|||
)
|
||||
}
|
||||
|
||||
lastExchangeRateUsdValue
|
||||
}.distinctUntilChanged()
|
||||
.onEach {
|
||||
Twig.info { "[USD] $it" }
|
||||
send(it)
|
||||
return lastExchangeRateUsdValue
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
awaitClose {
|
||||
// do nothing
|
||||
}
|
||||
}.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = ExchangeRateState.OptedOut
|
||||
)
|
||||
|
||||
override fun refreshExchangeRateUsd() {
|
||||
refreshExchangeRateUsdInternal()
|
||||
|
@ -167,7 +184,7 @@ class ExchangeRateRepositoryImpl(
|
|||
|
||||
private fun refreshExchangeRateUsdInternal() =
|
||||
scope.launch {
|
||||
val synchronizer = walletRepository.synchronizer.filterNotNull().first()
|
||||
val synchronizer = synchronizerProvider.getSynchronizer()
|
||||
val value = state.value
|
||||
if (value is ExchangeRateState.Data && value.isRefreshEnabled && !value.isLoading) {
|
||||
synchronizer.refreshExchangeRateUsd()
|
||||
|
|
|
@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -39,7 +38,7 @@ interface MetadataRepository {
|
|||
|
||||
suspend fun markTxMemoAsRead(txId: String)
|
||||
|
||||
fun observeTransactionMetadataByTxId(txId: String): Flow<TransactionMetadata?>
|
||||
fun observeTransactionMetadataByTxId(txId: String): Flow<TransactionMetadata>
|
||||
}
|
||||
|
||||
class MetadataRepositoryImpl(
|
||||
|
@ -161,9 +160,9 @@ class MetadataRepositoryImpl(
|
|||
}
|
||||
}
|
||||
|
||||
override fun observeTransactionMetadataByTxId(txId: String): Flow<TransactionMetadata?> =
|
||||
override fun observeTransactionMetadataByTxId(txId: String): Flow<TransactionMetadata> =
|
||||
metadata
|
||||
.map<Metadata?, TransactionMetadata?> { metadata ->
|
||||
.map { metadata ->
|
||||
val accountMetadata = metadata?.accountMetadata
|
||||
|
||||
TransactionMetadata(
|
||||
|
@ -173,7 +172,6 @@ class MetadataRepositoryImpl(
|
|||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onStart { emit(null) }
|
||||
|
||||
private suspend fun getMetadataKey(selectedAccount: WalletAccount): MetadataKey {
|
||||
val key = metadataKeyStorageProvider.get(selectedAccount)
|
||||
|
|
|
@ -70,7 +70,6 @@ interface WalletRepository {
|
|||
val synchronizer: StateFlow<Synchronizer?>
|
||||
val secretState: StateFlow<SecretState>
|
||||
val fastestServers: StateFlow<FastestServersState>
|
||||
val persistableWallet: Flow<PersistableWallet?>
|
||||
val onboardingState: Flow<OnboardingState>
|
||||
|
||||
val allAccounts: Flow<List<WalletAccount>?>
|
||||
|
@ -99,8 +98,6 @@ interface WalletRepository {
|
|||
|
||||
suspend fun getSynchronizer(): Synchronizer
|
||||
|
||||
suspend fun getPersistableWallet(): PersistableWallet
|
||||
|
||||
fun persistExistingWalletWithSeedPhrase(
|
||||
network: ZcashNetwork,
|
||||
seedPhrase: SeedPhrase,
|
||||
|
@ -110,7 +107,8 @@ interface WalletRepository {
|
|||
|
||||
class WalletRepositoryImpl(
|
||||
accountDataSource: AccountDataSource,
|
||||
persistableWalletProvider: PersistableWalletProvider,
|
||||
configurationRepository: ConfigurationRepository,
|
||||
private val persistableWalletProvider: PersistableWalletProvider,
|
||||
private val synchronizerProvider: SynchronizerProvider,
|
||||
private val application: Application,
|
||||
private val getDefaultServers: GetDefaultServersProvider,
|
||||
|
@ -143,27 +141,21 @@ class WalletRepositoryImpl(
|
|||
override val allAccounts: StateFlow<List<WalletAccount>?> = accountDataSource.allAccounts
|
||||
|
||||
override val secretState: StateFlow<SecretState> =
|
||||
combine(
|
||||
persistableWalletProvider.persistableWallet,
|
||||
onboardingState
|
||||
) { persistableWallet: PersistableWallet?, onboardingState: OnboardingState ->
|
||||
when {
|
||||
onboardingState == OnboardingState.NONE -> SecretState.None
|
||||
onboardingState == OnboardingState.NEEDS_WARN -> SecretState.NeedsWarning
|
||||
onboardingState == OnboardingState.NEEDS_BACKUP && persistableWallet != null -> {
|
||||
SecretState.NeedsBackup(persistableWallet)
|
||||
combine(configurationRepository.configurationFlow, onboardingState) { config, onboardingState ->
|
||||
if (config == null) {
|
||||
SecretState.LOADING
|
||||
} else {
|
||||
when (onboardingState) {
|
||||
OnboardingState.NEEDS_WARN,
|
||||
OnboardingState.NEEDS_BACKUP,
|
||||
OnboardingState.NONE -> SecretState.NONE
|
||||
OnboardingState.READY -> SecretState.READY
|
||||
}
|
||||
|
||||
onboardingState == OnboardingState.READY && persistableWallet != null -> {
|
||||
SecretState.Ready(persistableWallet)
|
||||
}
|
||||
|
||||
else -> SecretState.None
|
||||
}
|
||||
}.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
initialValue = SecretState.Loading
|
||||
initialValue = SecretState.LOADING
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
@ -204,11 +196,6 @@ class WalletRepositoryImpl(
|
|||
initialValue = FastestServersState(servers = emptyList(), isLoading = true)
|
||||
)
|
||||
|
||||
override val persistableWallet: Flow<PersistableWallet?> =
|
||||
secretState.map {
|
||||
(it as? SecretState.Ready?)?.persistableWallet
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
override val currentWalletSnapshot: StateFlow<WalletSnapshot?> =
|
||||
combine(synchronizer, currentAccount) { synchronizer, currentAccount ->
|
||||
|
@ -317,7 +304,7 @@ class WalletRepositoryImpl(
|
|||
}
|
||||
|
||||
override suspend fun getSelectedServer(): LightWalletEndpoint {
|
||||
return persistableWallet
|
||||
return persistableWalletProvider.persistableWallet
|
||||
.map {
|
||||
it?.endpoint
|
||||
}
|
||||
|
@ -338,8 +325,6 @@ class WalletRepositoryImpl(
|
|||
|
||||
override suspend fun getSynchronizer(): Synchronizer = synchronizerProvider.getSynchronizer()
|
||||
|
||||
override suspend fun getPersistableWallet(): PersistableWallet = persistableWallet.filterNotNull().first()
|
||||
|
||||
override fun persistExistingWalletWithSeedPhrase(
|
||||
network: ZcashNetwork,
|
||||
seedPhrase: SeedPhrase,
|
||||
|
|
|
@ -8,12 +8,8 @@ class ApplyTransactionFiltersUseCase(
|
|||
private val transactionFilterRepository: TransactionFilterRepository,
|
||||
private val navigationRouter: NavigationRouter,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
filters: List<TransactionFilter>,
|
||||
hideBottomSheet: suspend () -> Unit
|
||||
) {
|
||||
operator fun invoke(filters: List<TransactionFilter>) {
|
||||
transactionFilterRepository.apply(filters)
|
||||
hideBottomSheet()
|
||||
navigationRouter.back()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,11 +9,9 @@ class CreateOrUpdateTransactionNoteUseCase(
|
|||
) {
|
||||
suspend operator fun invoke(
|
||||
txId: String,
|
||||
note: String,
|
||||
closeBottomSheet: suspend () -> Unit
|
||||
note: String
|
||||
) {
|
||||
metadataRepository.createOrUpdateTxNote(txId, note.trim())
|
||||
closeBottomSheet()
|
||||
navigationRouter.back()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,12 +7,8 @@ class DeleteTransactionNoteUseCase(
|
|||
private val metadataRepository: MetadataRepository,
|
||||
private val navigationRouter: NavigationRouter
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
txId: String,
|
||||
closeBottomSheet: suspend () -> Unit
|
||||
) {
|
||||
suspend operator fun invoke(txId: String) {
|
||||
metadataRepository.deleteTxNote(txId)
|
||||
closeBottomSheet()
|
||||
navigationRouter.back()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.SecretState
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetBackupPersistableWalletUseCase(
|
||||
private val walletRepository: WalletRepository
|
||||
) {
|
||||
suspend operator fun invoke() =
|
||||
walletRepository.secretState
|
||||
.map { (it as? SecretState.NeedsBackup)?.persistableWallet }
|
||||
.filterNotNull()
|
||||
.first()
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
||||
import co.electriccoin.zcash.ui.common.repository.ConfigurationRepository
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetCoinbaseStatusUseCase(
|
||||
private val configurationRepository: ConfigurationRepository,
|
||||
private val accountDataSource: AccountDataSource,
|
||||
) {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun observe() =
|
||||
configurationRepository
|
||||
.isCoinbaseAvailable
|
||||
.filterNotNull()
|
||||
.flatMapLatest { isAvailable ->
|
||||
if (isAvailable) {
|
||||
accountDataSource.selectedAccount.map {
|
||||
Status.ENABLED
|
||||
}
|
||||
} else {
|
||||
flowOf(Status.UNAVAILABLE)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
|
@ -2,8 +2,8 @@ package co.electriccoin.zcash.ui.common.usecase
|
|||
|
||||
import co.electriccoin.zcash.ui.common.repository.ConfigurationRepository
|
||||
|
||||
class ObserveConfigurationUseCase(
|
||||
class GetConfigurationUseCase(
|
||||
private val configurationRepository: ConfigurationRepository
|
||||
) {
|
||||
operator fun invoke() = configurationRepository.configurationFlow
|
||||
fun observe() = configurationRepository.configurationFlow
|
||||
}
|
|
@ -13,11 +13,11 @@ import co.electriccoin.zcash.ui.common.repository.TransactionFilter
|
|||
import co.electriccoin.zcash.ui.common.repository.TransactionFilterRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.TransactionMetadata
|
||||
import co.electriccoin.zcash.ui.common.repository.TransactionRepository
|
||||
import co.electriccoin.zcash.ui.design.util.combineToFlow
|
||||
import co.electriccoin.zcash.ui.design.util.getString
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.util.CloseableScopeHolder
|
||||
import co.electriccoin.zcash.ui.util.CloseableScopeHolderImpl
|
||||
import co.electriccoin.zcash.ui.util.combineToFlow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
|
@ -232,23 +232,23 @@ class GetCurrentFilteredTransactionsUseCase(
|
|||
} else {
|
||||
val transactionMetadata = transaction.transactionMetadata
|
||||
|
||||
hasMemo && (transactionMetadata == null || transactionMetadata.isRead.not())
|
||||
hasMemo && transactionMetadata.isRead.not()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isBookmark(transaction: FilterTransactionData): Boolean {
|
||||
return transaction.transactionMetadata?.isBookmarked ?: false
|
||||
return transaction.transactionMetadata.isBookmarked
|
||||
}
|
||||
|
||||
private fun hasNotes(transaction: FilterTransactionData): Boolean {
|
||||
return transaction.transactionMetadata?.note != null
|
||||
return transaction.transactionMetadata.note != null
|
||||
}
|
||||
|
||||
private fun hasNotesWithFulltext(
|
||||
transaction: FilterTransactionData,
|
||||
fulltextFilter: String
|
||||
): Boolean {
|
||||
return transaction.transactionMetadata?.note
|
||||
return transaction.transactionMetadata.note
|
||||
?.contains(
|
||||
fulltextFilter,
|
||||
ignoreCase = true
|
||||
|
@ -288,7 +288,7 @@ private data class FilterTransactionData(
|
|||
val transaction: Transaction,
|
||||
val contact: AddressBookContact?,
|
||||
val recipientAddress: String?,
|
||||
val transactionMetadata: TransactionMetadata?
|
||||
val transactionMetadata: TransactionMetadata
|
||||
)
|
||||
|
||||
private const val MIN_TEXT_FILTER_LENGTH = 3
|
||||
|
|
|
@ -4,7 +4,7 @@ import co.electriccoin.zcash.ui.common.repository.MetadataRepository
|
|||
import co.electriccoin.zcash.ui.common.repository.Transaction
|
||||
import co.electriccoin.zcash.ui.common.repository.TransactionMetadata
|
||||
import co.electriccoin.zcash.ui.common.repository.TransactionRepository
|
||||
import co.electriccoin.zcash.ui.util.combineToFlow
|
||||
import co.electriccoin.zcash.ui.design.util.combineToFlow
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
@ -40,5 +40,5 @@ class GetCurrentTransactionsUseCase(
|
|||
|
||||
data class ListTransactionData(
|
||||
val transaction: Transaction,
|
||||
val metadata: TransactionMetadata?
|
||||
val metadata: TransactionMetadata
|
||||
)
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
||||
import co.electriccoin.zcash.ui.common.model.ZashiAccount
|
||||
import co.electriccoin.zcash.ui.common.repository.ConfigurationRepository
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetFlexaStatusUseCase(
|
||||
private val configurationRepository: ConfigurationRepository,
|
||||
private val accountDataSource: AccountDataSource,
|
||||
) {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun observe() =
|
||||
configurationRepository
|
||||
.isFlexaAvailable
|
||||
.filterNotNull()
|
||||
.flatMapLatest { isAvailable ->
|
||||
if (isAvailable) {
|
||||
accountDataSource.selectedAccount.map {
|
||||
if (it is ZashiAccount) {
|
||||
Status.ENABLED
|
||||
} else {
|
||||
Status.DISABLED
|
||||
}
|
||||
}
|
||||
} else {
|
||||
flowOf(Status.UNAVAILABLE)
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
||||
enum class Status {
|
||||
UNAVAILABLE,
|
||||
ENABLED,
|
||||
DISABLED
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
||||
import co.electriccoin.zcash.ui.common.model.KeystoneAccount
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class GetKeystoneStatusUseCase(
|
||||
private val accountDataSource: AccountDataSource,
|
||||
) {
|
||||
fun observe() =
|
||||
accountDataSource.allAccounts
|
||||
.map {
|
||||
val enabled = it?.none { account -> account is KeystoneAccount } ?: false
|
||||
if (enabled) {
|
||||
Status.ENABLED
|
||||
} else {
|
||||
Status.UNAVAILABLE
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||
import co.electriccoin.zcash.ui.common.provider.PersistableWalletProvider
|
||||
|
||||
class GetPersistableWalletUseCase(
|
||||
private val walletRepository: WalletRepository
|
||||
private val persistableWalletProvider: PersistableWalletProvider
|
||||
) {
|
||||
suspend operator fun invoke() = walletRepository.getPersistableWallet()
|
||||
suspend operator fun invoke() = persistableWalletProvider.getPersistableWallet()
|
||||
}
|
||||
|
|
|
@ -12,15 +12,16 @@ import co.electriccoin.zcash.ui.common.repository.TransactionRepository
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class GetTransactionDetailByIdUseCase(
|
||||
private val transactionRepository: TransactionRepository,
|
||||
|
@ -30,26 +31,41 @@ class GetTransactionDetailByIdUseCase(
|
|||
) {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun observe(txId: String) =
|
||||
transactionRepository
|
||||
.observeTransaction(txId).filterNotNull().flatMapLatest { transaction ->
|
||||
channelFlow {
|
||||
launch {
|
||||
val transactionFlow =
|
||||
transactionRepository
|
||||
.observeTransaction(txId)
|
||||
.filterNotNull()
|
||||
.stateIn(this)
|
||||
|
||||
val addressFlow =
|
||||
transactionFlow
|
||||
.mapLatest { getWalletAddress(transactionRepository.getRecipients(it)) }
|
||||
.onStart { emit(null) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
val memosFlow: Flow<List<String>?> =
|
||||
transactionFlow
|
||||
.mapLatest<Transaction, List<String>?> { transactionRepository.getMemos(it) }
|
||||
.onStart { emit(null) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
val metadataFlow =
|
||||
metadataRepository
|
||||
.observeTransactionMetadataByTxId(txId)
|
||||
|
||||
val contactFlow =
|
||||
addressFlow
|
||||
.flatMapLatest { addressBookRepository.observeContactByAddress(it?.address.orEmpty()) }
|
||||
.distinctUntilChanged()
|
||||
|
||||
combine(
|
||||
flow {
|
||||
emit(null)
|
||||
emit(getWalletAddress(transactionRepository.getRecipients(transaction)))
|
||||
},
|
||||
flow {
|
||||
emit(null)
|
||||
emit(transaction.let { transactionRepository.getMemos(it) })
|
||||
},
|
||||
metadataRepository.observeTransactionMetadataByTxId(txId)
|
||||
) { address, memos, metadata ->
|
||||
Triple(address, memos, metadata)
|
||||
}.flatMapLatest { (address, memos, metadata) ->
|
||||
addressBookRepository
|
||||
.observeContactByAddress(address?.address.orEmpty())
|
||||
.mapLatest { contact ->
|
||||
transactionFlow,
|
||||
addressFlow,
|
||||
memosFlow,
|
||||
metadataFlow,
|
||||
contactFlow
|
||||
) { transaction, address, memos, metadata, contact ->
|
||||
DetailedTransactionData(
|
||||
transaction = transaction,
|
||||
memos = memos,
|
||||
|
@ -57,15 +73,13 @@ class GetTransactionDetailByIdUseCase(
|
|||
recipientAddress = address,
|
||||
metadata = metadata
|
||||
)
|
||||
}
|
||||
}.collect {
|
||||
send(it)
|
||||
}
|
||||
}
|
||||
|
||||
awaitClose {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}.distinctUntilChanged().flowOn(Dispatchers.Default)
|
||||
|
||||
private suspend fun getWalletAddress(address: String?): WalletAddress? {
|
||||
|
@ -86,5 +100,5 @@ data class DetailedTransactionData(
|
|||
val memos: List<String>?,
|
||||
val contact: AddressBookContact?,
|
||||
val recipientAddress: WalletAddress?,
|
||||
val metadata: TransactionMetadata?
|
||||
val metadata: TransactionMetadata
|
||||
)
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.MetadataRepository
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class GetTransactionMetadataUseCase(
|
||||
private val metadataRepository: MetadataRepository,
|
||||
) {
|
||||
suspend operator fun invoke(txId: String) = observe(txId).filterNotNull().first()
|
||||
suspend operator fun invoke(txId: String) = observe(txId).first()
|
||||
|
||||
fun observe(txId: String) = metadataRepository.observeTransactionMetadataByTxId(txId)
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ package co.electriccoin.zcash.ui.common.usecase
|
|||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
|
||||
class ObserveWalletAccountsUseCase(private val accountDataSource: AccountDataSource) {
|
||||
operator fun invoke() = accountDataSource.allAccounts
|
||||
class GetWalletAccountsUseCase(private val accountDataSource: AccountDataSource) {
|
||||
fun observe() = accountDataSource.allAccounts
|
||||
|
||||
fun require() = accountDataSource.allAccounts.filterNotNull()
|
||||
}
|
|
@ -10,7 +10,7 @@ class NavigateToCoinbaseUseCase(
|
|||
private val navigationRouter: NavigationRouter
|
||||
) {
|
||||
suspend operator fun invoke(replaceCurrentScreen: Boolean) {
|
||||
val transparent = accountDataSource.getZashiAccount().transparent
|
||||
val transparent = accountDataSource.getSelectedAccount().transparent
|
||||
val url = getUrl(transparent.address.address)
|
||||
if (replaceCurrentScreen) {
|
||||
navigationRouter.replace(ExternalUrl(url))
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.repository.BiometricRepository
|
||||
import co.electriccoin.zcash.ui.common.repository.BiometricRequest
|
||||
import co.electriccoin.zcash.ui.common.repository.BiometricsCancelledException
|
||||
import co.electriccoin.zcash.ui.common.repository.BiometricsFailureException
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.seed.SeedRecovery
|
||||
|
||||
class NavigateToSeedRecoveryUseCase(
|
||||
private val navigationRouter: NavigationRouter,
|
||||
private val biometricRepository: BiometricRepository
|
||||
) {
|
||||
suspend operator fun invoke() {
|
||||
try {
|
||||
biometricRepository.requestBiometrics(
|
||||
BiometricRequest(
|
||||
message =
|
||||
stringRes(
|
||||
R.string.authentication_system_ui_subtitle,
|
||||
stringRes(R.string.authentication_use_case_seed_recovery)
|
||||
)
|
||||
)
|
||||
)
|
||||
navigationRouter.forward(SeedRecovery)
|
||||
} catch (_: BiometricsFailureException) {
|
||||
// do nothing
|
||||
} catch (_: BiometricsCancelledException) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||
import co.electriccoin.zcash.ui.common.provider.PersistableWalletProvider
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class ObserveSelectedEndpointUseCase(
|
||||
private val walletRepository: WalletRepository
|
||||
private val persistableWalletProvider: PersistableWalletProvider
|
||||
) {
|
||||
operator fun invoke() =
|
||||
walletRepository.persistableWallet
|
||||
persistableWalletProvider.persistableWallet
|
||||
.map {
|
||||
it?.endpoint
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import cash.z.ecc.android.sdk.type.AddressType
|
|||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanFlow
|
||||
import co.electriccoin.zcash.ui.screen.send.Send
|
||||
|
||||
class OnAddressScannedUseCase(
|
||||
|
@ -13,21 +14,21 @@ class OnAddressScannedUseCase(
|
|||
operator fun invoke(
|
||||
address: String,
|
||||
addressType: AddressType,
|
||||
scanFlow: Scan
|
||||
scanArgs: Scan
|
||||
) {
|
||||
require(addressType is AddressType.Valid)
|
||||
|
||||
when (scanFlow) {
|
||||
Scan.SEND -> {
|
||||
when (scanArgs.flow) {
|
||||
ScanFlow.SEND -> {
|
||||
prefillSend.request(PrefillSendData.FromAddressScan(address = address))
|
||||
navigationRouter.back()
|
||||
}
|
||||
|
||||
Scan.ADDRESS_BOOK -> {
|
||||
ScanFlow.ADDRESS_BOOK -> {
|
||||
navigationRouter.replace(AddContactArgs(address))
|
||||
}
|
||||
|
||||
Scan.HOMEPAGE -> {
|
||||
ScanFlow.HOMEPAGE -> {
|
||||
navigationRouter.replace(
|
||||
Send(
|
||||
address,
|
||||
|
|
|
@ -15,9 +15,9 @@ import co.electriccoin.zcash.ui.common.usecase.Zip321ParseUriValidationUseCase.Z
|
|||
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
|
||||
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewTransaction
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan.ADDRESS_BOOK
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan.HOMEPAGE
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan.SEND
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanFlow.ADDRESS_BOOK
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanFlow.HOMEPAGE
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanFlow.SEND
|
||||
import co.electriccoin.zcash.ui.screen.send.Send
|
||||
|
||||
class OnZip321ScannedUseCase(
|
||||
|
@ -29,20 +29,27 @@ class OnZip321ScannedUseCase(
|
|||
) {
|
||||
suspend operator fun invoke(
|
||||
zip321: Zip321ParseUriValidation.Valid,
|
||||
scanFlow: Scan
|
||||
scanArgs: Scan
|
||||
) {
|
||||
if (scanFlow == ADDRESS_BOOK) {
|
||||
navigationRouter.replace(AddContactArgs(zip321.payment.payments[0].recipientAddress.value))
|
||||
when (scanArgs.flow) {
|
||||
ADDRESS_BOOK -> addressBookFlow(zip321)
|
||||
SEND ->
|
||||
if (scanArgs.isScanZip321Enabled) {
|
||||
sendFlow(zip321)
|
||||
} else {
|
||||
createProposal(zip321, scanFlow)
|
||||
sendFlowWithDisabledZip321(zip321)
|
||||
}
|
||||
|
||||
HOMEPAGE -> homepageFlow(zip321)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addressBookFlow(zip321: Zip321ParseUriValidation.Valid) {
|
||||
navigationRouter.replace(AddContactArgs(zip321.payment.payments[0].recipientAddress.value))
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private suspend fun createProposal(
|
||||
zip321: Zip321ParseUriValidation.Valid,
|
||||
scanFlow: Scan
|
||||
) {
|
||||
private suspend fun homepageFlow(zip321: Zip321ParseUriValidation.Valid) {
|
||||
try {
|
||||
val proposal =
|
||||
when (accountDataSource.getSelectedAccount()) {
|
||||
|
@ -57,7 +64,6 @@ class OnZip321ScannedUseCase(
|
|||
}
|
||||
}
|
||||
|
||||
if (scanFlow == HOMEPAGE) {
|
||||
navigationRouter
|
||||
.replace(
|
||||
Send(
|
||||
|
@ -72,7 +78,28 @@ class OnZip321ScannedUseCase(
|
|||
),
|
||||
ReviewTransaction
|
||||
)
|
||||
} else if (scanFlow == SEND) {
|
||||
} catch (e: Exception) {
|
||||
keystoneProposalRepository.clear()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
private suspend fun sendFlow(zip321: Zip321ParseUriValidation.Valid) {
|
||||
try {
|
||||
val proposal =
|
||||
when (accountDataSource.getSelectedAccount()) {
|
||||
is KeystoneAccount -> {
|
||||
val result = keystoneProposalRepository.createZip321Proposal(zip321.zip321Uri)
|
||||
keystoneProposalRepository.createPCZTFromProposal()
|
||||
result
|
||||
}
|
||||
|
||||
is ZashiAccount -> {
|
||||
zashiProposalRepository.createZip321Proposal(zip321.zip321Uri)
|
||||
}
|
||||
}
|
||||
|
||||
prefillSend.request(
|
||||
PrefillSendData.All(
|
||||
amount = proposal.amount,
|
||||
|
@ -82,10 +109,18 @@ class OnZip321ScannedUseCase(
|
|||
)
|
||||
)
|
||||
navigationRouter.forward(ReviewTransaction)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
keystoneProposalRepository.clear()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendFlowWithDisabledZip321(zip321: Zip321ParseUriValidation.Valid) {
|
||||
prefillSend.request(
|
||||
PrefillSendData.FromAddressScan(
|
||||
address = zip321.payment.payments[0].recipientAddress.value
|
||||
)
|
||||
)
|
||||
navigationRouter.back()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import android.content.Context
|
||||
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.type.fromResources
|
||||
import co.electriccoin.zcash.ui.common.repository.WalletRepository
|
||||
|
||||
class RestoreWalletUseCase(
|
||||
private val walletRepository: WalletRepository,
|
||||
private val context: Context,
|
||||
) {
|
||||
operator fun invoke(
|
||||
seedPhrase: SeedPhrase,
|
||||
birthday: BlockHeight?
|
||||
) {
|
||||
walletRepository.persistExistingWalletWithSeedPhrase(
|
||||
network = ZcashNetwork.fromResources(context),
|
||||
seedPhrase = seedPhrase,
|
||||
birthday = birthday
|
||||
)
|
||||
}
|
||||
}
|
|
@ -8,12 +8,8 @@ class SelectWalletAccountUseCase(
|
|||
private val accountDataSource: AccountDataSource,
|
||||
private val navigationRouter: NavigationRouter
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
account: WalletAccount,
|
||||
hideBottomSheet: suspend () -> Unit
|
||||
) {
|
||||
suspend operator fun invoke(account: WalletAccount) {
|
||||
accountDataSource.selectAccount(account)
|
||||
hideBottomSheet()
|
||||
navigationRouter.back()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,10 @@ class SendTransactionAgainUseCase(
|
|||
) {
|
||||
operator fun invoke(value: DetailedTransactionData) {
|
||||
prefillSendUseCase.request(value)
|
||||
navigationRouter.forward(Send())
|
||||
navigationRouter.forward(
|
||||
Send(
|
||||
isScanZip321Enabled = false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package co.electriccoin.zcash.ui.common.usecase
|
||||
|
||||
import cash.z.ecc.android.bip39.Mnemonics
|
||||
import cash.z.ecc.android.sdk.model.SeedPhrase
|
||||
import java.util.Locale
|
||||
|
||||
class ValidateSeedUseCase {
|
||||
@Suppress("TooGenericExceptionCaught", "SwallowedException")
|
||||
operator fun invoke(words: List<String>): SeedPhrase? {
|
||||
return try {
|
||||
val seed = words.joinToString(" ") { it.trim() }.trim()
|
||||
Mnemonics.MnemonicCode(seed, Locale.ENGLISH.language).validate()
|
||||
SeedPhrase(words)
|
||||
} catch (e: Mnemonics.InvalidWordException) {
|
||||
null
|
||||
} catch (e: Mnemonics.ChecksumException) {
|
||||
null
|
||||
} catch (e: Mnemonics.WordCountException) {
|
||||
null
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -100,9 +100,7 @@ class AuthenticationViewModel(
|
|||
when {
|
||||
(!required || versionInfo.isRunningUnderTestService) -> AuthenticationUIState.NotRequired
|
||||
(state == AuthenticationUIState.Initial) -> {
|
||||
if (secretState == SecretState.None ||
|
||||
secretState == SecretState.NeedsWarning
|
||||
) {
|
||||
if (secretState == SecretState.NONE) {
|
||||
appAccessAuthentication.value = AuthenticationUIState.NotRequired
|
||||
AuthenticationUIState.NotRequired
|
||||
} else {
|
||||
|
@ -146,12 +144,6 @@ class AuthenticationViewModel(
|
|||
val isDeleteWalletAuthenticationRequired: StateFlow<Boolean?> =
|
||||
booleanStateFlow(StandardPreferenceKeys.IS_DELETE_WALLET_AUTHENTICATION)
|
||||
|
||||
val isSeedAuthenticationRequired: StateFlow<Boolean?> =
|
||||
booleanStateFlow(StandardPreferenceKeys.IS_SEED_AUTHENTICATION)
|
||||
|
||||
val isSendFundsAuthenticationRequired: StateFlow<Boolean?> =
|
||||
booleanStateFlow(StandardPreferenceKeys.IS_SEND_FUNDS_AUTHENTICATION)
|
||||
|
||||
/**
|
||||
* Authentication framework result
|
||||
*/
|
||||
|
@ -319,9 +311,6 @@ class AuthenticationViewModel(
|
|||
AuthenticationUseCase.ExportPrivateData ->
|
||||
R.string.authentication_use_case_export_data
|
||||
|
||||
AuthenticationUseCase.SeedRecovery ->
|
||||
R.string.authentication_use_case_seed_recovery
|
||||
|
||||
AuthenticationUseCase.SendFunds ->
|
||||
R.string.authentication_use_case_send_funds
|
||||
}
|
||||
|
|
|
@ -3,10 +3,8 @@ package co.electriccoin.zcash.ui.common.viewmodel
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.configuration.model.map.Configuration
|
||||
import co.electriccoin.zcash.preference.StandardPreferenceProvider
|
||||
import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase
|
||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
@ -16,7 +14,6 @@ import kotlinx.coroutines.flow.flow
|
|||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class OldHomeViewModel(
|
||||
observeConfiguration: ObserveConfigurationUseCase,
|
||||
private val standardPreferenceProvider: StandardPreferenceProvider,
|
||||
) : ViewModel() {
|
||||
/**
|
||||
|
@ -30,8 +27,6 @@ class OldHomeViewModel(
|
|||
*/
|
||||
val isHideBalances: StateFlow<Boolean?> = booleanStateFlow(StandardPreferenceKeys.IS_HIDE_BALANCES)
|
||||
|
||||
val configurationFlow: StateFlow<Configuration?> = observeConfiguration()
|
||||
|
||||
private fun booleanStateFlow(default: BooleanPreferenceDefault): StateFlow<Boolean?> =
|
||||
flow<Boolean?> {
|
||||
emitAll(default.observe(standardPreferenceProvider()))
|
||||
|
|
|
@ -161,16 +161,10 @@ class WalletViewModel(
|
|||
/**
|
||||
* Represents the state of the wallet secret.
|
||||
*/
|
||||
sealed class SecretState {
|
||||
object Loading : SecretState()
|
||||
|
||||
object None : SecretState()
|
||||
|
||||
object NeedsWarning : SecretState()
|
||||
|
||||
class NeedsBackup(val persistableWallet: PersistableWallet) : SecretState()
|
||||
|
||||
class Ready(val persistableWallet: PersistableWallet) : SecretState()
|
||||
enum class SecretState {
|
||||
LOADING,
|
||||
NONE,
|
||||
READY
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.configuration
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import co.electriccoin.zcash.configuration.model.map.Configuration
|
||||
import co.electriccoin.zcash.configuration.model.map.StringConfiguration
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
|
||||
@Suppress("CompositionLocalAllowlist", "CompositionLocalNaming")
|
||||
val RemoteConfig = compositionLocalOf<Configuration> { StringConfiguration(persistentMapOf(), null) }
|
|
@ -1,16 +1,13 @@
|
|||
package co.electriccoin.zcash.ui.screen.accountlist
|
||||
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState
|
||||
import co.electriccoin.zcash.ui.screen.accountlist.view.AccountListView
|
||||
import co.electriccoin.zcash.ui.screen.accountlist.viewmodel.AccountListViewModel
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
@ -20,38 +17,12 @@ import org.koin.androidx.compose.koinViewModel
|
|||
fun AndroidAccountList() {
|
||||
val viewModel = koinViewModel<AccountListViewModel>()
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
val parent = LocalView.current.parent
|
||||
|
||||
SideEffect {
|
||||
(parent as? DialogWindowProvider)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
(parent as? DialogWindowProvider)?.window?.setDimAmount(0f)
|
||||
}
|
||||
|
||||
state?.let {
|
||||
AccountListView(
|
||||
state = it,
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = {
|
||||
state?.onBack?.invoke()
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
sheetState.show()
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.hideBottomSheetRequest.collect {
|
||||
sheetState.hide()
|
||||
state?.onBottomSheetHidden?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
state?.onBack?.invoke()
|
||||
}
|
||||
AccountListView(it)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,16 +2,16 @@ package co.electriccoin.zcash.ui.screen.accountlist.model
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.ModalBottomSheetState
|
||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
|
||||
import co.electriccoin.zcash.ui.design.util.StringResource
|
||||
|
||||
data class AccountListState(
|
||||
val items: List<AccountListItem>?,
|
||||
val isLoading: Boolean,
|
||||
val onBottomSheetHidden: () -> Unit,
|
||||
val addWalletButton: ButtonState?,
|
||||
val onBack: () -> Unit,
|
||||
)
|
||||
override val onBack: () -> Unit,
|
||||
) : ModalBottomSheetState
|
||||
|
||||
data class ZashiAccountListItemState(
|
||||
@DrawableRes val icon: Int,
|
||||
|
|
|
@ -33,13 +33,14 @@ import co.electriccoin.zcash.ui.design.component.ButtonState
|
|||
import co.electriccoin.zcash.ui.design.component.LottieProgress
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiButton
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiButtonDefaults
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiModalBottomSheet
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiScreenModalBottomSheet
|
||||
import co.electriccoin.zcash.ui.design.component.listitem.BaseListItem
|
||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemColors
|
||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemDefaults
|
||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemDesignType
|
||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
|
||||
import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState
|
||||
import co.electriccoin.zcash.ui.design.component.rememberScreenModalBottomSheetState
|
||||
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
|
||||
|
@ -54,16 +55,15 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
internal fun AccountListView(
|
||||
onDismissRequest: () -> Unit,
|
||||
sheetState: SheetState,
|
||||
state: AccountListState
|
||||
state: AccountListState,
|
||||
sheetState: SheetState = rememberScreenModalBottomSheetState(),
|
||||
) {
|
||||
ZashiModalBottomSheet(
|
||||
ZashiScreenModalBottomSheet(
|
||||
state = state,
|
||||
sheetState = sheetState,
|
||||
content = {
|
||||
BottomSheetContent(state)
|
||||
},
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -111,7 +111,10 @@ private fun BottomSheetContent(state: AccountListState) {
|
|||
Spacer(modifier = Modifier.height(32.dp))
|
||||
ZashiButton(
|
||||
state = state.addWalletButton,
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp),
|
||||
colors =
|
||||
ZashiButtonDefaults.secondaryColors(
|
||||
borderColor = ZashiColors.Btns.Secondary.btnSecondaryBorder
|
||||
|
@ -270,11 +273,9 @@ private fun Preview() =
|
|||
)
|
||||
),
|
||||
isLoading = false,
|
||||
onBottomSheetHidden = {},
|
||||
onBack = {},
|
||||
addWalletButton = ButtonState(stringRes("Connect Hardware Wallet"))
|
||||
),
|
||||
onDismissRequest = {},
|
||||
sheetState =
|
||||
rememberModalBottomSheetState(
|
||||
skipHiddenState = true,
|
||||
|
@ -315,11 +316,9 @@ private fun HardwareWalletAddedPreview() =
|
|||
),
|
||||
),
|
||||
isLoading = false,
|
||||
onBottomSheetHidden = {},
|
||||
onBack = {},
|
||||
addWalletButton = null
|
||||
),
|
||||
onDismissRequest = {},
|
||||
sheetState =
|
||||
rememberModalBottomSheetState(
|
||||
skipHiddenState = true,
|
||||
|
|
|
@ -7,7 +7,7 @@ import co.electriccoin.zcash.ui.NavigationRouter
|
|||
import co.electriccoin.zcash.ui.common.model.KeystoneAccount
|
||||
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||
import co.electriccoin.zcash.ui.common.model.ZashiAccount
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveWalletAccountsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletAccountsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.SelectWalletAccountUseCase
|
||||
import co.electriccoin.zcash.ui.design.R
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
|
@ -19,26 +19,20 @@ import co.electriccoin.zcash.ui.screen.accountlist.model.AccountListState
|
|||
import co.electriccoin.zcash.ui.screen.accountlist.model.ZashiAccountListItemState
|
||||
import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.ADDRESS_MAX_LENGTH
|
||||
import co.electriccoin.zcash.ui.screen.connectkeystone.ConnectKeystone
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AccountListViewModel(
|
||||
observeWalletAccounts: ObserveWalletAccountsUseCase,
|
||||
getWalletAccounts: GetWalletAccountsUseCase,
|
||||
private val selectWalletAccount: SelectWalletAccountUseCase,
|
||||
private val navigationRouter: NavigationRouter,
|
||||
) : ViewModel() {
|
||||
val hideBottomSheetRequest = MutableSharedFlow<Unit>()
|
||||
|
||||
private val bottomSheetHiddenResponse = MutableSharedFlow<Unit>()
|
||||
|
||||
@Suppress("SpreadOperator")
|
||||
val state =
|
||||
observeWalletAccounts().map { accounts ->
|
||||
getWalletAccounts.observe().map { accounts ->
|
||||
val items =
|
||||
listOfNotNull(
|
||||
*accounts.orEmpty()
|
||||
|
@ -77,7 +71,6 @@ class AccountListViewModel(
|
|||
AccountListState(
|
||||
items = items,
|
||||
isLoading = accounts == null,
|
||||
onBottomSheetHidden = ::onBottomSheetHidden,
|
||||
onBack = ::onBack,
|
||||
addWalletButton =
|
||||
ButtonState(
|
||||
|
@ -94,35 +87,14 @@ class AccountListViewModel(
|
|||
)
|
||||
|
||||
private fun onShowKeystonePromoClicked() =
|
||||
viewModelScope.launch {
|
||||
hideBottomSheet()
|
||||
navigationRouter.replace(ExternalUrl("https://keyst.one/shop/products/keystone-3-pro?discount=Zashi"))
|
||||
}
|
||||
|
||||
private suspend fun hideBottomSheet() {
|
||||
hideBottomSheetRequest.emit(Unit)
|
||||
bottomSheetHiddenResponse.first()
|
||||
}
|
||||
|
||||
private fun onBottomSheetHidden() =
|
||||
viewModelScope.launch {
|
||||
bottomSheetHiddenResponse.emit(Unit)
|
||||
}
|
||||
|
||||
private fun onAccountClicked(account: WalletAccount) =
|
||||
viewModelScope.launch {
|
||||
selectWalletAccount(account) { hideBottomSheet() }
|
||||
selectWalletAccount(account)
|
||||
}
|
||||
|
||||
private fun onAddWalletButtonClicked() =
|
||||
viewModelScope.launch {
|
||||
hideBottomSheet()
|
||||
navigationRouter.forward(ConnectKeystone)
|
||||
}
|
||||
private fun onAddWalletButtonClicked() = navigationRouter.forward(ConnectKeystone)
|
||||
|
||||
private fun onBack() =
|
||||
viewModelScope.launch {
|
||||
hideBottomSheet()
|
||||
navigationRouter.back()
|
||||
}
|
||||
private fun onBack() = navigationRouter.back()
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import co.electriccoin.zcash.ui.screen.addressbook.model.AddressBookState
|
|||
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
|
||||
import co.electriccoin.zcash.ui.screen.contact.UpdateContactArgs
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanFlow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
|
@ -85,7 +86,7 @@ class AddressBookViewModel(
|
|||
|
||||
private fun onAddContactManuallyClick() = navigationRouter.forward(AddContactArgs(null))
|
||||
|
||||
private fun onScanContactClick() = navigationRouter.forward(Scan(Scan.ADDRESS_BOOK))
|
||||
private fun onScanContactClick() = navigationRouter.forward(Scan(ScanFlow.ADDRESS_BOOK))
|
||||
}
|
||||
|
||||
internal const val ADDRESS_MAX_LENGTH = 20
|
||||
|
|
|
@ -9,9 +9,9 @@ import co.electriccoin.zcash.ui.common.model.AddressBookContact
|
|||
import co.electriccoin.zcash.ui.common.model.KeystoneAccount
|
||||
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||
import co.electriccoin.zcash.ui.common.model.ZashiAccount
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletAccountsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveWalletAccountsUseCase
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiContactListItemState
|
||||
import co.electriccoin.zcash.ui.design.util.ImageResource
|
||||
|
@ -21,6 +21,7 @@ import co.electriccoin.zcash.ui.screen.addressbook.model.AddressBookItem
|
|||
import co.electriccoin.zcash.ui.screen.addressbook.model.AddressBookState
|
||||
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanFlow
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
|
@ -31,12 +32,12 @@ import kotlinx.coroutines.launch
|
|||
|
||||
class SelectRecipientViewModel(
|
||||
observeAddressBookContacts: ObserveAddressBookContactsUseCase,
|
||||
observeWalletAccountsUseCase: ObserveWalletAccountsUseCase,
|
||||
getWalletAccountsUseCase: GetWalletAccountsUseCase,
|
||||
private val observeContactPicked: ObserveContactPickedUseCase,
|
||||
private val navigationRouter: NavigationRouter
|
||||
) : ViewModel() {
|
||||
val state =
|
||||
combine(observeAddressBookContacts(), observeWalletAccountsUseCase()) { contacts, accounts ->
|
||||
combine(observeAddressBookContacts(), getWalletAccountsUseCase.observe()) { contacts, accounts ->
|
||||
if (accounts != null && accounts.size > 1) {
|
||||
createStateWithAccounts(contacts, accounts)
|
||||
} else {
|
||||
|
@ -174,5 +175,5 @@ class SelectRecipientViewModel(
|
|||
|
||||
private fun onAddContactManuallyClick() = navigationRouter.forward(AddContactArgs(null))
|
||||
|
||||
private fun onScanContactClick() = navigationRouter.forward(Scan(Scan.ADDRESS_BOOK))
|
||||
private fun onScanContactClick() = navigationRouter.forward(Scan(ScanFlow.ADDRESS_BOOK))
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.screen.advancedsettings.model
|
||||
package co.electriccoin.zcash.ui.screen.advancedsettings
|
||||
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
|
|
@ -1,4 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.screen.advancedsettings.view
|
||||
package co.electriccoin.zcash.ui.screen.advancedsettings
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
@ -40,8 +40,6 @@ import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
|||
import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions
|
||||
import co.electriccoin.zcash.ui.design.util.scaffoldScrollPadding
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsTag
|
||||
import co.electriccoin.zcash.ui.screen.advancedsettings.model.AdvancedSettingsState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
// TODO [#1271]: Add AdvancedSettingsView Tests
|
|
@ -1,4 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.screen.advancedsettings.viewmodel
|
||||
package co.electriccoin.zcash.ui.screen.advancedsettings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
|
@ -8,11 +8,11 @@ import co.electriccoin.zcash.ui.NavigationTargets
|
|||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToSeedRecoveryUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToTaxExportUseCase
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.advancedsettings.model.AdvancedSettingsState
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
@ -25,6 +25,7 @@ class AdvancedSettingsViewModel(
|
|||
getWalletRestoringState: GetWalletRestoringStateUseCase,
|
||||
private val navigationRouter: NavigationRouter,
|
||||
private val navigateToTaxExport: NavigateToTaxExportUseCase,
|
||||
private val navigateToSeedRecovery: NavigateToSeedRecoveryUseCase
|
||||
) : ViewModel() {
|
||||
val state: StateFlow<AdvancedSettingsState> =
|
||||
getWalletRestoringState.observe()
|
||||
|
@ -45,7 +46,7 @@ class AdvancedSettingsViewModel(
|
|||
ZashiListItemState(
|
||||
title = stringRes(R.string.advanced_settings_recovery),
|
||||
icon = R.drawable.ic_advanced_settings_recovery,
|
||||
onClick = {}
|
||||
onClick = ::onSeedRecoveryClick
|
||||
),
|
||||
ZashiListItemState(
|
||||
title = stringRes(R.string.advanced_settings_export),
|
||||
|
@ -93,4 +94,9 @@ class AdvancedSettingsViewModel(
|
|||
viewModelScope.launch {
|
||||
navigateToTaxExport()
|
||||
}
|
||||
|
||||
private fun onSeedRecoveryClick() =
|
||||
viewModelScope.launch {
|
||||
navigateToSeedRecovery()
|
||||
}
|
||||
}
|
|
@ -7,8 +7,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.advancedsettings.view.AdvancedSettings
|
||||
import co.electriccoin.zcash.ui.screen.advancedsettings.viewmodel.AdvancedSettingsViewModel
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
|
@ -16,7 +14,6 @@ import org.koin.androidx.compose.koinViewModel
|
|||
internal fun WrapAdvancedSettings(
|
||||
goDeleteWallet: () -> Unit,
|
||||
goExportPrivateData: () -> Unit,
|
||||
goSeedRecovery: () -> Unit,
|
||||
) {
|
||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||
val viewModel = koinViewModel<AdvancedSettingsViewModel>()
|
||||
|
@ -28,7 +25,6 @@ internal fun WrapAdvancedSettings(
|
|||
items =
|
||||
originalState.items.mapIndexed { index, item ->
|
||||
when (index) {
|
||||
0 -> item.copy(onClick = goSeedRecovery)
|
||||
1 -> item.copy(onClick = goExportPrivateData)
|
||||
else -> item
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||
private const val APP_ACCESS_TRIGGER_DELAY = 0
|
||||
private const val DELETE_WALLET_TRIGGER_DELAY = 0
|
||||
private const val EXPORT_PRIVATE_DATA_TRIGGER_DELAY = 0
|
||||
private const val SEED_RECOVERY_TRIGGER_DELAY = 0
|
||||
private const val SEND_FUNDS_DELAY = 0
|
||||
internal const val RETRY_TRIGGER_DELAY = 0
|
||||
|
||||
|
@ -82,16 +81,6 @@ private fun WrapAuthenticationUseCases(
|
|||
onFailed = onFailed
|
||||
)
|
||||
}
|
||||
AuthenticationUseCase.SeedRecovery -> {
|
||||
Twig.debug { "Seed Recovery Authentication" }
|
||||
WrapSeedRecoveryAuth(
|
||||
activity = activity,
|
||||
goSeedRecovery = onSuccess,
|
||||
goSupport = goSupport ?: {},
|
||||
onCancel = onCancel,
|
||||
onFailed = onFailed
|
||||
)
|
||||
}
|
||||
AuthenticationUseCase.SendFunds -> {
|
||||
Twig.debug { "Send Funds Authentication" }
|
||||
WrapSendFundsAuth(
|
||||
|
@ -251,79 +240,6 @@ private fun WrapAppExportPrivateDataAuth(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WrapSeedRecoveryAuth(
|
||||
activity: MainActivity,
|
||||
goSupport: () -> Unit,
|
||||
goSeedRecovery: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
onFailed: () -> Unit,
|
||||
) {
|
||||
val authenticationViewModel = koinActivityViewModel<AuthenticationViewModel>()
|
||||
|
||||
val authenticationResult =
|
||||
authenticationViewModel.authenticationResult
|
||||
.collectAsStateWithLifecycle(initialValue = AuthenticationResult.None).value
|
||||
|
||||
when (authenticationResult) {
|
||||
AuthenticationResult.None -> {
|
||||
Twig.info { "Authentication result: initiating" }
|
||||
// Initial state
|
||||
}
|
||||
AuthenticationResult.Success -> {
|
||||
Twig.info { "Authentication result: successful" }
|
||||
authenticationViewModel.resetAuthenticationResult()
|
||||
goSeedRecovery()
|
||||
}
|
||||
AuthenticationResult.Canceled -> {
|
||||
Twig.info { "Authentication result: canceled" }
|
||||
authenticationViewModel.resetAuthenticationResult()
|
||||
onCancel()
|
||||
}
|
||||
AuthenticationResult.Failed -> {
|
||||
Twig.warn { "Authentication result: failed" }
|
||||
authenticationViewModel.resetAuthenticationResult()
|
||||
onFailed()
|
||||
Toast.makeText(activity, stringResource(id = R.string.authentication_toast_failed), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
is AuthenticationResult.Error -> {
|
||||
Twig.error {
|
||||
"Authentication result: error: ${authenticationResult.errorCode}: ${authenticationResult.errorMessage}"
|
||||
}
|
||||
AuthenticationErrorDialog(
|
||||
onDismiss = {
|
||||
// Reset authentication states
|
||||
authenticationViewModel.resetAuthenticationResult()
|
||||
onCancel()
|
||||
},
|
||||
onRetry = {
|
||||
authenticationViewModel.resetAuthenticationResult()
|
||||
authenticationViewModel.authenticate(
|
||||
activity = activity,
|
||||
initialAuthSystemWindowDelay = RETRY_TRIGGER_DELAY.milliseconds,
|
||||
useCase = AuthenticationUseCase.SeedRecovery
|
||||
)
|
||||
},
|
||||
onSupport = {
|
||||
authenticationViewModel.resetAuthenticationResult()
|
||||
goSupport()
|
||||
},
|
||||
reason = authenticationResult
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Starting authentication
|
||||
LaunchedEffect(key1 = true) {
|
||||
authenticationViewModel.authenticate(
|
||||
activity = activity,
|
||||
initialAuthSystemWindowDelay = SEED_RECOVERY_TRIGGER_DELAY.milliseconds,
|
||||
useCase = AuthenticationUseCase.SeedRecovery
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
private fun WrapSendFundsAuth(
|
||||
|
@ -472,8 +388,6 @@ private fun WrapAppAccessAuth(
|
|||
sealed class AuthenticationUseCase {
|
||||
data object AppAccess : AuthenticationUseCase()
|
||||
|
||||
data object SeedRecovery : AuthenticationUseCase()
|
||||
|
||||
data object DeleteWallet : AuthenticationUseCase()
|
||||
|
||||
data object ExportPrivateData : AuthenticationUseCase()
|
||||
|
|
|
@ -2,8 +2,10 @@ package co.electriccoin.zcash.ui.screen.balances
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
||||
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||
import co.electriccoin.zcash.ui.common.repository.ExchangeRateRepository
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
|
@ -22,8 +24,23 @@ class BalanceViewModel(
|
|||
accountDataSource.selectedAccount.filterNotNull(),
|
||||
exchangeRateRepository.state,
|
||||
) { account, exchangeRateUsd ->
|
||||
when {
|
||||
createState(account, exchangeRateUsd)
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
createState(
|
||||
account = accountDataSource.allAccounts.value?.firstOrNull { it.isSelected },
|
||||
exchangeRateUsd = exchangeRateRepository.state.value
|
||||
)
|
||||
)
|
||||
|
||||
private fun createState(
|
||||
account: WalletAccount?,
|
||||
exchangeRateUsd: ExchangeRateState
|
||||
): BalanceState {
|
||||
return when {
|
||||
(
|
||||
account != null &&
|
||||
account.spendableBalance.value == 0L &&
|
||||
account.totalBalance.value > 0L &&
|
||||
(account.hasChangePending || account.hasValuePending)
|
||||
|
@ -37,15 +54,11 @@ class BalanceViewModel(
|
|||
|
||||
else -> {
|
||||
BalanceState.Available(
|
||||
totalBalance = account.totalBalance,
|
||||
spendableBalance = account.spendableBalance,
|
||||
totalBalance = account?.totalBalance ?: Zatoshi(0),
|
||||
spendableBalance = account?.spendableBalance ?: Zatoshi(0),
|
||||
exchangeRate = exchangeRateUsd,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
BalanceState.None(ExchangeRateState.OptedOut)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package co.electriccoin.zcash.ui.screen.home
|
||||
|
||||
import co.electriccoin.zcash.ui.design.component.BigIconButtonState
|
||||
import co.electriccoin.zcash.ui.screen.home.messages.HomeMessageState
|
||||
|
||||
data class HomeState(
|
||||
val receiveButton: BigIconButtonState,
|
||||
val sendButton: BigIconButtonState,
|
||||
val scanButton: BigIconButtonState,
|
||||
val moreButton: BigIconButtonState,
|
||||
val firstButton: BigIconButtonState,
|
||||
val secondButton: BigIconButtonState,
|
||||
val thirdButton: BigIconButtonState,
|
||||
val fourthButton: BigIconButtonState,
|
||||
val message: HomeMessageState?
|
||||
)
|
||||
|
||||
data class HomeRestoreDialogState(
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
package co.electriccoin.zcash.ui.screen.home
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.EnterTransition
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
@ -22,7 +18,6 @@ import androidx.compose.ui.unit.dp
|
|||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.appbar.ZashiMainTopAppBarState
|
||||
import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppBarWithAccountSelection
|
||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||
import co.electriccoin.zcash.ui.design.component.BigIconButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiBigIconButton
|
||||
|
@ -34,7 +29,7 @@ import co.electriccoin.zcash.ui.fixture.BalanceStateFixture
|
|||
import co.electriccoin.zcash.ui.fixture.ZashiMainTopAppBarStateFixture
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalanceState
|
||||
import co.electriccoin.zcash.ui.screen.balances.BalanceWidget
|
||||
import co.electriccoin.zcash.ui.screen.exchangerate.widget.StyledExchangeOptIn
|
||||
import co.electriccoin.zcash.ui.screen.home.messages.HomeMessage
|
||||
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetState
|
||||
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetStateFixture
|
||||
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.createTransactionHistoryWidgets
|
||||
|
@ -74,7 +69,6 @@ private fun Content(
|
|||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||
|
||||
BalanceWidget(
|
||||
modifier =
|
||||
Modifier
|
||||
|
@ -84,39 +78,10 @@ private fun Content(
|
|||
),
|
||||
balanceState = balanceState,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.scaffoldPadding(paddingValues, top = 0.dp, bottom = 0.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ZashiBigIconButton(
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.testTag(HomeTags.RECEIVE),
|
||||
state = state.receiveButton,
|
||||
)
|
||||
ZashiBigIconButton(
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.testTag(HomeTags.SEND),
|
||||
state = state.sendButton,
|
||||
)
|
||||
ZashiBigIconButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
state = state.scanButton,
|
||||
)
|
||||
ZashiBigIconButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
state = state.moreButton,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
|
||||
NavButtons(paddingValues, state)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
HomeMessage(state.message)
|
||||
LazyColumn(
|
||||
modifier =
|
||||
Modifier
|
||||
|
@ -128,29 +93,46 @@ private fun Content(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = balanceState.exchangeRate is ExchangeRateState.OptIn,
|
||||
enter = EnterTransition.None,
|
||||
exit = fadeOut() + slideOutVertically(),
|
||||
@Composable
|
||||
private fun NavButtons(
|
||||
paddingValues: PaddingValues,
|
||||
state: HomeState
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.scaffoldPadding(paddingValues, top = 0.dp, bottom = 0.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(66.dp + paddingValues.calculateTopPadding()))
|
||||
StyledExchangeOptIn(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
state =
|
||||
(balanceState.exchangeRate as? ExchangeRateState.OptIn) ?: ExchangeRateState.OptIn(
|
||||
onDismissClick = {},
|
||||
ZashiBigIconButton(
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.testTag(HomeTags.RECEIVE),
|
||||
state = state.firstButton,
|
||||
)
|
||||
ZashiBigIconButton(
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.testTag(HomeTags.SEND),
|
||||
state = state.secondButton,
|
||||
)
|
||||
ZashiBigIconButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
state = state.thirdButton,
|
||||
)
|
||||
ZashiBigIconButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
state = state.fourthButton,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun Preview() =
|
||||
private fun Preview() {
|
||||
ZcashTheme {
|
||||
HomeView(
|
||||
appBarState = ZashiMainTopAppBarStateFixture.new(),
|
||||
|
@ -158,30 +140,32 @@ private fun Preview() =
|
|||
transactionWidgetState = TransactionHistoryWidgetStateFixture.new(),
|
||||
state =
|
||||
HomeState(
|
||||
receiveButton =
|
||||
firstButton =
|
||||
BigIconButtonState(
|
||||
text = stringRes("Text"),
|
||||
icon = R.drawable.ic_warning,
|
||||
onClick = {}
|
||||
),
|
||||
sendButton =
|
||||
secondButton =
|
||||
BigIconButtonState(
|
||||
text = stringRes("Text"),
|
||||
icon = R.drawable.ic_warning,
|
||||
onClick = {}
|
||||
),
|
||||
scanButton =
|
||||
thirdButton =
|
||||
BigIconButtonState(
|
||||
text = stringRes("Text"),
|
||||
icon = R.drawable.ic_warning,
|
||||
onClick = {}
|
||||
),
|
||||
moreButton =
|
||||
fourthButton =
|
||||
BigIconButtonState(
|
||||
text = stringRes("Text"),
|
||||
icon = R.drawable.ic_warning,
|
||||
onClick = {}
|
||||
),
|
||||
message = null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,28 +4,44 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.NavigationTargets
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.DistributionDimension
|
||||
import co.electriccoin.zcash.ui.common.model.KeystoneAccount
|
||||
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.IsRestoreSuccessDialogVisibleUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase
|
||||
import co.electriccoin.zcash.ui.design.component.BigIconButtonState
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.home.messages.WalletBackupMessageState
|
||||
import co.electriccoin.zcash.ui.screen.integrations.DialogIntegrations
|
||||
import co.electriccoin.zcash.ui.screen.receive.Receive
|
||||
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType
|
||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||
import co.electriccoin.zcash.ui.screen.scan.ScanFlow
|
||||
import co.electriccoin.zcash.ui.screen.seed.backup.SeedBackup
|
||||
import co.electriccoin.zcash.ui.screen.send.Send
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class HomeViewModel(
|
||||
getVersionInfoProvider: GetVersionInfoProvider,
|
||||
getSelectedWalletAccountUseCase: GetSelectedWalletAccountUseCase,
|
||||
private val navigationRouter: NavigationRouter,
|
||||
private val isRestoreSuccessDialogVisible: IsRestoreSuccessDialogVisibleUseCase
|
||||
private val isRestoreSuccessDialogVisible: IsRestoreSuccessDialogVisibleUseCase,
|
||||
private val navigateToCoinbase: NavigateToCoinbaseUseCase
|
||||
) : ViewModel() {
|
||||
private val isMessageVisible = MutableStateFlow(true)
|
||||
|
||||
private val isRestoreDialogVisible: Flow<Boolean?> =
|
||||
isRestoreSuccessDialogVisible.observe()
|
||||
.stateIn(
|
||||
|
@ -48,36 +64,72 @@ class HomeViewModel(
|
|||
)
|
||||
|
||||
val state: StateFlow<HomeState?> =
|
||||
MutableStateFlow(
|
||||
HomeState(
|
||||
receiveButton =
|
||||
combine(getSelectedWalletAccountUseCase.observe(), isMessageVisible) { selectedAccount, isMessageVisible ->
|
||||
createState(getVersionInfoProvider, selectedAccount, isMessageVisible)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
initialValue = null
|
||||
)
|
||||
|
||||
private fun createState(
|
||||
getVersionInfoProvider: GetVersionInfoProvider,
|
||||
selectedAccount: WalletAccount?,
|
||||
isMessageVisible: Boolean
|
||||
) = HomeState(
|
||||
firstButton =
|
||||
BigIconButtonState(
|
||||
text = stringRes("Receive"),
|
||||
text = stringRes(R.string.home_button_receive),
|
||||
icon = R.drawable.ic_home_receive,
|
||||
onClick = ::onReceiveButtonClick,
|
||||
),
|
||||
sendButton =
|
||||
secondButton =
|
||||
BigIconButtonState(
|
||||
text = stringRes("Send"),
|
||||
text = stringRes(R.string.home_button_send),
|
||||
icon = R.drawable.ic_home_send,
|
||||
onClick = ::onSendButtonClick,
|
||||
),
|
||||
scanButton =
|
||||
thirdButton =
|
||||
BigIconButtonState(
|
||||
text = stringRes("Scan"),
|
||||
text = stringRes(R.string.home_button_scan),
|
||||
icon = R.drawable.ic_home_scan,
|
||||
onClick = ::onScanButtonClick,
|
||||
),
|
||||
moreButton =
|
||||
fourthButton =
|
||||
when {
|
||||
getVersionInfoProvider().distributionDimension == DistributionDimension.FOSS ->
|
||||
BigIconButtonState(
|
||||
text = stringRes("More"),
|
||||
text = stringRes(R.string.home_button_request),
|
||||
icon = R.drawable.ic_home_request,
|
||||
onClick = ::onRequestClick,
|
||||
)
|
||||
|
||||
selectedAccount is KeystoneAccount ->
|
||||
BigIconButtonState(
|
||||
text = stringRes(R.string.home_button_buy),
|
||||
icon = R.drawable.ic_home_buy,
|
||||
onClick = ::onBuyClick,
|
||||
)
|
||||
|
||||
else ->
|
||||
BigIconButtonState(
|
||||
text = stringRes(R.string.home_button_more),
|
||||
icon = R.drawable.ic_home_more,
|
||||
onClick = ::onMoreButtonClick,
|
||||
),
|
||||
)
|
||||
).asStateFlow()
|
||||
},
|
||||
message = createWalletBackupMessageState().takeIf { isMessageVisible }
|
||||
)
|
||||
|
||||
fun onRestoreDialogSeenClick() =
|
||||
private fun createWalletBackupMessageState(): WalletBackupMessageState {
|
||||
return WalletBackupMessageState(
|
||||
onClick = {
|
||||
navigationRouter.forward(SeedBackup)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onRestoreDialogSeenClick() =
|
||||
viewModelScope.launch {
|
||||
isRestoreSuccessDialogVisible.setSeen()
|
||||
}
|
||||
|
@ -95,6 +147,15 @@ class HomeViewModel(
|
|||
}
|
||||
|
||||
private fun onScanButtonClick() {
|
||||
navigationRouter.forward(Scan(Scan.HOMEPAGE))
|
||||
navigationRouter.forward(Scan(ScanFlow.HOMEPAGE))
|
||||
}
|
||||
|
||||
private fun onBuyClick() =
|
||||
viewModelScope.launch {
|
||||
navigateToCoinbase(replaceCurrentScreen = false)
|
||||
}
|
||||
|
||||
private fun onRequestClick() {
|
||||
navigationRouter.forward("${NavigationTargets.REQUEST}/${ReceiveAddressType.Unified.ordinal}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
package co.electriccoin.zcash.ui.screen.home.messages
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandIn
|
||||
import androidx.compose.animation.shrinkOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.draw.drawWithCache
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.ClipOp
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.DefaultShadowColor
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.addOutline
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
fun HomeMessage(state: HomeMessageState?) {
|
||||
val cutoutHeight = 16.dp
|
||||
var normalizedState: HomeMessageState? by remember { mutableStateOf(state) }
|
||||
var isVisible by remember { mutableStateOf(state != null) }
|
||||
val bottomCornerSize by animateDpAsState(
|
||||
if (isVisible) cutoutHeight else 0.dp,
|
||||
animationSpec = tween(350)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.background(Color.Gray)
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(cutoutHeight)
|
||||
.zIndex(2f)
|
||||
.bottomOnlyShadow(
|
||||
elevation = 2.dp,
|
||||
shape = RoundedCornerShape(bottomStart = 32.dp, bottomEnd = 32.dp),
|
||||
backgroundColor = ZashiColors.Surfaces.bgPrimary
|
||||
),
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.zIndex(0f),
|
||||
visible = isVisible,
|
||||
enter = expandIn(animationSpec = tween(350)),
|
||||
exit = shrinkOut(animationSpec = tween(350))
|
||||
) {
|
||||
when (normalizedState) {
|
||||
is WalletBackupMessageState ->
|
||||
WalletBackupMessage(
|
||||
state = normalizedState as WalletBackupMessageState,
|
||||
contentPadding =
|
||||
PaddingValues(
|
||||
vertical = cutoutHeight
|
||||
)
|
||||
)
|
||||
|
||||
null -> {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(cutoutHeight)
|
||||
.zIndex(1f)
|
||||
.align(Alignment.BottomCenter)
|
||||
.topOnlyShadow(
|
||||
elevation = 2.dp,
|
||||
shape = RoundedCornerShape(topStart = bottomCornerSize, topEnd = bottomCornerSize),
|
||||
backgroundColor = ZashiColors.Surfaces.bgPrimary
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(state) {
|
||||
if (state != null) {
|
||||
normalizedState = state
|
||||
isVisible = true
|
||||
} else {
|
||||
isVisible = false
|
||||
delay(350)
|
||||
normalizedState = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.bottomOnlyShadow(
|
||||
elevation: Dp,
|
||||
shape: Shape,
|
||||
backgroundColor: Color,
|
||||
clip: Boolean = elevation > 0.dp,
|
||||
ambientColor: Color = DefaultShadowColor,
|
||||
spotColor: Color = DefaultShadowColor,
|
||||
): Modifier =
|
||||
this
|
||||
.drawWithCache {
|
||||
// bottom shadow offset in Px based on elevation
|
||||
val bottomOffsetPx = elevation.toPx()
|
||||
// Adjust the size to extend the bottom by the bottom shadow offset
|
||||
val adjustedSize = Size(size.width, size.height + bottomOffsetPx)
|
||||
val outline = shape.createOutline(adjustedSize, layoutDirection, this)
|
||||
val path = Path().apply { addOutline(outline) }
|
||||
onDrawWithContent {
|
||||
clipPath(path, ClipOp.Intersect) {
|
||||
this@onDrawWithContent.drawContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
.shadow(elevation, shape, clip, ambientColor, spotColor)
|
||||
.background(
|
||||
backgroundColor,
|
||||
shape
|
||||
)
|
||||
|
||||
private fun Modifier.topOnlyShadow(
|
||||
elevation: Dp,
|
||||
shape: Shape,
|
||||
backgroundColor: Color,
|
||||
clip: Boolean = elevation > 0.dp,
|
||||
ambientColor: Color = DefaultShadowColor,
|
||||
spotColor: Color = DefaultShadowColor,
|
||||
): Modifier =
|
||||
this
|
||||
.drawWithCache {
|
||||
// Adjust the size to extend the bottom by the bottom shadow offset
|
||||
val adjustedSize = Size(size.width, size.height)
|
||||
val outline = shape.createOutline(adjustedSize, layoutDirection, this)
|
||||
val path = Path().apply { addOutline(outline) }
|
||||
onDrawWithContent {
|
||||
clipPath(path, ClipOp.Intersect) {
|
||||
this@onDrawWithContent.drawContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
.shadow(elevation, shape, clip, ambientColor, spotColor)
|
||||
.background(
|
||||
backgroundColor,
|
||||
shape
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
package co.electriccoin.zcash.ui.screen.home.messages
|
||||
|
||||
sealed interface HomeMessageState
|
|
@ -0,0 +1,38 @@
|
|||
package co.electriccoin.zcash.ui.screen.home.messages
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun HomeMessageWrapper(
|
||||
color: Color,
|
||||
contentPadding: PaddingValues,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
color = color,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.padding(contentPadding)
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 14.dp
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package co.electriccoin.zcash.ui.screen.home.messages
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.Modifier
|
||||
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.component.BlankSurface
|
||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||
import co.electriccoin.zcash.ui.design.component.HorizontalSpacer
|
||||
import co.electriccoin.zcash.ui.design.component.VerticalSpacer
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiButton
|
||||
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 WalletBackupMessage(
|
||||
state: WalletBackupMessageState,
|
||||
contentPadding: PaddingValues
|
||||
) {
|
||||
HomeMessageWrapper(
|
||||
color = ZashiColors.Utility.Espresso.utilityEspresso100,
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_warning_triangle),
|
||||
contentDescription = null
|
||||
)
|
||||
HorizontalSpacer(16.dp)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.home_message_backup_required_title),
|
||||
style = ZashiTypography.textSm,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = ZashiColors.Utility.Espresso.utilityEspresso900
|
||||
)
|
||||
VerticalSpacer(2.dp)
|
||||
Text(
|
||||
text = stringResource(R.string.home_message_backup_required_subtitle),
|
||||
style = ZashiTypography.textXs,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = ZashiColors.Utility.Espresso.utilityEspresso700
|
||||
)
|
||||
}
|
||||
ZashiButton(
|
||||
modifier = Modifier.height(36.dp),
|
||||
state =
|
||||
ButtonState(
|
||||
onClick = state.onClick,
|
||||
text = stringRes("Start")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class WalletBackupMessageState(
|
||||
val onClick: () -> Unit,
|
||||
) : HomeMessageState
|
||||
|
||||
@PreviewScreens
|
||||
@Composable
|
||||
private fun Preview() =
|
||||
ZcashTheme {
|
||||
BlankSurface {
|
||||
WalletBackupMessage(
|
||||
state =
|
||||
WalletBackupMessageState(
|
||||
onClick = {}
|
||||
),
|
||||
contentPadding = PaddingValues()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,18 +1,13 @@
|
|||
package co.electriccoin.zcash.ui.screen.integrations
|
||||
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState
|
||||
import co.electriccoin.zcash.ui.screen.integrations.view.IntegrationsDialogView
|
||||
import co.electriccoin.zcash.ui.screen.integrations.viewmodel.IntegrationsViewModel
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
@ -20,15 +15,10 @@ import org.koin.core.parameter.parametersOf
|
|||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AndroidDialogIntegrations() {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val parent = LocalView.current.parent
|
||||
val viewModel = koinViewModel<IntegrationsViewModel> { parametersOf(true) }
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
BackHandler(enabled = state != null) {
|
||||
state?.onBack?.invoke()
|
||||
}
|
||||
|
||||
SideEffect {
|
||||
(parent as? DialogWindowProvider)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||
(parent as? DialogWindowProvider)?.window?.setDimAmount(0f)
|
||||
|
@ -37,22 +27,7 @@ fun AndroidDialogIntegrations() {
|
|||
state?.let {
|
||||
IntegrationsDialogView(
|
||||
state = it,
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = {
|
||||
it.onBack()
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
sheetState.show()
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.hideBottomSheetRequest.collect {
|
||||
sheetState.hide()
|
||||
state?.onBottomSheetHidden?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.integrations.view.Integrations
|
||||
import co.electriccoin.zcash.ui.screen.integrations.viewmodel.IntegrationsViewModel
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package co.electriccoin.zcash.ui.screen.integrations.view
|
||||
package co.electriccoin.zcash.ui.screen.integrations
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
|
@ -14,34 +17,37 @@ import androidx.compose.material3.SheetValue
|
|||
import androidx.compose.material3.Text
|
||||
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.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiModalBottomSheet
|
||||
import co.electriccoin.zcash.ui.design.component.HorizontalSpacer
|
||||
import co.electriccoin.zcash.ui.design.component.ZashiScreenModalBottomSheet
|
||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
|
||||
import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState
|
||||
import co.electriccoin.zcash.ui.design.component.rememberScreenModalBottomSheetState
|
||||
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
|
||||
import co.electriccoin.zcash.ui.screen.integrations.model.IntegrationsState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
internal fun IntegrationsDialogView(
|
||||
onDismissRequest: () -> Unit,
|
||||
sheetState: SheetState,
|
||||
state: IntegrationsState
|
||||
state: IntegrationsState,
|
||||
sheetState: SheetState = rememberScreenModalBottomSheetState(),
|
||||
) {
|
||||
ZashiModalBottomSheet(
|
||||
ZashiScreenModalBottomSheet(
|
||||
state = state,
|
||||
sheetState = sheetState,
|
||||
content = {
|
||||
BottomSheetContent(state)
|
||||
},
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -56,8 +62,33 @@ fun BottomSheetContent(state: IntegrationsState) {
|
|||
color = ZashiColors.Text.textPrimary
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
IntegrationItems(state, contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp))
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
IntegrationItems(state, contentPadding = PaddingValues(horizontal = 20.dp, vertical = 16.dp))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = 24.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier,
|
||||
painter = painterResource(R.drawable.ic_info),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(ZashiColors.Text.textTertiary)
|
||||
)
|
||||
HorizontalSpacer(8.dp)
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(id = R.string.integrations_info),
|
||||
textAlign = TextAlign.Start,
|
||||
style = ZashiTypography.textXs,
|
||||
color = ZashiColors.Text.textTertiary
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +99,6 @@ fun BottomSheetContent(state: IntegrationsState) {
|
|||
private fun IntegrationSettings() =
|
||||
ZcashTheme {
|
||||
IntegrationsDialogView(
|
||||
onDismissRequest = {},
|
||||
sheetState =
|
||||
rememberModalBottomSheetState(
|
||||
skipHiddenState = true,
|
||||
|
@ -101,7 +131,6 @@ private fun IntegrationSettings() =
|
|||
onClick = {}
|
||||
),
|
||||
),
|
||||
onBottomSheetHidden = {}
|
||||
),
|
||||
)
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
package co.electriccoin.zcash.ui.screen.integrations.model
|
||||
package co.electriccoin.zcash.ui.screen.integrations
|
||||
|
||||
import co.electriccoin.zcash.ui.design.component.ModalBottomSheetState
|
||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
|
||||
import co.electriccoin.zcash.ui.design.util.StringResource
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class IntegrationsState(
|
||||
val disabledInfo: StringResource?,
|
||||
val onBack: () -> Unit,
|
||||
override val onBack: () -> Unit,
|
||||
val items: ImmutableList<ZashiListItemState>,
|
||||
val onBottomSheetHidden: () -> Unit,
|
||||
)
|
||||
) : ModalBottomSheetState
|
|
@ -1,4 +1,4 @@
|
|||
package co.electriccoin.zcash.ui.screen.integrations.view
|
||||
package co.electriccoin.zcash.ui.screen.integrations
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
@ -44,7 +44,6 @@ import co.electriccoin.zcash.ui.design.util.StringResource
|
|||
import co.electriccoin.zcash.ui.design.util.getValue
|
||||
import co.electriccoin.zcash.ui.design.util.scaffoldScrollPadding
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.integrations.model.IntegrationsState
|
||||
import co.electriccoin.zcash.ui.screen.settings.SettingsTag
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
|
@ -198,7 +197,6 @@ private fun IntegrationSettings() =
|
|||
onClick = {}
|
||||
),
|
||||
),
|
||||
onBottomSheetHidden = {}
|
||||
),
|
||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
||||
)
|
|
@ -0,0 +1,141 @@
|
|||
package co.electriccoin.zcash.ui.screen.integrations
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.KeystoneAccount
|
||||
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.model.ZashiAccount
|
||||
import co.electriccoin.zcash.ui.common.provider.GetZcashCurrencyProvider
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetCoinbaseStatusUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetFlexaStatusUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetKeystoneStatusUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetSelectedWalletAccountUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.Status
|
||||
import co.electriccoin.zcash.ui.common.usecase.Status.DISABLED
|
||||
import co.electriccoin.zcash.ui.common.usecase.Status.ENABLED
|
||||
import co.electriccoin.zcash.ui.common.usecase.Status.UNAVAILABLE
|
||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.connectkeystone.ConnectKeystone
|
||||
import co.electriccoin.zcash.ui.screen.flexa.Flexa
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class IntegrationsViewModel(
|
||||
getZcashCurrency: GetZcashCurrencyProvider,
|
||||
getWalletRestoringState: GetWalletRestoringStateUseCase,
|
||||
getSelectedWalletAccount: GetSelectedWalletAccountUseCase,
|
||||
getCoinbaseStatus: GetCoinbaseStatusUseCase,
|
||||
getFlexaStatus: GetFlexaStatusUseCase,
|
||||
getKeystoneStatus: GetKeystoneStatusUseCase,
|
||||
private val isDialog: Boolean,
|
||||
private val navigationRouter: NavigationRouter,
|
||||
private val navigateToCoinbase: NavigateToCoinbaseUseCase,
|
||||
) : ViewModel() {
|
||||
private val isRestoring = getWalletRestoringState.observe().map { it == WalletRestoringState.RESTORING }
|
||||
|
||||
val state =
|
||||
combine(
|
||||
isRestoring,
|
||||
getSelectedWalletAccount.observe(),
|
||||
getCoinbaseStatus.observe(),
|
||||
getFlexaStatus.observe(),
|
||||
getKeystoneStatus.observe(),
|
||||
) { isRestoring, selectedAccount, coinbaseStatus, flexaStatus, keystoneStatus ->
|
||||
createState(
|
||||
isRestoring = isRestoring,
|
||||
getZcashCurrency = getZcashCurrency,
|
||||
selectedAccount = selectedAccount,
|
||||
flexaStatus = flexaStatus,
|
||||
coinbaseStatus = coinbaseStatus,
|
||||
keystoneStatus = keystoneStatus,
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
initialValue = null
|
||||
)
|
||||
|
||||
private fun createState(
|
||||
isRestoring: Boolean,
|
||||
getZcashCurrency: GetZcashCurrencyProvider,
|
||||
selectedAccount: WalletAccount?,
|
||||
flexaStatus: Status,
|
||||
coinbaseStatus: Status,
|
||||
keystoneStatus: Status
|
||||
) = IntegrationsState(
|
||||
disabledInfo =
|
||||
when {
|
||||
isRestoring -> stringRes(R.string.integrations_disabled_info)
|
||||
selectedAccount is KeystoneAccount -> stringRes(R.string.integrations_disabled_info_flexa)
|
||||
else -> null
|
||||
},
|
||||
onBack = ::onBack,
|
||||
items =
|
||||
listOfNotNull(
|
||||
ZashiListItemState(
|
||||
// Set the wallet currency by app build is more future-proof, although we hide it from
|
||||
// the UI in the Testnet build
|
||||
icon = R.drawable.ic_integrations_coinbase,
|
||||
title = stringRes(R.string.integrations_coinbase, getZcashCurrency.getLocalizedName()),
|
||||
subtitle =
|
||||
stringRes(
|
||||
R.string.integrations_coinbase_subtitle,
|
||||
getZcashCurrency.getLocalizedName()
|
||||
),
|
||||
onClick = ::onBuyWithCoinbaseClicked
|
||||
).takeIf { coinbaseStatus != UNAVAILABLE },
|
||||
ZashiListItemState(
|
||||
// Set the wallet currency by app build is more future-proof, although we hide it from
|
||||
// the UI in the Testnet build
|
||||
isEnabled = isRestoring.not() && selectedAccount is ZashiAccount,
|
||||
icon =
|
||||
when (flexaStatus) {
|
||||
ENABLED -> R.drawable.ic_integrations_flexa
|
||||
DISABLED -> R.drawable.ic_integrations_flexa_disabled
|
||||
UNAVAILABLE -> R.drawable.ic_integrations_flexa_disabled
|
||||
},
|
||||
title = stringRes(R.string.integrations_flexa),
|
||||
subtitle = stringRes(R.string.integrations_flexa_subtitle),
|
||||
onClick = ::onFlexaClicked
|
||||
).takeIf { flexaStatus != UNAVAILABLE },
|
||||
ZashiListItemState(
|
||||
title = stringRes(R.string.integrations_keystone),
|
||||
subtitle = stringRes(R.string.integrations_keystone_subtitle),
|
||||
icon = R.drawable.ic_integrations_keystone,
|
||||
onClick = ::onConnectKeystoneClick
|
||||
).takeIf { keystoneStatus != UNAVAILABLE },
|
||||
).toImmutableList(),
|
||||
)
|
||||
|
||||
private fun onBack() = navigationRouter.back()
|
||||
|
||||
private fun onBuyWithCoinbaseClicked() =
|
||||
viewModelScope.launch {
|
||||
navigateToCoinbase(isDialog)
|
||||
}
|
||||
|
||||
private fun onConnectKeystoneClick() =
|
||||
viewModelScope.launch {
|
||||
navigationRouter.replace(ConnectKeystone)
|
||||
}
|
||||
|
||||
private fun onFlexaClicked() {
|
||||
if (isDialog) {
|
||||
navigationRouter.replace(Flexa)
|
||||
} else {
|
||||
navigationRouter.forward(Flexa)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.integrations.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.common.model.KeystoneAccount
|
||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.provider.GetZcashCurrencyProvider
|
||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.IsCoinbaseAvailableUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.IsFlexaAvailableUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase
|
||||
import co.electriccoin.zcash.ui.common.usecase.ObserveWalletAccountsUseCase
|
||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
|
||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||
import co.electriccoin.zcash.ui.screen.connectkeystone.ConnectKeystone
|
||||
import co.electriccoin.zcash.ui.screen.flexa.Flexa
|
||||
import co.electriccoin.zcash.ui.screen.integrations.model.IntegrationsState
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.WhileSubscribed
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class IntegrationsViewModel(
|
||||
getZcashCurrency: GetZcashCurrencyProvider,
|
||||
getWalletRestoringState: GetWalletRestoringStateUseCase,
|
||||
isFlexaAvailableUseCase: IsFlexaAvailableUseCase,
|
||||
isCoinbaseAvailable: IsCoinbaseAvailableUseCase,
|
||||
observeWalletAccounts: ObserveWalletAccountsUseCase,
|
||||
private val isDialog: Boolean,
|
||||
private val navigationRouter: NavigationRouter,
|
||||
private val navigateToCoinbase: NavigateToCoinbaseUseCase,
|
||||
) : ViewModel() {
|
||||
val hideBottomSheetRequest = MutableSharedFlow<Unit>()
|
||||
|
||||
private val bottomSheetHiddenResponse = MutableSharedFlow<Unit>()
|
||||
|
||||
private val isRestoring = getWalletRestoringState.observe().map { it == WalletRestoringState.RESTORING }
|
||||
|
||||
val state =
|
||||
combine(
|
||||
isFlexaAvailableUseCase.observe(),
|
||||
isCoinbaseAvailable.observe(),
|
||||
isRestoring,
|
||||
observeWalletAccounts()
|
||||
) { isFlexaAvailable, isCoinbaseAvailable, isRestoring, accounts ->
|
||||
IntegrationsState(
|
||||
disabledInfo =
|
||||
stringRes(R.string.integrations_disabled_info)
|
||||
.takeIf { isRestoring },
|
||||
onBack = ::onBack,
|
||||
items =
|
||||
listOfNotNull(
|
||||
ZashiListItemState(
|
||||
// Set the wallet currency by app build is more future-proof, although we hide it from
|
||||
// the UI in the Testnet build
|
||||
icon = R.drawable.ic_integrations_coinbase,
|
||||
title = stringRes(R.string.integrations_coinbase, getZcashCurrency.getLocalizedName()),
|
||||
subtitle =
|
||||
stringRes(
|
||||
R.string.integrations_coinbase_subtitle,
|
||||
getZcashCurrency.getLocalizedName()
|
||||
),
|
||||
onClick = ::onBuyWithCoinbaseClicked
|
||||
).takeIf { isCoinbaseAvailable == true },
|
||||
ZashiListItemState(
|
||||
// Set the wallet currency by app build is more future-proof, although we hide it from
|
||||
// the UI in the Testnet build
|
||||
isEnabled = isRestoring.not(),
|
||||
icon =
|
||||
if (isRestoring.not()) {
|
||||
R.drawable.ic_integrations_flexa
|
||||
} else {
|
||||
R.drawable.ic_integrations_flexa_disabled
|
||||
},
|
||||
title = stringRes(R.string.integrations_flexa),
|
||||
subtitle = stringRes(R.string.integrations_flexa_subtitle),
|
||||
onClick = ::onFlexaClicked
|
||||
).takeIf { isFlexaAvailable == true },
|
||||
ZashiListItemState(
|
||||
title = stringRes(R.string.integrations_keystone),
|
||||
subtitle = stringRes(R.string.integrations_keystone_subtitle),
|
||||
icon = R.drawable.ic_integrations_keystone,
|
||||
onClick = ::onConnectKeystoneClick
|
||||
).takeIf { accounts.orEmpty().none { it is KeystoneAccount } },
|
||||
).toImmutableList(),
|
||||
onBottomSheetHidden = ::onBottomSheetHidden
|
||||
)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||
initialValue = null
|
||||
)
|
||||
|
||||
private fun onBack() = navigationRouter.back()
|
||||
|
||||
private suspend fun hideBottomSheet() {
|
||||
if (isDialog) {
|
||||
hideBottomSheetRequest.emit(Unit)
|
||||
bottomSheetHiddenResponse.first()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBottomSheetHidden() =
|
||||
viewModelScope.launch {
|
||||
bottomSheetHiddenResponse.emit(Unit)
|
||||
}
|
||||
|
||||
private fun onBuyWithCoinbaseClicked() =
|
||||
viewModelScope.launch {
|
||||
hideBottomSheet()
|
||||
navigateToCoinbase(isDialog)
|
||||
}
|
||||
|
||||
private fun onConnectKeystoneClick() =
|
||||
viewModelScope.launch {
|
||||
hideBottomSheet()
|
||||
navigationRouter.replace(ConnectKeystone)
|
||||
}
|
||||
|
||||
private fun onFlexaClicked() =
|
||||
viewModelScope.launch {
|
||||
if (isDialog) {
|
||||
hideBottomSheet()
|
||||
navigationRouter.replace(Flexa)
|
||||
} else {
|
||||
hideBottomSheet()
|
||||
navigationRouter.forward(Flexa)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
package co.electriccoin.zcash.ui.screen.onboarding
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||
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.type.fromResources
|
||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalActivity
|
||||
import co.electriccoin.zcash.ui.common.model.OnboardingState
|
||||
import co.electriccoin.zcash.ui.common.model.VersionInfo
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.view.Onboarding
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
||||
import co.electriccoin.zcash.ui.screen.restore.WrapRestore
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun WrapOnboarding() {
|
||||
val activity = LocalActivity.current
|
||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||
val onboardingViewModel = koinActivityViewModel<OnboardingViewModel>()
|
||||
|
||||
val versionInfo = VersionInfo.new(activity.applicationContext)
|
||||
|
||||
// TODO [#383]: https://github.com/Electric-Coin-Company/zashi-android/issues/383
|
||||
// TODO [#383]: Refactoring of UI state retention into rememberSaveable fields
|
||||
|
||||
if (!onboardingViewModel.isImporting.collectAsStateWithLifecycle().value) {
|
||||
val onCreateWallet = {
|
||||
walletViewModel.persistOnboardingState(OnboardingState.NEEDS_WARN)
|
||||
}
|
||||
val onImportWallet = {
|
||||
// In the case of the app currently being messed with by the robo test runner on
|
||||
// Firebase Test Lab or Google Play pre-launch report, we want to skip creating
|
||||
// a new or restoring an existing wallet screens by persisting an existing wallet
|
||||
// with a mock seed.
|
||||
if (FirebaseTestLabUtil.isFirebaseTestLab(activity.applicationContext)) {
|
||||
persistExistingWalletWithSeedPhrase(
|
||||
activity.applicationContext,
|
||||
walletViewModel,
|
||||
SeedPhrase.new(WalletFixture.Alice.seedPhrase),
|
||||
birthday = WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(activity.applicationContext))
|
||||
)
|
||||
} else {
|
||||
onboardingViewModel.setIsImporting(true)
|
||||
}
|
||||
}
|
||||
|
||||
val onFixtureWallet: (String) -> Unit = { seed ->
|
||||
persistExistingWalletWithSeedPhrase(
|
||||
activity.applicationContext,
|
||||
walletViewModel,
|
||||
SeedPhrase.new(seed),
|
||||
birthday = WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(activity.applicationContext))
|
||||
)
|
||||
}
|
||||
|
||||
Onboarding(
|
||||
isDebugMenuEnabled = versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService,
|
||||
onImportWallet = onImportWallet,
|
||||
onCreateWallet = onCreateWallet,
|
||||
onFixtureWallet = onFixtureWallet
|
||||
)
|
||||
|
||||
activity.reportFullyDrawn()
|
||||
} else {
|
||||
WrapRestore()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists existing wallet together with the backup complete flag to disk. Be aware of that, it
|
||||
* triggers navigation changes, as we observe the WalletViewModel.secretState.
|
||||
*
|
||||
* Write the backup complete flag first, then the seed phrase. That avoids the UI flickering to
|
||||
* the backup screen. Assume if a user is restoring from a backup, then the user has a valid backup.
|
||||
*
|
||||
* @param seedPhrase to be persisted as part of the wallet.
|
||||
* @param birthday optional user provided birthday to be persisted as part of the wallet.
|
||||
*/
|
||||
internal fun persistExistingWalletWithSeedPhrase(
|
||||
context: Context,
|
||||
walletViewModel: WalletViewModel,
|
||||
seedPhrase: SeedPhrase,
|
||||
birthday: BlockHeight?
|
||||
) {
|
||||
walletViewModel.persistExistingWalletWithSeedPhrase(
|
||||
network = ZcashNetwork.fromResources(context),
|
||||
seedPhrase = seedPhrase,
|
||||
birthday = birthday
|
||||
)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package co.electriccoin.zcash.ui.screen.onboarding
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data object Onboarding
|
|
@ -0,0 +1,170 @@
|
|||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
package co.electriccoin.zcash.ui.screen.onboarding
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.dialog
|
||||
import androidx.navigation.toRoute
|
||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||
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.type.fromResources
|
||||
import co.electriccoin.zcash.spackle.FirebaseTestLabUtil
|
||||
import co.electriccoin.zcash.ui.MainActivity
|
||||
import co.electriccoin.zcash.ui.NavigationRouter
|
||||
import co.electriccoin.zcash.ui.Navigator
|
||||
import co.electriccoin.zcash.ui.NavigatorImpl
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalActivity
|
||||
import co.electriccoin.zcash.ui.common.compose.LocalNavController
|
||||
import co.electriccoin.zcash.ui.common.model.OnboardingState
|
||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
||||
import co.electriccoin.zcash.ui.design.LocalKeyboardManager
|
||||
import co.electriccoin.zcash.ui.design.LocalSheetStateManager
|
||||
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.enterTransition
|
||||
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.exitTransition
|
||||
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.popEnterTransition
|
||||
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.popExitTransition
|
||||
import co.electriccoin.zcash.ui.screen.flexa.FlexaViewModel
|
||||
import co.electriccoin.zcash.ui.screen.onboarding.view.Onboarding
|
||||
import co.electriccoin.zcash.ui.screen.restore.date.AndroidRestoreBDDate
|
||||
import co.electriccoin.zcash.ui.screen.restore.date.RestoreBDDate
|
||||
import co.electriccoin.zcash.ui.screen.restore.estimation.AndroidRestoreBDEstimation
|
||||
import co.electriccoin.zcash.ui.screen.restore.estimation.RestoreBDEstimation
|
||||
import co.electriccoin.zcash.ui.screen.restore.height.AndroidRestoreBDHeight
|
||||
import co.electriccoin.zcash.ui.screen.restore.height.RestoreBDHeight
|
||||
import co.electriccoin.zcash.ui.screen.restore.info.AndroidSeedInfo
|
||||
import co.electriccoin.zcash.ui.screen.restore.info.SeedInfo
|
||||
import co.electriccoin.zcash.ui.screen.restore.seed.AndroidRestoreSeed
|
||||
import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeed
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@Composable
|
||||
fun MainActivity.OnboardingNavigation() {
|
||||
val activity = LocalActivity.current
|
||||
val navigationRouter = koinInject<NavigationRouter>()
|
||||
val navController = LocalNavController.current
|
||||
val keyboardManager = LocalKeyboardManager.current
|
||||
val flexaViewModel = koinViewModel<FlexaViewModel>()
|
||||
val sheetStateManager = LocalSheetStateManager.current
|
||||
|
||||
val navigator: Navigator =
|
||||
remember(
|
||||
navController,
|
||||
flexaViewModel,
|
||||
keyboardManager,
|
||||
sheetStateManager
|
||||
) {
|
||||
NavigatorImpl(
|
||||
activity = this@OnboardingNavigation,
|
||||
navController = navController,
|
||||
flexaViewModel = flexaViewModel,
|
||||
keyboardManager = keyboardManager,
|
||||
sheetStateManager = sheetStateManager
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
navigationRouter.observePipeline().collect {
|
||||
navigator.executeCommand(it)
|
||||
}
|
||||
}
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Onboarding,
|
||||
enterTransition = { enterTransition() },
|
||||
exitTransition = { exitTransition() },
|
||||
popEnterTransition = { popEnterTransition() },
|
||||
popExitTransition = { popExitTransition() }
|
||||
) {
|
||||
composable<Onboarding> {
|
||||
Onboarding(
|
||||
onImportWallet = {
|
||||
// In the case of the app currently being messed with by the robo test runner on
|
||||
// Firebase Test Lab or Google Play pre-launch report, we want to skip creating
|
||||
// a new or restoring an existing wallet screens by persisting an existing wallet
|
||||
// with a mock seed.
|
||||
if (FirebaseTestLabUtil.isFirebaseTestLab(activity.applicationContext)) {
|
||||
persistExistingWalletWithSeedPhrase(
|
||||
activity.applicationContext,
|
||||
walletViewModel,
|
||||
SeedPhrase.new(WalletFixture.Alice.seedPhrase),
|
||||
WalletFixture.Alice.getBirthday(ZcashNetwork.fromResources(activity.applicationContext))
|
||||
)
|
||||
} else {
|
||||
navigationRouter.forward(RestoreSeed)
|
||||
}
|
||||
},
|
||||
onCreateWallet = {
|
||||
if (FirebaseTestLabUtil.isFirebaseTestLab(applicationContext)) {
|
||||
persistExistingWalletWithSeedPhrase(
|
||||
applicationContext,
|
||||
walletViewModel,
|
||||
SeedPhrase.new(WalletFixture.Alice.seedPhrase),
|
||||
WalletFixture.Alice.getBirthday(
|
||||
ZcashNetwork.fromResources(
|
||||
applicationContext
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
walletViewModel.persistOnboardingState(OnboardingState.READY)
|
||||
walletViewModel.persistNewWalletAndRestoringState(WalletRestoringState.INITIATING)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
composable<RestoreSeed> {
|
||||
AndroidRestoreSeed()
|
||||
}
|
||||
composable<RestoreBDHeight> {
|
||||
AndroidRestoreBDHeight(it.toRoute())
|
||||
}
|
||||
composable<RestoreBDDate> {
|
||||
AndroidRestoreBDDate()
|
||||
}
|
||||
composable<RestoreBDEstimation> {
|
||||
AndroidRestoreBDEstimation()
|
||||
}
|
||||
dialog<SeedInfo>(
|
||||
dialogProperties =
|
||||
DialogProperties(
|
||||
dismissOnBackPress = false,
|
||||
dismissOnClickOutside = false,
|
||||
)
|
||||
) {
|
||||
AndroidSeedInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists existing wallet together with the backup complete flag to disk. Be aware of that, it
|
||||
* triggers navigation changes, as we observe the WalletViewModel.secretState.
|
||||
*
|
||||
* Write the backup complete flag first, then the seed phrase. That avoids the UI flickering to
|
||||
* the backup screen. Assume if a user is restoring from a backup, then the user has a valid backup.
|
||||
*
|
||||
* @param seedPhrase to be persisted as part of the wallet.
|
||||
* @param birthday optional user provided birthday to be persisted as part of the wallet.
|
||||
*/
|
||||
internal fun persistExistingWalletWithSeedPhrase(
|
||||
context: Context,
|
||||
walletViewModel: WalletViewModel,
|
||||
seedPhrase: SeedPhrase,
|
||||
birthday: BlockHeight?
|
||||
) {
|
||||
walletViewModel.persistExistingWalletWithSeedPhrase(
|
||||
network = ZcashNetwork.fromResources(context),
|
||||
seedPhrase = seedPhrase,
|
||||
birthday = birthday
|
||||
)
|
||||
}
|
|
@ -4,7 +4,6 @@ package co.electriccoin.zcash.ui.screen.onboarding.view
|
|||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
@ -30,7 +29,6 @@ import androidx.compose.ui.res.painterResource
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
||||
import cash.z.ecc.sdk.type.ZcashCurrency
|
||||
import co.electriccoin.zcash.ui.R
|
||||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||
|
@ -49,10 +47,8 @@ import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
|||
private fun OnboardingComposablePreview() {
|
||||
ZcashTheme {
|
||||
Onboarding(
|
||||
isDebugMenuEnabled = true,
|
||||
onImportWallet = {},
|
||||
onCreateWallet = {},
|
||||
onFixtureWallet = {}
|
||||
onCreateWallet = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -63,10 +59,8 @@ private fun OnboardingComposablePreview() {
|
|||
*/
|
||||
@Composable
|
||||
fun Onboarding(
|
||||
isDebugMenuEnabled: Boolean,
|
||||
onImportWallet: () -> Unit,
|
||||
onCreateWallet: () -> Unit,
|
||||
onFixtureWallet: (String) -> Unit
|
||||
onCreateWallet: () -> Unit
|
||||
) {
|
||||
Scaffold { paddingValues ->
|
||||
Box(
|
||||
|
@ -90,10 +84,8 @@ fun Onboarding(
|
|||
)
|
||||
) {
|
||||
OnboardingMainContent(
|
||||
isDebugMenuEnabled = isDebugMenuEnabled,
|
||||
onCreateWallet = onCreateWallet,
|
||||
onFixtureWallet = onFixtureWallet,
|
||||
onImportWallet = onImportWallet,
|
||||
onCreateWallet = onCreateWallet,
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(
|
||||
|
@ -111,8 +103,6 @@ fun Onboarding(
|
|||
private fun OnboardingMainContent(
|
||||
onImportWallet: () -> Unit,
|
||||
onCreateWallet: () -> Unit,
|
||||
onFixtureWallet: (String) -> Unit,
|
||||
isDebugMenuEnabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
|
@ -123,18 +113,10 @@ private fun OnboardingMainContent(
|
|||
.then(modifier),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
var imageModifier =
|
||||
val imageModifier =
|
||||
Modifier
|
||||
.height(ZcashTheme.dimens.inScreenZcashLogoHeight)
|
||||
.width(ZcashTheme.dimens.inScreenZcashLogoWidth)
|
||||
if (isDebugMenuEnabled) {
|
||||
imageModifier =
|
||||
imageModifier.then(
|
||||
Modifier.clickable {
|
||||
onFixtureWallet(WalletFixture.Alice.seedPhrase)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
package co.electriccoin.zcash.ui.screen.onboarding.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
|
||||
/*
|
||||
* Android-specific ViewModel. This is used to save and restore state across Activity recreations
|
||||
* outside of the Compose framework.
|
||||
*/
|
||||
class OnboardingViewModel(
|
||||
application: Application,
|
||||
private val savedStateHandle: SavedStateHandle
|
||||
) : AndroidViewModel(application) {
|
||||
// This is a bit weird being placed here, but onboarding currently is considered complete when
|
||||
// the user has a persisted wallet. Also import allows the user to go back to onboarding, while
|
||||
// creating a new wallet does not.
|
||||
val isImporting = savedStateHandle.getStateFlow(KEY_IS_IMPORTING, false)
|
||||
|
||||
fun setIsImporting(isImporting: Boolean) {
|
||||
savedStateHandle[KEY_IS_IMPORTING] = isImporting
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val KEY_IS_IMPORTING = "is_importing" // $NON-NLS
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue