feat(upgrade): Replace upgrade dialog with full-screen upgrade screen

Add dedicated Compose upgrade screens for both FOSS and gplay flavors, presenting pro benefits before the purchase/sponsor action. Migrate all upgrade dialog callers to navigate to the new screen instead.
This commit is contained in:
darken
2026-03-03 20:35:32 +00:00
committed by Matthias Urhahn
parent e80bf1db6c
commit abb189f43c
24 changed files with 725 additions and 173 deletions
@@ -1,10 +1,5 @@
package eu.darken.capod.common.upgrade.core
import android.app.Activity
import android.widget.Toast
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.darken.capod.R
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.upgrade.UpgradeRepo
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@@ -15,7 +10,6 @@ import javax.inject.Singleton
@Singleton
class UpgradeControlFoss @Inject constructor(
private val fossCache: FossCache,
private val webpageTool: WebpageTool,
) : UpgradeRepo {
override val upgradeInfo: Flow<UpgradeRepo.Info> = fossCache.upgrade.flow.map { data ->
@@ -30,34 +24,11 @@ class UpgradeControlFoss @Inject constructor(
}
}
override fun launchBillingFlow(activity: Activity) {
MaterialAlertDialogBuilder(activity).apply {
setIcon(R.drawable.ic_heart)
setTitle(R.string.upgrade_capod_label)
setMessage(R.string.upgrade_capod_description)
setPositiveButton(R.string.foss_upgrade_donate_label) { _, _ ->
fossCache.upgrade.value = FossUpgrade(
upgradedAt = Instant.now(),
reason = FossUpgrade.Reason.DONATED
)
webpageTool.open("https://github.com/sponsors/d4rken")
Toast.makeText(activity, R.string.general_thank_you_label, Toast.LENGTH_SHORT).show()
}
setNegativeButton(R.string.foss_upgrade_alreadydonated_label) { _, _ ->
fossCache.upgrade.value = FossUpgrade(
upgradedAt = Instant.now(),
reason = FossUpgrade.Reason.ALREADY_DONATED
)
Toast.makeText(activity, R.string.general_thank_you_label, Toast.LENGTH_SHORT).show()
}
setNeutralButton(R.string.foss_upgrade_no_money_label) { _, _ ->
fossCache.upgrade.value = FossUpgrade(
upgradedAt = Instant.now(),
reason = FossUpgrade.Reason.NO_MONEY
)
Toast.makeText(activity, "¯\\_(ツ)_/¯", Toast.LENGTH_SHORT).show()
}
}.show()
fun upgrade(reason: FossUpgrade.Reason) {
fossCache.upgrade.value = FossUpgrade(
upgradedAt = Instant.now(),
reason = reason
)
}
data class Info(
@@ -0,0 +1,26 @@
package eu.darken.capod.upgrade.ui
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.NavKey
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import eu.darken.capod.common.navigation.Nav
import eu.darken.capod.common.navigation.NavigationEntry
import javax.inject.Inject
class UpgradeNavigation @Inject constructor() : NavigationEntry {
override fun EntryProviderScope<NavKey>.setup() {
entry<Nav.Main.Upgrade> { UpgradeScreenHost() }
}
@Module
@InstallIn(SingletonComponent::class)
abstract class Mod {
@Binds
@IntoSet
abstract fun bind(entry: UpgradeNavigation): NavigationEntry
}
}
@@ -0,0 +1,221 @@
package eu.darken.capod.upgrade.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.twotone.ArrowBack
import androidx.compose.material.icons.automirrored.twotone.Message
import androidx.compose.material.icons.twotone.BluetoothConnected
import androidx.compose.material.icons.twotone.Favorite
import androidx.compose.material.icons.twotone.Palette
import androidx.compose.material.icons.twotone.PlayCircle
import androidx.compose.material.icons.twotone.Widgets
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import eu.darken.capod.R
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler
@Composable
fun UpgradeScreenHost(vm: UpgradeViewModel = hiltViewModel()) {
ErrorEventHandler(vm)
NavigationEventHandler(vm)
UpgradeScreen(
onNavigateUp = { vm.navUp() },
onSponsor = { vm.sponsor() },
)
}
private data class Benefit(val icon: ImageVector, val textRes: Int)
@Composable
fun UpgradeScreen(
onNavigateUp: () -> Unit,
onSponsor: () -> Unit,
) {
val benefits = listOf(
Benefit(Icons.TwoTone.Palette, R.string.upgrade_benefit_themes),
Benefit(Icons.TwoTone.PlayCircle, R.string.upgrade_benefit_autoplay),
Benefit(Icons.TwoTone.BluetoothConnected, R.string.upgrade_benefit_autoconnect),
Benefit(Icons.AutoMirrored.TwoTone.Message, R.string.upgrade_benefit_popups),
Benefit(Icons.TwoTone.Widgets, R.string.upgrade_benefit_widgets),
Benefit(Icons.TwoTone.Favorite, R.string.upgrade_benefit_support),
)
Box(modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars)) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.height(16.dp))
Box(contentAlignment = Alignment.Center) {
Surface(
modifier = Modifier.size(120.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f),
) {}
Image(
painter = painterResource(R.drawable.splash_graphic2),
contentDescription = null,
modifier = Modifier.size(80.dp),
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = buildAnnotatedString {
append("CAPod ")
withStyle(SpanStyle(color = colorResource(R.color.brand_secondary), fontWeight = FontWeight.Bold)) {
append("FOSS")
}
},
style = MaterialTheme.typography.headlineLarge,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.upgrade_capod_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(24.dp))
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
),
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = stringResource(R.string.upgrade_foss_preamble),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(16.dp),
)
}
Spacer(modifier = Modifier.height(24.dp))
Card(
modifier = Modifier.fillMaxWidth(),
) {
Column(modifier = Modifier.padding(vertical = 4.dp)) {
benefits.forEach { benefit ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Surface(
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.secondaryContainer,
modifier = Modifier.size(28.dp),
) {
Box(contentAlignment = Alignment.Center) {
Icon(
imageVector = benefit.icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary,
modifier = Modifier.size(16.dp),
)
}
}
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(benefit.textRes),
style = MaterialTheme.typography.bodyLarge,
)
}
}
}
}
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = onSponsor,
modifier = Modifier
.fillMaxWidth()
.height(52.dp),
shape = RoundedCornerShape(12.dp),
) {
Icon(
imageVector = Icons.TwoTone.Favorite,
contentDescription = null,
modifier = Modifier.size(20.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.upgrade_foss_sponsor_action),
style = MaterialTheme.typography.titleMedium,
)
}
Spacer(modifier = Modifier.height(24.dp))
}
IconButton(
onClick = onNavigateUp,
modifier = Modifier.padding(4.dp),
) {
Icon(
imageVector = Icons.AutoMirrored.TwoTone.ArrowBack,
contentDescription = null,
)
}
}
}
@Preview2
@Composable
private fun UpgradeScreenPreview() = PreviewWrapper {
UpgradeScreen(
onNavigateUp = {},
onSponsor = {},
)
}
@@ -0,0 +1,23 @@
package eu.darken.capod.upgrade.ui
import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.uix.ViewModel4
import eu.darken.capod.common.upgrade.core.FossUpgrade
import eu.darken.capod.common.upgrade.core.UpgradeControlFoss
import javax.inject.Inject
@HiltViewModel
class UpgradeViewModel @Inject constructor(
dispatcherProvider: DispatcherProvider,
private val upgradeControlFoss: UpgradeControlFoss,
private val webpageTool: WebpageTool,
) : ViewModel4(dispatcherProvider) {
fun sponsor() {
upgradeControlFoss.upgrade(FossUpgrade.Reason.DONATED)
webpageTool.open("https://github.com/sponsors/d4rken")
navUp()
}
}
+2
View File
@@ -3,4 +3,6 @@
<string name="foss_upgrade_donate_label">Donate</string>
<string name="foss_upgrade_alreadydonated_label">I already donated</string>
<string name="foss_upgrade_no_money_label">I spend all my money on AirPods</string>
<string name="upgrade_foss_preamble">CAPod FOSS is free and open source. If you find it useful, consider sponsoring development to help keep the project going.</string>
<string name="upgrade_foss_sponsor_action">Sponsor development</string>
</resources>