Skip to content

Commit

Permalink
[#12] Home screen scaffold (#158)
Browse files Browse the repository at this point in the history
This provides a very basic scaffold of the home screen and navigation to other child screens.  Additional work needs to be done in both the SDK and this app to build all of the functionality.  Specific known gaps
 - Displaying synchronization status to the user, pending full designs on the progress and animation behaviors
 - Improved APIs for listing and filtering transactions.  This is the issue in the SDK Electric-Coin-Company/zcash-android-wallet-sdk#242
 - Implement the UI for displaying transaction history
 - Display fiat currency values
 - Hook up buttons to navigate to other screens (scan #137, profile #145, send #134, request #135)
  • Loading branch information
ccjernigan committed Dec 29, 2021
1 parent dcea0ec commit 205344c
Show file tree
Hide file tree
Showing 17 changed files with 455 additions and 59 deletions.
12 changes: 6 additions & 6 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -69,26 +69,26 @@ PLAY_PUBLISHER_PLUGIN_VERSION_MATCHER=3.7.0
ANDROIDX_ACTIVITY_VERSION=1.4.0
ANDROIDX_ANNOTATION_VERSION=1.3.0
ANDROIDX_APPCOMPAT_VERSION=1.3.1
ANDROIDX_COMPOSE_VERSION=1.0.5
ANDROIDX_COMPOSE_COMPILER_VERSION=1.1.0-rc02
ANDROIDX_COMPOSE_VERSION=1.0.5
ANDROIDX_CORE_VERSION=1.7.0
ANDROIDX_ESPRESSO_VERSION=3.4.0
ANDROIDX_LIFECYCLE_VERSION=2.4.0
ANDROIDX_NAVIGATION_VERSION=2.3.5
ANDROIDX_NAVIGATION_COMPOSE_VERSION=2.4.0-rc01
ANDROIDX_SECURITY_CRYPTO_VERSION=1.1.0-alpha03
ANDROIDX_SPLASH_SCREEN_VERSION=1.0.0-alpha02
ANDROIDX_TEST_VERSION=1.4.1-alpha03
ANDROIDX_TEST_JUNIT_VERSION=1.1.3
ANDROIDX_TEST_ORCHESTRATOR_VERSION=1.4.1
ANDROIDX_TEST_VERSION=1.4.1-alpha03
ANDROIDX_UI_AUTOMATOR_VERSION=2.2.0-alpha1
CORE_LIBRARY_DESUGARING_VERSION=1.1.5
GOOGLE_MATERIAL_VERSION=1.4.0
JACOCO_VERSION=0.8.7
KOTLINX_COROUTINES_VERSION=1.6.0
KOTLIN_VERSION=1.6.10
ZCASH_SDK_VERSION=1.3.0-beta19
ZCASH_BIP39_VERSION=1.0.2
KOTLINX_COROUTINES_VERSION=1.6.0
ZCASH_ANDROID_WALLET_PLUGINS_VERSION=1.0.0
ZCASH_BIP39_VERSION=1.0.2
ZCASH_SDK_VERSION=1.3.0-beta19

# Toolchain is the Java version used to build the application, which is separate from the
# Java version used to run the application. Android requires a minimum of 11.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package cash.z.ecc.ui.screen.onboarding.model
package cash.z.ecc.sdk.model

import androidx.test.filters.SmallTest
import org.junit.Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cash.z.ecc.sdk.model

import cash.z.ecc.android.sdk.block.CompactBlockProcessor

fun CompactBlockProcessor.ProcessorInfo.downloadProgress() = if (lastDownloadRange.isEmpty()) {
PercentDecimal.ONE_HUNDRED_PERCENT
} else {
val numerator = (lastDownloadedHeight - lastDownloadRange.first + 1)
.toFloat()
.coerceAtLeast(PercentDecimal.MIN)
val denominator = (lastDownloadRange.last - lastDownloadRange.first + 1).toFloat()

val progress = (numerator / denominator).coerceAtMost(PercentDecimal.MAX)

PercentDecimal(progress)
}

fun CompactBlockProcessor.ProcessorInfo.scanProgress() = if (lastScanRange.isEmpty()) {
PercentDecimal.ONE_HUNDRED_PERCENT
} else {
val numerator = (lastScannedHeight - lastScanRange.first + 1).toFloat().coerceAtLeast(PercentDecimal.MIN)
val demonimator = (lastScanRange.last - lastScanRange.first + 1).toFloat()

val progress = (numerator / demonimator).coerceAtMost(PercentDecimal.MAX)

PercentDecimal(progress)
}

// These are estimates
@Suppress("MagicNumber")
private val DOWNLOAD_WEIGHT = PercentDecimal(0.4f)
private val SCAN_WEIGHT = PercentDecimal(PercentDecimal.MAX - DOWNLOAD_WEIGHT.decimal)

fun CompactBlockProcessor.ProcessorInfo.totalProgress(): PercentDecimal {
val downloadWeighted = DOWNLOAD_WEIGHT.decimal * (downloadProgress().decimal).coerceAtMost(PercentDecimal.MAX)
val scanWeighted = SCAN_WEIGHT.decimal * (scanProgress().decimal).coerceAtMost(PercentDecimal.MAX)

return PercentDecimal(
downloadWeighted.coerceAtLeast(PercentDecimal.MIN) +
scanWeighted.coerceAtLeast(PercentDecimal.MIN)
)
}
25 changes: 25 additions & 0 deletions sdk-ext-lib/src/main/java/cash/z/ecc/sdk/model/PercentDecimal.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package cash.z.ecc.sdk.model

/**
* @param decimal A percent represented as a `Double` decimal value in the range of [0, 1].
*/
@JvmInline
value class PercentDecimal(val decimal: Float) {
init {
require(decimal >= MIN)
require(decimal <= MAX)
}

companion object {
const val MIN = 0.0f
const val MAX = 1.0f
val ZERO_PERCENT = PercentDecimal(MIN)
val ONE_HUNDRED_PERCENT = PercentDecimal(MAX)

fun newLenient(decimal: Float) = PercentDecimal(
decimal
.coerceAtLeast(MIN)
.coerceAtMost(MAX)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package cash.z.ecc.sdk.model

import cash.z.ecc.android.sdk.type.WalletBalance

// These go away if we update WalletBalance to expose a Zatoshi field type instead of long
val WalletBalance.total get() = Zatoshi(totalZatoshi.coerceAtLeast(0))
val WalletBalance.available get() = Zatoshi(availableZatoshi.coerceAtLeast(0))
val WalletBalance.pending get() = Zatoshi(pendingZatoshi.coerceAtLeast(0))
17 changes: 17 additions & 0 deletions sdk-ext-lib/src/main/java/cash/z/ecc/sdk/model/Zatoshi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package cash.z.ecc.sdk.model

import cash.z.ecc.android.sdk.ext.convertZatoshiToZecString

// Eventually, this could move into the SDK and provide a stronger API for amounts
@JvmInline
value class Zatoshi(val amount: Long) {
init {
require(amount >= 0)
}

override fun toString() = amount.convertZatoshiToZecString(DECIMALS, DECIMALS)

companion object {
private const val DECIMALS = 8
}
}
3 changes: 3 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ dependencyResolutionManagement {
val androidxCoreVersion = extra["ANDROIDX_CORE_VERSION"].toString()
val androidxEspressoVersion = extra["ANDROIDX_ESPRESSO_VERSION"].toString()
val androidxLifecycleVersion = extra["ANDROIDX_LIFECYCLE_VERSION"].toString()
val androidxNavigationComposeVersion = extra["ANDROIDX_NAVIGATION_COMPOSE_VERSION"].toString()
val androidxSecurityCryptoVersion = extra["ANDROIDX_SECURITY_CRYPTO_VERSION"].toString()
val androidxSplashScreenVersion = extra["ANDROIDX_SPLASH_SCREEN_VERSION"].toString()
val androidxTestJunitVersion = extra["ANDROIDX_TEST_JUNIT_VERSION"].toString()
Expand Down Expand Up @@ -89,6 +90,7 @@ dependencyResolutionManagement {
alias("androidx-compose-compiler").to("androidx.compose.compiler:compiler:$androidxComposeCompilerVersion")
alias("androidx-core").to("androidx.core:core-ktx:$androidxCoreVersion")
alias("androidx-lifecycle-livedata").to("androidx.lifecycle:lifecycle-livedata-ktx:$androidxLifecycleVersion")
alias("androidx-navigation-compose").to("androidx.navigation:navigation-compose:$androidxNavigationComposeVersion")
alias("androidx-security-crypto").to("androidx.security:security-crypto-ktx:$androidxSecurityCryptoVersion")
alias("androidx-splash").to("androidx.core:core-splashscreen:$androidxSplashScreenVersion")
alias("androidx-viewmodel-compose").to("androidx.lifecycle:lifecycle-viewmodel-compose:$androidxLifecycleVersion")
Expand Down Expand Up @@ -127,6 +129,7 @@ dependencyResolutionManagement {
"androidx-compose-material-icons-extended",
"androidx-compose-tooling",
"androidx-compose-ui",
"androidx-navigation-compose",
"androidx-viewmodel-compose"
)
)
Expand Down
3 changes: 2 additions & 1 deletion ui-lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ android {
getByName("main").apply {
res.setSrcDirs(
setOf(
"src/main/res/ui/backup",
"src/main/res/ui/common",
"src/main/res/ui/home",
"src/main/res/ui/onboarding",
"src/main/res/ui/backup",
"src/main/res/ui/restore"
)
)
Expand Down
54 changes: 42 additions & 12 deletions ui-lib/src/main/java/cash/z/ecc/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.os.SystemClock
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.annotation.FontRes
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
Expand All @@ -17,6 +18,9 @@ import androidx.compose.ui.Modifier
import androidx.core.content.res.ResourcesCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import cash.z.ecc.android.sdk.type.WalletBirthday
import cash.z.ecc.android.sdk.type.ZcashNetwork
import cash.z.ecc.sdk.model.PersistableWallet
Expand All @@ -26,7 +30,7 @@ import cash.z.ecc.ui.screen.backup.view.BackupWallet
import cash.z.ecc.ui.screen.backup.viewmodel.BackupViewModel
import cash.z.ecc.ui.screen.common.GradientSurface
import cash.z.ecc.ui.screen.home.view.Home
import cash.z.ecc.ui.screen.home.viewmodel.WalletState
import cash.z.ecc.ui.screen.home.viewmodel.SecretState
import cash.z.ecc.ui.screen.home.viewmodel.WalletViewModel
import cash.z.ecc.ui.screen.onboarding.view.Onboarding
import cash.z.ecc.ui.screen.onboarding.viewmodel.OnboardingViewModel
Expand Down Expand Up @@ -77,7 +81,7 @@ class MainActivity : ComponentActivity() {
}
}

WalletState.Loading == walletViewModel.state.value
SecretState.Loading == walletViewModel.secretState.value
}
}

Expand All @@ -89,21 +93,21 @@ class MainActivity : ComponentActivity() {
.fillMaxWidth()
.fillMaxHeight()
) {
val walletState = walletViewModel.state.collectAsState().value
val secretState = walletViewModel.secretState.collectAsState().value

when (walletState) {
WalletState.Loading -> {
when (secretState) {
SecretState.Loading -> {
// For now, keep displaying splash screen using condition above.
// In the future, we might consider displaying something different here.
}
WalletState.NoWallet -> {
SecretState.None -> {
WrapOnboarding()
}
is WalletState.NeedsBackup -> WrapBackup(walletState.persistableWallet)
is WalletState.Ready -> WrapHome(walletState.persistableWallet)
is SecretState.NeedsBackup -> WrapBackup(secretState.persistableWallet)
is SecretState.Ready -> Navigation()
}

if (walletState != WalletState.Loading) {
if (secretState != SecretState.Loading) {
reportFullyDrawn()
}
}
Expand Down Expand Up @@ -191,8 +195,34 @@ class MainActivity : ComponentActivity() {
}

@Composable
private fun WrapHome(persistableWallet: PersistableWallet) {
Home(persistableWallet)
private fun Navigation() {
val navController = rememberNavController()

NavHost(navController = navController, startDestination = "home") {
composable("home") { WrapHome({}, {}, {}, {}) }
}
}

@Composable
private fun WrapHome(
goScan: () -> Unit,
goProfile: () -> Unit,
goSend: () -> Unit,
goRequest: () -> Unit
) {
val walletSnapshot = walletViewModel.walletSnapshot.collectAsState().value
if (null == walletSnapshot) {
// Display loading indicator
} else {
Home(
walletSnapshot,
walletViewModel.transactionSnapshot.collectAsState().value,
goScan = goScan,
goRequest = goRequest,
goSend = goSend,
goProfile = goProfile
)
}
}

companion object {
Expand All @@ -210,7 +240,7 @@ class MainActivity : ComponentActivity() {
* caches the results. This moves that IO off the main thread, to prevent ANRs and
* jank during app startup.
*/
private suspend fun prefetchFontLegacy(context: Context, @androidx.annotation.FontRes fontRes: Int) =
private suspend fun prefetchFontLegacy(context: Context, @FontRes fontRes: Int) =
withContext(Dispatchers.IO) {
ResourcesCompat.getFont(context, fontRes)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package cash.z.ecc.ui.fixture

import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.ui.screen.home.model.WalletSnapshot

@Suppress("MagicNumber")
object WalletSnapshotFixture {
// Should fill in with non-empty values for better example values in tests and UI previews
@Suppress("LongParameterList")
fun new(
status: Synchronizer.Status = Synchronizer.Status.SYNCED,
processorInfo: CompactBlockProcessor.ProcessorInfo = CompactBlockProcessor.ProcessorInfo(),
orchardBalance: WalletBalance = WalletBalance(5, 2),
saplingBalance: WalletBalance = WalletBalance(4, 4),
transparentBalance: WalletBalance = WalletBalance(8, 1),
pendingCount: Int = 0
) = WalletSnapshot(status, processorInfo, orchardBalance, saplingBalance, transparentBalance, pendingCount)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cash.z.ecc.ui.screen.home.model

import cash.z.ecc.android.sdk.Synchronizer
import cash.z.ecc.android.sdk.block.CompactBlockProcessor
import cash.z.ecc.android.sdk.ext.ZcashSdk
import cash.z.ecc.android.sdk.type.WalletBalance
import cash.z.ecc.sdk.model.Zatoshi

data class WalletSnapshot(
val status: Synchronizer.Status,
val processorInfo: CompactBlockProcessor.ProcessorInfo,
val orchardBalance: WalletBalance,
val saplingBalance: WalletBalance,
val transparentBalance: WalletBalance,
val pendingCount: Int
) {
// Note: the wallet is effectively empty if it cannot cover the miner's fee
val hasFunds = saplingBalance.availableZatoshi >
(ZcashSdk.MINERS_FEE_ZATOSHI.toDouble() / ZcashSdk.ZATOSHI_PER_ZEC) // 0.00001
val hasSaplingBalance = saplingBalance.totalZatoshi > 0

val isSendEnabled: Boolean get() = status == Synchronizer.Status.SYNCED && hasFunds
}

fun WalletSnapshot.totalBalance(): Zatoshi {
val total = (orchardBalance + saplingBalance + transparentBalance).totalZatoshi

return Zatoshi(total.coerceAtLeast(0))
}
Loading

0 comments on commit 205344c

Please sign in to comment.