Skip to content

Commit

Permalink
(android) Rework the send screen to match iOS (#643)
Browse files Browse the repository at this point in the history
Instead of immediately display a full screen scanner, the Send
screen first display a smart text input (with completion for
domain names), buttons to paste, read an image, or scan a QR code,
as well as a list of contacts.

The android app also now use the new SendManager to parse payment 
data. The shared MVI ScanController has been removed.

---------

Co-authored-by: Robbie Hanson <304604+robbiehanson@users.noreply.github.com>
  • Loading branch information
dpad85 and robbiehanson authored Nov 8, 2024
1 parent 3f9d14f commit 9894ae9
Show file tree
Hide file tree
Showing 95 changed files with 2,005 additions and 1,919 deletions.
89 changes: 36 additions & 53 deletions phoenix-android/src/main/kotlin/fr/acinq/phoenix/android/AppView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ import fr.acinq.phoenix.android.init.CreateWalletView
import fr.acinq.phoenix.android.init.InitWallet
import fr.acinq.phoenix.android.init.RestoreWalletView
import fr.acinq.phoenix.android.intro.IntroView
import fr.acinq.phoenix.android.payments.ScanDataView
import fr.acinq.phoenix.android.payments.details.PaymentDetailsView
import fr.acinq.phoenix.android.payments.history.CsvExportView
import fr.acinq.phoenix.android.payments.history.PaymentsHistoryView
import fr.acinq.phoenix.android.payments.liquidity.RequestLiquidityView
import fr.acinq.phoenix.android.payments.receive.ReceiveView
import fr.acinq.phoenix.android.payments.send.SendView
import fr.acinq.phoenix.android.services.NodeServiceState
import fr.acinq.phoenix.android.settings.AboutView
import fr.acinq.phoenix.android.settings.AppAccessSettings
Expand Down Expand Up @@ -109,8 +109,8 @@ import fr.acinq.phoenix.android.startup.LegacySwitcherView
import fr.acinq.phoenix.android.startup.StartupView
import fr.acinq.phoenix.android.utils.SystemNotificationHelper
import fr.acinq.phoenix.android.utils.appBackground
import fr.acinq.phoenix.android.utils.extensions.findActivitySafe
import fr.acinq.phoenix.android.utils.logger
import fr.acinq.phoenix.android.utils.safeFindActivity
import fr.acinq.phoenix.data.BitcoinUnit
import fr.acinq.phoenix.data.FiatCurrency
import fr.acinq.phoenix.data.WalletPaymentId
Expand Down Expand Up @@ -210,7 +210,7 @@ fun AppView(
val next = nextScreenLink?.takeUnless { it.isBlank() }?.let { Uri.parse(it) }
if (next == null || !navController.graph.hasDeepLink(next)) {
log.debug("redirecting from startup to home")
popToHome(navController)
navController.popToHome()
} else {
log.debug("redirecting from startup to {}", next)
navController.navigate(next, navOptions = navOptions {
Expand Down Expand Up @@ -243,29 +243,30 @@ fun AppView(
onPaymentClick = { navigateToPaymentDetails(navController, id = it, isFromEvent = false) },
onSettingsClick = { navController.navigate(Screen.Settings.route) },
onReceiveClick = { navController.navigate(Screen.Receive.route) },
onSendClick = { navController.navigate(Screen.ScanData.route) { launchSingleTop = true } },
onSendClick = { navController.navigate(Screen.Send.route) },
onPaymentsHistoryClick = { navController.navigate(Screen.PaymentsHistory.route) },
onTorClick = { navController.navigate(Screen.TorConfig) },
onElectrumClick = { navController.navigate(Screen.ElectrumServer) },
onNavigateToSwapInWallet = { navController.navigate(Screen.WalletInfo.SwapInWallet) },
onNavigateToFinalWallet = { navController.navigate(Screen.WalletInfo.FinalWallet) },
onShowNotifications = { navController.navigate(Screen.Notifications) },
onTorClick = { navController.navigate(Screen.TorConfig.route) },
onElectrumClick = { navController.navigate(Screen.ElectrumServer.route) },
onNavigateToSwapInWallet = { navController.navigate(Screen.WalletInfo.SwapInWallet.route) },
onNavigateToFinalWallet = { navController.navigate(Screen.WalletInfo.FinalWallet.route) },
onShowNotifications = { navController.navigate(Screen.Notifications.route) },
onRequestLiquidityClick = { navController.navigate(Screen.LiquidityRequest.route) },
)
}
}
composable(Screen.Receive.route) {
ReceiveView(
onSwapInReceived = { popToHome(navController) },
onSwapInReceived = { navController.popToHome() },
onBackClick = { navController.popBackStack() },
onScanDataClick = { navController.navigate(Screen.ScanData.route) },
onScanDataClick = { navController.navigate(Screen.Send.route) },
onFeeManagementClick = { navController.navigate(Screen.LiquidityPolicy.route) },
)
}
composable(
route = "${Screen.ScanData.route}?input={input}",
route = "${Screen.Send}?input={input}&openScanner={openScanner}",
arguments = listOf(
navArgument("input") { type = NavType.StringType ; nullable = true },
navArgument("openScanner") { type = NavType.BoolType ; defaultValue = false }
),
deepLinks = listOf(
navDeepLink { uriPattern = "lightning:{data}" },
Expand All @@ -279,30 +280,26 @@ fun AppView(
navDeepLink { uriPattern = "scanview:{data}" },
)
) {
log.info("input arg=${it.arguments?.getString("input")}")
val intent = try {
it.arguments?.getParcelable<Intent>(NavController.KEY_DEEP_LINK_INTENT)
} catch (e: Exception) {
null
}
// prevents forwarding an internal deeplink intent coming from androidx-navigation framework.
// TODO properly parse deeplinks following f0ae90444a23cc17d6d7407dfe43c0c8d20e62fc
val isIntentFromNavigation = intent?.dataString?.contains("androidx.navigation") ?: true
log.debug("isIntentFromNavigation=$isIntentFromNavigation")
val input = if (isIntentFromNavigation) {
it.arguments?.getString("input")
} else {
intent?.data?.toString()?.substringAfter("scanview:")
}
RequireStarted(walletState, nextUri = "scanview:${intent?.data?.toString()}") {
val input = intent?.data?.toString()?.substringAfter("scanview:")?.takeIf {
// prevents forwarding an internal deeplink intent coming from androidx-navigation framework.
// TODO properly parse deeplinks following f0ae90444a23cc17d6d7407dfe43c0c8d20e62fc
!it.contains("androidx.navigation")
} ?: it.arguments?.getString("input")
ScanDataView(
input = input,
onBackClick = {
if (navController.previousBackStackEntry != null) {
navController.popBackStack()
} else {
popToHome(navController)
}
},
onAuthSchemeInfoClick = { navController.navigate("${Screen.PaymentSettings.route}/true") },
onFeeManagementClick = { navController.navigate(Screen.LiquidityPolicy.route) },
onProcessingFinished = { popToHome(navController) },
log.info("navigating to send-payment with input=$input")
SendView(
initialInput = input,
fromDeepLink = !isIntentFromNavigation,
immediatelyOpenScanner = it.arguments?.getBoolean("openScanner") ?: false
)
}
}
Expand Down Expand Up @@ -330,12 +327,12 @@ fun AppView(
paymentId = paymentId,
onBackClick = {
val previousNav = navController.previousBackStackEntry
if (fromEvent && previousNav?.destination?.route == Screen.ScanData.route) {
popToHome(navController)
if (fromEvent && previousNav?.destination?.route == Screen.Send.route) {
navController.popToHome()
} else if (navController.previousBackStackEntry != null) {
navController.popBackStack()
} else {
popToHome(navController)
navController.popToHome()
}
},
fromEvent = fromEvent
Expand All @@ -348,7 +345,7 @@ fun AppView(
onBackClick = { navController.popBackStack() },
paymentsViewModel = paymentsViewModel,
onPaymentClick = { navigateToPaymentDetails(navController, id = it, isFromEvent = false) },
onCsvExportClick = { navController.navigate(Screen.PaymentsCsvExport) },
onCsvExportClick = { navController.navigate(Screen.PaymentsCsvExport.route) },
)
}
composable(Screen.PaymentsCsvExport.route) {
Expand All @@ -373,12 +370,12 @@ fun AppView(
composable(Screen.Channels.route) {
ChannelsView(
onBackClick = {
navController.navigate(Screen.Settings) {
navController.navigate(Screen.Settings.route) {
popUpTo(Screen.Settings.route) { inclusive = true }
}
},
onChannelClick = { navController.navigate("${Screen.ChannelDetails.route}?id=$it") },
onImportChannelsDataClick = { navController.navigate(Screen.ImportChannelsData)},
onImportChannelsDataClick = { navController.navigate(Screen.ImportChannelsData.route)},
)
}
composable(
Expand All @@ -403,18 +400,11 @@ fun AppView(
composable(Screen.About.route) {
AboutView()
}
composable(Screen.PaymentSettings.route) {
PaymentSettingsView(
initialShowLnurlAuthSchemeDialog = false,
)
}
composable("${Screen.PaymentSettings.route}/{showAuthSchemeDialog}", arguments = listOf(
composable("${Screen.PaymentSettings.route}?showAuthSchemeDialog={showAuthSchemeDialog}", arguments = listOf(
navArgument("showAuthSchemeDialog") { type = NavType.BoolType }
)) {
val showAuthSchemeDialog = it.arguments?.getBoolean("showAuthSchemeDialog") ?: false
PaymentSettingsView(
initialShowLnurlAuthSchemeDialog = showAuthSchemeDialog,
)
PaymentSettingsView(initialShowLnurlAuthSchemeDialog = showAuthSchemeDialog)
}
composable(Screen.AppLock.route) {
AppAccessSettings(onBackClick = { navController.popBackStack() }, appViewModel = appVM)
Expand Down Expand Up @@ -521,7 +511,7 @@ fun AppView(
if ((isBiometricLockEnabled == true || isCustomPinLockEnabled == true) && isScreenLocked) {
BackHandler {
// back button minimises the app
context.safeFindActivity()?.moveTaskToBack(false)
context.findActivitySafe()?.moveTaskToBack(false)
}
LockPrompt(
promptScreenLockImmediately = appVM.promptScreenLockImmediately.value,
Expand Down Expand Up @@ -563,13 +553,6 @@ fun AppView(
}
}

/** Navigates to Home and pops everything from the backstack up to Home. This effectively resets the nav stack. */
private fun popToHome(navController: NavHostController) {
navController.navigate(Screen.Home.route) {
popUpTo(navController.graph.id) { inclusive = true }
}
}

fun navigateToPaymentDetails(navController: NavController, id: WalletPaymentId, isFromEvent: Boolean) {
try {
navController.navigate("${Screen.PaymentDetails.route}?direction=${id.dbType.value}&id=${id.dbId}&fromEvent=${isFromEvent}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
package fr.acinq.phoenix.android

import androidx.navigation.NavController
import androidx.navigation.NavOptionsBuilder
import org.slf4j.LoggerFactory


sealed class Screen(val route: String) {
data object SwitchToLegacy : Screen("switchtolegacy")
Expand All @@ -30,11 +27,7 @@ sealed class Screen(val route: String) {
data object Startup : Screen("startup")
data object Home : Screen("home")
data object Receive : Screen("receive")
/**
* This route also manages the payment flow.
* TODO: Separate scanning the data from processing the data (aka send payment, process lnurl...). Split to be done at the controller level.
*/
data object ScanData : Screen("readdata")
data object Send : Screen("send")
data object PaymentDetails : Screen("payments")
data object PaymentsHistory : Screen("payments/all")
data object PaymentsCsvExport : Screen("payments/export")
Expand Down Expand Up @@ -71,18 +64,10 @@ sealed class Screen(val route: String) {
data object Experimental: Screen("settings/experimental")
}

fun NavController.navigate(screen: Screen, arg: List<Any> = emptyList(), builder: NavOptionsBuilder.() -> Unit = {}) {
val log = LoggerFactory.getLogger("NavController")
val path = arg.joinToString{ "/$it" }
val route = "${screen.route}$path"
log.debug("navigating from ${currentDestination?.route} to $route")
try {
if (route == currentDestination?.route) {
log.warn("cannot navigate to same route")
} else {
navigate(route, builder)
}
} catch (e: Exception) {
log.error("failed to navigate to $route: " , e)
/** Navigates to Home and pops everything from the backstack up to Home. This effectively resets the nav stack. */
fun NavController.popToHome() {
val navController = this
navigate(Screen.Home.route) {
popUpTo(navController.graph.id) { inclusive = true }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ class NoticesViewModel(
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
log.info("power_saver=${powerManager.isPowerSaveMode}")
isPowerSaverModeOn = powerManager.isPowerSaveMode
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,12 +251,12 @@ fun AmountInput(
style = when {
errorMessage.isNotBlank() -> MaterialTheme.typography.body2.copy(color = negativeColor, fontSize = 14.sp)
isFocused -> MaterialTheme.typography.body2.copy(color = MaterialTheme.colors.primary, fontSize = 14.sp)
else -> MaterialTheme.typography.body1.copy(fontSize = 14.sp)
else -> MaterialTheme.typography.body2.copy(fontSize = 14.sp)
},
modifier = Modifier
.align(Alignment.TopStart)
.padding(start = 8.dp)
.clip(RoundedCornerShape(8.dp))
.clip(RoundedCornerShape(2.dp))
.background(MaterialTheme.colors.surface)
.padding(horizontal = 8.dp, vertical = 2.dp)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,8 @@ fun Clickable(
shape: Shape = RectangleShape,
clickDescription: String = "",
internalPadding: PaddingValues = PaddingValues(0.dp),
indication: Indication? = LocalIndication.current,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable () -> Unit,
) {
val colors = ButtonDefaults.buttonColors(
Expand All @@ -370,6 +372,8 @@ fun Clickable(
enabled = enabled,
role = Role.Button,
onClickLabel = clickDescription,
interactionSource = interactionSource,
indication = indication
)
.padding(internalPadding)
) {
Expand Down
Loading

0 comments on commit 9894ae9

Please sign in to comment.