Add buy flow for paid apps in app details
Resolves #1499 Surface a 'Buy @ price' action on the install error sheet for paid apps on signed-in accounts, opening the Play Store listing to purchase. For anonymous accounts, show the price on a disabled install button and disable manual downloads since paid apps can't be acquired.
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
package com.aurora.store.compose.ui.details
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -135,6 +136,8 @@ fun AppDetailsScreen(
|
||||
app = loadedApp,
|
||||
error = err.error,
|
||||
extra = err.extra,
|
||||
isAnonymous = viewModel.authProvider.isAnonymous,
|
||||
onBuy = { openPlayStore(context, loadedApp.packageName) },
|
||||
onDismiss = viewModel::dismissInstallError
|
||||
)
|
||||
}
|
||||
@@ -198,6 +201,21 @@ fun AppDetailsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the given app's Play Store listing, preferring the Play Store app when available and
|
||||
* falling back to whichever activity can handle the web listing (e.g. a browser).
|
||||
*/
|
||||
private fun openPlayStore(context: Context, packageName: String) {
|
||||
val uri = "${Constants.SHARE_URL}$packageName".toUri()
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply { data = uri }
|
||||
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(intent.apply { setPackage(Constants.PACKAGE_NAME_PLAY_STORE) })
|
||||
} else {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stateKey(state: AppState, app: App?): String = when {
|
||||
state is AppState.Error -> "error"
|
||||
state is AppState.Loading || app == null -> "loading"
|
||||
@@ -261,6 +279,11 @@ private fun ScreenContentApp(
|
||||
onForceRestart: () -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// Anonymous accounts can't purchase, so a paid app can neither be installed nor manually
|
||||
// downloaded (any version) by them. Free apps are always acquirable.
|
||||
val canAcquire = app.isFree || !isAnonymous
|
||||
|
||||
var scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo)
|
||||
|
||||
if (forceSinglePane) {
|
||||
@@ -317,7 +340,11 @@ private fun ScreenContentApp(
|
||||
|
||||
@Composable
|
||||
fun SetupMenu() {
|
||||
AppDetailsMenu(isFavorite = isFavorite, state = state) { menuItem ->
|
||||
AppDetailsMenu(
|
||||
isFavorite = isFavorite,
|
||||
state = state,
|
||||
canManualDownload = canAcquire
|
||||
) { menuItem ->
|
||||
when (menuItem) {
|
||||
MenuItem.FAVORITE -> onFavorite()
|
||||
|
||||
@@ -333,20 +360,7 @@ private fun ScreenContentApp(
|
||||
ShortcutManagerUtil.requestPinShortcut(context, app.packageName)
|
||||
}
|
||||
|
||||
MenuItem.PLAY_STORE -> {
|
||||
val uri = "${Constants.SHARE_URL}${app.packageName}".toUri()
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply { data = uri }
|
||||
|
||||
if (intent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(
|
||||
intent.apply {
|
||||
setPackage(Constants.PACKAGE_NAME_PLAY_STORE)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
MenuItem.PLAY_STORE -> openPlayStore(context, app.packageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,6 +427,8 @@ private fun ScreenContentApp(
|
||||
Actions(
|
||||
primaryActionDisplayName = primaryActionName,
|
||||
secondaryActionDisplayName = stringResource(R.string.title_manual_download),
|
||||
isPrimaryActionEnabled = canAcquire,
|
||||
isSecondaryActionEnabled = canAcquire,
|
||||
onPrimaryAction = ::onInstall,
|
||||
onSecondaryAction = { showExtraPane(ExtraScreen.ManualDownload) }
|
||||
)
|
||||
|
||||
@@ -42,6 +42,7 @@ fun AppDetailsMenu(
|
||||
state: AppState = AppState.Unavailable,
|
||||
isFavorite: Boolean = false,
|
||||
isExpanded: Boolean = false,
|
||||
canManualDownload: Boolean = true,
|
||||
onMenuItemClicked: (menuItem: MenuItem) -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -80,7 +81,7 @@ fun AppDetailsMenu(
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.title_manual_download)) },
|
||||
onClick = { onClick(MenuItem.MANUAL_DOWNLOAD) },
|
||||
enabled = !state.inProgress()
|
||||
enabled = canManualDownload && !state.inProgress()
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(text = stringResource(R.string.action_info)) },
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -41,9 +42,20 @@ import com.aurora.store.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun InstallErrorSheet(app: App, error: String?, extra: String?, onDismiss: () -> Unit) {
|
||||
fun InstallErrorSheet(
|
||||
app: App,
|
||||
error: String?,
|
||||
extra: String?,
|
||||
isAnonymous: Boolean = true,
|
||||
onBuy: () -> Unit = {},
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// A paid app that failed to install for a signed-in (non-anonymous) account is almost
|
||||
// always one that hasn't been purchased yet; offer a shortcut to buy it on the Play Store.
|
||||
val canBuy = !app.isFree && !isAnonymous
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
@@ -53,7 +65,7 @@ fun InstallErrorSheet(app: App, error: String?, extra: String?, onDismiss: () ->
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Header(app = app)
|
||||
Header(app = app, showBuy = canBuy, onBuy = onBuy)
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(
|
||||
@@ -101,7 +113,7 @@ fun InstallErrorSheet(app: App, error: String?, extra: String?, onDismiss: () ->
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header(app: App) {
|
||||
private fun Header(app: App, showBuy: Boolean = false, onBuy: () -> Unit = {}) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -137,5 +149,14 @@ private fun Header(app: App) {
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
if (showBuy) {
|
||||
Button(onClick = onBuy) {
|
||||
Text(
|
||||
text = stringResource(R.string.action_buy, app.price),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
<string name="action_back">"Back"</string>
|
||||
<string name="action_blacklist">"Blacklist"</string>
|
||||
<string name="action_blacklist_add">"Add to blacklist"</string>
|
||||
<string name="action_buy">"Buy @ %1$s"</string>
|
||||
<string name="action_cancel">"Cancel"</string>
|
||||
<string name="action_clear">"Clear"</string>
|
||||
<string name="action_close">"Close"</string>
|
||||
|
||||
Reference in New Issue
Block a user