zcash-android-wallet-sdk/demo-app/src/main/java/cash/z/ecc/android/sdk/demoapp/Navigation.kt

201 lines
7.6 KiB
Kotlin

package cash.z.ecc.android.sdk.demoapp
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.activity.viewModels
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.demoapp.NavigationTargets.BALANCE
import cash.z.ecc.android.sdk.demoapp.NavigationTargets.HOME
import cash.z.ecc.android.sdk.demoapp.NavigationTargets.SEND
import cash.z.ecc.android.sdk.demoapp.NavigationTargets.WALLET_ADDRESS_DETAILS
import cash.z.ecc.android.sdk.demoapp.ui.screen.addresses.view.Addresses
import cash.z.ecc.android.sdk.demoapp.ui.screen.balance.view.Balance
import cash.z.ecc.android.sdk.demoapp.ui.screen.home.view.Home
import cash.z.ecc.android.sdk.demoapp.ui.screen.home.viewmodel.WalletViewModel
import cash.z.ecc.android.sdk.demoapp.ui.screen.send.view.Send
import cash.z.ecc.android.sdk.demoapp.util.AndroidApiVersion
import cash.z.ecc.android.sdk.demoapp.util.fromResources
import cash.z.ecc.android.sdk.model.ZcashNetwork
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Composable
@Suppress("LongMethod")
internal fun ComposeActivity.Navigation() {
val navController = rememberNavController()
val walletViewModel by viewModels<WalletViewModel>()
NavHost(navController = navController, startDestination = HOME) {
composable(HOME) {
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
if (null == walletSnapshot) {
// Display loading indicator
} else {
Home(
walletSnapshot,
goBalance = { navController.navigateJustOnce(BALANCE) },
goSend = { navController.navigateJustOnce(SEND) },
goAddressDetails = { navController.navigateJustOnce(WALLET_ADDRESS_DETAILS) },
isTestnet = ZcashNetwork.fromResources(applicationContext) == ZcashNetwork.Testnet,
goTestnetFaucet = {
runCatching {
startActivity(newBrowserIntent("https://faucet.zecpages.com/")) // NON-NLS
}.onFailure {
// This could fail on devices without a browser.
// An improvement here in the future would be showing a snackbar or error dialog.
}
},
resetSdk = { walletViewModel.resetSdk() }
)
}
}
composable(BALANCE) {
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
if (null == walletSnapshot) {
// Display loading indicator
} else {
Balance(
walletSnapshot,
onShieldFunds = { walletViewModel.shieldFunds() },
onBack = { navController.popBackStackJustOnce(BALANCE) },
)
}
}
composable(WALLET_ADDRESS_DETAILS) {
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
if (null == synchronizer) {
// Display loading indicator
} else {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
// I don't like giving synchronizer directly over to the view, but for now it isolates each of the
// demo app views
Addresses(
synchronizer = synchronizer,
copyToClipboard = { tag, textToCopy ->
copyToClipboard(
applicationContext,
tag,
textToCopy,
scope,
snackbarHostState
)
},
onBack = { navController.popBackStackJustOnce(WALLET_ADDRESS_DETAILS) },
snackbarHostState = snackbarHostState
)
}
}
composable(SEND) {
val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value
val walletSnapshot = walletViewModel.walletSnapshot.collectAsStateWithLifecycle().value
val spendingKey = walletViewModel.spendingKey.collectAsStateWithLifecycle().value
if (null == synchronizer || null == walletSnapshot || null == spendingKey) {
// Display loading indicator
} else {
Send(
walletSnapshot = walletSnapshot,
onSend = {
// In the future, consider observing the flow and providing UI updates
walletViewModel.send(it)
navController.popBackStackJustOnce(SEND)
},
onBack = { navController.popBackStackJustOnce(SEND) }
)
}
}
}
}
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()
}
private fun copyToClipboard(
context: Context,
tag: String,
textToCopy: String,
scope: CoroutineScope,
snackbarHostState: SnackbarHostState
) {
val clipboardManager = if (AndroidApiVersion.isAtLeastM) {
context.getSystemService(ClipboardManager::class.java)
} else {
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
}
val data = ClipData.newPlainText(
tag,
textToCopy
)
clipboardManager.setPrimaryClip(data)
// Notify users with Snackbar only on Android level 32 and lower, as 33 and higher notifies users by its own system
// way
if (!AndroidApiVersion.isAtLeastT) {
scope.launch {
snackbarHostState.showSnackbar(
message = context.getString(R.string.address_copied, textToCopy),
duration = SnackbarDuration.Short
)
}
}
}
private fun newBrowserIntent(url: String): Intent {
val uri = Uri.parse(url)
val intent = Intent(Intent.ACTION_VIEW, uri).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
return intent
}
object NavigationTargets {
const val HOME = "home" // NON-NLS
const val BALANCE = "balance" // NON-NLS
const val WALLET_ADDRESS_DETAILS = "wallet_address_details" // NON-NLS
const val SEND = "send" // NON-NLS
}