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> {
|
override fun getConfigurationFlow(): Flow<Configuration> {
|
||||||
return if (configurationProviders.isEmpty()) {
|
return if (configurationProviders.isEmpty()) {
|
||||||
flowOf(MergingConfiguration(persistentListOf<Configuration>()))
|
flowOf(MergingConfiguration(persistentListOf()))
|
||||||
} else {
|
} else {
|
||||||
combine(configurationProviders.map { it.getConfigurationFlow() }) { configurations ->
|
combine(configurationProviders.map { it.getConfigurationFlow() }) { configurations ->
|
||||||
MergingConfiguration(configurations.toList().toPersistentList())
|
MergingConfiguration(configurations.toList().toPersistentList())
|
||||||
|
|
|
@ -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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
|
|
||||||
|
@ -66,7 +68,9 @@ fun LabeledCheckBox(
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
checked: Boolean = false,
|
checked: Boolean = false,
|
||||||
checkBoxTestTag: String? = null
|
checkBoxTestTag: String? = null,
|
||||||
|
color: Color = ZcashTheme.colors.textPrimary,
|
||||||
|
style: TextStyle = ZcashTheme.extendedTypography.checkboxText
|
||||||
) {
|
) {
|
||||||
val (checkedState, setCheckedState) = rememberSaveable { mutableStateOf(checked) }
|
val (checkedState, setCheckedState) = rememberSaveable { mutableStateOf(checked) }
|
||||||
|
|
||||||
|
@ -114,8 +118,8 @@ fun LabeledCheckBox(
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = AnnotatedString(text),
|
text = AnnotatedString(text),
|
||||||
color = ZcashTheme.colors.textPrimary,
|
color = color,
|
||||||
style = ZcashTheme.extendedTypography.checkboxText
|
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,
|
text: String,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
style: TextStyle = ZashiButtonDefaults.style,
|
|
||||||
shape: Shape = ZashiButtonDefaults.shape,
|
|
||||||
contentPadding: PaddingValues = ZashiButtonDefaults.contentPadding,
|
|
||||||
@DrawableRes icon: Int? = null,
|
@DrawableRes icon: Int? = null,
|
||||||
@DrawableRes trailingIcon: Int? = null,
|
@DrawableRes trailingIcon: Int? = null,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
isLoading: Boolean = false,
|
isLoading: Boolean = false,
|
||||||
|
style: TextStyle = ZashiButtonDefaults.style,
|
||||||
|
shape: Shape = ZashiButtonDefaults.shape,
|
||||||
|
contentPadding: PaddingValues = ZashiButtonDefaults.contentPadding,
|
||||||
colors: ZashiButtonColors = ZashiButtonDefaults.primaryColors(),
|
colors: ZashiButtonColors = ZashiButtonDefaults.primaryColors(),
|
||||||
content: @Composable RowScope.(ZashiButtonScope) -> Unit = ZashiButtonDefaults.content
|
content: @Composable RowScope.(ZashiButtonScope) -> Unit = ZashiButtonDefaults.content
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -21,7 +21,9 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import co.electriccoin.zcash.ui.design.R
|
import co.electriccoin.zcash.ui.design.R
|
||||||
|
@ -40,6 +42,9 @@ fun ZashiCheckbox(
|
||||||
isChecked: Boolean,
|
isChecked: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
style: TextStyle = ZashiTypography.textSm,
|
||||||
|
fontWeight: FontWeight = FontWeight.Medium,
|
||||||
|
color: Color = ZashiColors.Text.textPrimary,
|
||||||
) {
|
) {
|
||||||
ZashiCheckbox(
|
ZashiCheckbox(
|
||||||
state =
|
state =
|
||||||
|
@ -49,6 +54,9 @@ fun ZashiCheckbox(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
),
|
),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
style = style,
|
||||||
|
fontWeight = fontWeight,
|
||||||
|
color = color,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +64,9 @@ fun ZashiCheckbox(
|
||||||
fun ZashiCheckbox(
|
fun ZashiCheckbox(
|
||||||
state: CheckboxState,
|
state: CheckboxState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
style: TextStyle = ZashiTypography.textSm,
|
||||||
|
fontWeight: FontWeight = FontWeight.Medium,
|
||||||
|
color: Color = ZashiColors.Text.textPrimary,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
|
@ -70,9 +81,9 @@ fun ZashiCheckbox(
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = state.text.getValue(),
|
text = state.text.getValue(),
|
||||||
style = ZashiTypography.textSm,
|
style = style,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = fontWeight,
|
||||||
color = ZashiColors.Text.textPrimary,
|
color = color,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
@ -14,7 +13,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
@ -41,30 +39,17 @@ fun ZashiChipButton(
|
||||||
border: BorderStroke? = ZashiChipButtonDefaults.border,
|
border: BorderStroke? = ZashiChipButtonDefaults.border,
|
||||||
color: Color = ZashiChipButtonDefaults.color,
|
color: Color = ZashiChipButtonDefaults.color,
|
||||||
contentPadding: PaddingValues = ZashiChipButtonDefaults.contentPadding,
|
contentPadding: PaddingValues = ZashiChipButtonDefaults.contentPadding,
|
||||||
hasRippleEffect: Boolean = true,
|
|
||||||
textStyle: TextStyle = ZashiChipButtonDefaults.textStyle,
|
textStyle: TextStyle = ZashiChipButtonDefaults.textStyle,
|
||||||
endIconSpacer: Dp = ZashiChipButtonDefaults.endIconSpacer,
|
endIconSpacer: Dp = ZashiChipButtonDefaults.endIconSpacer,
|
||||||
) {
|
) {
|
||||||
val clickableModifier =
|
|
||||||
if (hasRippleEffect) {
|
|
||||||
modifier.clickable(onClick = state.onClick)
|
|
||||||
} else {
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
modifier.clickable(
|
|
||||||
onClick = state.onClick,
|
|
||||||
indication = null,
|
|
||||||
interactionSource = interactionSource
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = clickableModifier,
|
modifier = modifier,
|
||||||
shape = shape,
|
shape = shape,
|
||||||
border = border,
|
border = border,
|
||||||
color = color,
|
color = color,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.clickable(onClick = state.onClick) then Modifier.padding(contentPadding),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
if (state.startIcon != null) {
|
if (state.startIcon != null) {
|
||||||
|
|
|
@ -90,7 +90,7 @@ fun rememberModalBottomSheetState(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ExperimentalMaterial3Api
|
@ExperimentalMaterial3Api
|
||||||
private fun rememberSheetState(
|
fun rememberSheetState(
|
||||||
skipPartiallyExpanded: Boolean,
|
skipPartiallyExpanded: Boolean,
|
||||||
confirmValueChange: (SheetValue) -> Boolean,
|
confirmValueChange: (SheetValue) -> Boolean,
|
||||||
initialValue: SheetValue,
|
initialValue: SheetValue,
|
||||||
|
|
|
@ -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.border
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
@ -22,23 +23,32 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.takeOrElse
|
import androidx.compose.ui.graphics.takeOrElse
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.TextRange
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||||
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
||||||
|
import co.electriccoin.zcash.ui.design.util.StringResource
|
||||||
|
import co.electriccoin.zcash.ui.design.util.getString
|
||||||
import co.electriccoin.zcash.ui.design.util.getValue
|
import co.electriccoin.zcash.ui.design.util.getValue
|
||||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||||
|
|
||||||
|
@ -48,9 +58,18 @@ fun ZashiTextField(
|
||||||
value: String,
|
value: String,
|
||||||
onValueChange: (String) -> Unit,
|
onValueChange: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
innerModifier: Modifier = Modifier,
|
innerModifier: Modifier = ZashiTextFieldDefaults.innerModifier,
|
||||||
error: String? = null,
|
error: String? = null,
|
||||||
isEnabled: Boolean = true,
|
isEnabled: Boolean = true,
|
||||||
|
handle: ZashiTextFieldHandle =
|
||||||
|
rememberZashiTextFieldHandle(
|
||||||
|
TextFieldState(
|
||||||
|
value = stringRes(value),
|
||||||
|
error = error?.let { stringRes(it) },
|
||||||
|
isEnabled = isEnabled,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
)
|
||||||
|
),
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium),
|
textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium),
|
||||||
label: @Composable (() -> Unit)? = null,
|
label: @Composable (() -> Unit)? = null,
|
||||||
|
@ -97,7 +116,8 @@ fun ZashiTextField(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
shape = shape,
|
shape = shape,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
innerModifier = innerModifier
|
innerModifier = innerModifier,
|
||||||
|
handle = handle,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +126,8 @@ fun ZashiTextField(
|
||||||
fun ZashiTextField(
|
fun ZashiTextField(
|
||||||
state: TextFieldState,
|
state: TextFieldState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
innerModifier: Modifier = Modifier,
|
innerModifier: Modifier = ZashiTextFieldDefaults.innerModifier,
|
||||||
|
handle: ZashiTextFieldHandle = rememberZashiTextFieldHandle(state),
|
||||||
readOnly: Boolean = false,
|
readOnly: Boolean = false,
|
||||||
textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium),
|
textStyle: TextStyle = ZashiTypography.textMd.copy(fontWeight = FontWeight.Medium),
|
||||||
label: @Composable (() -> Unit)? = null,
|
label: @Composable (() -> Unit)? = null,
|
||||||
|
@ -124,6 +145,13 @@ fun ZashiTextField(
|
||||||
minLines: Int = 1,
|
minLines: Int = 1,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
shape: Shape = ZashiTextFieldDefaults.shape,
|
shape: Shape = ZashiTextFieldDefaults.shape,
|
||||||
|
contentPadding: PaddingValues =
|
||||||
|
PaddingValues(
|
||||||
|
start = if (leadingIcon != null) 8.dp else 14.dp,
|
||||||
|
end = if (suffix != null) 4.dp else 12.dp,
|
||||||
|
top = getVerticalPadding(trailingIcon, leadingIcon, suffix, prefix),
|
||||||
|
bottom = getVerticalPadding(trailingIcon, leadingIcon, suffix, prefix),
|
||||||
|
),
|
||||||
colors: ZashiTextFieldColors = ZashiTextFieldDefaults.defaultColors()
|
colors: ZashiTextFieldColors = ZashiTextFieldDefaults.defaultColors()
|
||||||
) {
|
) {
|
||||||
TextFieldInternal(
|
TextFieldInternal(
|
||||||
|
@ -147,10 +175,39 @@ fun ZashiTextField(
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
shape = shape,
|
shape = shape,
|
||||||
colors = colors,
|
colors = colors,
|
||||||
innerModifier = innerModifier
|
contentPadding = contentPadding,
|
||||||
|
innerModifier = innerModifier,
|
||||||
|
handle = handle
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ZashiTextFieldPlaceholder(res: StringResource) {
|
||||||
|
Text(
|
||||||
|
text = res.getValue(),
|
||||||
|
style = ZashiTypography.textMd,
|
||||||
|
color = ZashiColors.Inputs.Default.text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class ZashiTextFieldHandle(text: String) {
|
||||||
|
var textFieldValueState by mutableStateOf(TextFieldValue(text = text))
|
||||||
|
|
||||||
|
fun moveCursorToEnd() {
|
||||||
|
textFieldValueState =
|
||||||
|
textFieldValueState.copy(
|
||||||
|
selection = TextRange(textFieldValueState.text.length),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberZashiTextFieldHandle(state: TextFieldState): ZashiTextFieldHandle {
|
||||||
|
val context = LocalContext.current
|
||||||
|
return remember { ZashiTextFieldHandle(state.value.getString(context)) }
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("LongParameterList", "LongMethod")
|
@Suppress("LongParameterList", "LongMethod")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -174,10 +231,32 @@ private fun TextFieldInternal(
|
||||||
interactionSource: MutableInteractionSource,
|
interactionSource: MutableInteractionSource,
|
||||||
shape: Shape,
|
shape: Shape,
|
||||||
colors: ZashiTextFieldColors,
|
colors: ZashiTextFieldColors,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
handle: ZashiTextFieldHandle,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
innerModifier: Modifier = Modifier,
|
innerModifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val borderColor by colors.borderColor(state)
|
val context = LocalContext.current
|
||||||
|
val value = remember(state.value) { state.value.getString(context) }
|
||||||
|
// Holds the latest internal TextFieldValue state. We need to keep it to have the correct value
|
||||||
|
// of the composition.
|
||||||
|
val textFieldValueState = handle.textFieldValueState
|
||||||
|
// Holds the latest TextFieldValue that BasicTextField was recomposed with. We couldn't simply
|
||||||
|
// pass `TextFieldValue(text = value)` to the CoreTextField because we need to preserve the
|
||||||
|
// composition.
|
||||||
|
val textFieldValue = textFieldValueState.copy(text = value, selection = textFieldValueState.selection)
|
||||||
|
|
||||||
|
SideEffect {
|
||||||
|
if (textFieldValue.text != textFieldValueState.text ||
|
||||||
|
textFieldValue.selection != textFieldValueState.selection ||
|
||||||
|
textFieldValue.composition != textFieldValueState.composition
|
||||||
|
) {
|
||||||
|
handle.textFieldValueState = textFieldValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||||
|
val borderColor by colors.borderColor(state, isFocused)
|
||||||
val androidColors = colors.toTextFieldColors()
|
val androidColors = colors.toTextFieldColors()
|
||||||
// If color is not provided via the text style, use content color as a default
|
// If color is not provided via the text style, use content color as a default
|
||||||
val textColor =
|
val textColor =
|
||||||
|
@ -186,24 +265,35 @@ private fun TextFieldInternal(
|
||||||
}
|
}
|
||||||
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
|
val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
|
||||||
|
|
||||||
|
var lastTextValue by remember(value) { mutableStateOf(value) }
|
||||||
|
|
||||||
CompositionLocalProvider(LocalTextSelectionColors provides androidColors.selectionColors) {
|
CompositionLocalProvider(LocalTextSelectionColors provides androidColors.selectionColors) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
value = state.value.getValue(),
|
value = textFieldValue,
|
||||||
modifier =
|
modifier =
|
||||||
innerModifier.fillMaxWidth() then
|
innerModifier then
|
||||||
if (borderColor == Color.Unspecified) {
|
if (borderColor == Color.Unspecified) {
|
||||||
Modifier
|
Modifier
|
||||||
} else {
|
} else {
|
||||||
Modifier.border(
|
Modifier.border(
|
||||||
width = 1.dp,
|
width = 1.dp,
|
||||||
color = borderColor,
|
color = borderColor,
|
||||||
shape = ZashiTextFieldDefaults.shape
|
shape = shape
|
||||||
)
|
)
|
||||||
} then Modifier.defaultMinSize(minWidth = TextFieldDefaults.MinWidth),
|
},
|
||||||
onValueChange = state.onValueChange,
|
onValueChange = { newTextFieldValueState ->
|
||||||
|
handle.textFieldValueState = newTextFieldValueState
|
||||||
|
|
||||||
|
val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
|
||||||
|
lastTextValue = newTextFieldValueState.text
|
||||||
|
|
||||||
|
if (stringChangedSinceLastInvocation) {
|
||||||
|
state.onValueChange(newTextFieldValueState.text)
|
||||||
|
}
|
||||||
|
},
|
||||||
enabled = state.isEnabled,
|
enabled = state.isEnabled,
|
||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
textStyle = mergedTextStyle,
|
textStyle = mergedTextStyle,
|
||||||
|
@ -215,44 +305,37 @@ private fun TextFieldInternal(
|
||||||
singleLine = singleLine,
|
singleLine = singleLine,
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
minLines = minLines,
|
minLines = minLines,
|
||||||
decorationBox = @Composable { innerTextField ->
|
) { innerTextField: @Composable () -> Unit ->
|
||||||
// places leading icon, text field with label and placeholder, trailing icon
|
// places leading icon, text field with label and placeholder, trailing icon
|
||||||
TextFieldDefaults.DecorationBox(
|
TextFieldDefaults.DecorationBox(
|
||||||
value = state.value.getValue(),
|
value = state.value.getValue(),
|
||||||
visualTransformation = visualTransformation,
|
visualTransformation = visualTransformation,
|
||||||
innerTextField = {
|
innerTextField = {
|
||||||
DecorationBox(prefix = prefix, suffix = suffix, content = innerTextField)
|
DecorationBox(prefix = prefix, suffix = suffix, content = innerTextField)
|
||||||
|
},
|
||||||
|
placeholder =
|
||||||
|
if (placeholder != null) {
|
||||||
|
{
|
||||||
|
DecorationBox(prefix, suffix, placeholder)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
},
|
},
|
||||||
placeholder =
|
label = label,
|
||||||
if (placeholder != null) {
|
leadingIcon = leadingIcon,
|
||||||
{
|
trailingIcon = trailingIcon,
|
||||||
DecorationBox(prefix, suffix, placeholder)
|
prefix = prefix,
|
||||||
}
|
suffix = suffix,
|
||||||
} else {
|
supportingText = supportingText,
|
||||||
null
|
shape = shape,
|
||||||
},
|
singleLine = singleLine,
|
||||||
label = label,
|
enabled = state.isEnabled,
|
||||||
leadingIcon = leadingIcon,
|
isError = state.isError,
|
||||||
trailingIcon = trailingIcon,
|
interactionSource = interactionSource,
|
||||||
prefix = prefix,
|
colors = androidColors,
|
||||||
suffix = suffix,
|
contentPadding = contentPadding
|
||||||
supportingText = supportingText,
|
)
|
||||||
shape = shape,
|
}
|
||||||
singleLine = singleLine,
|
|
||||||
enabled = state.isEnabled,
|
|
||||||
isError = state.isError,
|
|
||||||
interactionSource = interactionSource,
|
|
||||||
colors = androidColors,
|
|
||||||
contentPadding =
|
|
||||||
PaddingValues(
|
|
||||||
start = if (leadingIcon != null) 8.dp else 14.dp,
|
|
||||||
end = if (suffix != null) 4.dp else 12.dp,
|
|
||||||
top = getVerticalPadding(trailingIcon, leadingIcon, suffix, prefix),
|
|
||||||
bottom = getVerticalPadding(trailingIcon, leadingIcon, suffix, prefix),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (state.error != null && state.error.getValue().isNotEmpty()) {
|
if (state.error != null && state.error.getValue().isNotEmpty()) {
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
@ -303,7 +386,9 @@ data class ZashiTextFieldColors(
|
||||||
val textColor: Color,
|
val textColor: Color,
|
||||||
val hintColor: Color,
|
val hintColor: Color,
|
||||||
val borderColor: Color,
|
val borderColor: Color,
|
||||||
|
val focusedBorderColor: Color,
|
||||||
val containerColor: Color,
|
val containerColor: Color,
|
||||||
|
val focusedContainerColor: Color,
|
||||||
val placeholderColor: Color,
|
val placeholderColor: Color,
|
||||||
val disabledTextColor: Color,
|
val disabledTextColor: Color,
|
||||||
val disabledHintColor: Color,
|
val disabledHintColor: Color,
|
||||||
|
@ -317,11 +402,15 @@ data class ZashiTextFieldColors(
|
||||||
val errorPlaceholderColor: Color,
|
val errorPlaceholderColor: Color,
|
||||||
) {
|
) {
|
||||||
@Composable
|
@Composable
|
||||||
internal fun borderColor(state: TextFieldState): State<Color> {
|
internal fun borderColor(
|
||||||
|
state: TextFieldState,
|
||||||
|
isFocused: Boolean
|
||||||
|
): State<Color> {
|
||||||
val targetValue =
|
val targetValue =
|
||||||
when {
|
when {
|
||||||
!state.isEnabled -> disabledBorderColor
|
!state.isEnabled -> disabledBorderColor
|
||||||
state.isError -> errorBorderColor
|
state.isError -> errorBorderColor
|
||||||
|
isFocused -> focusedBorderColor.takeOrElse { borderColor }
|
||||||
else -> borderColor
|
else -> borderColor
|
||||||
}
|
}
|
||||||
return rememberUpdatedState(targetValue)
|
return rememberUpdatedState(targetValue)
|
||||||
|
@ -345,7 +434,7 @@ data class ZashiTextFieldColors(
|
||||||
unfocusedTextColor = textColor,
|
unfocusedTextColor = textColor,
|
||||||
disabledTextColor = disabledTextColor,
|
disabledTextColor = disabledTextColor,
|
||||||
errorTextColor = errorTextColor,
|
errorTextColor = errorTextColor,
|
||||||
focusedContainerColor = containerColor,
|
focusedContainerColor = focusedContainerColor.takeOrElse { containerColor },
|
||||||
unfocusedContainerColor = containerColor,
|
unfocusedContainerColor = containerColor,
|
||||||
disabledContainerColor = disabledContainerColor,
|
disabledContainerColor = disabledContainerColor,
|
||||||
errorContainerColor = errorContainerColor,
|
errorContainerColor = errorContainerColor,
|
||||||
|
@ -391,13 +480,21 @@ object ZashiTextFieldDefaults {
|
||||||
val shape: Shape
|
val shape: Shape
|
||||||
get() = RoundedCornerShape(8.dp)
|
get() = RoundedCornerShape(8.dp)
|
||||||
|
|
||||||
|
val innerModifier: Modifier
|
||||||
|
get() =
|
||||||
|
Modifier
|
||||||
|
.defaultMinSize(minWidth = TextFieldDefaults.MinWidth)
|
||||||
|
.fillMaxWidth()
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
@Composable
|
@Composable
|
||||||
fun defaultColors(
|
fun defaultColors(
|
||||||
textColor: Color = ZashiColors.Inputs.Filled.text,
|
textColor: Color = ZashiColors.Inputs.Filled.text,
|
||||||
hintColor: Color = ZashiColors.Inputs.Default.hint,
|
hintColor: Color = ZashiColors.Inputs.Default.hint,
|
||||||
borderColor: Color = Color.Unspecified,
|
borderColor: Color = Color.Unspecified,
|
||||||
|
focusedBorderColor: Color = ZashiColors.Inputs.Focused.stroke,
|
||||||
containerColor: Color = ZashiColors.Inputs.Default.bg,
|
containerColor: Color = ZashiColors.Inputs.Default.bg,
|
||||||
|
focusedContainerColor: Color = ZashiColors.Inputs.Focused.bg,
|
||||||
placeholderColor: Color = ZashiColors.Inputs.Default.text,
|
placeholderColor: Color = ZashiColors.Inputs.Default.text,
|
||||||
disabledTextColor: Color = ZashiColors.Inputs.Disabled.text,
|
disabledTextColor: Color = ZashiColors.Inputs.Disabled.text,
|
||||||
disabledHintColor: Color = ZashiColors.Inputs.Disabled.hint,
|
disabledHintColor: Color = ZashiColors.Inputs.Disabled.hint,
|
||||||
|
@ -413,7 +510,9 @@ object ZashiTextFieldDefaults {
|
||||||
textColor = textColor,
|
textColor = textColor,
|
||||||
hintColor = hintColor,
|
hintColor = hintColor,
|
||||||
borderColor = borderColor,
|
borderColor = borderColor,
|
||||||
|
focusedBorderColor = focusedBorderColor,
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
|
focusedContainerColor = focusedContainerColor,
|
||||||
placeholderColor = placeholderColor,
|
placeholderColor = placeholderColor,
|
||||||
disabledTextColor = disabledTextColor,
|
disabledTextColor = disabledTextColor,
|
||||||
disabledHintColor = disabledHintColor,
|
disabledHintColor = disabledHintColor,
|
||||||
|
@ -428,6 +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
|
@PreviewScreens
|
||||||
@Composable
|
@Composable
|
||||||
private fun DefaultPreview() =
|
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.material3.RippleDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import co.electriccoin.zcash.ui.design.LocalKeyboardManager
|
||||||
|
import co.electriccoin.zcash.ui.design.LocalSheetStateManager
|
||||||
|
import co.electriccoin.zcash.ui.design.rememberKeyboardManager
|
||||||
|
import co.electriccoin.zcash.ui.design.rememberSheetStateManager
|
||||||
import co.electriccoin.zcash.ui.design.theme.balances.LocalBalancesAvailable
|
import co.electriccoin.zcash.ui.design.theme.balances.LocalBalancesAvailable
|
||||||
import co.electriccoin.zcash.ui.design.theme.colors.DarkZashiColorsInternal
|
import co.electriccoin.zcash.ui.design.theme.colors.DarkZashiColorsInternal
|
||||||
import co.electriccoin.zcash.ui.design.theme.colors.LightZashiColorsInternal
|
import co.electriccoin.zcash.ui.design.theme.colors.LightZashiColorsInternal
|
||||||
|
@ -49,7 +53,9 @@ fun ZcashTheme(
|
||||||
LocalZashiColors provides zashiColors,
|
LocalZashiColors provides zashiColors,
|
||||||
LocalZashiTypography provides ZashiTypographyInternal,
|
LocalZashiTypography provides ZashiTypographyInternal,
|
||||||
LocalRippleConfiguration provides MaterialRippleConfig,
|
LocalRippleConfiguration provides MaterialRippleConfig,
|
||||||
LocalBalancesAvailable provides balancesAvailable
|
LocalBalancesAvailable provides balancesAvailable,
|
||||||
|
LocalKeyboardManager provides rememberKeyboardManager(),
|
||||||
|
LocalSheetStateManager provides rememberSheetStateManager()
|
||||||
) {
|
) {
|
||||||
ProvideDimens {
|
ProvideDimens {
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
|
|
|
@ -551,8 +551,8 @@ val DarkZashiColorsInternal =
|
||||||
utilityEspresso600 = Espresso.`300`,
|
utilityEspresso600 = Espresso.`300`,
|
||||||
utilityEspresso500 = Espresso.`400`,
|
utilityEspresso500 = Espresso.`400`,
|
||||||
utilityEspresso200 = Espresso.`700`,
|
utilityEspresso200 = Espresso.`700`,
|
||||||
utilityEspresso50 = Espresso.`900`,
|
utilityEspresso50 = Espresso.`950`,
|
||||||
utilityEspresso100 = Espresso.`800`,
|
utilityEspresso100 = Espresso.`900`,
|
||||||
utilityEspresso400 = Espresso.`500`,
|
utilityEspresso400 = Espresso.`500`,
|
||||||
utilityEspresso300 = Espresso.`600`,
|
utilityEspresso300 = Espresso.`600`,
|
||||||
utilityEspresso900 = Espresso.`50`,
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
|
@ -3,4 +3,5 @@
|
||||||
<string name="hide_balance_placeholder">-----</string>
|
<string name="hide_balance_placeholder">-----</string>
|
||||||
<string name="back_navigation_content_description">Atrás</string>
|
<string name="back_navigation_content_description">Atrás</string>
|
||||||
<string name="triple_dots">…</string>
|
<string name="triple_dots">…</string>
|
||||||
|
<string name="seed_recovery_reveal">Mostrar frase de recuperación</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -3,4 +3,5 @@
|
||||||
<string name="hide_balance_placeholder">-----</string>
|
<string name="hide_balance_placeholder">-----</string>
|
||||||
<string name="back_navigation_content_description">Back</string>
|
<string name="back_navigation_content_description">Back</string>
|
||||||
<string name="triple_dots">…</string>
|
<string name="triple_dots">…</string>
|
||||||
|
<string name="seed_recovery_reveal">Reveal recovery phrase</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -28,10 +28,8 @@ class OnboardingTestSetup(
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
Onboarding(
|
Onboarding(
|
||||||
// Debug only UI state does not need to be tested
|
// Debug only UI state does not need to be tested
|
||||||
isDebugMenuEnabled = false,
|
|
||||||
onImportWallet = { onImportWalletCallbackCount.incrementAndGet() },
|
onImportWallet = { onImportWalletCallbackCount.incrementAndGet() },
|
||||||
onCreateWallet = { onCreateWalletCallbackCount.incrementAndGet() },
|
onCreateWallet = { onCreateWalletCallbackCount.incrementAndGet() }
|
||||||
onFixtureWallet = {}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,8 @@ import cash.z.ecc.sdk.fixture.SeedPhraseFixture
|
||||||
import co.electriccoin.zcash.test.UiTestPrerequisites
|
import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||||
import co.electriccoin.zcash.ui.R
|
import co.electriccoin.zcash.ui.R
|
||||||
import co.electriccoin.zcash.ui.design.component.CommonTag
|
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.model.RestoreStage
|
||||||
|
import co.electriccoin.zcash.ui.screen.restore.seed.RestoreSeedTag
|
||||||
import co.electriccoin.zcash.ui.test.getAppContext
|
import co.electriccoin.zcash.ui.test.getAppContext
|
||||||
import co.electriccoin.zcash.ui.test.getStringResource
|
import co.electriccoin.zcash.ui.test.getStringResource
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
@ -55,7 +55,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
composeTestRule.waitForIdle()
|
composeTestRule.waitForIdle()
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.assertIsFocused()
|
it.assertIsFocused()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
|
||||||
SeedPhraseFixture.SEED_PHRASE + " " + SeedPhraseFixture.SEED_PHRASE
|
SeedPhraseFixture.SEED_PHRASE + " " + SeedPhraseFixture.SEED_PHRASE
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.performKeyInput {
|
it.performKeyInput {
|
||||||
withKeyDown(Key.CtrlLeft) {
|
withKeyDown(Key.CtrlLeft) {
|
||||||
pressKey(Key.V)
|
pressKey(Key.V)
|
||||||
|
@ -94,11 +94,11 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
|
||||||
|
|
||||||
assertEquals(SeedPhrase.SEED_PHRASE_SIZE, testSetup.getUserInputWords().size)
|
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()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.AUTOCOMPLETE_LAYOUT).also {
|
||||||
it.assertDoesNotExist()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
|
||||||
composeTestRule.waitForIdle()
|
composeTestRule.waitForIdle()
|
||||||
|
|
||||||
// Insert uncompleted seed words
|
// Insert uncompleted seed words
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.performTextInput("test")
|
it.performTextInput("test")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ class RestoreViewAndroidTest : UiTestPrerequisites() {
|
||||||
composeTestRule.waitForIdle()
|
composeTestRule.waitForIdle()
|
||||||
|
|
||||||
// Insert complete seed words
|
// 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)
|
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.LocalScreenSecurity
|
||||||
import co.electriccoin.zcash.ui.common.compose.ScreenSecurity
|
import co.electriccoin.zcash.ui.common.compose.ScreenSecurity
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
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 co.electriccoin.zcash.ui.screen.restore.state.WordList
|
||||||
import kotlinx.collections.immutable.toPersistentSet
|
import kotlinx.collections.immutable.toPersistentSet
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
@ -43,9 +44,9 @@ class RestoreViewSecuredScreenTest : UiTestPrerequisites() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) {
|
CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) {
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
RestoreWallet(
|
RestoreSeedView(
|
||||||
ZcashNetwork.Mainnet,
|
ZcashNetwork.Mainnet,
|
||||||
RestoreState(),
|
RestoreSeedState(),
|
||||||
Mnemonics.getCachedWords(Locale.ENGLISH.language).toPersistentSet(),
|
Mnemonics.getCachedWords(Locale.ENGLISH.language).toPersistentSet(),
|
||||||
WordList(emptyList()),
|
WordList(emptyList()),
|
||||||
restoreHeight = null,
|
restoreHeight = null,
|
||||||
|
|
|
@ -25,9 +25,10 @@ import co.electriccoin.zcash.test.UiTestPrerequisites
|
||||||
import co.electriccoin.zcash.ui.R
|
import co.electriccoin.zcash.ui.R
|
||||||
import co.electriccoin.zcash.ui.design.component.CommonTag
|
import co.electriccoin.zcash.ui.design.component.CommonTag
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
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.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.screen.restore.state.WordList
|
||||||
import co.electriccoin.zcash.ui.test.getStringResource
|
import co.electriccoin.zcash.ui.test.getStringResource
|
||||||
import kotlinx.collections.immutable.toPersistentSet
|
import kotlinx.collections.immutable.toPersistentSet
|
||||||
|
@ -54,7 +55,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
fun seed_autocomplete_suggestions_appear() {
|
fun seed_autocomplete_suggestions_appear() {
|
||||||
newTestSetup()
|
newTestSetup()
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.performTextInput("ab")
|
it.performTextInput("ab")
|
||||||
|
|
||||||
// Make sure text isn't cleared
|
// Make sure text isn't cleared
|
||||||
|
@ -62,13 +63,13 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNode(
|
composeTestRule.onNode(
|
||||||
matcher = hasText("abandon", substring = true) and hasTestTag(RestoreTag.AUTOCOMPLETE_ITEM)
|
matcher = hasText("abandon", substring = true) and hasTestTag(RestoreSeedTag.AUTOCOMPLETE_ITEM)
|
||||||
).also {
|
).also {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNode(
|
composeTestRule.onNode(
|
||||||
matcher = hasText("able", substring = true) and hasTestTag(RestoreTag.AUTOCOMPLETE_ITEM)
|
matcher = hasText("able", substring = true) and hasTestTag(RestoreSeedTag.AUTOCOMPLETE_ITEM)
|
||||||
).also {
|
).also {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
|
@ -79,17 +80,17 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
fun seed_choose_autocomplete() {
|
fun seed_choose_autocomplete() {
|
||||||
newTestSetup()
|
newTestSetup()
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.performTextInput("ab")
|
it.performTextInput("ab")
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNode(
|
composeTestRule.onNode(
|
||||||
matcher = hasText("abandon", substring = true) and hasTestTag(RestoreTag.AUTOCOMPLETE_ITEM)
|
matcher = hasText("abandon", substring = true) and hasTestTag(RestoreSeedTag.AUTOCOMPLETE_ITEM)
|
||||||
).also {
|
).also {
|
||||||
it.performClick()
|
it.performClick()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.AUTOCOMPLETE_LAYOUT).also {
|
||||||
it.assertDoesNotExist()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +98,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
it.assertExists()
|
it.assertExists()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.assertTextEquals("abandon ", includeEditableText = true)
|
it.assertTextEquals("abandon ", includeEditableText = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,15 +108,15 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
fun seed_type_full_word() {
|
fun seed_type_full_word() {
|
||||||
newTestSetup()
|
newTestSetup()
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.performTextInput("abandon")
|
it.performTextInput("abandon")
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.SEED_WORD_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.SEED_WORD_TEXT_FIELD).also {
|
||||||
it.assertTextEquals("abandon ", includeEditableText = true)
|
it.assertTextEquals("abandon ", includeEditableText = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.AUTOCOMPLETE_LAYOUT).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.AUTOCOMPLETE_LAYOUT).also {
|
||||||
it.assertDoesNotExist()
|
it.assertDoesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,7 +210,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
initialWordsList = SeedPhraseFixture.new().split
|
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())
|
it.performTextInput(ZcashNetwork.Mainnet.saplingActivationHeight.value.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,7 +242,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
it.assertIsEnabled()
|
it.assertIsEnabled()
|
||||||
}
|
}
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.BIRTHDAY_TEXT_FIELD).also {
|
||||||
it.performTextInput((ZcashNetwork.Mainnet.saplingActivationHeight.value - 1L).toString())
|
it.performTextInput((ZcashNetwork.Mainnet.saplingActivationHeight.value - 1L).toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,7 +267,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
initialWordsList = SeedPhraseFixture.new().split
|
initialWordsList = SeedPhraseFixture.new().split
|
||||||
)
|
)
|
||||||
|
|
||||||
composeTestRule.onNodeWithTag(RestoreTag.BIRTHDAY_TEXT_FIELD).also {
|
composeTestRule.onNodeWithTag(RestoreSeedTag.BIRTHDAY_TEXT_FIELD).also {
|
||||||
it.performTextInput("1.2")
|
it.performTextInput("1.2")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -353,7 +354,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
initialStage: RestoreStage,
|
initialStage: RestoreStage,
|
||||||
initialWordsList: List<String>
|
initialWordsList: List<String>
|
||||||
) {
|
) {
|
||||||
private val state = RestoreState(initialStage)
|
private val state = RestoreSeedState(initialStage)
|
||||||
|
|
||||||
private val wordList = WordList(initialWordsList)
|
private val wordList = WordList(initialWordsList)
|
||||||
|
|
||||||
|
@ -391,7 +392,7 @@ class RestoreViewTest : UiTestPrerequisites() {
|
||||||
init {
|
init {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
RestoreWallet(
|
RestoreSeedView(
|
||||||
ZcashNetwork.Mainnet,
|
ZcashNetwork.Mainnet,
|
||||||
state,
|
state,
|
||||||
Mnemonics.getCachedWords(Locale.ENGLISH.language).toPersistentSet(),
|
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.DeriveKeystoneAccountUnifiedAddressUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ExportTaxUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ExportTaxUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.FlipTransactionBookmarkUseCase
|
import co.electriccoin.zcash.ui.common.usecase.FlipTransactionBookmarkUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetBackupPersistableWalletUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetCoinbaseStatusUseCase
|
||||||
|
import co.electriccoin.zcash.ui.common.usecase.GetConfigurationUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetContactByAddressUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetContactByAddressUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetCurrentFilteredTransactionsUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetCurrentFilteredTransactionsUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetCurrentTransactionsUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetCurrentTransactionsUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetExchangeRateUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetExchangeRateUseCase
|
||||||
|
import co.electriccoin.zcash.ui.common.usecase.GetFlexaStatusUseCase
|
||||||
|
import co.electriccoin.zcash.ui.common.usecase.GetKeystoneStatusUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetMetadataUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetMetadataUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetProposalUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetProposalUseCase
|
||||||
|
@ -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.GetTransactionFiltersUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetTransactionMetadataUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetTransactionMetadataUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase
|
||||||
|
import co.electriccoin.zcash.ui.common.usecase.GetWalletAccountsUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletStateInformationUseCase
|
import co.electriccoin.zcash.ui.common.usecase.GetWalletStateInformationUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetZashiAccountUseCase
|
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.MarkTxMemoAsReadUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToAddressBookUseCase
|
import co.electriccoin.zcash.ui.common.usecase.NavigateToAddressBookUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase
|
import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase
|
||||||
|
import co.electriccoin.zcash.ui.common.usecase.NavigateToSeedRecoveryUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.NavigateToTaxExportUseCase
|
import co.electriccoin.zcash.ui.common.usecase.NavigateToTaxExportUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ObserveClearSendUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ObserveClearSendUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase
|
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ObserveContactByAddressUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ObserveContactByAddressUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ObserveFastestServersUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ObserveFastestServersUseCase
|
||||||
|
@ -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.ObserveSelectedWalletAccountUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ObserveTransactionSubmitStateUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ObserveTransactionSubmitStateUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ObserveWalletAccountsUseCase
|
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ObserveZashiAccountUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ObserveZashiAccountUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.OnAddressScannedUseCase
|
import co.electriccoin.zcash.ui.common.usecase.OnAddressScannedUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.OnZip321ScannedUseCase
|
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.ResetInMemoryDataUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ResetSharedPrefsDataUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ResetSharedPrefsDataUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ResetTransactionFiltersUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ResetTransactionFiltersUseCase
|
||||||
|
import co.electriccoin.zcash.ui.common.usecase.RestoreWalletUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.SaveContactUseCase
|
import co.electriccoin.zcash.ui.common.usecase.SaveContactUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.SelectWalletAccountUseCase
|
import co.electriccoin.zcash.ui.common.usecase.SelectWalletAccountUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.SendEmailUseCase
|
import co.electriccoin.zcash.ui.common.usecase.SendEmailUseCase
|
||||||
|
@ -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.ValidateContactAddressUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ValidateContactNameUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ValidateContactNameUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ValidateEndpointUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ValidateEndpointUseCase
|
||||||
|
import co.electriccoin.zcash.ui.common.usecase.ValidateSeedUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ViewTransactionDetailAfterSuccessfulProposalUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ViewTransactionDetailAfterSuccessfulProposalUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ViewTransactionsAfterSuccessfulProposalUseCase
|
import co.electriccoin.zcash.ui.common.usecase.ViewTransactionsAfterSuccessfulProposalUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.Zip321BuildUriUseCase
|
import co.electriccoin.zcash.ui.common.usecase.Zip321BuildUriUseCase
|
||||||
|
@ -102,7 +107,7 @@ val useCaseModule =
|
||||||
factoryOf(::ValidateEndpointUseCase)
|
factoryOf(::ValidateEndpointUseCase)
|
||||||
factoryOf(::GetPersistableWalletUseCase)
|
factoryOf(::GetPersistableWalletUseCase)
|
||||||
factoryOf(::GetSelectedEndpointUseCase)
|
factoryOf(::GetSelectedEndpointUseCase)
|
||||||
factoryOf(::ObserveConfigurationUseCase)
|
factoryOf(::GetConfigurationUseCase)
|
||||||
factoryOf(::RescanBlockchainUseCase)
|
factoryOf(::RescanBlockchainUseCase)
|
||||||
factoryOf(::GetTransparentAddressUseCase)
|
factoryOf(::GetTransparentAddressUseCase)
|
||||||
factoryOf(::ValidateContactAddressUseCase)
|
factoryOf(::ValidateContactAddressUseCase)
|
||||||
|
@ -121,12 +126,11 @@ val useCaseModule =
|
||||||
factoryOf(::IsCoinbaseAvailableUseCase)
|
factoryOf(::IsCoinbaseAvailableUseCase)
|
||||||
factoryOf(::GetZashiSpendingKeyUseCase)
|
factoryOf(::GetZashiSpendingKeyUseCase)
|
||||||
factoryOf(::ObservePersistableWalletUseCase)
|
factoryOf(::ObservePersistableWalletUseCase)
|
||||||
factoryOf(::GetBackupPersistableWalletUseCase)
|
|
||||||
factoryOf(::GetSupportUseCase)
|
factoryOf(::GetSupportUseCase)
|
||||||
factoryOf(::SendEmailUseCase)
|
factoryOf(::SendEmailUseCase)
|
||||||
factoryOf(::SendSupportEmailUseCase)
|
factoryOf(::SendSupportEmailUseCase)
|
||||||
factoryOf(::IsFlexaAvailableUseCase)
|
factoryOf(::IsFlexaAvailableUseCase)
|
||||||
factoryOf(::ObserveWalletAccountsUseCase)
|
factoryOf(::GetWalletAccountsUseCase)
|
||||||
factoryOf(::SelectWalletAccountUseCase)
|
factoryOf(::SelectWalletAccountUseCase)
|
||||||
factoryOf(::ObserveSelectedWalletAccountUseCase)
|
factoryOf(::ObserveSelectedWalletAccountUseCase)
|
||||||
factoryOf(::ObserveZashiAccountUseCase)
|
factoryOf(::ObserveZashiAccountUseCase)
|
||||||
|
@ -178,4 +182,10 @@ val useCaseModule =
|
||||||
factoryOf(::NavigateToTaxExportUseCase)
|
factoryOf(::NavigateToTaxExportUseCase)
|
||||||
factoryOf(::CreateFlexaTransactionUseCase)
|
factoryOf(::CreateFlexaTransactionUseCase)
|
||||||
factoryOf(::IsRestoreSuccessDialogVisibleUseCase)
|
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.accountlist.viewmodel.AccountListViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.AddressBookViewModel
|
import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.AddressBookViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.SelectRecipientViewModel
|
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.balances.BalanceViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.chooseserver.ChooseServerViewModel
|
import co.electriccoin.zcash.ui.screen.chooseserver.ChooseServerViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.contact.viewmodel.AddContactViewModel
|
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.feedback.viewmodel.FeedbackViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.flexa.FlexaViewModel
|
import co.electriccoin.zcash.ui.screen.flexa.FlexaViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.home.HomeViewModel
|
import co.electriccoin.zcash.ui.screen.home.HomeViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.integrations.viewmodel.IntegrationsViewModel
|
import co.electriccoin.zcash.ui.screen.integrations.IntegrationsViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
|
|
||||||
import co.electriccoin.zcash.ui.screen.qrcode.viewmodel.QrCodeViewModel
|
import co.electriccoin.zcash.ui.screen.qrcode.viewmodel.QrCodeViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.receive.viewmodel.ReceiveViewModel
|
import co.electriccoin.zcash.ui.screen.receive.viewmodel.ReceiveViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.request.viewmodel.RequestViewModel
|
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.restoresuccess.viewmodel.RestoreSuccessViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewTransactionViewModel
|
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewTransactionViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||||
import co.electriccoin.zcash.ui.screen.scan.viewmodel.ScanViewModel
|
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.ScanKeystonePCZTViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.scankeystone.viewmodel.ScanKeystoneSignInRequestViewModel
|
import co.electriccoin.zcash.ui.screen.scankeystone.viewmodel.ScanKeystoneSignInRequestViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.seed.SeedNavigationArgs
|
import co.electriccoin.zcash.ui.screen.seed.SeedRecoveryViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.seed.viewmodel.SeedViewModel
|
|
||||||
import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.SelectKeystoneAccount
|
import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.SelectKeystoneAccount
|
||||||
import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.viewmodel.SelectKeystoneAccountViewModel
|
import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.viewmodel.SelectKeystoneAccountViewModel
|
||||||
import co.electriccoin.zcash.ui.screen.send.SendViewModel
|
import co.electriccoin.zcash.ui.screen.send.SendViewModel
|
||||||
|
@ -57,9 +58,8 @@ val viewModelModule =
|
||||||
viewModelOf(::WalletViewModel)
|
viewModelOf(::WalletViewModel)
|
||||||
viewModelOf(::AuthenticationViewModel)
|
viewModelOf(::AuthenticationViewModel)
|
||||||
viewModelOf(::OldHomeViewModel)
|
viewModelOf(::OldHomeViewModel)
|
||||||
viewModelOf(::OnboardingViewModel)
|
|
||||||
viewModelOf(::StorageCheckViewModel)
|
viewModelOf(::StorageCheckViewModel)
|
||||||
viewModelOf(::RestoreViewModel)
|
viewModelOf(::RestoreSeedViewModel)
|
||||||
viewModelOf(::ScreenBrightnessViewModel)
|
viewModelOf(::ScreenBrightnessViewModel)
|
||||||
viewModelOf(::SettingsViewModel)
|
viewModelOf(::SettingsViewModel)
|
||||||
viewModelOf(::AdvancedSettingsViewModel)
|
viewModelOf(::AdvancedSettingsViewModel)
|
||||||
|
@ -92,27 +92,10 @@ val viewModelModule =
|
||||||
}
|
}
|
||||||
viewModelOf(::ScanKeystoneSignInRequestViewModel)
|
viewModelOf(::ScanKeystoneSignInRequestViewModel)
|
||||||
viewModelOf(::ScanKeystonePCZTViewModel)
|
viewModelOf(::ScanKeystonePCZTViewModel)
|
||||||
viewModel { (isDialog: Boolean) ->
|
viewModelOf(::IntegrationsViewModel)
|
||||||
IntegrationsViewModel(
|
|
||||||
isDialog = isDialog,
|
|
||||||
getZcashCurrency = get(),
|
|
||||||
isFlexaAvailableUseCase = get(),
|
|
||||||
isCoinbaseAvailable = get(),
|
|
||||||
observeWalletAccounts = get(),
|
|
||||||
navigationRouter = get(),
|
|
||||||
navigateToCoinbase = get(),
|
|
||||||
getWalletRestoringState = get()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
viewModelOf(::FlexaViewModel)
|
viewModelOf(::FlexaViewModel)
|
||||||
viewModelOf(::SendViewModel)
|
viewModelOf(::SendViewModel)
|
||||||
viewModel { (args: SeedNavigationArgs) ->
|
viewModelOf(::SeedRecoveryViewModel)
|
||||||
SeedViewModel(
|
|
||||||
observePersistableWallet = get(),
|
|
||||||
args = args,
|
|
||||||
walletRepository = get(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
viewModelOf(::FeedbackViewModel)
|
viewModelOf(::FeedbackViewModel)
|
||||||
viewModelOf(::SignKeystoneTransactionViewModel)
|
viewModelOf(::SignKeystoneTransactionViewModel)
|
||||||
viewModelOf(::AccountListViewModel)
|
viewModelOf(::AccountListViewModel)
|
||||||
|
@ -156,4 +139,7 @@ val viewModelModule =
|
||||||
viewModelOf(::TaxExportViewModel)
|
viewModelOf(::TaxExportViewModel)
|
||||||
viewModelOf(::BalanceViewModel)
|
viewModelOf(::BalanceViewModel)
|
||||||
viewModelOf(::HomeViewModel)
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
@ -23,22 +22,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.NavHostController
|
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.spackle.Twig
|
||||||
import co.electriccoin.zcash.ui.common.compose.BindCompLocalProvider
|
import co.electriccoin.zcash.ui.common.compose.BindCompLocalProvider
|
||||||
import co.electriccoin.zcash.ui.common.extension.setContentCompat
|
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.AuthenticationUIState
|
||||||
import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationViewModel
|
import co.electriccoin.zcash.ui.common.viewmodel.AuthenticationViewModel
|
||||||
import co.electriccoin.zcash.ui.common.viewmodel.OldHomeViewModel
|
import co.electriccoin.zcash.ui.common.viewmodel.OldHomeViewModel
|
||||||
import co.electriccoin.zcash.ui.common.viewmodel.SecretState
|
import co.electriccoin.zcash.ui.common.viewmodel.SecretState
|
||||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
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.BlankSurface
|
||||||
import co.electriccoin.zcash.ui.design.component.ConfigurationOverride
|
import co.electriccoin.zcash.ui.design.component.ConfigurationOverride
|
||||||
import co.electriccoin.zcash.ui.design.component.Override
|
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.WrapAuthentication
|
||||||
import co.electriccoin.zcash.ui.screen.authentication.view.AnimationConstants
|
import co.electriccoin.zcash.ui.screen.authentication.view.AnimationConstants
|
||||||
import co.electriccoin.zcash.ui.screen.authentication.view.WelcomeAnimationAutostart
|
import co.electriccoin.zcash.ui.screen.authentication.view.WelcomeAnimationAutostart
|
||||||
import co.electriccoin.zcash.ui.screen.onboarding.WrapOnboarding
|
import co.electriccoin.zcash.ui.screen.onboarding.OnboardingNavigation
|
||||||
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.warning.viewmodel.StorageCheckViewModel
|
import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel
|
||||||
import co.electriccoin.zcash.work.WorkIds
|
import co.electriccoin.zcash.work.WorkIds
|
||||||
import kotlinx.coroutines.delay
|
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()
|
SecretState.LOADING == walletViewModel.secretState.value
|
||||||
oldHomeViewModel.configurationFlow.value == null || SecretState.Loading == walletViewModel.secretState.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,58 +221,20 @@ class MainActivity : FragmentActivity() {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MainContent() {
|
private fun MainContent() {
|
||||||
val configuration = oldHomeViewModel.configurationFlow.collectAsStateWithLifecycle().value
|
val secretState by walletViewModel.secretState.collectAsStateWithLifecycle()
|
||||||
val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value
|
|
||||||
|
|
||||||
// Note this condition needs to be kept in sync with the condition in setupSplashScreen()
|
when (secretState) {
|
||||||
if (null == configuration || secretState == SecretState.Loading) {
|
SecretState.NONE -> {
|
||||||
// For now, keep displaying splash screen using condition above.
|
OnboardingNavigation()
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
is SecretState.NeedsWarning -> {
|
SecretState.READY -> {
|
||||||
WrapSecurityWarning(
|
Navigation()
|
||||||
onBack = { walletViewModel.persistOnboardingState(OnboardingState.NONE) },
|
}
|
||||||
onConfirm = {
|
|
||||||
walletViewModel.persistOnboardingState(OnboardingState.NEEDS_BACKUP)
|
|
||||||
|
|
||||||
if (FirebaseTestLabUtil.isFirebaseTestLab(applicationContext)) {
|
SecretState.LOADING -> {
|
||||||
persistExistingWalletWithSeedPhrase(
|
// For now, keep displaying splash screen using condition above.
|
||||||
applicationContext,
|
// In the future, we might consider displaying something different here.
|
||||||
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 -> {
|
|
||||||
Navigation()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
error("Unhandled secret state: $secretState")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -294,7 +242,7 @@ class MainActivity : FragmentActivity() {
|
||||||
private fun monitorForBackgroundSync() {
|
private fun monitorForBackgroundSync() {
|
||||||
val isEnableBackgroundSyncFlow =
|
val isEnableBackgroundSyncFlow =
|
||||||
run {
|
run {
|
||||||
val isSecretReadyFlow = walletViewModel.secretState.map { it is SecretState.Ready }
|
val isSecretReadyFlow = walletViewModel.secretState.map { it == SecretState.READY }
|
||||||
val isBackgroundSyncEnabledFlow = oldHomeViewModel.isBackgroundSyncEnabled.filterNotNull()
|
val isBackgroundSyncEnabledFlow = oldHomeViewModel.isBackgroundSyncEnabled.filterNotNull()
|
||||||
|
|
||||||
isSecretReadyFlow.combine(isBackgroundSyncEnabledFlow) { isSecretReady, isBackgroundSyncEnabled ->
|
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.NOT_ENOUGH_SPACE
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.QR_CODE
|
import co.electriccoin.zcash.ui.NavigationTargets.QR_CODE
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.REQUEST
|
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
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS_EXCHANGE_RATE_OPT_IN
|
import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS_EXCHANGE_RATE_OPT_IN
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT
|
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.compose.LocalNavController
|
||||||
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider
|
import co.electriccoin.zcash.ui.common.provider.ApplicationStateProvider
|
||||||
import co.electriccoin.zcash.ui.common.provider.isInForeground
|
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.enterTransition
|
||||||
import co.electriccoin.zcash.ui.design.animation.ScreenAnimation.exitTransition
|
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.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.Receive
|
||||||
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType
|
import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType
|
||||||
import co.electriccoin.zcash.ui.screen.request.WrapRequest
|
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.AndroidReviewTransaction
|
||||||
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewTransaction
|
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewTransaction
|
||||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
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.ScanKeystoneSignInRequest
|
||||||
import co.electriccoin.zcash.ui.screen.scankeystone.WrapScanKeystonePCZTRequest
|
import co.electriccoin.zcash.ui.screen.scankeystone.WrapScanKeystonePCZTRequest
|
||||||
import co.electriccoin.zcash.ui.screen.scankeystone.WrapScanKeystoneSignInRequest
|
import co.electriccoin.zcash.ui.screen.scankeystone.WrapScanKeystoneSignInRequest
|
||||||
import co.electriccoin.zcash.ui.screen.seed.SeedNavigationArgs
|
import co.electriccoin.zcash.ui.screen.seed.AndroidSeedRecovery
|
||||||
import co.electriccoin.zcash.ui.screen.seed.WrapSeed
|
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.AndroidSelectKeystoneAccount
|
||||||
import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.SelectKeystoneAccount
|
import co.electriccoin.zcash.ui.screen.selectkeystoneaccount.SelectKeystoneAccount
|
||||||
import co.electriccoin.zcash.ui.screen.send.Send
|
import co.electriccoin.zcash.ui.screen.send.Send
|
||||||
|
@ -120,18 +125,32 @@ import org.koin.compose.koinInject
|
||||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||||
internal fun MainActivity.Navigation() {
|
internal fun MainActivity.Navigation() {
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
|
val keyboardManager = LocalKeyboardManager.current
|
||||||
val flexaViewModel = koinViewModel<FlexaViewModel>()
|
val flexaViewModel = koinViewModel<FlexaViewModel>()
|
||||||
val navigationRouter = koinInject<NavigationRouter>()
|
val navigationRouter = koinInject<NavigationRouter>()
|
||||||
|
val sheetStateManager = LocalSheetStateManager.current
|
||||||
|
|
||||||
// Helper properties for triggering the system security UI from callbacks
|
// Helper properties for triggering the system security UI from callbacks
|
||||||
val (exportPrivateDataAuthentication, setExportPrivateDataAuthentication) =
|
val (exportPrivateDataAuthentication, setExportPrivateDataAuthentication) =
|
||||||
rememberSaveable { mutableStateOf(false) }
|
rememberSaveable { mutableStateOf(false) }
|
||||||
val (seedRecoveryAuthentication, setSeedRecoveryAuthentication) =
|
|
||||||
rememberSaveable { mutableStateOf(false) }
|
|
||||||
val (deleteWalletAuthentication, setDeleteWalletAuthentication) =
|
val (deleteWalletAuthentication, setDeleteWalletAuthentication) =
|
||||||
rememberSaveable { mutableStateOf(false) }
|
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) {
|
LaunchedEffect(Unit) {
|
||||||
navigationRouter.observePipeline().collect {
|
navigationRouter.observePipeline().collect {
|
||||||
|
@ -163,14 +182,6 @@ internal fun MainActivity.Navigation() {
|
||||||
unProtectedDestination = EXPORT_PRIVATE_DATA
|
unProtectedDestination = EXPORT_PRIVATE_DATA
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
goSeedRecovery = {
|
|
||||||
navController.checkProtectedDestination(
|
|
||||||
scope = lifecycleScope,
|
|
||||||
propertyToCheck = authenticationViewModel.isSeedAuthenticationRequired,
|
|
||||||
setCheckedProperty = setSeedRecoveryAuthentication,
|
|
||||||
unProtectedDestination = SEED_RECOVERY
|
|
||||||
)
|
|
||||||
},
|
|
||||||
goDeleteWallet = {
|
goDeleteWallet = {
|
||||||
navController.checkProtectedDestination(
|
navController.checkProtectedDestination(
|
||||||
scope = lifecycleScope,
|
scope = lifecycleScope,
|
||||||
|
@ -199,27 +210,13 @@ internal fun MainActivity.Navigation() {
|
||||||
setCheckedProperty = setExportPrivateDataAuthentication
|
setCheckedProperty = setExportPrivateDataAuthentication
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
seedRecoveryAuthentication -> {
|
|
||||||
ShowSystemAuthentication(
|
|
||||||
navHostController = navController,
|
|
||||||
protectedDestination = SEED_RECOVERY,
|
|
||||||
protectedUseCase = AuthenticationUseCase.SeedRecovery,
|
|
||||||
setCheckedProperty = setSeedRecoveryAuthentication
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable(CHOOSE_SERVER) {
|
composable(CHOOSE_SERVER) {
|
||||||
WrapChooseServer()
|
WrapChooseServer()
|
||||||
}
|
}
|
||||||
composable(SEED_RECOVERY) {
|
composable<SeedRecovery> {
|
||||||
WrapSeed(
|
AndroidSeedRecovery()
|
||||||
args = SeedNavigationArgs.RECOVERY,
|
|
||||||
goBackOverride = {
|
|
||||||
setSeedRecoveryAuthentication(false)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
composable(SUPPORT) {
|
composable(SUPPORT) {
|
||||||
// Pop back stack won't be right if we deep link into support
|
// Pop back stack won't be right if we deep link into support
|
||||||
|
@ -275,21 +272,8 @@ internal fun MainActivity.Navigation() {
|
||||||
) {
|
) {
|
||||||
AndroidAccountList()
|
AndroidAccountList()
|
||||||
}
|
}
|
||||||
composable(
|
composable<Scan> {
|
||||||
route = Scan.ROUTE,
|
WrapScanValidator(it.toRoute())
|
||||||
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(EXPORT_PRIVATE_DATA) {
|
composable(EXPORT_PRIVATE_DATA) {
|
||||||
WrapExportPrivateData(
|
WrapExportPrivateData(
|
||||||
|
@ -405,6 +389,18 @@ internal fun MainActivity.Navigation() {
|
||||||
composable<Send> {
|
composable<Send> {
|
||||||
WrapSend(it.toRoute())
|
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 NOT_ENOUGH_SPACE = "not_enough_space"
|
||||||
const val QR_CODE = "qr_code"
|
const val QR_CODE = "qr_code"
|
||||||
const val REQUEST = "request"
|
const val REQUEST = "request"
|
||||||
const val SEED_RECOVERY = "seed_recovery"
|
|
||||||
const val SETTINGS = "settings"
|
const val SETTINGS = "settings"
|
||||||
const val SETTINGS_EXCHANGE_RATE_OPT_IN = "settings_exchange_rate_opt_in"
|
const val SETTINGS_EXCHANGE_RATE_OPT_IN = "settings_exchange_rate_opt_in"
|
||||||
const val SUPPORT = "support"
|
const val SUPPORT = "support"
|
||||||
|
|
|
@ -5,6 +5,8 @@ import androidx.activity.ComponentActivity
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.NavOptionsBuilder
|
import androidx.navigation.NavOptionsBuilder
|
||||||
import androidx.navigation.serialization.generateHashCode
|
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.ExternalUrl
|
||||||
import co.electriccoin.zcash.ui.screen.about.util.WebBrowserUtil
|
import co.electriccoin.zcash.ui.screen.about.util.WebBrowserUtil
|
||||||
import co.electriccoin.zcash.ui.screen.flexa.FlexaViewModel
|
import co.electriccoin.zcash.ui.screen.flexa.FlexaViewModel
|
||||||
|
@ -14,15 +16,19 @@ import kotlinx.serialization.InternalSerializationApi
|
||||||
import kotlinx.serialization.serializer
|
import kotlinx.serialization.serializer
|
||||||
|
|
||||||
interface Navigator {
|
interface Navigator {
|
||||||
fun executeCommand(command: NavigationCommand)
|
suspend fun executeCommand(command: NavigationCommand)
|
||||||
}
|
}
|
||||||
|
|
||||||
class NavigatorImpl(
|
class NavigatorImpl(
|
||||||
private val activity: ComponentActivity,
|
private val activity: ComponentActivity,
|
||||||
private val navController: NavHostController,
|
private val navController: NavHostController,
|
||||||
private val flexaViewModel: FlexaViewModel,
|
private val flexaViewModel: FlexaViewModel,
|
||||||
|
private val keyboardManager: KeyboardManager,
|
||||||
|
private val sheetStateManager: SheetStateManager,
|
||||||
) : Navigator {
|
) : Navigator {
|
||||||
override fun executeCommand(command: NavigationCommand) {
|
override suspend fun executeCommand(command: NavigationCommand) {
|
||||||
|
keyboardManager.close()
|
||||||
|
sheetStateManager.hide()
|
||||||
when (command) {
|
when (command) {
|
||||||
is NavigationCommand.Forward -> forward(command)
|
is NavigationCommand.Forward -> forward(command)
|
||||||
is NavigationCommand.Replace -> replace(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.TopAppBarSubTitleState
|
||||||
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||||
import co.electriccoin.zcash.ui.common.model.ZashiAccount
|
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.GetWalletStateInformationUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedWalletAccountUseCase
|
|
||||||
import co.electriccoin.zcash.ui.design.R
|
import co.electriccoin.zcash.ui.design.R
|
||||||
import co.electriccoin.zcash.ui.design.component.IconButtonState
|
import co.electriccoin.zcash.ui.design.component.IconButtonState
|
||||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||||
|
@ -30,7 +31,8 @@ import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ZashiTopAppBarViewModel(
|
class ZashiTopAppBarViewModel(
|
||||||
observeSelectedWalletAccount: ObserveSelectedWalletAccountUseCase,
|
getWalletAccountUseCase: GetWalletAccountsUseCase,
|
||||||
|
getSelectedWalletAccount: GetSelectedWalletAccountUseCase,
|
||||||
getWalletStateInformation: GetWalletStateInformationUseCase,
|
getWalletStateInformation: GetWalletStateInformationUseCase,
|
||||||
private val standardPreferenceProvider: StandardPreferenceProvider,
|
private val standardPreferenceProvider: StandardPreferenceProvider,
|
||||||
private val navigationRouter: NavigationRouter,
|
private val navigationRouter: NavigationRouter,
|
||||||
|
@ -39,7 +41,7 @@ class ZashiTopAppBarViewModel(
|
||||||
|
|
||||||
val state =
|
val state =
|
||||||
combine(
|
combine(
|
||||||
observeSelectedWalletAccount.require(),
|
getSelectedWalletAccount.observe().filterNotNull(),
|
||||||
isHideBalances,
|
isHideBalances,
|
||||||
getWalletStateInformation.observe()
|
getWalletStateInformation.observe()
|
||||||
) { currentAccount, isHideBalances, walletState ->
|
) { currentAccount, isHideBalances, walletState ->
|
||||||
|
@ -47,11 +49,16 @@ class ZashiTopAppBarViewModel(
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
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(
|
private fun createState(
|
||||||
currentAccount: WalletAccount,
|
currentAccount: WalletAccount?,
|
||||||
isHideBalances: Boolean?,
|
isHideBalances: Boolean?,
|
||||||
topAppBarSubTitleState: TopAppBarSubTitleState
|
topAppBarSubTitleState: TopAppBarSubTitleState
|
||||||
) = ZashiMainTopAppBarState(
|
) = ZashiMainTopAppBarState(
|
||||||
|
@ -61,6 +68,7 @@ class ZashiTopAppBarViewModel(
|
||||||
when (currentAccount) {
|
when (currentAccount) {
|
||||||
is KeystoneAccount -> ZashiMainTopAppBarState.AccountType.KEYSTONE
|
is KeystoneAccount -> ZashiMainTopAppBarState.AccountType.KEYSTONE
|
||||||
is ZashiAccount -> ZashiMainTopAppBarState.AccountType.ZASHI
|
is ZashiAccount -> ZashiMainTopAppBarState.AccountType.ZASHI
|
||||||
|
else -> ZashiMainTopAppBarState.AccountType.ZASHI
|
||||||
},
|
},
|
||||||
onAccountTypeClick = ::onAccountTypeClicked,
|
onAccountTypeClick = ::onAccountTypeClicked,
|
||||||
),
|
),
|
||||||
|
|
|
@ -49,7 +49,7 @@ class TransactionHistoryMapper {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
val transactionMetadata = data.metadata
|
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),
|
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||||
initialValue = null
|
initialValue = null
|
||||||
)
|
)
|
||||||
|
|
||||||
override val isCoinbaseAvailable: StateFlow<Boolean?> =
|
override val isCoinbaseAvailable: StateFlow<Boolean?> =
|
||||||
flow {
|
flow {
|
||||||
val versionInfo = getVersionInfo()
|
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.spackle.Twig
|
||||||
import co.electriccoin.zcash.ui.NavigationRouter
|
import co.electriccoin.zcash.ui.NavigationRouter
|
||||||
import co.electriccoin.zcash.ui.NavigationTargets.EXCHANGE_RATE_OPT_IN
|
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.ExchangeRateState
|
||||||
import co.electriccoin.zcash.ui.common.wallet.RefreshLock
|
import co.electriccoin.zcash.ui.common.wallet.RefreshLock
|
||||||
import co.electriccoin.zcash.ui.common.wallet.StaleLock
|
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.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.emitAll
|
import kotlinx.coroutines.flow.emitAll
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
@ -34,6 +34,7 @@ import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
interface ExchangeRateRepository {
|
interface ExchangeRateRepository {
|
||||||
val isExchangeRateUsdOptedIn: StateFlow<Boolean?>
|
val isExchangeRateUsdOptedIn: StateFlow<Boolean?>
|
||||||
|
@ -48,7 +49,7 @@ interface ExchangeRateRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExchangeRateRepositoryImpl(
|
class ExchangeRateRepositoryImpl(
|
||||||
private val walletRepository: WalletRepository,
|
private val synchronizerProvider: SynchronizerProvider,
|
||||||
private val standardPreferenceProvider: StandardPreferenceProvider,
|
private val standardPreferenceProvider: StandardPreferenceProvider,
|
||||||
private val navigationRouter: NavigationRouter,
|
private val navigationRouter: NavigationRouter,
|
||||||
) : ExchangeRateRepository {
|
) : ExchangeRateRepository {
|
||||||
|
@ -61,7 +62,8 @@ class ExchangeRateRepositoryImpl(
|
||||||
private val exchangeRateUsdInternal =
|
private val exchangeRateUsdInternal =
|
||||||
isExchangeRateUsdOptedIn.flatMapLatest { optedIn ->
|
isExchangeRateUsdOptedIn.flatMapLatest { optedIn ->
|
||||||
if (optedIn == true) {
|
if (optedIn == true) {
|
||||||
walletRepository.synchronizer
|
synchronizerProvider
|
||||||
|
.synchronizer
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
.flatMapLatest { synchronizer ->
|
.flatMapLatest { synchronizer ->
|
||||||
synchronizer.exchangeRateUsd
|
synchronizer.exchangeRateUsd
|
||||||
|
@ -105,46 +107,7 @@ class ExchangeRateRepositoryImpl(
|
||||||
staleExchangeRateUsdLock.state,
|
staleExchangeRateUsdLock.state,
|
||||||
refreshExchangeRateUsdLock.state,
|
refreshExchangeRateUsdLock.state,
|
||||||
) { isOptedIn, exchangeRate, isStale, isRefreshEnabled ->
|
) { isOptedIn, exchangeRate, isStale, isRefreshEnabled ->
|
||||||
lastExchangeRateUsdValue =
|
createState(isOptedIn, exchangeRate, isStale, isRefreshEnabled)
|
||||||
when (isOptedIn) {
|
|
||||||
true ->
|
|
||||||
when (val lastValue = lastExchangeRateUsdValue) {
|
|
||||||
is ExchangeRateState.Data ->
|
|
||||||
lastValue.copy(
|
|
||||||
isLoading = exchangeRate.isLoading,
|
|
||||||
isStale = isStale,
|
|
||||||
isRefreshEnabled = isRefreshEnabled,
|
|
||||||
currencyConversion = exchangeRate.currencyConversion,
|
|
||||||
)
|
|
||||||
|
|
||||||
ExchangeRateState.OptedOut ->
|
|
||||||
ExchangeRateState.Data(
|
|
||||||
isLoading = exchangeRate.isLoading,
|
|
||||||
isStale = isStale,
|
|
||||||
isRefreshEnabled = isRefreshEnabled,
|
|
||||||
currencyConversion = exchangeRate.currencyConversion,
|
|
||||||
onRefresh = ::refreshExchangeRateUsd
|
|
||||||
)
|
|
||||||
|
|
||||||
is ExchangeRateState.OptIn ->
|
|
||||||
ExchangeRateState.Data(
|
|
||||||
isLoading = exchangeRate.isLoading,
|
|
||||||
isStale = isStale,
|
|
||||||
isRefreshEnabled = isRefreshEnabled,
|
|
||||||
currencyConversion = exchangeRate.currencyConversion,
|
|
||||||
onRefresh = ::refreshExchangeRateUsd
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
false -> ExchangeRateState.OptedOut
|
|
||||||
null ->
|
|
||||||
ExchangeRateState.OptIn(
|
|
||||||
onDismissClick = ::dismissWidgetOptInExchangeRateUsd,
|
|
||||||
onPrimaryClick = ::showOptInExchangeRateUsd
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
lastExchangeRateUsdValue
|
|
||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
.onEach {
|
.onEach {
|
||||||
Twig.info { "[USD] $it" }
|
Twig.info { "[USD] $it" }
|
||||||
|
@ -157,17 +120,71 @@ class ExchangeRateRepositoryImpl(
|
||||||
}
|
}
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
started = SharingStarted.WhileSubscribed(),
|
started = SharingStarted.WhileSubscribed(5.seconds, 5.seconds),
|
||||||
initialValue = ExchangeRateState.OptedOut
|
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 ->
|
||||||
|
when (val lastValue = lastExchangeRateUsdValue) {
|
||||||
|
is ExchangeRateState.Data ->
|
||||||
|
lastValue.copy(
|
||||||
|
isLoading = exchangeRate.isLoading,
|
||||||
|
isStale = isStale,
|
||||||
|
isRefreshEnabled = isRefreshEnabled,
|
||||||
|
currencyConversion = exchangeRate.currencyConversion,
|
||||||
|
)
|
||||||
|
|
||||||
|
ExchangeRateState.OptedOut ->
|
||||||
|
ExchangeRateState.Data(
|
||||||
|
isLoading = exchangeRate.isLoading,
|
||||||
|
isStale = isStale,
|
||||||
|
isRefreshEnabled = isRefreshEnabled,
|
||||||
|
currencyConversion = exchangeRate.currencyConversion,
|
||||||
|
onRefresh = ::refreshExchangeRateUsd
|
||||||
|
)
|
||||||
|
|
||||||
|
is ExchangeRateState.OptIn ->
|
||||||
|
ExchangeRateState.Data(
|
||||||
|
isLoading = exchangeRate.isLoading,
|
||||||
|
isStale = isStale,
|
||||||
|
isRefreshEnabled = isRefreshEnabled,
|
||||||
|
currencyConversion = exchangeRate.currencyConversion,
|
||||||
|
onRefresh = ::refreshExchangeRateUsd
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
false -> ExchangeRateState.OptedOut
|
||||||
|
null ->
|
||||||
|
ExchangeRateState.OptIn(
|
||||||
|
onDismissClick = ::dismissWidgetOptInExchangeRateUsd,
|
||||||
|
onPrimaryClick = ::showOptInExchangeRateUsd
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastExchangeRateUsdValue
|
||||||
|
}
|
||||||
|
|
||||||
override fun refreshExchangeRateUsd() {
|
override fun refreshExchangeRateUsd() {
|
||||||
refreshExchangeRateUsdInternal()
|
refreshExchangeRateUsdInternal()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshExchangeRateUsdInternal() =
|
private fun refreshExchangeRateUsdInternal() =
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val synchronizer = walletRepository.synchronizer.filterNotNull().first()
|
val synchronizer = synchronizerProvider.getSynchronizer()
|
||||||
val value = state.value
|
val value = state.value
|
||||||
if (value is ExchangeRateState.Data && value.isRefreshEnabled && !value.isLoading) {
|
if (value is ExchangeRateState.Data && value.isRefreshEnabled && !value.isLoading) {
|
||||||
synchronizer.refreshExchangeRateUsd()
|
synchronizer.refreshExchangeRateUsd()
|
||||||
|
|
|
@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -39,7 +38,7 @@ interface MetadataRepository {
|
||||||
|
|
||||||
suspend fun markTxMemoAsRead(txId: String)
|
suspend fun markTxMemoAsRead(txId: String)
|
||||||
|
|
||||||
fun observeTransactionMetadataByTxId(txId: String): Flow<TransactionMetadata?>
|
fun observeTransactionMetadataByTxId(txId: String): Flow<TransactionMetadata>
|
||||||
}
|
}
|
||||||
|
|
||||||
class MetadataRepositoryImpl(
|
class MetadataRepositoryImpl(
|
||||||
|
@ -161,9 +160,9 @@ class MetadataRepositoryImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun observeTransactionMetadataByTxId(txId: String): Flow<TransactionMetadata?> =
|
override fun observeTransactionMetadataByTxId(txId: String): Flow<TransactionMetadata> =
|
||||||
metadata
|
metadata
|
||||||
.map<Metadata?, TransactionMetadata?> { metadata ->
|
.map { metadata ->
|
||||||
val accountMetadata = metadata?.accountMetadata
|
val accountMetadata = metadata?.accountMetadata
|
||||||
|
|
||||||
TransactionMetadata(
|
TransactionMetadata(
|
||||||
|
@ -173,7 +172,6 @@ class MetadataRepositoryImpl(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.onStart { emit(null) }
|
|
||||||
|
|
||||||
private suspend fun getMetadataKey(selectedAccount: WalletAccount): MetadataKey {
|
private suspend fun getMetadataKey(selectedAccount: WalletAccount): MetadataKey {
|
||||||
val key = metadataKeyStorageProvider.get(selectedAccount)
|
val key = metadataKeyStorageProvider.get(selectedAccount)
|
||||||
|
|
|
@ -70,7 +70,6 @@ interface WalletRepository {
|
||||||
val synchronizer: StateFlow<Synchronizer?>
|
val synchronizer: StateFlow<Synchronizer?>
|
||||||
val secretState: StateFlow<SecretState>
|
val secretState: StateFlow<SecretState>
|
||||||
val fastestServers: StateFlow<FastestServersState>
|
val fastestServers: StateFlow<FastestServersState>
|
||||||
val persistableWallet: Flow<PersistableWallet?>
|
|
||||||
val onboardingState: Flow<OnboardingState>
|
val onboardingState: Flow<OnboardingState>
|
||||||
|
|
||||||
val allAccounts: Flow<List<WalletAccount>?>
|
val allAccounts: Flow<List<WalletAccount>?>
|
||||||
|
@ -99,8 +98,6 @@ interface WalletRepository {
|
||||||
|
|
||||||
suspend fun getSynchronizer(): Synchronizer
|
suspend fun getSynchronizer(): Synchronizer
|
||||||
|
|
||||||
suspend fun getPersistableWallet(): PersistableWallet
|
|
||||||
|
|
||||||
fun persistExistingWalletWithSeedPhrase(
|
fun persistExistingWalletWithSeedPhrase(
|
||||||
network: ZcashNetwork,
|
network: ZcashNetwork,
|
||||||
seedPhrase: SeedPhrase,
|
seedPhrase: SeedPhrase,
|
||||||
|
@ -110,7 +107,8 @@ interface WalletRepository {
|
||||||
|
|
||||||
class WalletRepositoryImpl(
|
class WalletRepositoryImpl(
|
||||||
accountDataSource: AccountDataSource,
|
accountDataSource: AccountDataSource,
|
||||||
persistableWalletProvider: PersistableWalletProvider,
|
configurationRepository: ConfigurationRepository,
|
||||||
|
private val persistableWalletProvider: PersistableWalletProvider,
|
||||||
private val synchronizerProvider: SynchronizerProvider,
|
private val synchronizerProvider: SynchronizerProvider,
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val getDefaultServers: GetDefaultServersProvider,
|
private val getDefaultServers: GetDefaultServersProvider,
|
||||||
|
@ -143,27 +141,21 @@ class WalletRepositoryImpl(
|
||||||
override val allAccounts: StateFlow<List<WalletAccount>?> = accountDataSource.allAccounts
|
override val allAccounts: StateFlow<List<WalletAccount>?> = accountDataSource.allAccounts
|
||||||
|
|
||||||
override val secretState: StateFlow<SecretState> =
|
override val secretState: StateFlow<SecretState> =
|
||||||
combine(
|
combine(configurationRepository.configurationFlow, onboardingState) { config, onboardingState ->
|
||||||
persistableWalletProvider.persistableWallet,
|
if (config == null) {
|
||||||
onboardingState
|
SecretState.LOADING
|
||||||
) { persistableWallet: PersistableWallet?, onboardingState: OnboardingState ->
|
} else {
|
||||||
when {
|
when (onboardingState) {
|
||||||
onboardingState == OnboardingState.NONE -> SecretState.None
|
OnboardingState.NEEDS_WARN,
|
||||||
onboardingState == OnboardingState.NEEDS_WARN -> SecretState.NeedsWarning
|
OnboardingState.NEEDS_BACKUP,
|
||||||
onboardingState == OnboardingState.NEEDS_BACKUP && persistableWallet != null -> {
|
OnboardingState.NONE -> SecretState.NONE
|
||||||
SecretState.NeedsBackup(persistableWallet)
|
OnboardingState.READY -> SecretState.READY
|
||||||
}
|
}
|
||||||
|
|
||||||
onboardingState == OnboardingState.READY && persistableWallet != null -> {
|
|
||||||
SecretState.Ready(persistableWallet)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> SecretState.None
|
|
||||||
}
|
}
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||||
initialValue = SecretState.Loading
|
initialValue = SecretState.LOADING
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
@ -204,11 +196,6 @@ class WalletRepositoryImpl(
|
||||||
initialValue = FastestServersState(servers = emptyList(), isLoading = true)
|
initialValue = FastestServersState(servers = emptyList(), isLoading = true)
|
||||||
)
|
)
|
||||||
|
|
||||||
override val persistableWallet: Flow<PersistableWallet?> =
|
|
||||||
secretState.map {
|
|
||||||
(it as? SecretState.Ready?)?.persistableWallet
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
override val currentWalletSnapshot: StateFlow<WalletSnapshot?> =
|
override val currentWalletSnapshot: StateFlow<WalletSnapshot?> =
|
||||||
combine(synchronizer, currentAccount) { synchronizer, currentAccount ->
|
combine(synchronizer, currentAccount) { synchronizer, currentAccount ->
|
||||||
|
@ -317,7 +304,7 @@ class WalletRepositoryImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getSelectedServer(): LightWalletEndpoint {
|
override suspend fun getSelectedServer(): LightWalletEndpoint {
|
||||||
return persistableWallet
|
return persistableWalletProvider.persistableWallet
|
||||||
.map {
|
.map {
|
||||||
it?.endpoint
|
it?.endpoint
|
||||||
}
|
}
|
||||||
|
@ -338,8 +325,6 @@ class WalletRepositoryImpl(
|
||||||
|
|
||||||
override suspend fun getSynchronizer(): Synchronizer = synchronizerProvider.getSynchronizer()
|
override suspend fun getSynchronizer(): Synchronizer = synchronizerProvider.getSynchronizer()
|
||||||
|
|
||||||
override suspend fun getPersistableWallet(): PersistableWallet = persistableWallet.filterNotNull().first()
|
|
||||||
|
|
||||||
override fun persistExistingWalletWithSeedPhrase(
|
override fun persistExistingWalletWithSeedPhrase(
|
||||||
network: ZcashNetwork,
|
network: ZcashNetwork,
|
||||||
seedPhrase: SeedPhrase,
|
seedPhrase: SeedPhrase,
|
||||||
|
|
|
@ -8,12 +8,8 @@ class ApplyTransactionFiltersUseCase(
|
||||||
private val transactionFilterRepository: TransactionFilterRepository,
|
private val transactionFilterRepository: TransactionFilterRepository,
|
||||||
private val navigationRouter: NavigationRouter,
|
private val navigationRouter: NavigationRouter,
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(
|
operator fun invoke(filters: List<TransactionFilter>) {
|
||||||
filters: List<TransactionFilter>,
|
|
||||||
hideBottomSheet: suspend () -> Unit
|
|
||||||
) {
|
|
||||||
transactionFilterRepository.apply(filters)
|
transactionFilterRepository.apply(filters)
|
||||||
hideBottomSheet()
|
|
||||||
navigationRouter.back()
|
navigationRouter.back()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,9 @@ class CreateOrUpdateTransactionNoteUseCase(
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(
|
||||||
txId: String,
|
txId: String,
|
||||||
note: String,
|
note: String
|
||||||
closeBottomSheet: suspend () -> Unit
|
|
||||||
) {
|
) {
|
||||||
metadataRepository.createOrUpdateTxNote(txId, note.trim())
|
metadataRepository.createOrUpdateTxNote(txId, note.trim())
|
||||||
closeBottomSheet()
|
|
||||||
navigationRouter.back()
|
navigationRouter.back()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,8 @@ class DeleteTransactionNoteUseCase(
|
||||||
private val metadataRepository: MetadataRepository,
|
private val metadataRepository: MetadataRepository,
|
||||||
private val navigationRouter: NavigationRouter
|
private val navigationRouter: NavigationRouter
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(txId: String) {
|
||||||
txId: String,
|
|
||||||
closeBottomSheet: suspend () -> Unit
|
|
||||||
) {
|
|
||||||
metadataRepository.deleteTxNote(txId)
|
metadataRepository.deleteTxNote(txId)
|
||||||
closeBottomSheet()
|
|
||||||
navigationRouter.back()
|
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
|
import co.electriccoin.zcash.ui.common.repository.ConfigurationRepository
|
||||||
|
|
||||||
class ObserveConfigurationUseCase(
|
class GetConfigurationUseCase(
|
||||||
private val configurationRepository: ConfigurationRepository
|
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.TransactionFilterRepository
|
||||||
import co.electriccoin.zcash.ui.common.repository.TransactionMetadata
|
import co.electriccoin.zcash.ui.common.repository.TransactionMetadata
|
||||||
import co.electriccoin.zcash.ui.common.repository.TransactionRepository
|
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.getString
|
||||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||||
import co.electriccoin.zcash.ui.util.CloseableScopeHolder
|
import co.electriccoin.zcash.ui.util.CloseableScopeHolder
|
||||||
import co.electriccoin.zcash.ui.util.CloseableScopeHolderImpl
|
import co.electriccoin.zcash.ui.util.CloseableScopeHolderImpl
|
||||||
import co.electriccoin.zcash.ui.util.combineToFlow
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
@ -232,23 +232,23 @@ class GetCurrentFilteredTransactionsUseCase(
|
||||||
} else {
|
} else {
|
||||||
val transactionMetadata = transaction.transactionMetadata
|
val transactionMetadata = transaction.transactionMetadata
|
||||||
|
|
||||||
hasMemo && (transactionMetadata == null || transactionMetadata.isRead.not())
|
hasMemo && transactionMetadata.isRead.not()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isBookmark(transaction: FilterTransactionData): Boolean {
|
private fun isBookmark(transaction: FilterTransactionData): Boolean {
|
||||||
return transaction.transactionMetadata?.isBookmarked ?: false
|
return transaction.transactionMetadata.isBookmarked
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hasNotes(transaction: FilterTransactionData): Boolean {
|
private fun hasNotes(transaction: FilterTransactionData): Boolean {
|
||||||
return transaction.transactionMetadata?.note != null
|
return transaction.transactionMetadata.note != null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hasNotesWithFulltext(
|
private fun hasNotesWithFulltext(
|
||||||
transaction: FilterTransactionData,
|
transaction: FilterTransactionData,
|
||||||
fulltextFilter: String
|
fulltextFilter: String
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return transaction.transactionMetadata?.note
|
return transaction.transactionMetadata.note
|
||||||
?.contains(
|
?.contains(
|
||||||
fulltextFilter,
|
fulltextFilter,
|
||||||
ignoreCase = true
|
ignoreCase = true
|
||||||
|
@ -288,7 +288,7 @@ private data class FilterTransactionData(
|
||||||
val transaction: Transaction,
|
val transaction: Transaction,
|
||||||
val contact: AddressBookContact?,
|
val contact: AddressBookContact?,
|
||||||
val recipientAddress: String?,
|
val recipientAddress: String?,
|
||||||
val transactionMetadata: TransactionMetadata?
|
val transactionMetadata: TransactionMetadata
|
||||||
)
|
)
|
||||||
|
|
||||||
private const val MIN_TEXT_FILTER_LENGTH = 3
|
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.Transaction
|
||||||
import co.electriccoin.zcash.ui.common.repository.TransactionMetadata
|
import co.electriccoin.zcash.ui.common.repository.TransactionMetadata
|
||||||
import co.electriccoin.zcash.ui.common.repository.TransactionRepository
|
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.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
@ -40,5 +40,5 @@ class GetCurrentTransactionsUseCase(
|
||||||
|
|
||||||
data class ListTransactionData(
|
data class ListTransactionData(
|
||||||
val transaction: Transaction,
|
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
|
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(
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.channelFlow
|
import kotlinx.coroutines.flow.channelFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
|
||||||
class GetTransactionDetailByIdUseCase(
|
class GetTransactionDetailByIdUseCase(
|
||||||
private val transactionRepository: TransactionRepository,
|
private val transactionRepository: TransactionRepository,
|
||||||
|
@ -30,43 +31,56 @@ class GetTransactionDetailByIdUseCase(
|
||||||
) {
|
) {
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun observe(txId: String) =
|
fun observe(txId: String) =
|
||||||
transactionRepository
|
channelFlow {
|
||||||
.observeTransaction(txId).filterNotNull().flatMapLatest { transaction ->
|
val transactionFlow =
|
||||||
channelFlow {
|
transactionRepository
|
||||||
launch {
|
.observeTransaction(txId)
|
||||||
combine(
|
.filterNotNull()
|
||||||
flow {
|
.stateIn(this)
|
||||||
emit(null)
|
|
||||||
emit(getWalletAddress(transactionRepository.getRecipients(transaction)))
|
val addressFlow =
|
||||||
},
|
transactionFlow
|
||||||
flow {
|
.mapLatest { getWalletAddress(transactionRepository.getRecipients(it)) }
|
||||||
emit(null)
|
.onStart { emit(null) }
|
||||||
emit(transaction.let { transactionRepository.getMemos(it) })
|
.distinctUntilChanged()
|
||||||
},
|
|
||||||
metadataRepository.observeTransactionMetadataByTxId(txId)
|
val memosFlow: Flow<List<String>?> =
|
||||||
) { address, memos, metadata ->
|
transactionFlow
|
||||||
Triple(address, memos, metadata)
|
.mapLatest<Transaction, List<String>?> { transactionRepository.getMemos(it) }
|
||||||
}.flatMapLatest { (address, memos, metadata) ->
|
.onStart { emit(null) }
|
||||||
addressBookRepository
|
.distinctUntilChanged()
|
||||||
.observeContactByAddress(address?.address.orEmpty())
|
|
||||||
.mapLatest { contact ->
|
val metadataFlow =
|
||||||
DetailedTransactionData(
|
metadataRepository
|
||||||
transaction = transaction,
|
.observeTransactionMetadataByTxId(txId)
|
||||||
memos = memos,
|
|
||||||
contact = contact,
|
val contactFlow =
|
||||||
recipientAddress = address,
|
addressFlow
|
||||||
metadata = metadata
|
.flatMapLatest { addressBookRepository.observeContactByAddress(it?.address.orEmpty()) }
|
||||||
)
|
.distinctUntilChanged()
|
||||||
}
|
|
||||||
}.collect {
|
combine(
|
||||||
send(it)
|
transactionFlow,
|
||||||
}
|
addressFlow,
|
||||||
}
|
memosFlow,
|
||||||
awaitClose {
|
metadataFlow,
|
||||||
// do nothing
|
contactFlow
|
||||||
}
|
) { transaction, address, memos, metadata, contact ->
|
||||||
}
|
DetailedTransactionData(
|
||||||
}.distinctUntilChanged().flowOn(Dispatchers.Default)
|
transaction = transaction,
|
||||||
|
memos = memos,
|
||||||
|
contact = contact,
|
||||||
|
recipientAddress = address,
|
||||||
|
metadata = metadata
|
||||||
|
)
|
||||||
|
}.collect {
|
||||||
|
send(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
awaitClose {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}.distinctUntilChanged().flowOn(Dispatchers.Default)
|
||||||
|
|
||||||
private suspend fun getWalletAddress(address: String?): WalletAddress? {
|
private suspend fun getWalletAddress(address: String?): WalletAddress? {
|
||||||
if (address == null) return null
|
if (address == null) return null
|
||||||
|
@ -86,5 +100,5 @@ data class DetailedTransactionData(
|
||||||
val memos: List<String>?,
|
val memos: List<String>?,
|
||||||
val contact: AddressBookContact?,
|
val contact: AddressBookContact?,
|
||||||
val recipientAddress: WalletAddress?,
|
val recipientAddress: WalletAddress?,
|
||||||
val metadata: TransactionMetadata?
|
val metadata: TransactionMetadata
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
package co.electriccoin.zcash.ui.common.usecase
|
package co.electriccoin.zcash.ui.common.usecase
|
||||||
|
|
||||||
import co.electriccoin.zcash.ui.common.repository.MetadataRepository
|
import co.electriccoin.zcash.ui.common.repository.MetadataRepository
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
class GetTransactionMetadataUseCase(
|
class GetTransactionMetadataUseCase(
|
||||||
private val metadataRepository: MetadataRepository,
|
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)
|
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 co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
|
||||||
class ObserveWalletAccountsUseCase(private val accountDataSource: AccountDataSource) {
|
class GetWalletAccountsUseCase(private val accountDataSource: AccountDataSource) {
|
||||||
operator fun invoke() = accountDataSource.allAccounts
|
fun observe() = accountDataSource.allAccounts
|
||||||
|
|
||||||
fun require() = accountDataSource.allAccounts.filterNotNull()
|
fun require() = accountDataSource.allAccounts.filterNotNull()
|
||||||
}
|
}
|
|
@ -10,7 +10,7 @@ class NavigateToCoinbaseUseCase(
|
||||||
private val navigationRouter: NavigationRouter
|
private val navigationRouter: NavigationRouter
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(replaceCurrentScreen: Boolean) {
|
suspend operator fun invoke(replaceCurrentScreen: Boolean) {
|
||||||
val transparent = accountDataSource.getZashiAccount().transparent
|
val transparent = accountDataSource.getSelectedAccount().transparent
|
||||||
val url = getUrl(transparent.address.address)
|
val url = getUrl(transparent.address.address)
|
||||||
if (replaceCurrentScreen) {
|
if (replaceCurrentScreen) {
|
||||||
navigationRouter.replace(ExternalUrl(url))
|
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
|
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.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
class ObserveSelectedEndpointUseCase(
|
class ObserveSelectedEndpointUseCase(
|
||||||
private val walletRepository: WalletRepository
|
private val persistableWalletProvider: PersistableWalletProvider
|
||||||
) {
|
) {
|
||||||
operator fun invoke() =
|
operator fun invoke() =
|
||||||
walletRepository.persistableWallet
|
persistableWalletProvider.persistableWallet
|
||||||
.map {
|
.map {
|
||||||
it?.endpoint
|
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.NavigationRouter
|
||||||
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
|
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
|
||||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.ScanFlow
|
||||||
import co.electriccoin.zcash.ui.screen.send.Send
|
import co.electriccoin.zcash.ui.screen.send.Send
|
||||||
|
|
||||||
class OnAddressScannedUseCase(
|
class OnAddressScannedUseCase(
|
||||||
|
@ -13,21 +14,21 @@ class OnAddressScannedUseCase(
|
||||||
operator fun invoke(
|
operator fun invoke(
|
||||||
address: String,
|
address: String,
|
||||||
addressType: AddressType,
|
addressType: AddressType,
|
||||||
scanFlow: Scan
|
scanArgs: Scan
|
||||||
) {
|
) {
|
||||||
require(addressType is AddressType.Valid)
|
require(addressType is AddressType.Valid)
|
||||||
|
|
||||||
when (scanFlow) {
|
when (scanArgs.flow) {
|
||||||
Scan.SEND -> {
|
ScanFlow.SEND -> {
|
||||||
prefillSend.request(PrefillSendData.FromAddressScan(address = address))
|
prefillSend.request(PrefillSendData.FromAddressScan(address = address))
|
||||||
navigationRouter.back()
|
navigationRouter.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
Scan.ADDRESS_BOOK -> {
|
ScanFlow.ADDRESS_BOOK -> {
|
||||||
navigationRouter.replace(AddContactArgs(address))
|
navigationRouter.replace(AddContactArgs(address))
|
||||||
}
|
}
|
||||||
|
|
||||||
Scan.HOMEPAGE -> {
|
ScanFlow.HOMEPAGE -> {
|
||||||
navigationRouter.replace(
|
navigationRouter.replace(
|
||||||
Send(
|
Send(
|
||||||
address,
|
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.contact.AddContactArgs
|
||||||
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewTransaction
|
import co.electriccoin.zcash.ui.screen.reviewtransaction.ReviewTransaction
|
||||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
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.ScanFlow.ADDRESS_BOOK
|
||||||
import co.electriccoin.zcash.ui.screen.scan.Scan.HOMEPAGE
|
import co.electriccoin.zcash.ui.screen.scan.ScanFlow.HOMEPAGE
|
||||||
import co.electriccoin.zcash.ui.screen.scan.Scan.SEND
|
import co.electriccoin.zcash.ui.screen.scan.ScanFlow.SEND
|
||||||
import co.electriccoin.zcash.ui.screen.send.Send
|
import co.electriccoin.zcash.ui.screen.send.Send
|
||||||
|
|
||||||
class OnZip321ScannedUseCase(
|
class OnZip321ScannedUseCase(
|
||||||
|
@ -29,20 +29,27 @@ class OnZip321ScannedUseCase(
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(
|
||||||
zip321: Zip321ParseUriValidation.Valid,
|
zip321: Zip321ParseUriValidation.Valid,
|
||||||
scanFlow: Scan
|
scanArgs: Scan
|
||||||
) {
|
) {
|
||||||
if (scanFlow == ADDRESS_BOOK) {
|
when (scanArgs.flow) {
|
||||||
navigationRouter.replace(AddContactArgs(zip321.payment.payments[0].recipientAddress.value))
|
ADDRESS_BOOK -> addressBookFlow(zip321)
|
||||||
} else {
|
SEND ->
|
||||||
createProposal(zip321, scanFlow)
|
if (scanArgs.isScanZip321Enabled) {
|
||||||
|
sendFlow(zip321)
|
||||||
|
} else {
|
||||||
|
sendFlowWithDisabledZip321(zip321)
|
||||||
|
}
|
||||||
|
|
||||||
|
HOMEPAGE -> homepageFlow(zip321)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addressBookFlow(zip321: Zip321ParseUriValidation.Valid) {
|
||||||
|
navigationRouter.replace(AddContactArgs(zip321.payment.payments[0].recipientAddress.value))
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("TooGenericExceptionCaught")
|
@Suppress("TooGenericExceptionCaught")
|
||||||
private suspend fun createProposal(
|
private suspend fun homepageFlow(zip321: Zip321ParseUriValidation.Valid) {
|
||||||
zip321: Zip321ParseUriValidation.Valid,
|
|
||||||
scanFlow: Scan
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
val proposal =
|
val proposal =
|
||||||
when (accountDataSource.getSelectedAccount()) {
|
when (accountDataSource.getSelectedAccount()) {
|
||||||
|
@ -57,35 +64,63 @@ class OnZip321ScannedUseCase(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scanFlow == HOMEPAGE) {
|
navigationRouter
|
||||||
navigationRouter
|
.replace(
|
||||||
.replace(
|
Send(
|
||||||
Send(
|
recipientAddress = proposal.destination.address,
|
||||||
recipientAddress = proposal.destination.address,
|
recipientAddressType =
|
||||||
recipientAddressType =
|
when (proposal.destination) {
|
||||||
when (proposal.destination) {
|
is WalletAddress.Sapling -> SAPLING
|
||||||
is WalletAddress.Sapling -> SAPLING
|
is WalletAddress.Tex -> TEX
|
||||||
is WalletAddress.Tex -> TEX
|
is WalletAddress.Transparent -> TRANSPARENT
|
||||||
is WalletAddress.Transparent -> TRANSPARENT
|
is WalletAddress.Unified -> UNIFIED
|
||||||
is WalletAddress.Unified -> UNIFIED
|
}
|
||||||
}
|
),
|
||||||
),
|
ReviewTransaction
|
||||||
ReviewTransaction
|
|
||||||
)
|
|
||||||
} else if (scanFlow == SEND) {
|
|
||||||
prefillSend.request(
|
|
||||||
PrefillSendData.All(
|
|
||||||
amount = proposal.amount,
|
|
||||||
address = proposal.destination.address,
|
|
||||||
fee = proposal.proposal.totalFeeRequired(),
|
|
||||||
memos = proposal.memo.value.takeIf { it.isNotEmpty() }?.let { listOf(it) }
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
navigationRouter.forward(ReviewTransaction)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
keystoneProposalRepository.clear()
|
keystoneProposalRepository.clear()
|
||||||
throw e
|
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,
|
||||||
|
address = proposal.destination.address,
|
||||||
|
fee = proposal.proposal.totalFeeRequired(),
|
||||||
|
memos = proposal.memo.value.takeIf { it.isNotEmpty() }?.let { listOf(it) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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 accountDataSource: AccountDataSource,
|
||||||
private val navigationRouter: NavigationRouter
|
private val navigationRouter: NavigationRouter
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(
|
suspend operator fun invoke(account: WalletAccount) {
|
||||||
account: WalletAccount,
|
|
||||||
hideBottomSheet: suspend () -> Unit
|
|
||||||
) {
|
|
||||||
accountDataSource.selectAccount(account)
|
accountDataSource.selectAccount(account)
|
||||||
hideBottomSheet()
|
|
||||||
navigationRouter.back()
|
navigationRouter.back()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,10 @@ class SendTransactionAgainUseCase(
|
||||||
) {
|
) {
|
||||||
operator fun invoke(value: DetailedTransactionData) {
|
operator fun invoke(value: DetailedTransactionData) {
|
||||||
prefillSendUseCase.request(value)
|
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 {
|
when {
|
||||||
(!required || versionInfo.isRunningUnderTestService) -> AuthenticationUIState.NotRequired
|
(!required || versionInfo.isRunningUnderTestService) -> AuthenticationUIState.NotRequired
|
||||||
(state == AuthenticationUIState.Initial) -> {
|
(state == AuthenticationUIState.Initial) -> {
|
||||||
if (secretState == SecretState.None ||
|
if (secretState == SecretState.NONE) {
|
||||||
secretState == SecretState.NeedsWarning
|
|
||||||
) {
|
|
||||||
appAccessAuthentication.value = AuthenticationUIState.NotRequired
|
appAccessAuthentication.value = AuthenticationUIState.NotRequired
|
||||||
AuthenticationUIState.NotRequired
|
AuthenticationUIState.NotRequired
|
||||||
} else {
|
} else {
|
||||||
|
@ -146,12 +144,6 @@ class AuthenticationViewModel(
|
||||||
val isDeleteWalletAuthenticationRequired: StateFlow<Boolean?> =
|
val isDeleteWalletAuthenticationRequired: StateFlow<Boolean?> =
|
||||||
booleanStateFlow(StandardPreferenceKeys.IS_DELETE_WALLET_AUTHENTICATION)
|
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
|
* Authentication framework result
|
||||||
*/
|
*/
|
||||||
|
@ -319,9 +311,6 @@ class AuthenticationViewModel(
|
||||||
AuthenticationUseCase.ExportPrivateData ->
|
AuthenticationUseCase.ExportPrivateData ->
|
||||||
R.string.authentication_use_case_export_data
|
R.string.authentication_use_case_export_data
|
||||||
|
|
||||||
AuthenticationUseCase.SeedRecovery ->
|
|
||||||
R.string.authentication_use_case_seed_recovery
|
|
||||||
|
|
||||||
AuthenticationUseCase.SendFunds ->
|
AuthenticationUseCase.SendFunds ->
|
||||||
R.string.authentication_use_case_send_funds
|
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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
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.StandardPreferenceProvider
|
||||||
import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
|
import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase
|
|
||||||
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
@ -16,7 +14,6 @@ import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
|
||||||
class OldHomeViewModel(
|
class OldHomeViewModel(
|
||||||
observeConfiguration: ObserveConfigurationUseCase,
|
|
||||||
private val standardPreferenceProvider: StandardPreferenceProvider,
|
private val standardPreferenceProvider: StandardPreferenceProvider,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
/**
|
/**
|
||||||
|
@ -30,8 +27,6 @@ class OldHomeViewModel(
|
||||||
*/
|
*/
|
||||||
val isHideBalances: StateFlow<Boolean?> = booleanStateFlow(StandardPreferenceKeys.IS_HIDE_BALANCES)
|
val isHideBalances: StateFlow<Boolean?> = booleanStateFlow(StandardPreferenceKeys.IS_HIDE_BALANCES)
|
||||||
|
|
||||||
val configurationFlow: StateFlow<Configuration?> = observeConfiguration()
|
|
||||||
|
|
||||||
private fun booleanStateFlow(default: BooleanPreferenceDefault): StateFlow<Boolean?> =
|
private fun booleanStateFlow(default: BooleanPreferenceDefault): StateFlow<Boolean?> =
|
||||||
flow<Boolean?> {
|
flow<Boolean?> {
|
||||||
emitAll(default.observe(standardPreferenceProvider()))
|
emitAll(default.observe(standardPreferenceProvider()))
|
||||||
|
|
|
@ -161,16 +161,10 @@ class WalletViewModel(
|
||||||
/**
|
/**
|
||||||
* Represents the state of the wallet secret.
|
* Represents the state of the wallet secret.
|
||||||
*/
|
*/
|
||||||
sealed class SecretState {
|
enum class SecretState {
|
||||||
object Loading : SecretState()
|
LOADING,
|
||||||
|
NONE,
|
||||||
object None : SecretState()
|
READY
|
||||||
|
|
||||||
object NeedsWarning : SecretState()
|
|
||||||
|
|
||||||
class NeedsBackup(val persistableWallet: PersistableWallet) : SecretState()
|
|
||||||
|
|
||||||
class Ready(val persistableWallet: PersistableWallet) : SecretState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
package co.electriccoin.zcash.ui.screen.accountlist
|
||||||
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.window.DialogWindowProvider
|
import androidx.compose.ui.window.DialogWindowProvider
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
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.view.AccountListView
|
||||||
import co.electriccoin.zcash.ui.screen.accountlist.viewmodel.AccountListViewModel
|
import co.electriccoin.zcash.ui.screen.accountlist.viewmodel.AccountListViewModel
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
@ -20,38 +17,12 @@ import org.koin.androidx.compose.koinViewModel
|
||||||
fun AndroidAccountList() {
|
fun AndroidAccountList() {
|
||||||
val viewModel = koinViewModel<AccountListViewModel>()
|
val viewModel = koinViewModel<AccountListViewModel>()
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
|
||||||
|
|
||||||
val parent = LocalView.current.parent
|
val parent = LocalView.current.parent
|
||||||
|
|
||||||
SideEffect {
|
SideEffect {
|
||||||
(parent as? DialogWindowProvider)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
(parent as? DialogWindowProvider)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||||
(parent as? DialogWindowProvider)?.window?.setDimAmount(0f)
|
(parent as? DialogWindowProvider)?.window?.setDimAmount(0f)
|
||||||
}
|
}
|
||||||
|
|
||||||
state?.let {
|
state?.let {
|
||||||
AccountListView(
|
AccountListView(it)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,16 @@ package co.electriccoin.zcash.ui.screen.accountlist.model
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
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.component.listitem.ZashiListItemState
|
||||||
import co.electriccoin.zcash.ui.design.util.StringResource
|
import co.electriccoin.zcash.ui.design.util.StringResource
|
||||||
|
|
||||||
data class AccountListState(
|
data class AccountListState(
|
||||||
val items: List<AccountListItem>?,
|
val items: List<AccountListItem>?,
|
||||||
val isLoading: Boolean,
|
val isLoading: Boolean,
|
||||||
val onBottomSheetHidden: () -> Unit,
|
|
||||||
val addWalletButton: ButtonState?,
|
val addWalletButton: ButtonState?,
|
||||||
val onBack: () -> Unit,
|
override val onBack: () -> Unit,
|
||||||
)
|
) : ModalBottomSheetState
|
||||||
|
|
||||||
data class ZashiAccountListItemState(
|
data class ZashiAccountListItemState(
|
||||||
@DrawableRes val icon: Int,
|
@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.LottieProgress
|
||||||
import co.electriccoin.zcash.ui.design.component.ZashiButton
|
import co.electriccoin.zcash.ui.design.component.ZashiButton
|
||||||
import co.electriccoin.zcash.ui.design.component.ZashiButtonDefaults
|
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.BaseListItem
|
||||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemColors
|
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.ZashiListItemDefaults
|
||||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemDesignType
|
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.listitem.ZashiListItemState
|
||||||
import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState
|
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.newcomponent.PreviewScreens
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||||
|
@ -54,16 +55,15 @@ import kotlinx.collections.immutable.persistentListOf
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
internal fun AccountListView(
|
internal fun AccountListView(
|
||||||
onDismissRequest: () -> Unit,
|
state: AccountListState,
|
||||||
sheetState: SheetState,
|
sheetState: SheetState = rememberScreenModalBottomSheetState(),
|
||||||
state: AccountListState
|
|
||||||
) {
|
) {
|
||||||
ZashiModalBottomSheet(
|
ZashiScreenModalBottomSheet(
|
||||||
|
state = state,
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
content = {
|
content = {
|
||||||
BottomSheetContent(state)
|
BottomSheetContent(state)
|
||||||
},
|
},
|
||||||
onDismissRequest = onDismissRequest
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +111,10 @@ private fun BottomSheetContent(state: AccountListState) {
|
||||||
Spacer(modifier = Modifier.height(32.dp))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
ZashiButton(
|
ZashiButton(
|
||||||
state = state.addWalletButton,
|
state = state.addWalletButton,
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp),
|
||||||
colors =
|
colors =
|
||||||
ZashiButtonDefaults.secondaryColors(
|
ZashiButtonDefaults.secondaryColors(
|
||||||
borderColor = ZashiColors.Btns.Secondary.btnSecondaryBorder
|
borderColor = ZashiColors.Btns.Secondary.btnSecondaryBorder
|
||||||
|
@ -270,11 +273,9 @@ private fun Preview() =
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
onBottomSheetHidden = {},
|
|
||||||
onBack = {},
|
onBack = {},
|
||||||
addWalletButton = ButtonState(stringRes("Connect Hardware Wallet"))
|
addWalletButton = ButtonState(stringRes("Connect Hardware Wallet"))
|
||||||
),
|
),
|
||||||
onDismissRequest = {},
|
|
||||||
sheetState =
|
sheetState =
|
||||||
rememberModalBottomSheetState(
|
rememberModalBottomSheetState(
|
||||||
skipHiddenState = true,
|
skipHiddenState = true,
|
||||||
|
@ -315,11 +316,9 @@ private fun HardwareWalletAddedPreview() =
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
onBottomSheetHidden = {},
|
|
||||||
onBack = {},
|
onBack = {},
|
||||||
addWalletButton = null
|
addWalletButton = null
|
||||||
),
|
),
|
||||||
onDismissRequest = {},
|
|
||||||
sheetState =
|
sheetState =
|
||||||
rememberModalBottomSheetState(
|
rememberModalBottomSheetState(
|
||||||
skipHiddenState = true,
|
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.KeystoneAccount
|
||||||
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||||
import co.electriccoin.zcash.ui.common.model.ZashiAccount
|
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.common.usecase.SelectWalletAccountUseCase
|
||||||
import co.electriccoin.zcash.ui.design.R
|
import co.electriccoin.zcash.ui.design.R
|
||||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
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.accountlist.model.ZashiAccountListItemState
|
||||||
import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.ADDRESS_MAX_LENGTH
|
import co.electriccoin.zcash.ui.screen.addressbook.viewmodel.ADDRESS_MAX_LENGTH
|
||||||
import co.electriccoin.zcash.ui.screen.connectkeystone.ConnectKeystone
|
import co.electriccoin.zcash.ui.screen.connectkeystone.ConnectKeystone
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.WhileSubscribed
|
import kotlinx.coroutines.flow.WhileSubscribed
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class AccountListViewModel(
|
class AccountListViewModel(
|
||||||
observeWalletAccounts: ObserveWalletAccountsUseCase,
|
getWalletAccounts: GetWalletAccountsUseCase,
|
||||||
private val selectWalletAccount: SelectWalletAccountUseCase,
|
private val selectWalletAccount: SelectWalletAccountUseCase,
|
||||||
private val navigationRouter: NavigationRouter,
|
private val navigationRouter: NavigationRouter,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val hideBottomSheetRequest = MutableSharedFlow<Unit>()
|
|
||||||
|
|
||||||
private val bottomSheetHiddenResponse = MutableSharedFlow<Unit>()
|
|
||||||
|
|
||||||
@Suppress("SpreadOperator")
|
@Suppress("SpreadOperator")
|
||||||
val state =
|
val state =
|
||||||
observeWalletAccounts().map { accounts ->
|
getWalletAccounts.observe().map { accounts ->
|
||||||
val items =
|
val items =
|
||||||
listOfNotNull(
|
listOfNotNull(
|
||||||
*accounts.orEmpty()
|
*accounts.orEmpty()
|
||||||
|
@ -77,7 +71,6 @@ class AccountListViewModel(
|
||||||
AccountListState(
|
AccountListState(
|
||||||
items = items,
|
items = items,
|
||||||
isLoading = accounts == null,
|
isLoading = accounts == null,
|
||||||
onBottomSheetHidden = ::onBottomSheetHidden,
|
|
||||||
onBack = ::onBack,
|
onBack = ::onBack,
|
||||||
addWalletButton =
|
addWalletButton =
|
||||||
ButtonState(
|
ButtonState(
|
||||||
|
@ -94,35 +87,14 @@ class AccountListViewModel(
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun onShowKeystonePromoClicked() =
|
private fun onShowKeystonePromoClicked() =
|
||||||
viewModelScope.launch {
|
navigationRouter.replace(ExternalUrl("https://keyst.one/shop/products/keystone-3-pro?discount=Zashi"))
|
||||||
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) =
|
private fun onAccountClicked(account: WalletAccount) =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
selectWalletAccount(account) { hideBottomSheet() }
|
selectWalletAccount(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onAddWalletButtonClicked() =
|
private fun onAddWalletButtonClicked() = navigationRouter.forward(ConnectKeystone)
|
||||||
viewModelScope.launch {
|
|
||||||
hideBottomSheet()
|
|
||||||
navigationRouter.forward(ConnectKeystone)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onBack() =
|
private fun onBack() = navigationRouter.back()
|
||||||
viewModelScope.launch {
|
|
||||||
hideBottomSheet()
|
|
||||||
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.AddContactArgs
|
||||||
import co.electriccoin.zcash.ui.screen.contact.UpdateContactArgs
|
import co.electriccoin.zcash.ui.screen.contact.UpdateContactArgs
|
||||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.ScanFlow
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.WhileSubscribed
|
import kotlinx.coroutines.flow.WhileSubscribed
|
||||||
|
@ -85,7 +86,7 @@ class AddressBookViewModel(
|
||||||
|
|
||||||
private fun onAddContactManuallyClick() = navigationRouter.forward(AddContactArgs(null))
|
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
|
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.KeystoneAccount
|
||||||
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
import co.electriccoin.zcash.ui.common.model.WalletAccount
|
||||||
import co.electriccoin.zcash.ui.common.model.ZashiAccount
|
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.ObserveAddressBookContactsUseCase
|
||||||
import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase
|
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.ButtonState
|
||||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiContactListItemState
|
import co.electriccoin.zcash.ui.design.component.listitem.ZashiContactListItemState
|
||||||
import co.electriccoin.zcash.ui.design.util.ImageResource
|
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.addressbook.model.AddressBookState
|
||||||
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
|
import co.electriccoin.zcash.ui.screen.contact.AddContactArgs
|
||||||
import co.electriccoin.zcash.ui.screen.scan.Scan
|
import co.electriccoin.zcash.ui.screen.scan.Scan
|
||||||
|
import co.electriccoin.zcash.ui.screen.scan.ScanFlow
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.WhileSubscribed
|
import kotlinx.coroutines.flow.WhileSubscribed
|
||||||
|
@ -31,12 +32,12 @@ import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class SelectRecipientViewModel(
|
class SelectRecipientViewModel(
|
||||||
observeAddressBookContacts: ObserveAddressBookContactsUseCase,
|
observeAddressBookContacts: ObserveAddressBookContactsUseCase,
|
||||||
observeWalletAccountsUseCase: ObserveWalletAccountsUseCase,
|
getWalletAccountsUseCase: GetWalletAccountsUseCase,
|
||||||
private val observeContactPicked: ObserveContactPickedUseCase,
|
private val observeContactPicked: ObserveContactPickedUseCase,
|
||||||
private val navigationRouter: NavigationRouter
|
private val navigationRouter: NavigationRouter
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val state =
|
val state =
|
||||||
combine(observeAddressBookContacts(), observeWalletAccountsUseCase()) { contacts, accounts ->
|
combine(observeAddressBookContacts(), getWalletAccountsUseCase.observe()) { contacts, accounts ->
|
||||||
if (accounts != null && accounts.size > 1) {
|
if (accounts != null && accounts.size > 1) {
|
||||||
createStateWithAccounts(contacts, accounts)
|
createStateWithAccounts(contacts, accounts)
|
||||||
} else {
|
} else {
|
||||||
|
@ -174,5 +175,5 @@ class SelectRecipientViewModel(
|
||||||
|
|
||||||
private fun onAddContactManuallyClick() = navigationRouter.forward(AddContactArgs(null))
|
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.ButtonState
|
||||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
|
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.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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.theme.dimensions.ZashiDimensions
|
||||||
import co.electriccoin.zcash.ui.design.util.scaffoldScrollPadding
|
import co.electriccoin.zcash.ui.design.util.scaffoldScrollPadding
|
||||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
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
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
// TODO [#1271]: Add AdvancedSettingsView Tests
|
// 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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.R
|
||||||
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
|
||||||
import co.electriccoin.zcash.ui.common.usecase.GetWalletRestoringStateUseCase
|
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.common.usecase.NavigateToTaxExportUseCase
|
||||||
import co.electriccoin.zcash.ui.design.component.ButtonState
|
import co.electriccoin.zcash.ui.design.component.ButtonState
|
||||||
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
|
import co.electriccoin.zcash.ui.design.component.listitem.ZashiListItemState
|
||||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||||
import co.electriccoin.zcash.ui.screen.advancedsettings.model.AdvancedSettingsState
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
@ -25,6 +25,7 @@ class AdvancedSettingsViewModel(
|
||||||
getWalletRestoringState: GetWalletRestoringStateUseCase,
|
getWalletRestoringState: GetWalletRestoringStateUseCase,
|
||||||
private val navigationRouter: NavigationRouter,
|
private val navigationRouter: NavigationRouter,
|
||||||
private val navigateToTaxExport: NavigateToTaxExportUseCase,
|
private val navigateToTaxExport: NavigateToTaxExportUseCase,
|
||||||
|
private val navigateToSeedRecovery: NavigateToSeedRecoveryUseCase
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val state: StateFlow<AdvancedSettingsState> =
|
val state: StateFlow<AdvancedSettingsState> =
|
||||||
getWalletRestoringState.observe()
|
getWalletRestoringState.observe()
|
||||||
|
@ -45,7 +46,7 @@ class AdvancedSettingsViewModel(
|
||||||
ZashiListItemState(
|
ZashiListItemState(
|
||||||
title = stringRes(R.string.advanced_settings_recovery),
|
title = stringRes(R.string.advanced_settings_recovery),
|
||||||
icon = R.drawable.ic_advanced_settings_recovery,
|
icon = R.drawable.ic_advanced_settings_recovery,
|
||||||
onClick = {}
|
onClick = ::onSeedRecoveryClick
|
||||||
),
|
),
|
||||||
ZashiListItemState(
|
ZashiListItemState(
|
||||||
title = stringRes(R.string.advanced_settings_export),
|
title = stringRes(R.string.advanced_settings_export),
|
||||||
|
@ -93,4 +94,9 @@ class AdvancedSettingsViewModel(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
navigateToTaxExport()
|
navigateToTaxExport()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onSeedRecoveryClick() =
|
||||||
|
viewModelScope.launch {
|
||||||
|
navigateToSeedRecovery()
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -7,8 +7,6 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import co.electriccoin.zcash.di.koinActivityViewModel
|
import co.electriccoin.zcash.di.koinActivityViewModel
|
||||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
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 kotlinx.collections.immutable.toImmutableList
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
@ -16,7 +14,6 @@ import org.koin.androidx.compose.koinViewModel
|
||||||
internal fun WrapAdvancedSettings(
|
internal fun WrapAdvancedSettings(
|
||||||
goDeleteWallet: () -> Unit,
|
goDeleteWallet: () -> Unit,
|
||||||
goExportPrivateData: () -> Unit,
|
goExportPrivateData: () -> Unit,
|
||||||
goSeedRecovery: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
val walletViewModel = koinActivityViewModel<WalletViewModel>()
|
||||||
val viewModel = koinViewModel<AdvancedSettingsViewModel>()
|
val viewModel = koinViewModel<AdvancedSettingsViewModel>()
|
||||||
|
@ -28,7 +25,6 @@ internal fun WrapAdvancedSettings(
|
||||||
items =
|
items =
|
||||||
originalState.items.mapIndexed { index, item ->
|
originalState.items.mapIndexed { index, item ->
|
||||||
when (index) {
|
when (index) {
|
||||||
0 -> item.copy(onClick = goSeedRecovery)
|
|
||||||
1 -> item.copy(onClick = goExportPrivateData)
|
1 -> item.copy(onClick = goExportPrivateData)
|
||||||
else -> item
|
else -> item
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||||
private const val APP_ACCESS_TRIGGER_DELAY = 0
|
private const val APP_ACCESS_TRIGGER_DELAY = 0
|
||||||
private const val DELETE_WALLET_TRIGGER_DELAY = 0
|
private const val DELETE_WALLET_TRIGGER_DELAY = 0
|
||||||
private const val EXPORT_PRIVATE_DATA_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
|
private const val SEND_FUNDS_DELAY = 0
|
||||||
internal const val RETRY_TRIGGER_DELAY = 0
|
internal const val RETRY_TRIGGER_DELAY = 0
|
||||||
|
|
||||||
|
@ -82,16 +81,6 @@ private fun WrapAuthenticationUseCases(
|
||||||
onFailed = onFailed
|
onFailed = onFailed
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AuthenticationUseCase.SeedRecovery -> {
|
|
||||||
Twig.debug { "Seed Recovery Authentication" }
|
|
||||||
WrapSeedRecoveryAuth(
|
|
||||||
activity = activity,
|
|
||||||
goSeedRecovery = onSuccess,
|
|
||||||
goSupport = goSupport ?: {},
|
|
||||||
onCancel = onCancel,
|
|
||||||
onFailed = onFailed
|
|
||||||
)
|
|
||||||
}
|
|
||||||
AuthenticationUseCase.SendFunds -> {
|
AuthenticationUseCase.SendFunds -> {
|
||||||
Twig.debug { "Send Funds Authentication" }
|
Twig.debug { "Send Funds Authentication" }
|
||||||
WrapSendFundsAuth(
|
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
|
@Composable
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
private fun WrapSendFundsAuth(
|
private fun WrapSendFundsAuth(
|
||||||
|
@ -472,8 +388,6 @@ private fun WrapAppAccessAuth(
|
||||||
sealed class AuthenticationUseCase {
|
sealed class AuthenticationUseCase {
|
||||||
data object AppAccess : AuthenticationUseCase()
|
data object AppAccess : AuthenticationUseCase()
|
||||||
|
|
||||||
data object SeedRecovery : AuthenticationUseCase()
|
|
||||||
|
|
||||||
data object DeleteWallet : AuthenticationUseCase()
|
data object DeleteWallet : AuthenticationUseCase()
|
||||||
|
|
||||||
data object ExportPrivateData : AuthenticationUseCase()
|
data object ExportPrivateData : AuthenticationUseCase()
|
||||||
|
|
|
@ -2,8 +2,10 @@ package co.electriccoin.zcash.ui.screen.balances
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import cash.z.ecc.android.sdk.model.Zatoshi
|
||||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||||
import co.electriccoin.zcash.ui.common.datasource.AccountDataSource
|
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.repository.ExchangeRateRepository
|
||||||
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
import co.electriccoin.zcash.ui.common.wallet.ExchangeRateState
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
@ -22,30 +24,41 @@ class BalanceViewModel(
|
||||||
accountDataSource.selectedAccount.filterNotNull(),
|
accountDataSource.selectedAccount.filterNotNull(),
|
||||||
exchangeRateRepository.state,
|
exchangeRateRepository.state,
|
||||||
) { account, exchangeRateUsd ->
|
) { account, exchangeRateUsd ->
|
||||||
when {
|
createState(account, exchangeRateUsd)
|
||||||
(
|
|
||||||
account.spendableBalance.value == 0L &&
|
|
||||||
account.totalBalance.value > 0L &&
|
|
||||||
(account.hasChangePending || account.hasValuePending)
|
|
||||||
) -> {
|
|
||||||
BalanceState.Loading(
|
|
||||||
totalBalance = account.totalBalance,
|
|
||||||
spendableBalance = account.spendableBalance,
|
|
||||||
exchangeRate = exchangeRateUsd,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
BalanceState.Available(
|
|
||||||
totalBalance = account.totalBalance,
|
|
||||||
spendableBalance = account.spendableBalance,
|
|
||||||
exchangeRate = exchangeRateUsd,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
viewModelScope,
|
viewModelScope,
|
||||||
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
|
||||||
BalanceState.None(ExchangeRateState.OptedOut)
|
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)
|
||||||
|
) -> {
|
||||||
|
BalanceState.Loading(
|
||||||
|
totalBalance = account.totalBalance,
|
||||||
|
spendableBalance = account.spendableBalance,
|
||||||
|
exchangeRate = exchangeRateUsd,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
BalanceState.Available(
|
||||||
|
totalBalance = account?.totalBalance ?: Zatoshi(0),
|
||||||
|
spendableBalance = account?.spendableBalance ?: Zatoshi(0),
|
||||||
|
exchangeRate = exchangeRateUsd,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
package co.electriccoin.zcash.ui.screen.home
|
package co.electriccoin.zcash.ui.screen.home
|
||||||
|
|
||||||
import co.electriccoin.zcash.ui.design.component.BigIconButtonState
|
import co.electriccoin.zcash.ui.design.component.BigIconButtonState
|
||||||
|
import co.electriccoin.zcash.ui.screen.home.messages.HomeMessageState
|
||||||
|
|
||||||
data class HomeState(
|
data class HomeState(
|
||||||
val receiveButton: BigIconButtonState,
|
val firstButton: BigIconButtonState,
|
||||||
val sendButton: BigIconButtonState,
|
val secondButton: BigIconButtonState,
|
||||||
val scanButton: BigIconButtonState,
|
val thirdButton: BigIconButtonState,
|
||||||
val moreButton: BigIconButtonState,
|
val fourthButton: BigIconButtonState,
|
||||||
|
val message: HomeMessageState?
|
||||||
)
|
)
|
||||||
|
|
||||||
data class HomeRestoreDialogState(
|
data class HomeRestoreDialogState(
|
||||||
|
|
|
@ -1,9 +1,5 @@
|
||||||
package co.electriccoin.zcash.ui.screen.home
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.R
|
||||||
import co.electriccoin.zcash.ui.common.appbar.ZashiMainTopAppBarState
|
import co.electriccoin.zcash.ui.common.appbar.ZashiMainTopAppBarState
|
||||||
import co.electriccoin.zcash.ui.common.appbar.ZashiTopAppBarWithAccountSelection
|
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.BigIconButtonState
|
||||||
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
|
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
|
||||||
import co.electriccoin.zcash.ui.design.component.ZashiBigIconButton
|
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.fixture.ZashiMainTopAppBarStateFixture
|
||||||
import co.electriccoin.zcash.ui.screen.balances.BalanceState
|
import co.electriccoin.zcash.ui.screen.balances.BalanceState
|
||||||
import co.electriccoin.zcash.ui.screen.balances.BalanceWidget
|
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.TransactionHistoryWidgetState
|
||||||
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetStateFixture
|
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.TransactionHistoryWidgetStateFixture
|
||||||
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.createTransactionHistoryWidgets
|
import co.electriccoin.zcash.ui.screen.transactionhistory.widget.createTransactionHistoryWidgets
|
||||||
|
@ -74,7 +69,6 @@ private fun Content(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
|
||||||
|
|
||||||
BalanceWidget(
|
BalanceWidget(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
@ -84,39 +78,10 @@ private fun Content(
|
||||||
),
|
),
|
||||||
balanceState = balanceState,
|
balanceState = balanceState,
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
|
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge))
|
||||||
|
NavButtons(paddingValues, state)
|
||||||
Row(
|
Spacer(Modifier.height(16.dp))
|
||||||
modifier = Modifier.scaffoldPadding(paddingValues, top = 0.dp, bottom = 0.dp),
|
HomeMessage(state.message)
|
||||||
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))
|
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
@ -128,29 +93,46 @@ private fun Content(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
@Composable
|
||||||
visible = balanceState.exchangeRate is ExchangeRateState.OptIn,
|
private fun NavButtons(
|
||||||
enter = EnterTransition.None,
|
paddingValues: PaddingValues,
|
||||||
exit = fadeOut() + slideOutVertically(),
|
state: HomeState
|
||||||
) {
|
) {
|
||||||
Column {
|
Row(
|
||||||
Spacer(modifier = Modifier.height(66.dp + paddingValues.calculateTopPadding()))
|
modifier = Modifier.scaffoldPadding(paddingValues, top = 0.dp, bottom = 0.dp),
|
||||||
StyledExchangeOptIn(
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
modifier = Modifier.padding(horizontal = 24.dp),
|
) {
|
||||||
state =
|
ZashiBigIconButton(
|
||||||
(balanceState.exchangeRate as? ExchangeRateState.OptIn) ?: ExchangeRateState.OptIn(
|
modifier =
|
||||||
onDismissClick = {},
|
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
|
@PreviewScreens
|
||||||
@Composable
|
@Composable
|
||||||
private fun Preview() =
|
private fun Preview() {
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
HomeView(
|
HomeView(
|
||||||
appBarState = ZashiMainTopAppBarStateFixture.new(),
|
appBarState = ZashiMainTopAppBarStateFixture.new(),
|
||||||
|
@ -158,30 +140,32 @@ private fun Preview() =
|
||||||
transactionWidgetState = TransactionHistoryWidgetStateFixture.new(),
|
transactionWidgetState = TransactionHistoryWidgetStateFixture.new(),
|
||||||
state =
|
state =
|
||||||
HomeState(
|
HomeState(
|
||||||
receiveButton =
|
firstButton =
|
||||||
BigIconButtonState(
|
BigIconButtonState(
|
||||||
text = stringRes("Text"),
|
text = stringRes("Text"),
|
||||||
icon = R.drawable.ic_warning,
|
icon = R.drawable.ic_warning,
|
||||||
onClick = {}
|
onClick = {}
|
||||||
),
|
),
|
||||||
sendButton =
|
secondButton =
|
||||||
BigIconButtonState(
|
BigIconButtonState(
|
||||||
text = stringRes("Text"),
|
text = stringRes("Text"),
|
||||||
icon = R.drawable.ic_warning,
|
icon = R.drawable.ic_warning,
|
||||||
onClick = {}
|
onClick = {}
|
||||||
),
|
),
|
||||||
scanButton =
|
thirdButton =
|
||||||
BigIconButtonState(
|
BigIconButtonState(
|
||||||
text = stringRes("Text"),
|
text = stringRes("Text"),
|
||||||
icon = R.drawable.ic_warning,
|
icon = R.drawable.ic_warning,
|
||||||
onClick = {}
|
onClick = {}
|
||||||
),
|
),
|
||||||
moreButton =
|
fourthButton =
|
||||||
BigIconButtonState(
|
BigIconButtonState(
|
||||||
text = stringRes("Text"),
|
text = stringRes("Text"),
|
||||||
icon = R.drawable.ic_warning,
|
icon = R.drawable.ic_warning,
|
||||||
onClick = {}
|
onClick = {}
|
||||||
),
|
),
|
||||||
|
message = null
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,28 +4,44 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
|
||||||
import co.electriccoin.zcash.ui.NavigationRouter
|
import co.electriccoin.zcash.ui.NavigationRouter
|
||||||
|
import co.electriccoin.zcash.ui.NavigationTargets
|
||||||
import co.electriccoin.zcash.ui.R
|
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.IsRestoreSuccessDialogVisibleUseCase
|
||||||
|
import co.electriccoin.zcash.ui.common.usecase.NavigateToCoinbaseUseCase
|
||||||
import co.electriccoin.zcash.ui.design.component.BigIconButtonState
|
import co.electriccoin.zcash.ui.design.component.BigIconButtonState
|
||||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
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.integrations.DialogIntegrations
|
||||||
import co.electriccoin.zcash.ui.screen.receive.Receive
|
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.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 co.electriccoin.zcash.ui.screen.send.Send
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.WhileSubscribed
|
import kotlinx.coroutines.flow.WhileSubscribed
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class HomeViewModel(
|
class HomeViewModel(
|
||||||
|
getVersionInfoProvider: GetVersionInfoProvider,
|
||||||
|
getSelectedWalletAccountUseCase: GetSelectedWalletAccountUseCase,
|
||||||
private val navigationRouter: NavigationRouter,
|
private val navigationRouter: NavigationRouter,
|
||||||
private val isRestoreSuccessDialogVisible: IsRestoreSuccessDialogVisibleUseCase
|
private val isRestoreSuccessDialogVisible: IsRestoreSuccessDialogVisibleUseCase,
|
||||||
|
private val navigateToCoinbase: NavigateToCoinbaseUseCase
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
private val isMessageVisible = MutableStateFlow(true)
|
||||||
|
|
||||||
private val isRestoreDialogVisible: Flow<Boolean?> =
|
private val isRestoreDialogVisible: Flow<Boolean?> =
|
||||||
isRestoreSuccessDialogVisible.observe()
|
isRestoreSuccessDialogVisible.observe()
|
||||||
.stateIn(
|
.stateIn(
|
||||||
|
@ -48,36 +64,72 @@ class HomeViewModel(
|
||||||
)
|
)
|
||||||
|
|
||||||
val state: StateFlow<HomeState?> =
|
val state: StateFlow<HomeState?> =
|
||||||
MutableStateFlow(
|
combine(getSelectedWalletAccountUseCase.observe(), isMessageVisible) { selectedAccount, isMessageVisible ->
|
||||||
HomeState(
|
createState(getVersionInfoProvider, selectedAccount, isMessageVisible)
|
||||||
receiveButton =
|
}.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(R.string.home_button_receive),
|
||||||
|
icon = R.drawable.ic_home_receive,
|
||||||
|
onClick = ::onReceiveButtonClick,
|
||||||
|
),
|
||||||
|
secondButton =
|
||||||
|
BigIconButtonState(
|
||||||
|
text = stringRes(R.string.home_button_send),
|
||||||
|
icon = R.drawable.ic_home_send,
|
||||||
|
onClick = ::onSendButtonClick,
|
||||||
|
),
|
||||||
|
thirdButton =
|
||||||
|
BigIconButtonState(
|
||||||
|
text = stringRes(R.string.home_button_scan),
|
||||||
|
icon = R.drawable.ic_home_scan,
|
||||||
|
onClick = ::onScanButtonClick,
|
||||||
|
),
|
||||||
|
fourthButton =
|
||||||
|
when {
|
||||||
|
getVersionInfoProvider().distributionDimension == DistributionDimension.FOSS ->
|
||||||
BigIconButtonState(
|
BigIconButtonState(
|
||||||
text = stringRes("Receive"),
|
text = stringRes(R.string.home_button_request),
|
||||||
icon = R.drawable.ic_home_receive,
|
icon = R.drawable.ic_home_request,
|
||||||
onClick = ::onReceiveButtonClick,
|
onClick = ::onRequestClick,
|
||||||
),
|
)
|
||||||
sendButton =
|
|
||||||
|
selectedAccount is KeystoneAccount ->
|
||||||
BigIconButtonState(
|
BigIconButtonState(
|
||||||
text = stringRes("Send"),
|
text = stringRes(R.string.home_button_buy),
|
||||||
icon = R.drawable.ic_home_send,
|
icon = R.drawable.ic_home_buy,
|
||||||
onClick = ::onSendButtonClick,
|
onClick = ::onBuyClick,
|
||||||
),
|
)
|
||||||
scanButton =
|
|
||||||
|
else ->
|
||||||
BigIconButtonState(
|
BigIconButtonState(
|
||||||
text = stringRes("Scan"),
|
text = stringRes(R.string.home_button_more),
|
||||||
icon = R.drawable.ic_home_scan,
|
|
||||||
onClick = ::onScanButtonClick,
|
|
||||||
),
|
|
||||||
moreButton =
|
|
||||||
BigIconButtonState(
|
|
||||||
text = stringRes("More"),
|
|
||||||
icon = R.drawable.ic_home_more,
|
icon = R.drawable.ic_home_more,
|
||||||
onClick = ::onMoreButtonClick,
|
onClick = ::onMoreButtonClick,
|
||||||
),
|
)
|
||||||
)
|
},
|
||||||
).asStateFlow()
|
message = createWalletBackupMessageState().takeIf { isMessageVisible }
|
||||||
|
)
|
||||||
|
|
||||||
fun onRestoreDialogSeenClick() =
|
private fun createWalletBackupMessageState(): WalletBackupMessageState {
|
||||||
|
return WalletBackupMessageState(
|
||||||
|
onClick = {
|
||||||
|
navigationRouter.forward(SeedBackup)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onRestoreDialogSeenClick() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
isRestoreSuccessDialogVisible.setSeen()
|
isRestoreSuccessDialogVisible.setSeen()
|
||||||
}
|
}
|
||||||
|
@ -95,6 +147,15 @@ class HomeViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onScanButtonClick() {
|
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
|
package co.electriccoin.zcash.ui.screen.integrations
|
||||||
|
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.compose.BackHandler
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.SideEffect
|
import androidx.compose.runtime.SideEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.window.DialogWindowProvider
|
import androidx.compose.ui.window.DialogWindowProvider
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
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 kotlinx.serialization.Serializable
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
|
@ -20,15 +15,10 @@ import org.koin.core.parameter.parametersOf
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AndroidDialogIntegrations() {
|
fun AndroidDialogIntegrations() {
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
|
||||||
val parent = LocalView.current.parent
|
val parent = LocalView.current.parent
|
||||||
val viewModel = koinViewModel<IntegrationsViewModel> { parametersOf(true) }
|
val viewModel = koinViewModel<IntegrationsViewModel> { parametersOf(true) }
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
BackHandler(enabled = state != null) {
|
|
||||||
state?.onBack?.invoke()
|
|
||||||
}
|
|
||||||
|
|
||||||
SideEffect {
|
SideEffect {
|
||||||
(parent as? DialogWindowProvider)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
(parent as? DialogWindowProvider)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
||||||
(parent as? DialogWindowProvider)?.window?.setDimAmount(0f)
|
(parent as? DialogWindowProvider)?.window?.setDimAmount(0f)
|
||||||
|
@ -37,22 +27,7 @@ fun AndroidDialogIntegrations() {
|
||||||
state?.let {
|
state?.let {
|
||||||
IntegrationsDialogView(
|
IntegrationsDialogView(
|
||||||
state = it,
|
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.compose.runtime.getValue
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
|
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 kotlinx.serialization.Serializable
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
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.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
@ -14,34 +17,37 @@ import androidx.compose.material3.SheetValue
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import co.electriccoin.zcash.ui.R
|
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.listitem.ZashiListItemState
|
||||||
import co.electriccoin.zcash.ui.design.component.rememberModalBottomSheetState
|
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.newcomponent.PreviewScreens
|
||||||
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
|
||||||
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors
|
||||||
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
||||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
import co.electriccoin.zcash.ui.design.util.stringRes
|
||||||
import co.electriccoin.zcash.ui.screen.integrations.model.IntegrationsState
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
internal fun IntegrationsDialogView(
|
internal fun IntegrationsDialogView(
|
||||||
onDismissRequest: () -> Unit,
|
state: IntegrationsState,
|
||||||
sheetState: SheetState,
|
sheetState: SheetState = rememberScreenModalBottomSheetState(),
|
||||||
state: IntegrationsState
|
|
||||||
) {
|
) {
|
||||||
ZashiModalBottomSheet(
|
ZashiScreenModalBottomSheet(
|
||||||
|
state = state,
|
||||||
sheetState = sheetState,
|
sheetState = sheetState,
|
||||||
content = {
|
content = {
|
||||||
BottomSheetContent(state)
|
BottomSheetContent(state)
|
||||||
},
|
},
|
||||||
onDismissRequest = onDismissRequest
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,8 +62,33 @@ fun BottomSheetContent(state: IntegrationsState) {
|
||||||
color = ZashiColors.Text.textPrimary
|
color = ZashiColors.Text.textPrimary
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
IntegrationItems(state, contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp))
|
IntegrationItems(state, contentPadding = PaddingValues(horizontal = 20.dp, vertical = 16.dp))
|
||||||
Spacer(modifier = Modifier.height(24.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))
|
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.systemBars))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +99,6 @@ fun BottomSheetContent(state: IntegrationsState) {
|
||||||
private fun IntegrationSettings() =
|
private fun IntegrationSettings() =
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
IntegrationsDialogView(
|
IntegrationsDialogView(
|
||||||
onDismissRequest = {},
|
|
||||||
sheetState =
|
sheetState =
|
||||||
rememberModalBottomSheetState(
|
rememberModalBottomSheetState(
|
||||||
skipHiddenState = true,
|
skipHiddenState = true,
|
||||||
|
@ -101,7 +131,6 @@ private fun IntegrationSettings() =
|
||||||
onClick = {}
|
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.component.listitem.ZashiListItemState
|
||||||
import co.electriccoin.zcash.ui.design.util.StringResource
|
import co.electriccoin.zcash.ui.design.util.StringResource
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class IntegrationsState(
|
data class IntegrationsState(
|
||||||
val disabledInfo: StringResource?,
|
val disabledInfo: StringResource?,
|
||||||
val onBack: () -> Unit,
|
override val onBack: () -> Unit,
|
||||||
val items: ImmutableList<ZashiListItemState>,
|
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.Image
|
||||||
import androidx.compose.foundation.layout.Column
|
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.getValue
|
||||||
import co.electriccoin.zcash.ui.design.util.scaffoldScrollPadding
|
import co.electriccoin.zcash.ui.design.util.scaffoldScrollPadding
|
||||||
import co.electriccoin.zcash.ui.design.util.stringRes
|
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 co.electriccoin.zcash.ui.screen.settings.SettingsTag
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
@ -198,7 +197,6 @@ private fun IntegrationSettings() =
|
||||||
onClick = {}
|
onClick = {}
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onBottomSheetHidden = {}
|
|
||||||
),
|
),
|
||||||
topAppBarSubTitleState = TopAppBarSubTitleState.None,
|
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.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import cash.z.ecc.android.sdk.fixture.WalletFixture
|
|
||||||
import cash.z.ecc.sdk.type.ZcashCurrency
|
import cash.z.ecc.sdk.type.ZcashCurrency
|
||||||
import co.electriccoin.zcash.ui.R
|
import co.electriccoin.zcash.ui.R
|
||||||
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
|
||||||
|
@ -49,10 +47,8 @@ import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
|
||||||
private fun OnboardingComposablePreview() {
|
private fun OnboardingComposablePreview() {
|
||||||
ZcashTheme {
|
ZcashTheme {
|
||||||
Onboarding(
|
Onboarding(
|
||||||
isDebugMenuEnabled = true,
|
|
||||||
onImportWallet = {},
|
onImportWallet = {},
|
||||||
onCreateWallet = {},
|
onCreateWallet = {}
|
||||||
onFixtureWallet = {}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,10 +59,8 @@ private fun OnboardingComposablePreview() {
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun Onboarding(
|
fun Onboarding(
|
||||||
isDebugMenuEnabled: Boolean,
|
|
||||||
onImportWallet: () -> Unit,
|
onImportWallet: () -> Unit,
|
||||||
onCreateWallet: () -> Unit,
|
onCreateWallet: () -> Unit
|
||||||
onFixtureWallet: (String) -> Unit
|
|
||||||
) {
|
) {
|
||||||
Scaffold { paddingValues ->
|
Scaffold { paddingValues ->
|
||||||
Box(
|
Box(
|
||||||
|
@ -90,10 +84,8 @@ fun Onboarding(
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
OnboardingMainContent(
|
OnboardingMainContent(
|
||||||
isDebugMenuEnabled = isDebugMenuEnabled,
|
|
||||||
onCreateWallet = onCreateWallet,
|
|
||||||
onFixtureWallet = onFixtureWallet,
|
|
||||||
onImportWallet = onImportWallet,
|
onImportWallet = onImportWallet,
|
||||||
|
onCreateWallet = onCreateWallet,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(
|
.padding(
|
||||||
|
@ -111,8 +103,6 @@ fun Onboarding(
|
||||||
private fun OnboardingMainContent(
|
private fun OnboardingMainContent(
|
||||||
onImportWallet: () -> Unit,
|
onImportWallet: () -> Unit,
|
||||||
onCreateWallet: () -> Unit,
|
onCreateWallet: () -> Unit,
|
||||||
onFixtureWallet: (String) -> Unit,
|
|
||||||
isDebugMenuEnabled: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
@ -123,18 +113,10 @@ private fun OnboardingMainContent(
|
||||||
.then(modifier),
|
.then(modifier),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
var imageModifier =
|
val imageModifier =
|
||||||
Modifier
|
Modifier
|
||||||
.height(ZcashTheme.dimens.inScreenZcashLogoHeight)
|
.height(ZcashTheme.dimens.inScreenZcashLogoHeight)
|
||||||
.width(ZcashTheme.dimens.inScreenZcashLogoWidth)
|
.width(ZcashTheme.dimens.inScreenZcashLogoWidth)
|
||||||
if (isDebugMenuEnabled) {
|
|
||||||
imageModifier =
|
|
||||||
imageModifier.then(
|
|
||||||
Modifier.clickable {
|
|
||||||
onFixtureWallet(WalletFixture.Alice.seedPhrase)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier.weight(1f))
|
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