From 334dc3c41fa1a8cfebb4a0546fde3b85225675a2 Mon Sep 17 00:00:00 2001 From: Carter Jernigan Date: Tue, 9 Aug 2022 15:23:38 -0400 Subject: [PATCH] [#185] Refactor navigation This moves navigation to its own file, simplifies the MainActivity, and moves Android integration composables to their own files. Navigation still does not have tests, so a separate change will need to be implemented to add testing to navigation. --- .../electriccoin/zcash/app/ScreenshotTest.kt | 5 +- .../co/electriccoin/zcash/ui/MainActivity.kt | 327 +----------------- .../co/electriccoin/zcash/ui/Navigation.kt | 173 +++++++++ .../screen/address/AndroidWalletAddresses.kt | 36 ++ .../zcash/ui/screen/request/AndroidRequest.kt | 59 ++++ .../zcash/ui/screen/scan/AndroidScan.kt | 43 ++- .../zcash/ui/screen/seed/AndroidSeed.kt | 49 +++ .../zcash/ui/screen/send/AndroidSend.kt | 45 +++ .../zcash/ui/screen/update/AndroidUpdate.kt | 41 ++- 9 files changed, 446 insertions(+), 332 deletions(-) create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/address/AndroidWalletAddresses.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/request/AndroidRequest.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seed/AndroidSeed.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/AndroidSend.kt diff --git a/app/src/androidTest/java/co/electriccoin/zcash/app/ScreenshotTest.kt b/app/src/androidTest/java/co/electriccoin/zcash/app/ScreenshotTest.kt index 699f4d39..f6799678 100644 --- a/app/src/androidTest/java/co/electriccoin/zcash/app/ScreenshotTest.kt +++ b/app/src/androidTest/java/co/electriccoin/zcash/app/ScreenshotTest.kt @@ -32,6 +32,7 @@ import cash.z.ecc.sdk.fixture.WalletAddressFixture import co.electriccoin.zcash.spackle.FirebaseTestLabUtil import co.electriccoin.zcash.test.UiTestPrerequisites import co.electriccoin.zcash.ui.MainActivity +import co.electriccoin.zcash.ui.NavigationTargets import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.design.component.ConfigurationOverride import co.electriccoin.zcash.ui.design.component.UiMode @@ -301,7 +302,7 @@ class ScreenshotTest : UiTestPrerequisites() { composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.walletSnapshot.value != null } requestZecScreenshots(resContext, tag, composeTestRule) - navigateTo(MainActivity.NAV_HOME) + navigateTo(NavigationTargets.HOME) composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.secretState.value is SecretState.Ready } composeTestRule.onNode(hasText(resContext.getString(R.string.home_button_send))).also { @@ -313,7 +314,7 @@ class ScreenshotTest : UiTestPrerequisites() { composeTestRule.waitUntil { composeTestRule.activity.walletViewModel.walletSnapshot.value != null } sendZecScreenshots(resContext, tag, composeTestRule) - navigateTo(MainActivity.NAV_HOME) + navigateTo(NavigationTargets.HOME) } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt index 25c12c52..75232a05 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt @@ -1,8 +1,5 @@ package co.electriccoin.zcash.ui -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent import android.os.Bundle import android.os.SystemClock import androidx.activity.ComponentActivity @@ -11,67 +8,31 @@ import androidx.activity.viewModels import androidx.annotation.VisibleForTesting import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.lifecycleScope import androidx.navigation.NavHostController -import androidx.navigation.NavOptionsBuilder -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import cash.z.ecc.sdk.model.ZecRequest -import cash.z.ecc.sdk.send import co.electriccoin.zcash.ui.design.compat.FontCompat import co.electriccoin.zcash.ui.design.component.ConfigurationOverride import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.Override import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import co.electriccoin.zcash.ui.screen.about.WrapAbout -import co.electriccoin.zcash.ui.screen.address.view.WalletAddresses import co.electriccoin.zcash.ui.screen.backup.WrapBackup -import co.electriccoin.zcash.ui.screen.backup.copyToClipboard -import co.electriccoin.zcash.ui.screen.home.WrapHome -import co.electriccoin.zcash.ui.screen.home.model.spendableBalance -import co.electriccoin.zcash.ui.screen.home.viewmodel.CheckUpdateViewModel import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.screen.onboarding.WrapOnboarding -import co.electriccoin.zcash.ui.screen.profile.WrapProfile -import co.electriccoin.zcash.ui.screen.request.view.Request -import co.electriccoin.zcash.ui.screen.scan.WrapScan -import co.electriccoin.zcash.ui.screen.seed.view.Seed -import co.electriccoin.zcash.ui.screen.send.view.Send -import co.electriccoin.zcash.ui.screen.settings.WrapSettings -import co.electriccoin.zcash.ui.screen.support.WrapSupport -import co.electriccoin.zcash.ui.screen.update.AppUpdateCheckerImp -import co.electriccoin.zcash.ui.screen.update.WrapUpdate -import co.electriccoin.zcash.ui.screen.update.model.UpdateState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds -@Suppress("TooManyFunctions") class MainActivity : ComponentActivity() { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) val walletViewModel by viewModels() - // TODO [#382]: https://github.com/zcash/secant-android-wallet/issues/382 - // TODO [#403]: https://github.com/zcash/secant-android-wallet/issues/403 - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - val checkUpdateViewModel by viewModels { - CheckUpdateViewModel.CheckUpdateViewModelFactory( - application, - AppUpdateCheckerImp.new() - ) - } - @VisibleForTesting(otherwise = VisibleForTesting.NONE) lateinit var navControllerForTesting: NavHostController @@ -128,11 +89,15 @@ class MainActivity : ComponentActivity() { SecretState.None -> { WrapOnboarding() } - is SecretState.NeedsBackup -> WrapBackup( - secretState.persistableWallet, - onBackupComplete = { walletViewModel.persistBackupComplete() } - ) - is SecretState.Ready -> Navigation() + is SecretState.NeedsBackup -> { + WrapBackup( + secretState.persistableWallet, + onBackupComplete = { walletViewModel.persistBackupComplete() } + ) + } + is SecretState.Ready -> { + Navigation() + } } } } @@ -148,282 +113,8 @@ class MainActivity : ComponentActivity() { } } - @Suppress("LongMethod") - @Composable - @SuppressWarnings("LongMethod") - private fun Navigation() { - val navController = rememberNavController().also { - // This suppress is necessary, as this is how we set up the nav controller for tests. - @SuppressLint("RestrictedApi") - navControllerForTesting = it - } - - NavHost(navController = navController, startDestination = NAV_HOME) { - composable(NAV_HOME) { - WrapHome( - goScan = { navController.navigateJustOnce(NAV_SCAN) }, - goProfile = { navController.navigateJustOnce(NAV_PROFILE) }, - goSend = { navController.navigateJustOnce(NAV_SEND) }, - goRequest = { navController.navigateJustOnce(NAV_REQUEST) } - ) - - WrapCheckForUpdate() - } - composable(NAV_PROFILE) { - WrapProfile( - onBack = { navController.popBackStackJustOnce(NAV_PROFILE) }, - onAddressDetails = { navController.navigateJustOnce(NAV_WALLET_ADDRESS_DETAILS) }, - onAddressBook = { }, - onSettings = { navController.navigateJustOnce(NAV_SETTINGS) }, - onCoinholderVote = { }, - onSupport = { navController.navigateJustOnce(NAV_SUPPORT) }, - onAbout = { navController.navigateJustOnce(NAV_ABOUT) } - ) - } - composable(NAV_WALLET_ADDRESS_DETAILS) { - WrapWalletAddresses( - goBack = { - navController.popBackStackJustOnce(NAV_WALLET_ADDRESS_DETAILS) - } - ) - } - composable(NAV_SETTINGS) { - WrapSettings( - goBack = { - navController.popBackStackJustOnce(NAV_SETTINGS) - }, - goWalletBackup = { - navController.navigateJustOnce(NAV_SEED) - } - ) - } - composable(NAV_SEED) { - WrapSeed( - goBack = { - navController.popBackStackJustOnce(NAV_SEED) - } - ) - } - composable(NAV_REQUEST) { - WrapRequest(goBack = { navController.popBackStackJustOnce(NAV_REQUEST) }) - } - composable(NAV_SEND) { - WrapSend(goBack = { navController.popBackStackJustOnce(NAV_SEND) }) - } - composable(NAV_SUPPORT) { - // Pop back stack won't be right if we deep link into support - WrapSupport(goBack = { navController.popBackStackJustOnce(NAV_SUPPORT) }) - } - composable(NAV_ABOUT) { - WrapAbout(goBack = { navController.popBackStackJustOnce(NAV_ABOUT) }) - } - composable(NAV_SCAN) { - WrapScanValidator( - onScanValid = { - // TODO [#449] https://github.com/zcash/secant-android-wallet/issues/449 - navController.navigateJustOnce(NAV_SEND) { - popUpTo(NAV_HOME) { inclusive = false } - } - }, - goBack = { navController.popBackStackJustOnce(NAV_SCAN) } - ) - } - } - } - - @Composable - private fun WrapScanValidator( - onScanValid: (address: String) -> Unit, - goBack: () -> Unit - ) { - val synchronizer = walletViewModel.synchronizer.collectAsState().value - if (synchronizer == null) { - // Display loading indicator - } else { - WrapScan( - onScanDone = { result -> - lifecycleScope.launch { - val isAddressValid = !synchronizer.validateAddress(result).isNotValid - if (isAddressValid) { - onScanValid(result) - } - } - }, - goBack = goBack - ) - } - } - - @Composable - private fun WrapCheckForUpdate() { - val updateInfo = checkUpdateViewModel.updateInfo.collectAsState().value - - updateInfo?.let { - if (it.appUpdateInfo != null && it.state == UpdateState.Prepared) { - WrapUpdate(updateInfo) - } - } - - // Check for an app update asynchronously. We create an effect that matches the activity - // lifecycle. If the wrapping compose recomposes, the check shouldn't run again. - LaunchedEffect(true) { - checkUpdateViewModel.checkForAppUpdate() - } - } - - @Composable - private fun WrapWalletAddresses( - goBack: () -> Unit - ) { - val walletAddresses = walletViewModel.addresses.collectAsState().value - if (null == walletAddresses) { - // Display loading indicator - } else { - WalletAddresses( - walletAddresses, - goBack - ) - } - } - - @Composable - private fun WrapSeed( - goBack: () -> Unit - ) { - val persistableWallet = run { - val secretState = walletViewModel.secretState.collectAsState().value - if (secretState is SecretState.Ready) { - secretState.persistableWallet - } else { - null - } - } - val synchronizer = walletViewModel.synchronizer.collectAsState().value - if (null == synchronizer || null == persistableWallet) { - // Display loading indicator - } else { - Seed( - persistableWallet = persistableWallet, - onBack = goBack, - onCopyToClipboard = { - copyToClipboard(applicationContext, persistableWallet) - } - ) - } - } - - @Composable - private fun WrapRequest( - goBack: () -> Unit - ) { - val walletAddresses = walletViewModel.addresses.collectAsState().value - if (null == walletAddresses) { - // Display loading indicator - } else { - Request( - walletAddresses.unified, - goBack = goBack, - onCreateAndSend = { - val chooserIntent = Intent.createChooser(it.newShareIntent(applicationContext), null) - - startActivity(chooserIntent) - - goBack() - } - ) - } - } - - @Composable - private fun WrapSend( - goBack: () -> Unit - ) { - val synchronizer = walletViewModel.synchronizer.collectAsState().value - val spendableBalance = walletViewModel.walletSnapshot.collectAsState().value?.spendableBalance() - val spendingKey = walletViewModel.spendingKey.collectAsState().value - if (null == synchronizer || null == spendableBalance || null == spendingKey) { - // Display loading indicator - } else { - Send( - mySpendableBalance = spendableBalance, - goBack = goBack, - onCreateAndSend = { - synchronizer.send(spendingKey, it) - - goBack() - } - ) - } - } - companion object { @VisibleForTesting internal val SPLASH_SCREEN_DELAY = 0.seconds - - @VisibleForTesting - const val NAV_HOME = "home" - - @VisibleForTesting - const val NAV_PROFILE = "profile" - - @VisibleForTesting - const val NAV_WALLET_ADDRESS_DETAILS = "wallet_address_details" - - @VisibleForTesting - const val NAV_SETTINGS = "settings" - - @VisibleForTesting - const val NAV_SEED = "seed" - - @VisibleForTesting - const val NAV_REQUEST = "request" - - @VisibleForTesting - const val NAV_SEND = "send" - - @VisibleForTesting - const val NAV_SUPPORT = "support" - - @VisibleForTesting - const val NAV_ABOUT = "about" - - @VisibleForTesting - const val NAV_SCAN = "scan" } } - -private fun ZecRequest.newShareIntent(context: Context) = runBlocking { - Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, context.getString(R.string.request_template_format, toUri())) - type = "text/plain" - } -} - -private fun NavHostController.navigateJustOnce( - route: String, - navOptionsBuilder: (NavOptionsBuilder.() -> Unit)? = null -) { - if (currentDestination?.route == route) { - return - } - - if (navOptionsBuilder != null) { - navigate(route, navOptionsBuilder) - } else { - navigate(route) - } -} - -/** - * Pops up the current screen from the back stack. Parameter currentRouteToBePopped is meant to be - * set only to the current screen so we can easily debounce multiple screen popping from the back stack. - * - * @param currentRouteToBePopped current screen which should be popped up. - */ -private fun NavHostController.popBackStackJustOnce(currentRouteToBePopped: String) { - if (currentDestination?.route != currentRouteToBePopped) { - return - } - popBackStack() -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt new file mode 100644 index 00000000..8abf37d3 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt @@ -0,0 +1,173 @@ +package co.electriccoin.zcash.ui + +import android.annotation.SuppressLint +import androidx.annotation.VisibleForTesting +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.NavOptionsBuilder +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import co.electriccoin.zcash.ui.NavigationTargets.ABOUT +import co.electriccoin.zcash.ui.NavigationTargets.HOME +import co.electriccoin.zcash.ui.NavigationTargets.PROFILE +import co.electriccoin.zcash.ui.NavigationTargets.REQUEST +import co.electriccoin.zcash.ui.NavigationTargets.SCAN +import co.electriccoin.zcash.ui.NavigationTargets.SEED +import co.electriccoin.zcash.ui.NavigationTargets.SEND +import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS +import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT +import co.electriccoin.zcash.ui.NavigationTargets.WALLET_ADDRESS_DETAILS +import co.electriccoin.zcash.ui.screen.about.WrapAbout +import co.electriccoin.zcash.ui.screen.address.WrapWalletAddresses +import co.electriccoin.zcash.ui.screen.home.WrapHome +import co.electriccoin.zcash.ui.screen.profile.WrapProfile +import co.electriccoin.zcash.ui.screen.request.WrapRequest +import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator +import co.electriccoin.zcash.ui.screen.seed.WrapSeed +import co.electriccoin.zcash.ui.screen.send.WrapSend +import co.electriccoin.zcash.ui.screen.settings.WrapSettings +import co.electriccoin.zcash.ui.screen.support.WrapSupport +import co.electriccoin.zcash.ui.screen.update.WrapCheckForUpdate + +@Composable +@Suppress("LongMethod") +internal fun MainActivity.Navigation() { + val navController = rememberNavController().also { + // This suppress is necessary, as this is how we set up the nav controller for tests. + @SuppressLint("RestrictedApi") + navControllerForTesting = it + } + + NavHost(navController = navController, startDestination = HOME) { + composable(HOME) { + WrapHome( + goScan = { navController.navigateJustOnce(SCAN) }, + goProfile = { navController.navigateJustOnce(PROFILE) }, + goSend = { navController.navigateJustOnce(SEND) }, + goRequest = { navController.navigateJustOnce(REQUEST) } + ) + + WrapCheckForUpdate() + } + composable(PROFILE) { + WrapProfile( + onBack = { navController.popBackStackJustOnce(PROFILE) }, + onAddressDetails = { navController.navigateJustOnce(WALLET_ADDRESS_DETAILS) }, + onAddressBook = { }, + onSettings = { navController.navigateJustOnce(SETTINGS) }, + onCoinholderVote = { }, + onSupport = { navController.navigateJustOnce(SUPPORT) }, + onAbout = { navController.navigateJustOnce(ABOUT) } + ) + } + composable(WALLET_ADDRESS_DETAILS) { + WrapWalletAddresses( + goBack = { + navController.popBackStackJustOnce(WALLET_ADDRESS_DETAILS) + } + ) + } + composable(SETTINGS) { + WrapSettings( + goBack = { + navController.popBackStackJustOnce(SETTINGS) + }, + goWalletBackup = { + navController.navigateJustOnce(SEED) + } + ) + } + composable(SEED) { + WrapSeed( + goBack = { + navController.popBackStackJustOnce(SEED) + } + ) + } + composable(REQUEST) { + WrapRequest(goBack = { navController.popBackStackJustOnce(REQUEST) }) + } + composable(SEND) { + WrapSend(goBack = { navController.popBackStackJustOnce(SEND) }) + } + composable(SUPPORT) { + // Pop back stack won't be right if we deep link into support + WrapSupport(goBack = { navController.popBackStackJustOnce(SUPPORT) }) + } + composable(ABOUT) { + WrapAbout(goBack = { navController.popBackStackJustOnce(ABOUT) }) + } + composable(SCAN) { + WrapScanValidator( + onScanValid = { + // TODO [#449] https://github.com/zcash/secant-android-wallet/issues/449 + navController.navigateJustOnce(SEND) { + popUpTo(HOME) { inclusive = false } + } + }, + goBack = { navController.popBackStackJustOnce(SCAN) } + ) + } + } +} + +private fun NavHostController.navigateJustOnce( + route: String, + navOptionsBuilder: (NavOptionsBuilder.() -> Unit)? = null +) { + if (currentDestination?.route == route) { + return + } + + if (navOptionsBuilder != null) { + navigate(route, navOptionsBuilder) + } else { + navigate(route) + } +} + +/** + * Pops up the current screen from the back stack. Parameter currentRouteToBePopped is meant to be + * set only to the current screen so we can easily debounce multiple screen popping from the back stack. + * + * @param currentRouteToBePopped current screen which should be popped up. + */ +private fun NavHostController.popBackStackJustOnce(currentRouteToBePopped: String) { + if (currentDestination?.route != currentRouteToBePopped) { + return + } + popBackStack() +} + +object NavigationTargets { + @VisibleForTesting + const val HOME = "home" + + @VisibleForTesting + const val PROFILE = "profile" + + @VisibleForTesting + const val WALLET_ADDRESS_DETAILS = "wallet_address_details" + + @VisibleForTesting + const val SETTINGS = "settings" + + @VisibleForTesting + const val SEED = "seed" + + @VisibleForTesting + const val REQUEST = "request" + + @VisibleForTesting + const val SEND = "send" + + @VisibleForTesting + const val SUPPORT = "support" + + @VisibleForTesting + const val ABOUT = "about" + + @VisibleForTesting + const val SCAN = "scan" +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/address/AndroidWalletAddresses.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/address/AndroidWalletAddresses.kt new file mode 100644 index 00000000..f22e5310 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/address/AndroidWalletAddresses.kt @@ -0,0 +1,36 @@ +@file:Suppress("ktlint:filename") + +package co.electriccoin.zcash.ui.screen.address + +import androidx.activity.ComponentActivity +import androidx.activity.viewModels +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import co.electriccoin.zcash.ui.MainActivity +import co.electriccoin.zcash.ui.screen.address.view.WalletAddresses +import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel + +@Composable +internal fun MainActivity.WrapWalletAddresses( + goBack: () -> Unit +) { + WrapWalletAddresses(this, goBack) +} + +@Composable +private fun WrapWalletAddresses( + activity: ComponentActivity, + goBack: () -> Unit +) { + val walletViewModel by activity.viewModels() + + val walletAddresses = walletViewModel.addresses.collectAsState().value + if (null == walletAddresses) { + // Display loading indicator + } else { + WalletAddresses( + walletAddresses, + goBack + ) + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/request/AndroidRequest.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/request/AndroidRequest.kt new file mode 100644 index 00000000..fbbc5949 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/request/AndroidRequest.kt @@ -0,0 +1,59 @@ +@file:Suppress("ktlint:filename") + +package co.electriccoin.zcash.ui.screen.request + +import android.content.Context +import android.content.Intent +import androidx.activity.ComponentActivity +import androidx.activity.viewModels +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import cash.z.ecc.sdk.model.ZecRequest +import co.electriccoin.zcash.ui.MainActivity +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel +import co.electriccoin.zcash.ui.screen.request.view.Request +import kotlinx.coroutines.runBlocking + +@Composable +internal fun MainActivity.WrapRequest( + goBack: () -> Unit +) { + WrapRequest(this, goBack) +} + +@Composable +private fun WrapRequest( + activity: ComponentActivity, + goBack: () -> Unit +) { + val walletViewModel by activity.viewModels() + val walletAddresses = walletViewModel.addresses.collectAsState().value + + if (null == walletAddresses) { + // Display loading indicator + } else { + Request( + walletAddresses.unified, + goBack = goBack, + onCreateAndSend = { + val chooserIntent = Intent.createChooser( + it.newShareIntent(activity.applicationContext), + null + ) + + activity.startActivity(chooserIntent) + + goBack() + } + ) + } +} + +private fun ZecRequest.newShareIntent(context: Context) = runBlocking { + Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, context.getString(R.string.request_template_format, toUri())) + type = "text/plain" + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/AndroidScan.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/AndroidScan.kt index 3cf879c7..6c9f76e0 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/AndroidScan.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/AndroidScan.kt @@ -3,22 +3,57 @@ package co.electriccoin.zcash.ui.screen.scan import androidx.activity.ComponentActivity +import androidx.activity.viewModels import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.lifecycle.lifecycleScope import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.screen.scan.util.SettingsUtil import co.electriccoin.zcash.ui.screen.scan.view.Scan import kotlinx.coroutines.launch @Composable -internal fun MainActivity.WrapScan( - goBack: () -> Unit, - onScanDone: (result: String) -> Unit +internal fun MainActivity.WrapScanValidator( + onScanValid: (address: String) -> Unit, + goBack: () -> Unit ) { - WrapScan(this, onScanDone, goBack) + WrapScanValidator( + this, + onScanValid = onScanValid, + goBack = goBack + ) +} + +@Composable +private fun WrapScanValidator( + activity: ComponentActivity, + onScanValid: (address: String) -> Unit, + goBack: () -> Unit +) { + val walletViewModel by activity.viewModels() + + val synchronizer = walletViewModel.synchronizer.collectAsState().value + if (synchronizer == null) { + // Display loading indicator + } else { + WrapScan( + activity, + onScanned = { result -> + activity.lifecycleScope.launch { + val isAddressValid = !synchronizer.validateAddress(result).isNotValid + if (isAddressValid) { + onScanValid(result) + } + } + }, + goBack = goBack + ) + } } @Composable diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seed/AndroidSeed.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seed/AndroidSeed.kt new file mode 100644 index 00000000..6f3cee2c --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seed/AndroidSeed.kt @@ -0,0 +1,49 @@ +@file:Suppress("ktlint:filename") + +package co.electriccoin.zcash.ui.screen.seed + +import androidx.activity.ComponentActivity +import androidx.activity.viewModels +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import co.electriccoin.zcash.ui.MainActivity +import co.electriccoin.zcash.ui.screen.backup.copyToClipboard +import co.electriccoin.zcash.ui.screen.home.viewmodel.SecretState +import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel +import co.electriccoin.zcash.ui.screen.seed.view.Seed + +@Composable +internal fun MainActivity.WrapSeed( + goBack: () -> Unit +) { + WrapSeed(this, goBack) +} + +@Composable +private fun WrapSeed( + activity: ComponentActivity, + goBack: () -> Unit +) { + val walletViewModel by activity.viewModels() + + val persistableWallet = run { + val secretState = walletViewModel.secretState.collectAsState().value + if (secretState is SecretState.Ready) { + secretState.persistableWallet + } else { + null + } + } + val synchronizer = walletViewModel.synchronizer.collectAsState().value + if (null == synchronizer || null == persistableWallet) { + // Display loading indicator + } else { + Seed( + persistableWallet = persistableWallet, + onBack = goBack, + onCopyToClipboard = { + copyToClipboard(activity.applicationContext, persistableWallet) + } + ) + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/AndroidSend.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/AndroidSend.kt new file mode 100644 index 00000000..ca5e8864 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/AndroidSend.kt @@ -0,0 +1,45 @@ +@file:Suppress("ktlint:filename") + +package co.electriccoin.zcash.ui.screen.send + +import androidx.activity.ComponentActivity +import androidx.activity.viewModels +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import cash.z.ecc.sdk.send +import co.electriccoin.zcash.ui.MainActivity +import co.electriccoin.zcash.ui.screen.home.model.spendableBalance +import co.electriccoin.zcash.ui.screen.home.viewmodel.WalletViewModel +import co.electriccoin.zcash.ui.screen.send.view.Send + +@Composable +internal fun MainActivity.WrapSend( + goBack: () -> Unit +) { + WrapSend(this, goBack) +} + +@Composable +private fun WrapSend( + activity: ComponentActivity, + goBack: () -> Unit +) { + val walletViewModel by activity.viewModels() + + val synchronizer = walletViewModel.synchronizer.collectAsState().value + val spendableBalance = walletViewModel.walletSnapshot.collectAsState().value?.spendableBalance() + val spendingKey = walletViewModel.spendingKey.collectAsState().value + if (null == synchronizer || null == spendableBalance || null == spendingKey) { + // Display loading indicator + } else { + Send( + mySpendableBalance = spendableBalance, + goBack = goBack, + onCreateAndSend = { + synchronizer.send(spendingKey, it) + + goBack() + } + ) + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/AndroidUpdate.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/AndroidUpdate.kt index 3fae1544..1c65176f 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/AndroidUpdate.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/update/AndroidUpdate.kt @@ -3,13 +3,16 @@ package co.electriccoin.zcash.ui.screen.update import android.content.Context import androidx.activity.ComponentActivity import androidx.activity.viewModels +import androidx.annotation.VisibleForTesting import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.screen.home.viewmodel.CheckUpdateViewModel import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo import co.electriccoin.zcash.ui.screen.update.model.UpdateState import co.electriccoin.zcash.ui.screen.update.util.PlayStoreUtil @@ -19,17 +22,39 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @Composable -internal fun MainActivity.WrapUpdate( - updateInfo: UpdateInfo -) { - WrapUpdate( - activity = this, - inputUpdateInfo = updateInfo - ) +internal fun MainActivity.WrapCheckForUpdate() { + WrapCheckForUpdate(this) } @Composable -internal fun WrapUpdate( +private fun WrapCheckForUpdate(activity: ComponentActivity) { + // TODO [#382]: https://github.com/zcash/secant-android-wallet/issues/382 + // TODO [#403]: https://github.com/zcash/secant-android-wallet/issues/403 + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + val checkUpdateViewModel by activity.viewModels { + CheckUpdateViewModel.CheckUpdateViewModelFactory( + activity.application, + AppUpdateCheckerImp.new() + ) + } + + val updateInfo = checkUpdateViewModel.updateInfo.collectAsState().value + + updateInfo?.let { + if (it.appUpdateInfo != null && it.state == UpdateState.Prepared) { + WrapUpdate(activity, updateInfo) + } + } + + // Check for an app update asynchronously. We create an effect that matches the activity + // lifecycle. If the wrapping compose recomposes, the check shouldn't run again. + LaunchedEffect(true) { + checkUpdateViewModel.checkForAppUpdate() + } +} + +@Composable +private fun WrapUpdate( activity: ComponentActivity, inputUpdateInfo: UpdateInfo ) {