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
|
package com.aurora.store.compose.ui.details
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
@@ -135,6 +136,8 @@ fun AppDetailsScreen(
|
|||||||
app = loadedApp,
|
app = loadedApp,
|
||||||
error = err.error,
|
error = err.error,
|
||||||
extra = err.extra,
|
extra = err.extra,
|
||||||
|
isAnonymous = viewModel.authProvider.isAnonymous,
|
||||||
|
onBuy = { openPlayStore(context, loadedApp.packageName) },
|
||||||
onDismiss = viewModel::dismissInstallError
|
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 {
|
private fun stateKey(state: AppState, app: App?): String = when {
|
||||||
state is AppState.Error -> "error"
|
state is AppState.Error -> "error"
|
||||||
state is AppState.Loading || app == null -> "loading"
|
state is AppState.Loading || app == null -> "loading"
|
||||||
@@ -261,6 +279,11 @@ private fun ScreenContentApp(
|
|||||||
onForceRestart: () -> Unit = {}
|
onForceRestart: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
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)
|
var scaffoldDirective = calculatePaneScaffoldDirective(windowAdaptiveInfo)
|
||||||
|
|
||||||
if (forceSinglePane) {
|
if (forceSinglePane) {
|
||||||
@@ -317,7 +340,11 @@ private fun ScreenContentApp(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SetupMenu() {
|
fun SetupMenu() {
|
||||||
AppDetailsMenu(isFavorite = isFavorite, state = state) { menuItem ->
|
AppDetailsMenu(
|
||||||
|
isFavorite = isFavorite,
|
||||||
|
state = state,
|
||||||
|
canManualDownload = canAcquire
|
||||||
|
) { menuItem ->
|
||||||
when (menuItem) {
|
when (menuItem) {
|
||||||
MenuItem.FAVORITE -> onFavorite()
|
MenuItem.FAVORITE -> onFavorite()
|
||||||
|
|
||||||
@@ -333,20 +360,7 @@ private fun ScreenContentApp(
|
|||||||
ShortcutManagerUtil.requestPinShortcut(context, app.packageName)
|
ShortcutManagerUtil.requestPinShortcut(context, app.packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem.PLAY_STORE -> {
|
MenuItem.PLAY_STORE -> openPlayStore(context, app.packageName)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,6 +427,8 @@ private fun ScreenContentApp(
|
|||||||
Actions(
|
Actions(
|
||||||
primaryActionDisplayName = primaryActionName,
|
primaryActionDisplayName = primaryActionName,
|
||||||
secondaryActionDisplayName = stringResource(R.string.title_manual_download),
|
secondaryActionDisplayName = stringResource(R.string.title_manual_download),
|
||||||
|
isPrimaryActionEnabled = canAcquire,
|
||||||
|
isSecondaryActionEnabled = canAcquire,
|
||||||
onPrimaryAction = ::onInstall,
|
onPrimaryAction = ::onInstall,
|
||||||
onSecondaryAction = { showExtraPane(ExtraScreen.ManualDownload) }
|
onSecondaryAction = { showExtraPane(ExtraScreen.ManualDownload) }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ fun AppDetailsMenu(
|
|||||||
state: AppState = AppState.Unavailable,
|
state: AppState = AppState.Unavailable,
|
||||||
isFavorite: Boolean = false,
|
isFavorite: Boolean = false,
|
||||||
isExpanded: Boolean = false,
|
isExpanded: Boolean = false,
|
||||||
|
canManualDownload: Boolean = true,
|
||||||
onMenuItemClicked: (menuItem: MenuItem) -> Unit = {}
|
onMenuItemClicked: (menuItem: MenuItem) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -80,7 +81,7 @@ fun AppDetailsMenu(
|
|||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(text = stringResource(R.string.title_manual_download)) },
|
text = { Text(text = stringResource(R.string.title_manual_download)) },
|
||||||
onClick = { onClick(MenuItem.MANUAL_DOWNLOAD) },
|
onClick = { onClick(MenuItem.MANUAL_DOWNLOAD) },
|
||||||
enabled = !state.inProgress()
|
enabled = canManualDownload && !state.inProgress()
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(text = stringResource(R.string.action_info)) },
|
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.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -41,9 +42,20 @@ import com.aurora.store.R
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@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
|
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(
|
ModalBottomSheet(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
@@ -53,7 +65,7 @@ fun InstallErrorSheet(app: App, error: String?, extra: String?, onDismiss: () ->
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
Header(app = app)
|
Header(app = app, showBuy = canBuy, onBuy = onBuy)
|
||||||
|
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
@@ -101,7 +113,7 @@ fun InstallErrorSheet(app: App, error: String?, extra: String?, onDismiss: () ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Header(app: App) {
|
private fun Header(app: App, showBuy: Boolean = false, onBuy: () -> Unit = {}) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -137,5 +149,14 @@ private fun Header(app: App) {
|
|||||||
overflow = TextOverflow.Ellipsis
|
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_back">"Back"</string>
|
||||||
<string name="action_blacklist">"Blacklist"</string>
|
<string name="action_blacklist">"Blacklist"</string>
|
||||||
<string name="action_blacklist_add">"Add to 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_cancel">"Cancel"</string>
|
||||||
<string name="action_clear">"Clear"</string>
|
<string name="action_clear">"Clear"</string>
|
||||||
<string name="action_close">"Close"</string>
|
<string name="action_close">"Close"</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user