Restoration connected to sdk

This commit is contained in:
Milan Cerovsky 2025-04-16 11:43:34 +02:00
parent a07d7fcc8e
commit aa72d1f92f
15 changed files with 226 additions and 78 deletions

View File

@ -211,7 +211,7 @@ ZIP_321_VERSION = 0.0.6
# WARNING: Ensure a non-snapshot version is used before releasing to production
ZCASH_BIP39_VERSION=1.0.9
# WARNING: Ensure a non-snapshot version is used before releasing to production
ZCASH_SDK_VERSION=2.2.11
ZCASH_SDK_VERSION=2.2.11-SNAPSHOT
# Toolchain is the Java version used to build the application, which is separate from the
# Java version used to run the application.

View File

@ -13,6 +13,7 @@ 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.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@ -39,40 +40,34 @@ import java.text.DateFormatSymbols
import java.time.Month
import java.time.Year
import java.time.YearMonth
import kotlin.math.absoluteValue
import kotlin.math.pow
@Suppress("MagicNumber")
@Composable
fun ZashiYearMonthWheelDatePicker(
selection: YearMonth,
onSelectionChange: (YearMonth) -> Unit,
modifier: Modifier = Modifier,
verticallyVisibleItems: Int = 3,
startYear: Year = Year.of(2016),
endYear: Year = Year.now(),
selectionYear: YearMonth = YearMonth.now(),
startInclusive: YearMonth = YearMonth.of(2018, 10),
endInclusive: YearMonth = YearMonth.now(),
) {
val latestOnSelectionChanged by rememberUpdatedState(onSelectionChange)
var selectedDate by remember { mutableStateOf(selectionYear) }
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)
var state by remember {
mutableStateOf(
InternalState(
selectedDate = selection,
months = getMonthsForYear(Year.of(selection.year), startInclusive, endInclusive),
years = (startInclusive.year..endInclusive.year).map { Year.of(it) }.toList()
)
)
}
LaunchedEffect(state.selectedDate) {
Twig.debug { "Selection changed: ${state.selectedDate}" }
latestOnSelectionChanged(state.selectedDate)
}
Box(modifier = modifier) {
@ -92,14 +87,18 @@ fun ZashiYearMonthWheelDatePicker(
Spacer(Modifier.weight(.5f))
WheelLazyList(
modifier = Modifier.weight(1f),
selection = maxOf(months.indexOf(selectedDate.month), 0),
itemCount = months.size,
selection = state.selectedMonthIndex,
itemCount = state.months.size,
itemVerticalOffset = verticallyVisibleItems,
isInfiniteScroll = true,
onFocusItem = { selectedDate = selectedDate.withMonth(months[it].value) },
isInfiniteScroll = false,
onFocusItem = {
state = state.copy(
selectedDate = state.selectedDate.withMonth(state.months[it].value)
)
},
itemContent = {
Text(
text = DateFormatSymbols().months[months[it].value - 1],
text = DateFormatSymbols().months[state.months[it].value - 1],
textAlign = TextAlign.Center,
modifier = Modifier.fillParentMaxWidth(),
style = ZashiTypography.header6,
@ -110,14 +109,28 @@ fun ZashiYearMonthWheelDatePicker(
)
WheelLazyList(
modifier = Modifier.weight(.75f),
selection = years.indexOf(selectedDate.year),
itemCount = years.size,
selection = state.selectedYearIndex,
itemCount = state.years.size,
itemVerticalOffset = verticallyVisibleItems,
isInfiniteScroll = false,
onFocusItem = { selectedDate = selectedDate.withYear(years[it]) },
onFocusItem = {
val year = state.years[it]
val normalizedSelectedMonth = getSelectedMonthForYear(
year = year,
selectedMonth = state.selectedDate.month,
startYearMonth = startInclusive,
endYearMonth = endInclusive
)
val months = getMonthsForYear(year, startInclusive, endInclusive)
val selectedDate = state.selectedDate.withYear(year.value).withMonth(normalizedSelectedMonth.value)
state = state.copy(
selectedDate = selectedDate,
months = months
)
},
itemContent = {
Text(
text = years[it].toString(),
text = state.years[it].toString(),
textAlign = TextAlign.Center,
modifier = Modifier.fillParentMaxWidth(),
style = ZashiTypography.header6,
@ -131,6 +144,79 @@ fun ZashiYearMonthWheelDatePicker(
}
}
private fun getMonthsForYear(year: Year, startYearMonth: YearMonth, endYearMonth: YearMonth): List<Month> {
return when (year.value) {
startYearMonth.year -> {
(startYearMonth.month.value..Month.DECEMBER.value).map { index ->
Month.entries.first { it.value == index }
}
}
endYearMonth.year -> {
(Month.JANUARY.value..endYearMonth.month.value).map { index ->
Month.entries.first { it.value == index }
}
}
else -> {
listOf(
Month.JANUARY,
Month.FEBRUARY,
Month.MARCH,
Month.APRIL,
Month.MAY,
Month.JUNE,
Month.JULY,
Month.AUGUST,
Month.SEPTEMBER,
Month.OCTOBER,
Month.NOVEMBER,
Month.DECEMBER
)
}
}
}
private fun getSelectedMonthForYear(
year: Year,
selectedMonth: Month,
startYearMonth: YearMonth,
endYearMonth: YearMonth
): Month {
return when (year.value) {
startYearMonth.year -> {
val months = (startYearMonth.month.value..Month.DECEMBER.value).map { index ->
Month.entries.first { it.value == index }
}
if (selectedMonth in months) selectedMonth else months.findClosest(selectedMonth)
}
endYearMonth.year -> {
val months = (Month.JANUARY.value..endYearMonth.month.value).map { index ->
Month.entries.first { it.value == index }
}
if (selectedMonth in months) selectedMonth else months.findClosest(selectedMonth)
}
else -> selectedMonth
}
}
private fun List<Month>.findClosest(target: Month): Month {
var closestNumber = this[0] // Initialize with the first element
var minDifference = (this[0].value - target.value).absoluteValue
for (number in this) {
val difference = (number.value - target.value).absoluteValue
if (difference < minDifference) {
minDifference = difference
closestNumber = number
}
}
return closestNumber
}
@Suppress("MagicNumber", "ContentSlotReused")
@Composable
private fun WheelLazyList(
@ -155,9 +241,7 @@ private fun WheelLazyList(
val isScrollInProgress = state.isScrollInProgress
LaunchedEffect(itemCount) {
coroutineScope.launch {
state.scrollToItem(startIndex)
}
state.scrollToItem(startIndex)
}
LaunchedEffect(key1 = isScrollInProgress) {
@ -286,3 +370,13 @@ private fun calculateIndexToFocus(
}
return index
}
@Immutable
private data class InternalState(
val selectedDate: YearMonth,
val months: List<Month>,
val years: List<Year>
) {
val selectedYearIndex = years.map { it.value }.indexOf(selectedDate.year)
val selectedMonthIndex = maxOf(months.indexOf(selectedDate.month), 0)
}

View File

@ -134,10 +134,10 @@ fun MainActivity.OnboardingNavigation() {
AndroidRestoreBDHeight(it.toRoute())
}
composable<RestoreBDDate> {
AndroidRestoreBDDate()
AndroidRestoreBDDate(it.toRoute())
}
composable<RestoreBDEstimation> {
AndroidRestoreBDEstimation()
AndroidRestoreBDEstimation(it.toRoute())
}
dialog<SeedInfo>(
dialogProperties =

View File

@ -6,14 +6,15 @@ import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.serialization.Serializable
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
@Composable
fun AndroidRestoreBDDate() {
val vm = koinViewModel<RestoreBDDateViewModel>()
fun AndroidRestoreBDDate(args: RestoreBDDate) {
val vm = koinViewModel<RestoreBDDateViewModel> { parametersOf(args) }
val state by vm.state.collectAsStateWithLifecycle()
RestoreBDDateView(state)
BackHandler { state.onBack() }
BackHandler(enabled = state != null) { state?.onBack?.invoke() }
state?.let { RestoreBDDateView(it) }
}
@Serializable
data object RestoreBDDate
data class RestoreBDDate(val seed: String)

View File

@ -2,9 +2,12 @@ package co.electriccoin.zcash.ui.screen.restore.date
import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.IconButtonState
import java.time.YearMonth
data class RestoreBDDateState(
val selection: YearMonth,
val next: ButtonState,
val dialogButton: IconButtonState,
val onBack: () -> Unit
val onBack: () -> Unit,
val onYearMonthChange: (YearMonth) -> Unit,
)

View File

@ -42,6 +42,7 @@ import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography
import co.electriccoin.zcash.ui.design.util.orDark
import co.electriccoin.zcash.ui.design.util.scaffoldPadding
import co.electriccoin.zcash.ui.design.util.stringRes
import java.time.YearMonth
@Composable
fun RestoreBDDateView(state: RestoreBDDateState) {
@ -84,7 +85,8 @@ private fun Content(
Spacer(Modifier.height(24.dp))
ZashiYearMonthWheelDatePicker(
onSelectionChange = {},
selection = state.selection,
onSelectionChange = state.onYearMonthChange,
modifier = Modifier.fillMaxWidth(),
)
@ -150,8 +152,10 @@ private fun Preview() =
state =
RestoreBDDateState(
next = ButtonState(stringRes("Estimate")) {},
dialogButton = IconButtonState(R.drawable.ic_restore_dialog) {},
onBack = {}
dialogButton = IconButtonState(R.drawable.ic_help) {},
onBack = {},
onYearMonthChange = {},
selection = YearMonth.now()
)
)
}

View File

@ -1,6 +1,12 @@
package co.electriccoin.zcash.ui.screen.restore.date
import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.android.sdk.SdkSynchronizer
import cash.z.ecc.android.sdk.model.ZcashNetwork
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import cash.z.ecc.sdk.type.fromResources
import co.electriccoin.zcash.ui.NavigationRouter
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.ButtonState
@ -9,27 +15,64 @@ import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.restore.estimation.RestoreBDEstimation
import co.electriccoin.zcash.ui.screen.restore.info.SeedInfo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
import kotlinx.datetime.toKotlinInstant
import java.time.YearMonth
import java.time.ZoneId
class RestoreBDDateViewModel(
private val navigationRouter: NavigationRouter
private val args: RestoreBDDate,
private val navigationRouter: NavigationRouter,
private val context: Context,
) : ViewModel() {
val state: StateFlow<RestoreBDDateState> = MutableStateFlow(createState()).asStateFlow()
private fun createState() =
private val selection = MutableStateFlow<YearMonth>(YearMonth.now())
val state: StateFlow<RestoreBDDateState?> = selection
.map {
createState(it)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT),
initialValue = null
)
private fun createState(selection: YearMonth) =
RestoreBDDateState(
next = ButtonState(stringRes(R.string.restore_bd_height_btn), onClick = ::onEstimateClick),
dialogButton =
IconButtonState(
icon = co.electriccoin.zcash.ui.design.R.drawable.ic_info,
icon = R.drawable.ic_help,
onClick = ::onInfoButtonClick,
),
onBack = ::onBack,
onYearMonthChange = ::onYearMonthChange,
selection = selection
)
private fun onEstimateClick() {
navigationRouter.forward(RestoreBDEstimation)
viewModelScope.launch {
val instant = selection.value.atDay(1)
.atStartOfDay()
.atZone(ZoneId.systemDefault())
.toInstant()
.toKotlinInstant()
val bday = SdkSynchronizer.estimateBirthdayHeight(
context = context,
date = instant,
network = ZcashNetwork.fromResources(context)
)
navigationRouter.forward(RestoreBDEstimation(seed = args.seed, blockHeight = bday.value))
}
}
private fun onBack() {
@ -39,4 +82,8 @@ class RestoreBDDateViewModel(
private fun onInfoButtonClick() {
navigationRouter.forward(SeedInfo)
}
private fun onYearMonthChange(yearMonth: YearMonth) {
selection.update { yearMonth }
}
}

View File

@ -6,14 +6,18 @@ import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.serialization.Serializable
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
@Composable
fun AndroidRestoreBDEstimation() {
val vm = koinViewModel<RestoreBDEstimationViewModel>()
fun AndroidRestoreBDEstimation(args: RestoreBDEstimation) {
val vm = koinViewModel<RestoreBDEstimationViewModel> { parametersOf(args) }
val state by vm.state.collectAsStateWithLifecycle()
RestoreBDEstimationView(state)
BackHandler { state.onBack() }
}
@Serializable
data object RestoreBDEstimation
data class RestoreBDEstimation(
val seed: String,
val blockHeight: Long
)

View File

@ -133,7 +133,7 @@ private fun Preview() =
state =
RestoreBDEstimationState(
restore = ButtonState(stringRes("Estimate")) {},
dialogButton = IconButtonState(R.drawable.ic_restore_dialog) {},
dialogButton = IconButtonState(R.drawable.ic_help) {},
onBack = {},
text = stringRes("123456"),
copy = ButtonState(stringRes("Copy"), icon = R.drawable.ic_copy) {}

View File

@ -1,8 +1,11 @@
package co.electriccoin.zcash.ui.screen.restore.estimation
import androidx.lifecycle.ViewModel
import cash.z.ecc.android.sdk.model.BlockHeight
import cash.z.ecc.android.sdk.model.SeedPhrase
import co.electriccoin.zcash.ui.NavigationRouter
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.usecase.RestoreWalletUseCase
import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.IconButtonState
import co.electriccoin.zcash.ui.design.util.stringRes
@ -12,7 +15,9 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class RestoreBDEstimationViewModel(
private val navigationRouter: NavigationRouter
private val args: RestoreBDEstimation,
private val navigationRouter: NavigationRouter,
private val restoreWallet: RestoreWalletUseCase
) : ViewModel() {
val state: StateFlow<RestoreBDEstimationState> = MutableStateFlow(createState()).asStateFlow()
@ -20,17 +25,20 @@ class RestoreBDEstimationViewModel(
RestoreBDEstimationState(
dialogButton =
IconButtonState(
icon = co.electriccoin.zcash.ui.design.R.drawable.ic_info,
icon = R.drawable.ic_help,
onClick = ::onInfoButtonClick,
),
onBack = ::onBack,
text = stringRes("123456"),
text = stringRes(args.blockHeight.toString()),
copy = ButtonState(stringRes(R.string.restore_bd_estimation_copy), icon = R.drawable.ic_copy) {},
restore = ButtonState(stringRes(R.string.restore_bd_estimation_restore), onClick = ::onRestoreClick),
)
private fun onRestoreClick() {
// do nothing
restoreWallet(
seedPhrase = SeedPhrase.new(args.seed),
birthday = BlockHeight.new(args.blockHeight)
)
}
private fun onBack() {

View File

@ -205,7 +205,7 @@ private fun Preview() =
state =
RestoreBDHeightState(
onBack = {},
dialogButton = IconButtonState(R.drawable.ic_restore_dialog) {},
dialogButton = IconButtonState(R.drawable.ic_help) {},
blockHeight = TextFieldState(stringRes("")) {},
estimate = ButtonState(stringRes("Estimate")) {},
restore = ButtonState(stringRes("Restore")) {}

View File

@ -54,7 +54,7 @@ class RestoreBDHeightViewModel(
onBack = ::onBack,
dialogButton =
IconButtonState(
icon = co.electriccoin.zcash.ui.design.R.drawable.ic_info,
icon = R.drawable.ic_help,
onClick = ::onInfoButtonClick,
),
restore =
@ -74,7 +74,7 @@ class RestoreBDHeightViewModel(
}
private fun onEstimateClick() {
navigationRouter.forward(RestoreBDDate)
navigationRouter.forward(RestoreBDDate(seed = restoreBDHeight.seed))
}
private fun onRestoreClick() {

View File

@ -297,7 +297,7 @@ private fun Preview() =
}
),
onBack = {},
dialogButton = IconButtonState(R.drawable.ic_restore_dialog) {},
dialogButton = IconButtonState(R.drawable.ic_help) {},
nextButton =
ButtonState(
text = stringRes("Next"),

View File

@ -130,7 +130,7 @@ class RestoreSeedViewModel(
onBack = ::onBack,
dialogButton =
IconButtonState(
icon = co.electriccoin.zcash.ui.design.R.drawable.ic_info,
icon = R.drawable.ic_help,
onClick = ::onInfoButtonClick
),
nextButton =

View File

@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M9.09,9C9.325,8.332 9.789,7.768 10.4,7.409C11.011,7.05 11.729,6.919 12.427,7.039C13.125,7.158 13.759,7.522 14.215,8.064C14.671,8.606 14.921,9.292 14.92,10C14.92,12 11.92,13 11.92,13M12,17H12.01M22,12C22,17.523 17.523,22 12,22C6.477,22 2,17.523 2,12C2,6.477 6.477,2 12,2C17.523,2 22,6.477 22,12Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>