From 42deed3391dec270504cbeda847b1fb6095d7fc2 Mon Sep 17 00:00:00 2001 From: Milan Date: Mon, 1 Jul 2024 12:44:51 +0200 Subject: [PATCH] #814 Compose back handler (#1494) * #814 Compose back handler Closes #814 * #814 Test hotfixes Closes #814 * Back handling for onboarding Closes #814 * [#814] Changelog update Closes #814 --------- Co-authored-by: Milan Cerovsky --- CHANGELOG.md | 3 + .../zcash/ui/screen/send/SendViewTestSetup.kt | 7 - .../integration/SendViewIntegrationTest.kt | 2 - .../update/view/UpdateViewAndroidTestSetup.kt | 11 +- .../co/electriccoin/zcash/ui/Navigation.kt | 8 +- .../common/compose/CompositionLocalBinder.kt | 12 +- .../zcash/ui/common/compose/LocalActivity.kt | 10 ++ .../ui/common/compose/LocalNavController.kt | 10 ++ .../ui/common/viewmodel/HomeViewModel.kt | 13 -- .../zcash/ui/screen/about/AndroidAboutView.kt | 28 ++-- .../zcash/ui/screen/account/AndroidAccount.kt | 10 +- .../ui/screen/balances/AndroidBalances.kt | 7 +- .../exportdata/AndroidExportPrivateData.kt | 6 +- .../zcash/ui/screen/home/AndroidHome.kt | 127 +++++++++--------- .../zcash/ui/screen/home/view/HomeView.kt | 42 +----- .../AndroidNewWalletRecovery.kt | 14 +- .../ui/screen/onboarding/AndroidOnboarding.kt | 11 +- .../zcash/ui/screen/receive/AndroidReceive.kt | 9 +- .../ui/screen/restore/view/RestoreView.kt | 6 + .../zcash/ui/screen/scan/AndroidScan.kt | 11 +- .../zcash/ui/screen/scan/view/ScanView.kt | 12 +- .../securitywarning/AndroidSecurityWarning.kt | 23 ++-- .../seedrecovery/AndroidSeedRecovery.kt | 13 +- .../zcash/ui/screen/send/AndroidSend.kt | 26 +--- .../zcash/ui/screen/send/view/SendView.kt | 21 +-- .../ui/screen/settings/AndroidSettings.kt | 38 +----- .../zcash/ui/screen/support/AndroidSupport.kt | 22 ++- .../zcash/ui/screen/update/AndroidUpdate.kt | 21 ++- 28 files changed, 219 insertions(+), 304 deletions(-) create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/LocalActivity.kt create mode 100644 ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/LocalNavController.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 742515a2..cb6f6983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ directly impact users rather than highlighting other key architectural updates.* ## [Unreleased] +### Fixed +- The app navigation has been improved to always behave the same for system, gesture, or top app bar back navigation actions. + ### Added - Proper ZEC amount abbreviation has been added across the entire app as described by the design document diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/SendViewTestSetup.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/SendViewTestSetup.kt index 36b44eb2..6437a63d 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/SendViewTestSetup.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/SendViewTestSetup.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.test.junit4.ComposeContentTestRule import cash.z.ecc.android.sdk.fixture.WalletBalanceFixture import cash.z.ecc.android.sdk.model.MonetarySeparators @@ -69,11 +68,6 @@ class SendViewTestSetup( return lastZecSend } - fun getLastSendStage(): SendStage? { - composeTestRule.waitForIdle() - return lastSendStage - } - @Composable @Suppress("TestFunctionName") fun DefaultContent() { @@ -111,7 +105,6 @@ class SendViewTestSetup( balanceState = BalanceStateFixture.new(), sendStage = sendStage, onCreateZecSend = setZecSend, - focusManager = LocalFocusManager.current, onBack = onBackAction, onSettings = { onSettingsCount.incrementAndGet() }, onQrScannerOpen = { diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/integration/SendViewIntegrationTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/integration/SendViewIntegrationTest.kt index 4eb644cd..198c1ed2 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/integration/SendViewIntegrationTest.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/send/integration/SendViewIntegrationTest.kt @@ -1,6 +1,5 @@ package co.electriccoin.zcash.ui.screen.send.integration -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.test.junit4.StateRestorationTester import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText @@ -71,7 +70,6 @@ class SendViewIntegrationTest { synchronizer = synchronizer, walletSnapshot = walletSnapshot, spendingKey = spendingKey, - focusManager = LocalFocusManager.current, goToQrScanner = {}, goBack = {}, goBalances = {}, diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/view/UpdateViewAndroidTestSetup.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/view/UpdateViewAndroidTestSetup.kt index 4a5aad76..ed7c660b 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/view/UpdateViewAndroidTestSetup.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/update/view/UpdateViewAndroidTestSetup.kt @@ -1,7 +1,9 @@ package co.electriccoin.zcash.ui.screen.update.view import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import co.electriccoin.zcash.ui.common.compose.LocalActivity import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.screen.update.WrapUpdate import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo @@ -13,10 +15,11 @@ class UpdateViewAndroidTestSetup( @Composable @Suppress("TestFunctionName") fun DefaultContent() { - WrapUpdate( - composeTestRule.activity, - updateInfo - ) + CompositionLocalProvider(LocalActivity provides composeTestRule.activity) { + WrapUpdate( + updateInfo + ) + } } fun setDefaultContent() { 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 index 84e52bc7..43ec9c1f 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt @@ -13,7 +13,6 @@ 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.android.sdk.Synchronizer import cash.z.ecc.android.sdk.model.ZecSend import co.electriccoin.zcash.spackle.Twig @@ -36,6 +35,7 @@ import co.electriccoin.zcash.ui.NavigationTargets.SEED_RECOVERY import co.electriccoin.zcash.ui.NavigationTargets.SEND_CONFIRMATION import co.electriccoin.zcash.ui.NavigationTargets.SETTINGS import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT +import co.electriccoin.zcash.ui.common.compose.LocalNavController import co.electriccoin.zcash.ui.common.model.SerializableAddress import co.electriccoin.zcash.ui.configuration.ConfigurationEntries import co.electriccoin.zcash.ui.configuration.RemoteConfig @@ -74,10 +74,7 @@ import kotlinx.serialization.json.Json @Composable @Suppress("LongMethod") internal fun MainActivity.Navigation() { - val navController = - rememberNavController().also { - navControllerForTesting = it - } + val navController = LocalNavController.current // Helper properties for triggering the system security UI from callbacks val (exportPrivateDataAuthentication, setExportPrivateDataAuthentication) = @@ -268,7 +265,6 @@ private fun MainActivity.NavigationHome( backStack: NavBackStackEntry ) { WrapHome( - goBack = { finish() }, goScan = { navController.navigateJustOnce(SCAN) }, goSendConfirmation = { zecSend -> navController.currentBackStackEntry?.savedStateHandle?.let { handle -> diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/CompositionLocalBinder.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/CompositionLocalBinder.kt index 13dfcf9a..cdcc7200 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/CompositionLocalBinder.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/CompositionLocalBinder.kt @@ -5,13 +5,15 @@ import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.lifecycle.lifecycleScope +import androidx.navigation.compose.rememberNavController import cash.z.ecc.android.sdk.ext.collectWith import co.electriccoin.zcash.spackle.EmulatorWtfUtil import co.electriccoin.zcash.spackle.FirebaseTestLabUtil +import co.electriccoin.zcash.ui.MainActivity import kotlinx.coroutines.flow.map @Composable -fun ComponentActivity.BindCompLocalProvider(content: @Composable () -> Unit) { +fun MainActivity.BindCompLocalProvider(content: @Composable () -> Unit) { val screenSecurity = ScreenSecurity() observeScreenSecurityFlag(screenSecurity) @@ -20,10 +22,18 @@ fun ComponentActivity.BindCompLocalProvider(content: @Composable () -> Unit) { val screenTimeout = ScreenTimeout() observeScreenTimeoutFlag(screenTimeout) + + val navController = + rememberNavController().also { + navControllerForTesting = it + } + CompositionLocalProvider( LocalScreenSecurity provides screenSecurity, LocalScreenBrightness provides screenBrightness, LocalScreenTimeout provides screenTimeout, + LocalNavController provides navController, + LocalActivity provides this, content = content ) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/LocalActivity.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/LocalActivity.kt new file mode 100644 index 00000000..13496924 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/LocalActivity.kt @@ -0,0 +1,10 @@ +package co.electriccoin.zcash.ui.common.compose + +import androidx.activity.ComponentActivity +import androidx.compose.runtime.compositionLocalOf + +@Suppress("CompositionLocalAllowlist") +val LocalActivity = + compositionLocalOf { + error("Activity not provided") + } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/LocalNavController.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/LocalNavController.kt new file mode 100644 index 00000000..b0968ec1 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/LocalNavController.kt @@ -0,0 +1,10 @@ +package co.electriccoin.zcash.ui.common.compose + +import androidx.compose.runtime.compositionLocalOf +import androidx.navigation.NavHostController + +@Suppress("CompositionLocalAllowlist") +val LocalNavController = + compositionLocalOf { + error("NavController not provided") + } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/HomeViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/HomeViewModel.kt index b0bd50e9..f89f37a7 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/HomeViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/viewmodel/HomeViewModel.kt @@ -9,8 +9,6 @@ import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault import co.electriccoin.zcash.ui.common.ANDROID_STATE_FLOW_TIMEOUT import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys import co.electriccoin.zcash.ui.preference.StandardPreferenceSingleton -import co.electriccoin.zcash.ui.screen.home.HomeScreenIndex -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.WhileSubscribed @@ -20,11 +18,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch class HomeViewModel(application: Application) : AndroidViewModel(application) { - /** - * Current Home sub-screen index in flow. - */ - val screenIndex: MutableStateFlow = MutableStateFlow(HomeScreenIndex.ACCOUNT) - /** * A flow of whether background sync is enabled */ @@ -37,12 +30,6 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) { val isKeepScreenOnWhileSyncing: StateFlow = booleanStateFlow(StandardPreferenceKeys.IS_KEEP_SCREEN_ON_DURING_SYNC) - /** - * A flow of whether the app uses simple or detailed block synchronization status information for the UI - */ - val isDetailedSyncStatus: StateFlow = - booleanStateFlow(StandardPreferenceKeys.IS_DETAILED_SYNC_STATUS) - /** * A flow of whether the app presented the user with an initial restoring dialog */ diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/AndroidAboutView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/AndroidAboutView.kt index a5ad0bd7..1f701db4 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/AndroidAboutView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/AndroidAboutView.kt @@ -3,7 +3,7 @@ package co.electriccoin.zcash.ui.screen.about import android.content.Context -import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler import androidx.activity.viewModels import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable @@ -12,9 +12,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.lifecycle.compose.collectAsStateWithLifecycle import co.electriccoin.zcash.configuration.AndroidConfigurationFactory -import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState +import co.electriccoin.zcash.ui.common.compose.LocalActivity import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.screen.about.util.WebBrowserUtil @@ -24,24 +23,17 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @Composable -internal fun MainActivity.WrapAbout(goBack: () -> Unit) { - val walletViewModel by viewModels() +internal fun WrapAbout(goBack: () -> Unit) { + val activity = LocalActivity.current + + val walletViewModel by activity.viewModels() val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value - WrapAbout( - activity = this, - goBack = goBack, - topAppBarSubTitleState = walletState - ) -} + BackHandler { + goBack() + } -@Composable -internal fun WrapAbout( - activity: ComponentActivity, - goBack: () -> Unit, - topAppBarSubTitleState: TopAppBarSubTitleState, -) { val configInfo = ConfigInfo.new(AndroidConfigurationFactory.getInstance(activity.applicationContext)) val versionInfo = VersionInfo.new(activity.applicationContext) @@ -66,7 +58,7 @@ internal fun WrapAbout( ) }, snackbarHostState = snackbarHostState, - topAppBarSubTitleState = topAppBarSubTitleState, + topAppBarSubTitleState = walletState, ) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/account/AndroidAccount.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/account/AndroidAccount.kt index ea6a60fd..ea9a8a84 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/account/AndroidAccount.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/account/AndroidAccount.kt @@ -3,19 +3,20 @@ package co.electriccoin.zcash.ui.screen.account import android.content.Context -import androidx.activity.ComponentActivity import androidx.activity.viewModels import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.internal.Twig import co.electriccoin.zcash.spackle.ClipboardManagerUtil import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.common.compose.BalanceState +import co.electriccoin.zcash.ui.common.compose.LocalActivity import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState import co.electriccoin.zcash.ui.common.model.WalletRestoringState import co.electriccoin.zcash.ui.common.model.WalletSnapshot @@ -33,10 +34,11 @@ import org.jetbrains.annotations.VisibleForTesting @Composable internal fun WrapAccount( - activity: ComponentActivity, goBalances: () -> Unit, goSettings: () -> Unit, ) { + val activity = LocalActivity.current + val walletViewModel by activity.viewModels() val transactionHistoryViewModel by activity.viewModels() @@ -59,7 +61,6 @@ internal fun WrapAccount( WrapAccount( balanceState = balanceState, - context = activity.applicationContext, goBalances = goBalances, goSettings = goSettings, synchronizer = synchronizer, @@ -79,7 +80,6 @@ internal fun WrapAccount( @Suppress("LongParameterList", "LongMethod") internal fun WrapAccount( balanceState: BalanceState, - context: Context, goBalances: () -> Unit, goSettings: () -> Unit, transactionsUiState: TransactionUiState, @@ -89,6 +89,8 @@ internal fun WrapAccount( walletRestoringState: WalletRestoringState, walletSnapshot: WalletSnapshot? ) { + val context = LocalContext.current + val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/balances/AndroidBalances.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/balances/AndroidBalances.kt index 294e017f..c9db3d22 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/balances/AndroidBalances.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/balances/AndroidBalances.kt @@ -19,9 +19,9 @@ import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.model.UnifiedSpendingKey import cash.z.ecc.android.sdk.model.Zatoshi import co.electriccoin.zcash.spackle.Twig -import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.common.compose.BalanceState +import co.electriccoin.zcash.ui.common.compose.LocalActivity import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState import co.electriccoin.zcash.ui.common.model.WalletRestoringState import co.electriccoin.zcash.ui.common.model.WalletSnapshot @@ -45,10 +45,11 @@ import org.jetbrains.annotations.VisibleForTesting @Composable internal fun WrapBalances( - activity: MainActivity, goSettings: () -> Unit, goMultiTrxSubmissionFailure: () -> Unit, ) { + val activity = LocalActivity.current + val walletViewModel by activity.viewModels() val createTransactionsViewModel by activity.viewModels() @@ -89,9 +90,9 @@ internal fun WrapBalances( const val DEFAULT_SHIELDING_THRESHOLD = 100000L +// This function should be refactored into smaller chunks @Composable @VisibleForTesting -// This function should be refactored into smaller chunks @Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod") internal fun WrapBalances( balanceState: BalanceState, diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exportdata/AndroidExportPrivateData.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exportdata/AndroidExportPrivateData.kt index cf2bc816..960000e1 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exportdata/AndroidExportPrivateData.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exportdata/AndroidExportPrivateData.kt @@ -1,7 +1,6 @@ package co.electriccoin.zcash.ui.screen.exportdata import android.content.Context -import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.viewModels import androidx.compose.material3.SnackbarHostState @@ -15,6 +14,7 @@ import cash.z.ecc.android.sdk.model.ZcashNetwork import cash.z.ecc.sdk.type.fromResources import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.compose.LocalActivity import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel @@ -38,7 +38,6 @@ internal fun MainActivity.WrapExportPrivateData( val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value WrapExportPrivateData( - this, goBack = goBack, onShare = onConfirm, synchronizer = synchronizer, @@ -48,12 +47,13 @@ internal fun MainActivity.WrapExportPrivateData( @Composable internal fun WrapExportPrivateData( - activity: ComponentActivity, goBack: () -> Unit, onShare: () -> Unit, synchronizer: Synchronizer?, topAppBarSubTitleState: TopAppBarSubTitleState, ) { + val activity = LocalActivity.current + BackHandler { goBack() } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/AndroidHome.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/AndroidHome.kt index fe572e24..75f6b463 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/AndroidHome.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/AndroidHome.kt @@ -4,18 +4,23 @@ package co.electriccoin.zcash.ui.screen.home import androidx.activity.compose.BackHandler import androidx.activity.viewModels +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState +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.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import cash.z.ecc.android.sdk.model.ZecSend -import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.compose.LocalActivity import co.electriccoin.zcash.ui.common.compose.RestoreScreenBrightness import co.electriccoin.zcash.ui.common.model.WalletRestoringState import co.electriccoin.zcash.ui.common.model.WalletSnapshot @@ -30,25 +35,23 @@ import co.electriccoin.zcash.ui.screen.receive.WrapReceive import co.electriccoin.zcash.ui.screen.send.WrapSend import co.electriccoin.zcash.ui.screen.send.model.SendArguments import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch @Composable @Suppress("LongParameterList") -internal fun MainActivity.WrapHome( - goBack: () -> Unit, +internal fun WrapHome( goSettings: () -> Unit, goMultiTrxSubmissionFailure: () -> Unit, goScan: () -> Unit, goSendConfirmation: (ZecSend) -> Unit, sendArguments: SendArguments ) { - val homeViewModel by viewModels() + val activity = LocalActivity.current - val walletViewModel by viewModels() + val homeViewModel by activity.viewModels() - val homeScreenIndex = homeViewModel.screenIndex.collectAsStateWithLifecycle().value + val walletViewModel by activity.viewModels() val isKeepScreenOnWhileSyncing = homeViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value @@ -82,56 +85,58 @@ internal fun MainActivity.WrapHome( } WrapHome( - this, - goBack = goBack, goScan = goScan, goSendConfirmation = goSendConfirmation, goSettings = goSettings, goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure, - homeScreenIndex = homeScreenIndex, isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing, isShowingRestoreInitDialog = isShowingRestoreInitDialog, - onPageChange = { - homeViewModel.screenIndex.value = it - }, sendArguments = sendArguments, setShowingRestoreInitDialog = setShowingRestoreInitDialog, walletSnapshot = walletSnapshot ) } +@OptIn(ExperimentalFoundationApi::class) @Suppress("LongParameterList", "LongMethod") @Composable internal fun WrapHome( - activity: MainActivity, - goBack: () -> Unit, goSettings: () -> Unit, goMultiTrxSubmissionFailure: () -> Unit, goScan: () -> Unit, goSendConfirmation: (ZecSend) -> Unit, - homeScreenIndex: HomeScreenIndex, isKeepScreenOnWhileSyncing: Boolean?, isShowingRestoreInitDialog: Boolean, - onPageChange: (HomeScreenIndex) -> Unit, sendArguments: SendArguments, setShowingRestoreInitDialog: () -> Unit, walletSnapshot: WalletSnapshot?, ) { - // Flow for propagating the new page index to the pager in the view layer - val forceHomePageIndexFlow: MutableSharedFlow = - MutableSharedFlow( - Int.MAX_VALUE, - Int.MAX_VALUE, - BufferOverflow.SUSPEND - ) - val forceIndex = forceHomePageIndexFlow.collectAsState(initial = null).value + val focusManager = LocalFocusManager.current - val homeGoBack: () -> Unit = { - when (homeScreenIndex) { - HomeScreenIndex.ACCOUNT -> goBack() - HomeScreenIndex.SEND, - HomeScreenIndex.RECEIVE, - HomeScreenIndex.BALANCES -> forceHomePageIndexFlow.tryEmit(ForcePage(HomeScreenIndex.ACCOUNT)) + val activity = LocalActivity.current + + val scope = rememberCoroutineScope() + + val pagerState = + rememberPagerState( + initialPage = 0, + initialPageOffsetFraction = 0f, + pageCount = { 4 } + ) + + val homeGoBack: () -> Unit by remember(pagerState.currentPage, scope) { + derivedStateOf { + { + when (pagerState.currentPage) { + HomeScreenIndex.ACCOUNT.pageIndex -> activity.finish() + HomeScreenIndex.SEND.pageIndex, + HomeScreenIndex.RECEIVE.pageIndex, + HomeScreenIndex.BALANCES.pageIndex -> + scope.launch { + pagerState.animateScrollToPage(HomeScreenIndex.ACCOUNT.pageIndex) + } + } + } } } @@ -140,7 +145,7 @@ internal fun WrapHome( } // Reset the screen brightness for all pages except Receive which maintain the screen brightness by itself - if (homeScreenIndex != HomeScreenIndex.RECEIVE) { + if (pagerState.currentPage != HomeScreenIndex.RECEIVE.pageIndex) { RestoreScreenBrightness() } @@ -152,8 +157,11 @@ internal fun WrapHome( testTag = HomeTag.TAB_ACCOUNT, screenContent = { WrapAccount( - activity = activity, - goBalances = { forceHomePageIndexFlow.tryEmit(ForcePage(HomeScreenIndex.BALANCES)) }, + goBalances = { + scope.launch { + pagerState.animateScrollToPage(HomeScreenIndex.BALANCES.pageIndex) + } + }, goSettings = goSettings ) } @@ -164,10 +172,13 @@ internal fun WrapHome( testTag = HomeTag.TAB_SEND, screenContent = { WrapSend( - activity = activity, goToQrScanner = goScan, goBack = homeGoBack, - goBalances = { forceHomePageIndexFlow.tryEmit(ForcePage(HomeScreenIndex.BALANCES)) }, + goBalances = { + scope.launch { + pagerState.animateScrollToPage(HomeScreenIndex.BALANCES.pageIndex) + } + }, goSendConfirmation = goSendConfirmation, goSettings = goSettings, sendArguments = sendArguments @@ -179,10 +190,7 @@ internal fun WrapHome( title = stringResource(id = R.string.home_tab_receive), testTag = HomeTag.TAB_RECEIVE, screenContent = { - WrapReceive( - activity = activity, - onSettings = goSettings - ) + WrapReceive(onSettings = goSettings) } ), TabItem( @@ -191,7 +199,6 @@ internal fun WrapHome( testTag = HomeTag.TAB_BALANCES, screenContent = { WrapBalances( - activity = activity, goSettings = goSettings, goMultiTrxSubmissionFailure = goMultiTrxSubmissionFailure ) @@ -201,33 +208,27 @@ internal fun WrapHome( Home( subScreens = tabs, - forcePage = forceIndex, isKeepScreenOnWhileSyncing = isKeepScreenOnWhileSyncing, isShowingRestoreInitDialog = isShowingRestoreInitDialog, - onPageChange = onPageChange, setShowingRestoreInitDialog = setShowingRestoreInitDialog, - walletSnapshot = walletSnapshot + walletSnapshot = walletSnapshot, + pagerState = pagerState, ) -} -/** - * Wrapper class used to pass forced pages index into the view layer - */ -class ForcePage( - val currentPage: HomeScreenIndex, -) + LaunchedEffect(pagerState.currentPage) { + if (pagerState.currentPage != HomeScreenIndex.SEND.pageIndex) { + focusManager.clearFocus(true) + } + } +} /** * Enum of the Home screen sub-screens */ -enum class HomeScreenIndex { - // WARN: Be careful when re-ordering these, as the ordinal number states for their order - ACCOUNT, - SEND, - RECEIVE, - BALANCES, ; - - companion object { - fun fromIndex(index: Int) = entries[index] - } +@Suppress("MagicNumber") +enum class HomeScreenIndex(val pageIndex: Int) { + ACCOUNT(0), + SEND(1), + RECEIVE(2), + BALANCES(3) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/view/HomeView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/view/HomeView.kt index 72ff9130..8c558b90 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/view/HomeView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/home/view/HomeView.kt @@ -16,10 +16,7 @@ import androidx.compose.material3.TabRow import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource @@ -28,7 +25,6 @@ import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import cash.z.ecc.android.sdk.Synchronizer -import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.common.compose.DisableScreenTimeout import co.electriccoin.zcash.ui.common.model.WalletSnapshot @@ -37,24 +33,20 @@ import co.electriccoin.zcash.ui.design.component.BlankSurface import co.electriccoin.zcash.ui.design.component.NavigationTabText import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.fixture.WalletSnapshotFixture -import co.electriccoin.zcash.ui.screen.home.ForcePage -import co.electriccoin.zcash.ui.screen.home.HomeScreenIndex import co.electriccoin.zcash.ui.screen.home.model.TabItem import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch +@OptIn(ExperimentalFoundationApi::class) @Preview("Home") @Composable private fun ComposablePreview() { ZcashTheme(forceDarkMode = false) { BlankSurface { Home( - forcePage = null, isKeepScreenOnWhileSyncing = false, isShowingRestoreInitDialog = false, - onPageChange = {}, setShowingRestoreInitDialog = {}, subScreens = persistentListOf(), walletSnapshot = WalletSnapshotFixture.new(), @@ -67,44 +59,18 @@ private fun ComposablePreview() { @Suppress("LongParameterList") @Composable fun Home( - forcePage: ForcePage?, isKeepScreenOnWhileSyncing: Boolean?, isShowingRestoreInitDialog: Boolean, - onPageChange: (HomeScreenIndex) -> Unit, setShowingRestoreInitDialog: () -> Unit, subScreens: ImmutableList, walletSnapshot: WalletSnapshot?, -) { - val pagerState = + pagerState: PagerState = rememberPagerState( initialPage = 0, initialPageOffsetFraction = 0f, pageCount = { subScreens.size } ) - - // Using [rememberUpdatedState] to ensure that always the latest lambda is captured - // And to avoid Detekt warning: Lambda parameters in a @Composable that are referenced directly inside of - // restarting effects can cause issues or unpredictable behavior. - val currentOnPageChange = rememberUpdatedState(newValue = onPageChange) - - // Listening for the current page change - LaunchedEffect(pagerState, currentOnPageChange) { - snapshotFlow { - pagerState.currentPage - }.distinctUntilChanged() - .collect { page -> - Twig.info { "Current pager page: $page" } - currentOnPageChange.value(HomeScreenIndex.fromIndex(page)) - } - } - - // Force page change e.g. when system back navigation event detected - forcePage?.let { - LaunchedEffect(forcePage) { - pagerState.animateScrollToPage(forcePage.currentPage.ordinal) - } - } - +) { HomeContent( pagerState = pagerState, subScreens = subScreens, @@ -124,7 +90,7 @@ fun Home( @Composable @Suppress("LongMethod") @OptIn(ExperimentalFoundationApi::class) -fun HomeContent( +private fun HomeContent( pagerState: PagerState, subScreens: ImmutableList ) { diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/AndroidNewWalletRecovery.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/AndroidNewWalletRecovery.kt index f8633a11..1dc687b7 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/AndroidNewWalletRecovery.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/AndroidNewWalletRecovery.kt @@ -1,28 +1,20 @@ package co.electriccoin.zcash.ui.screen.newwalletrecovery -import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable import cash.z.ecc.android.sdk.model.PersistableWallet import co.electriccoin.zcash.spackle.ClipboardManagerUtil -import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.compose.LocalActivity import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.screen.newwalletrecovery.view.NewWalletRecovery @Composable -fun MainActivity.WrapNewWalletRecovery( +fun WrapNewWalletRecovery( persistableWallet: PersistableWallet, onBackupComplete: () -> Unit ) { - WrapNewWalletRecovery(this, persistableWallet, onBackupComplete) -} + val activity = LocalActivity.current -@Composable -private fun WrapNewWalletRecovery( - activity: ComponentActivity, - persistableWallet: PersistableWallet, - onBackupComplete: () -> Unit -) { val versionInfo = VersionInfo.new(activity.applicationContext) NewWalletRecovery( diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/AndroidOnboarding.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/AndroidOnboarding.kt index f40d34ea..96927d6f 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/AndroidOnboarding.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/onboarding/AndroidOnboarding.kt @@ -3,7 +3,6 @@ package co.electriccoin.zcash.ui.screen.onboarding import android.content.Context -import androidx.activity.ComponentActivity import androidx.activity.viewModels import androidx.compose.runtime.Composable import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -15,7 +14,7 @@ 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.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.model.WalletRestoringState @@ -25,14 +24,10 @@ 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 -@Composable -internal fun MainActivity.WrapOnboarding() { - WrapOnboarding(this) -} - @Suppress("LongMethod") @Composable -internal fun WrapOnboarding(activity: ComponentActivity) { +internal fun WrapOnboarding() { + val activity = LocalActivity.current val walletViewModel by activity.viewModels() val onboardingViewModel by activity.viewModels() diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/AndroidReceive.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/AndroidReceive.kt index 485d5813..7f7d47dd 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/AndroidReceive.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/AndroidReceive.kt @@ -4,7 +4,6 @@ package co.electriccoin.zcash.ui.screen.receive import android.content.Context import android.graphics.Bitmap -import androidx.activity.ComponentActivity import androidx.activity.viewModels import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable @@ -16,6 +15,7 @@ import co.electriccoin.zcash.spackle.ClipboardManagerUtil import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.spackle.getInternalCacheDirSuspend import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.compose.LocalActivity import co.electriccoin.zcash.ui.common.compose.ScreenBrightnessState import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel @@ -31,10 +31,9 @@ import kotlinx.coroutines.withContext import java.io.File @Composable -internal fun WrapReceive( - activity: ComponentActivity, - onSettings: () -> Unit, -) { +internal fun WrapReceive(onSettings: () -> Unit) { + val activity = LocalActivity.current + val walletViewModel by activity.viewModels() val brightnessViewModel by activity.viewModels() diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreView.kt index 3ae44102..5bb648f8 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreView.kt @@ -2,6 +2,7 @@ package co.electriccoin.zcash.ui.screen.restore.view +import androidx.activity.compose.BackHandler import androidx.compose.animation.animateContentSize import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi @@ -229,6 +230,11 @@ fun RestoreWallet( val currentStage = restoreState.current.collectAsStateWithLifecycle().value var isSeedValid by rememberSaveable { mutableStateOf(false) } + + BackHandler { + onBack() + } + // To avoid unnecessary recompositions that this flow produces SideEffect { scope.launch { 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 9b27af6b..0eb3782e 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 @@ -1,6 +1,6 @@ package co.electriccoin.zcash.ui.screen.scan -import android.content.Context +import androidx.activity.compose.BackHandler import androidx.activity.viewModels import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable @@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.type.AddressType @@ -33,8 +34,11 @@ internal fun MainActivity.WrapScanValidator( val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value + BackHandler { + goBack() + } + WrapScan( - context = this, onScanValid = onScanValid, goBack = goBack, synchronizer = synchronizer, @@ -44,12 +48,13 @@ internal fun MainActivity.WrapScanValidator( @Composable fun WrapScan( - context: Context, goBack: () -> Unit, onScanValid: (address: SerializableAddress) -> Unit, synchronizer: Synchronizer?, topAppBarSubTitleState: TopAppBarSubTitleState, ) { + val context = LocalContext.current + val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/view/ScanView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/view/ScanView.kt index a024a540..be011977 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/view/ScanView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/scan/view/ScanView.kt @@ -1,7 +1,6 @@ package co.electriccoin.zcash.ui.screen.scan.view import android.Manifest -import android.content.Context import android.view.ViewGroup import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -696,7 +695,6 @@ fun ScanCameraView( ) imageAnalysis.qrCodeFlow( - context = context, framePosition = framePosition, ).collectAsState(initial = null).value?.let { onScanned(it) @@ -707,11 +705,10 @@ fun ScanCameraView( // Using callbackFlow because QrCodeAnalyzer has a non-suspending callback which makes // a basic flow builder not work here. @Composable -fun ImageAnalysis.qrCodeFlow( - context: Context, - framePosition: FramePosition, -): Flow = - remember { +fun ImageAnalysis.qrCodeFlow(framePosition: FramePosition): Flow { + val context = LocalContext.current + + return remember { callbackFlow { setAnalyzer( ContextCompat.getMainExecutor(context), @@ -732,3 +729,4 @@ fun ImageAnalysis.qrCodeFlow( } } } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/securitywarning/AndroidSecurityWarning.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/securitywarning/AndroidSecurityWarning.kt index 7784ec3c..6abf25b0 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/securitywarning/AndroidSecurityWarning.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/securitywarning/AndroidSecurityWarning.kt @@ -1,31 +1,24 @@ package co.electriccoin.zcash.ui.screen.securitywarning -import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import co.electriccoin.zcash.configuration.AndroidConfigurationFactory -import co.electriccoin.zcash.ui.MainActivity +import co.electriccoin.zcash.ui.common.compose.LocalActivity import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.screen.securitywarning.view.SecurityWarning -@Composable -internal fun MainActivity.WrapSecurityWarning( - onBack: () -> Unit, - onConfirm: () -> Unit -) { - WrapSecurityWarning( - this, - onBack = onBack, - onConfirm = onConfirm - ) -} - @Composable internal fun WrapSecurityWarning( - activity: ComponentActivity, onBack: () -> Unit, onConfirm: () -> Unit ) { + val activity = LocalActivity.current + + BackHandler { + onBack() + } + SecurityWarning( versionInfo = VersionInfo.new(activity.applicationContext), onBack = onBack, diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seedrecovery/AndroidSeedRecovery.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seedrecovery/AndroidSeedRecovery.kt index 015e7435..5a2a3e74 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seedrecovery/AndroidSeedRecovery.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seedrecovery/AndroidSeedRecovery.kt @@ -1,14 +1,13 @@ package co.electriccoin.zcash.ui.screen.seedrecovery -import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.viewModels import androidx.compose.runtime.Composable import androidx.lifecycle.compose.collectAsStateWithLifecycle import cash.z.ecc.android.sdk.Synchronizer import co.electriccoin.zcash.spackle.ClipboardManagerUtil -import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.compose.LocalActivity import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.viewmodel.SecretState @@ -17,11 +16,13 @@ import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.screen.seedrecovery.view.SeedRecovery @Composable -internal fun MainActivity.WrapSeedRecovery( +internal fun WrapSeedRecovery( goBack: () -> Unit, onDone: () -> Unit, ) { - val walletViewModel by viewModels() + val activity = LocalActivity.current + + val walletViewModel by activity.viewModels() val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value @@ -30,7 +31,6 @@ internal fun MainActivity.WrapSeedRecovery( val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value WrapSeedRecovery( - activity = this, goBack = goBack, onDone = onDone, secretState = secretState, @@ -42,13 +42,14 @@ internal fun MainActivity.WrapSeedRecovery( @Composable @Suppress("LongParameterList") private fun WrapSeedRecovery( - activity: ComponentActivity, goBack: () -> Unit, onDone: () -> Unit, topAppBarSubTitleState: TopAppBarSubTitleState, synchronizer: Synchronizer?, secretState: SecretState, ) { + val activity = LocalActivity.current + BackHandler { goBack() } 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 index b79ed7f6..b75e9eae 100644 --- 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 @@ -3,8 +3,6 @@ package co.electriccoin.zcash.ui.screen.send import android.content.pm.PackageManager -import androidx.activity.ComponentActivity -import androidx.activity.compose.BackHandler import androidx.activity.viewModels import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable @@ -12,9 +10,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager import androidx.lifecycle.compose.collectAsStateWithLifecycle import cash.z.ecc.android.sdk.Synchronizer import cash.z.ecc.android.sdk.model.MonetarySeparators @@ -25,12 +21,11 @@ import cash.z.ecc.android.sdk.model.toZecString import cash.z.ecc.android.sdk.type.AddressType import co.electriccoin.zcash.spackle.Twig import co.electriccoin.zcash.ui.common.compose.BalanceState +import co.electriccoin.zcash.ui.common.compose.LocalActivity import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState import co.electriccoin.zcash.ui.common.model.WalletSnapshot -import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator -import co.electriccoin.zcash.ui.screen.home.HomeScreenIndex import co.electriccoin.zcash.ui.screen.send.ext.Saver import co.electriccoin.zcash.ui.screen.send.model.AmountState import co.electriccoin.zcash.ui.screen.send.model.MemoState @@ -44,7 +39,6 @@ import java.util.Locale @Composable @Suppress("LongParameterList") internal fun WrapSend( - activity: ComponentActivity, sendArguments: SendArguments?, goToQrScanner: () -> Unit, goBack: () -> Unit, @@ -52,6 +46,8 @@ internal fun WrapSend( goSendConfirmation: (ZecSend) -> Unit, goSettings: () -> Unit, ) { + val activity = LocalActivity.current + val walletViewModel by activity.viewModels() val hasCameraFeature = activity.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) @@ -62,15 +58,6 @@ internal fun WrapSend( val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value - val homeViewModel by activity.viewModels() - - val focusManager = LocalFocusManager.current - - if (homeViewModel.screenIndex.collectAsStateWithLifecycle().value != HomeScreenIndex.SEND) { - // Clear focus on Send Form text fields - focusManager.clearFocus(true) - } - // TODO [#1171]: Remove default MonetarySeparators locale // TODO [#1171]: https://github.com/Electric-Coin-Company/zashi-android/issues/1171 val monetarySeparators = MonetarySeparators.current(Locale.US) @@ -85,7 +72,6 @@ internal fun WrapSend( synchronizer = synchronizer, walletSnapshot = walletSnapshot, spendingKey = spendingKey, - focusManager = focusManager, goToQrScanner = goToQrScanner, goBack = goBack, goBalances = goBalances, @@ -102,7 +88,6 @@ internal fun WrapSend( @Composable internal fun WrapSend( balanceState: BalanceState, - focusManager: FocusManager, goToQrScanner: () -> Unit, goBack: () -> Unit, goBalances: () -> Unit, @@ -188,10 +173,6 @@ internal fun WrapSend( } } - BackHandler { - onBackAction() - } - if (null == synchronizer || null == walletSnapshot || null == spendingKey) { // TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer // TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available @@ -217,7 +198,6 @@ internal fun WrapSend( } } }, - focusManager = focusManager, onBack = onBackAction, onSettings = goSettings, recipientAddressState = recipientAddressState, diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/view/SendView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/view/SendView.kt index 09bbe92c..c4646c1c 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/view/SendView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/send/view/SendView.kt @@ -31,7 +31,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection -import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInRoot @@ -93,7 +92,6 @@ private fun PreviewSendForm() { Send( sendStage = SendStage.Form, onCreateZecSend = {}, - focusManager = LocalFocusManager.current, onBack = {}, onSettings = {}, onQrScannerOpen = {}, @@ -119,7 +117,6 @@ private fun SendFormTransparentAddressPreview() { Send( sendStage = SendStage.Form, onCreateZecSend = {}, - focusManager = LocalFocusManager.current, onBack = {}, onSettings = {}, onQrScannerOpen = {}, @@ -151,7 +148,6 @@ fun Send( balanceState: BalanceState, sendStage: SendStage, onCreateZecSend: (ZecSend) -> Unit, - focusManager: FocusManager, onBack: () -> Unit, onSettings: () -> Unit, onQrScannerOpen: () -> Unit, @@ -176,7 +172,6 @@ fun Send( balanceState = balanceState, walletSnapshot = walletSnapshot, onBack = onBack, - focusManager = focusManager, sendStage = sendStage, onCreateZecSend = onCreateZecSend, recipientAddressState = recipientAddressState, @@ -232,7 +227,6 @@ private fun SendTopAppBar( private fun SendMainContent( balanceState: BalanceState, walletSnapshot: WalletSnapshot, - focusManager: FocusManager, onBack: () -> Unit, goBalances: () -> Unit, onCreateZecSend: (ZecSend) -> Unit, @@ -260,7 +254,6 @@ private fun SendMainContent( memoState = memoState, setMemoState = setMemoState, onCreateZecSend = onCreateZecSend, - focusManager = focusManager, onQrScannerOpen = onQrScannerOpen, goBalances = goBalances, hasCameraFeature = hasCameraFeature, @@ -286,7 +279,6 @@ private fun SendMainContent( private fun SendForm( balanceState: BalanceState, walletSnapshot: WalletSnapshot, - focusManager: FocusManager, recipientAddressState: RecipientAddressState, onRecipientAddressChange: (String) -> Unit, amountState: AmountState, @@ -329,7 +321,6 @@ private fun SendForm( // TODO [#1256]: https://github.com/Electric-Coin-Company/zashi-android/issues/1256 SendFormAddressTextField( - focusManager = focusManager, hasCameraFeature = hasCameraFeature, onQrScannerOpen = onQrScannerOpen, recipientAddressState = recipientAddressState, @@ -340,7 +331,6 @@ private fun SendForm( SendFormAmountTextField( amountSate = amountState, - focusManager = focusManager, imeAction = if (recipientAddressState.type == AddressType.Transparent) { ImeAction.Done @@ -358,7 +348,6 @@ private fun SendForm( SendFormMemoTextField( memoState = memoState, setMemoState = setMemoState, - focusManager = focusManager, isMemoFieldAvailable = ( recipientAddressState.address.isEmpty() || recipientAddressState.type is AddressType.Invalid || @@ -475,12 +464,13 @@ fun SendButton( @Suppress("LongMethod") @Composable fun SendFormAddressTextField( - focusManager: FocusManager, hasCameraFeature: Boolean, onQrScannerOpen: () -> Unit, recipientAddressState: RecipientAddressState, setRecipientAddress: (String) -> Unit, ) { + val focusManager = LocalFocusManager.current + val bringIntoViewRequester = remember { BringIntoViewRequester() } Column( @@ -560,13 +550,14 @@ fun SendFormAddressTextField( @Composable fun SendFormAmountTextField( amountSate: AmountState, - focusManager: FocusManager, imeAction: ImeAction, isTransparentRecipient: Boolean, monetarySeparators: MonetarySeparators, setAmountState: (AmountState) -> Unit, walletSnapshot: WalletSnapshot, ) { + val focusManager = LocalFocusManager.current + val context = LocalContext.current val zcashCurrency = ZcashCurrency.getLocalizedName(context) @@ -580,6 +571,7 @@ fun SendFormAmountTextField( stringResource(id = R.string.send_amount_invalid) } } + is AmountState.Valid -> { if (walletSnapshot.spendableBalance() < amountSate.zatoshi) { stringResource(id = R.string.send_amount_insufficient_balance) @@ -651,13 +643,14 @@ fun SendFormAmountTextField( @Suppress("LongMethod", "LongParameterList") @Composable fun SendFormMemoTextField( - focusManager: FocusManager, isMemoFieldAvailable: Boolean, memoState: MemoState, setMemoState: (MemoState) -> Unit, scrollState: ScrollState, scrollTo: Int ) { + val focusManager = LocalFocusManager.current + val scope = rememberCoroutineScope() val bringIntoViewRequester = remember { BringIntoViewRequester() } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/settings/AndroidSettings.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/settings/AndroidSettings.kt index 1935dd01..c202eb9e 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/settings/AndroidSettings.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/settings/AndroidSettings.kt @@ -1,12 +1,10 @@ package co.electriccoin.zcash.ui.screen.settings -import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.viewModels import androidx.compose.runtime.Composable import androidx.lifecycle.compose.collectAsStateWithLifecycle -import co.electriccoin.zcash.ui.MainActivity -import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState +import co.electriccoin.zcash.ui.common.compose.LocalActivity import co.electriccoin.zcash.ui.common.model.VersionInfo import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.configuration.ConfigurationEntries @@ -17,42 +15,20 @@ import co.electriccoin.zcash.ui.screen.settings.view.Settings import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel @Composable -internal fun MainActivity.WrapSettings( +internal fun WrapSettings( goAbout: () -> Unit, goAdvancedSettings: () -> Unit, goBack: () -> Unit, goFeedback: () -> Unit, ) { - val walletViewModel by viewModels() + val activity = LocalActivity.current - val settingsViewModel by viewModels() + val walletViewModel by activity.viewModels() + + val settingsViewModel by activity.viewModels() val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value - WrapSettings( - activity = this, - goAbout = goAbout, - goAdvancedSettings = goAdvancedSettings, - goBack = goBack, - goFeedback = goFeedback, - settingsViewModel = settingsViewModel, - topAppBarSubTitleState = walletState, - walletViewModel = walletViewModel, - ) -} - -@Composable -@Suppress("LongParameterList") -private fun WrapSettings( - activity: ComponentActivity, - goAbout: () -> Unit, - goAdvancedSettings: () -> Unit, - goBack: () -> Unit, - goFeedback: () -> Unit, - settingsViewModel: SettingsViewModel, - topAppBarSubTitleState: TopAppBarSubTitleState, - walletViewModel: WalletViewModel, -) { val isBackgroundSyncEnabled = settingsViewModel.isBackgroundSync.collectAsStateWithLifecycle().value val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value val isAnalyticsEnabled = settingsViewModel.isAnalyticsEnabled.collectAsStateWithLifecycle().value @@ -97,7 +73,7 @@ private fun WrapSettings( onAnalyticsSettingsChanged = { settingsViewModel.setAnalyticsEnabled(it) }, - topAppBarSubTitleState = topAppBarSubTitleState, + topAppBarSubTitleState = walletState, ) } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/AndroidSupport.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/AndroidSupport.kt index 776c4a7c..a597ddd5 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/AndroidSupport.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/AndroidSupport.kt @@ -3,8 +3,9 @@ package co.electriccoin.zcash.ui.screen.support import android.content.Intent -import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler import androidx.activity.viewModels +import androidx.annotation.VisibleForTesting import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf @@ -12,8 +13,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.lifecycle.compose.collectAsStateWithLifecycle -import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.compose.LocalActivity import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel import co.electriccoin.zcash.ui.screen.support.model.SupportInfo @@ -24,30 +25,37 @@ import co.electriccoin.zcash.ui.util.EmailUtil import kotlinx.coroutines.launch @Composable -internal fun MainActivity.WrapSupport(goBack: () -> Unit) { - val supportViewModel by viewModels() +internal fun WrapSupport(goBack: () -> Unit) { + val activity = LocalActivity.current - val walletViewModel by viewModels() + val supportViewModel by activity.viewModels() + + val walletViewModel by activity.viewModels() val supportInfo = supportViewModel.supportInfo.collectAsStateWithLifecycle().value val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value + BackHandler { + goBack() + } + WrapSupport( - activity = this, goBack = goBack, supportInfo = supportInfo, topAppBarSubTitleState = walletState ) } +@VisibleForTesting @Composable internal fun WrapSupport( - activity: ComponentActivity, goBack: () -> Unit, supportInfo: SupportInfo?, topAppBarSubTitleState: TopAppBarSubTitleState, ) { + val activity = LocalActivity.current + val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() 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 3761033c..e2b5eb96 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 @@ -1,7 +1,6 @@ package co.electriccoin.zcash.ui.screen.update import android.content.Context -import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.viewModels import androidx.annotation.VisibleForTesting @@ -11,8 +10,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.lifecycle.compose.collectAsStateWithLifecycle -import co.electriccoin.zcash.ui.MainActivity import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.compose.LocalActivity import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel import co.electriccoin.zcash.ui.screen.update.model.UpdateInfo import co.electriccoin.zcash.ui.screen.update.model.UpdateState @@ -23,12 +22,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @Composable -internal fun MainActivity.WrapCheckForUpdate() { - WrapCheckForUpdate(this) -} +internal fun WrapCheckForUpdate() { + val activity = LocalActivity.current -@Composable -private fun WrapCheckForUpdate(activity: ComponentActivity) { // TODO [#403]: Manual testing of already implemented in-app update mechanisms // TODO [#403]: https://github.com/Electric-Coin-Company/zashi-android/issues/403 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @@ -43,7 +39,7 @@ private fun WrapCheckForUpdate(activity: ComponentActivity) { updateInfo?.let { if (it.appUpdateInfo != null && it.state == UpdateState.Prepared) { - WrapUpdate(activity, updateInfo) + WrapUpdate(updateInfo) } } @@ -56,10 +52,9 @@ private fun WrapCheckForUpdate(activity: ComponentActivity) { @VisibleForTesting @Composable -internal fun WrapUpdate( - activity: ComponentActivity, - inputUpdateInfo: UpdateInfo -) { +internal fun WrapUpdate(inputUpdateInfo: UpdateInfo) { + val activity = LocalActivity.current + val viewModel by activity.viewModels { UpdateViewModel.UpdateViewModelFactory( activity.application, @@ -78,10 +73,12 @@ internal fun WrapUpdate( // just return as we are already in Home compose return } + UpdateState.Failed -> { // we need to refresh AppUpdateInfo object, as it can be used only once viewModel.checkForAppUpdate() } + UpdateState.Prepared, UpdateState.Running -> { // valid stages }