diff --git a/app/src/debug/java/eu/darken/capod/screenshots/ScreenshotContent.kt b/app/src/debug/java/eu/darken/capod/screenshots/ScreenshotContent.kt index 01c467ed..fa441129 100644 --- a/app/src/debug/java/eu/darken/capod/screenshots/ScreenshotContent.kt +++ b/app/src/debug/java/eu/darken/capod/screenshots/ScreenshotContent.kt @@ -116,7 +116,7 @@ internal fun AddProfileContent() = PreviewWrapper { @Composable internal fun SettingsIndexContent() = PreviewWrapper { SettingsScreen( - state = SettingsViewModel.State(sponsorUrl = "https://example.com"), + state = SettingsViewModel.State(isPro = true, sponsorUrl = "https://example.com"), onNavigateUp = {}, onGeneralSettings = {}, onDeviceManager = {}, diff --git a/app/src/foss/java/eu/darken/capod/common/upgrade/core/UpgradeControlFoss.kt b/app/src/foss/java/eu/darken/capod/common/upgrade/core/UpgradeControlFoss.kt index d73063fa..b70d98f4 100644 --- a/app/src/foss/java/eu/darken/capod/common/upgrade/core/UpgradeControlFoss.kt +++ b/app/src/foss/java/eu/darken/capod/common/upgrade/core/UpgradeControlFoss.kt @@ -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 = 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( diff --git a/app/src/foss/java/eu/darken/capod/upgrade/ui/UpgradeNavigation.kt b/app/src/foss/java/eu/darken/capod/upgrade/ui/UpgradeNavigation.kt new file mode 100644 index 00000000..b0b460e5 --- /dev/null +++ b/app/src/foss/java/eu/darken/capod/upgrade/ui/UpgradeNavigation.kt @@ -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.setup() { + entry { UpgradeScreenHost() } + } + + @Module + @InstallIn(SingletonComponent::class) + abstract class Mod { + @Binds + @IntoSet + abstract fun bind(entry: UpgradeNavigation): NavigationEntry + } +} diff --git a/app/src/foss/java/eu/darken/capod/upgrade/ui/UpgradeScreen.kt b/app/src/foss/java/eu/darken/capod/upgrade/ui/UpgradeScreen.kt new file mode 100644 index 00000000..21809f91 --- /dev/null +++ b/app/src/foss/java/eu/darken/capod/upgrade/ui/UpgradeScreen.kt @@ -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 = {}, + ) +} diff --git a/app/src/foss/java/eu/darken/capod/upgrade/ui/UpgradeViewModel.kt b/app/src/foss/java/eu/darken/capod/upgrade/ui/UpgradeViewModel.kt new file mode 100644 index 00000000..91d66de3 --- /dev/null +++ b/app/src/foss/java/eu/darken/capod/upgrade/ui/UpgradeViewModel.kt @@ -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() + } +} diff --git a/app/src/foss/res/values/strings.xml b/app/src/foss/res/values/strings.xml index 437c73a2..155a70c2 100644 --- a/app/src/foss/res/values/strings.xml +++ b/app/src/foss/res/values/strings.xml @@ -3,4 +3,6 @@ Donate I already donated I spend all my money on AirPods + CAPod FOSS is free and open source. If you find it useful, consider sponsoring development to help keep the project going. + Sponsor development \ No newline at end of file diff --git a/app/src/gplay/java/eu/darken/capod/common/upgrade/core/UpgradeRepoGplay.kt b/app/src/gplay/java/eu/darken/capod/common/upgrade/core/UpgradeRepoGplay.kt index 9561916d..2e72431a 100644 --- a/app/src/gplay/java/eu/darken/capod/common/upgrade/core/UpgradeRepoGplay.kt +++ b/app/src/gplay/java/eu/darken/capod/common/upgrade/core/UpgradeRepoGplay.kt @@ -1,16 +1,9 @@ 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.coroutine.AppScope -import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE -import eu.darken.capod.common.debug.logging.asLog import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.logTag -import eu.darken.capod.common.error.asErrorDialogBuilder import eu.darken.capod.common.flow.replayingShare import eu.darken.capod.common.upgrade.UpgradeRepo import eu.darken.capod.common.upgrade.core.data.BillingData @@ -21,8 +14,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.retryWhen -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.time.Instant import javax.inject.Inject import javax.inject.Singleton @@ -31,7 +22,6 @@ import kotlin.math.pow @Singleton class UpgradeRepoGplay @Inject constructor( @AppScope private val scope: CoroutineScope, - private val dispatcherProvider: DispatcherProvider, private val billingDataRepo: BillingDataRepo, private val billingCache: BillingCache, ) : UpgradeRepo { @@ -76,49 +66,6 @@ class UpgradeRepoGplay @Inject constructor( } .replayingShare(scope) - 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.general_upgrade_action) { _, _ -> - scope.launch { - try { - billingDataRepo.startIapFlow(activity, CapodSku.PRO_UPGRADE.sku) - } catch (e: Exception) { - log(TAG) { "startIapFlow failed:${e.asLog()}" } - withContext(dispatcherProvider.Main) { - e.asErrorDialogBuilder(activity).show() - } - } - } - } - setNeutralButton(R.string.general_check_action) { dialog, _ -> - log(TAG) { "recheck()" } - scope.launch { - try { - val data = billingDataRepo.getIapData() - log(TAG) { "Recheck successful: $data" } - withContext(dispatcherProvider.Main) { - if (data.purchases.isEmpty()) { - Toast.makeText( - activity, - R.string.upgrades_no_purchases_found_check_account, - Toast.LENGTH_LONG - ).show() - } - } - } catch (e: Exception) { - log(TAG) { "Recheck failed:${e.asLog()}" } - withContext(dispatcherProvider.Main) { - e.asErrorDialogBuilder(activity).show() - } - } - } - } - }.show() - } - data class Info( private val gracePeriod: Boolean = false, private val billingData: BillingData?, diff --git a/app/src/gplay/java/eu/darken/capod/upgrade/ui/UpgradeNavigation.kt b/app/src/gplay/java/eu/darken/capod/upgrade/ui/UpgradeNavigation.kt new file mode 100644 index 00000000..b0b460e5 --- /dev/null +++ b/app/src/gplay/java/eu/darken/capod/upgrade/ui/UpgradeNavigation.kt @@ -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.setup() { + entry { UpgradeScreenHost() } + } + + @Module + @InstallIn(SingletonComponent::class) + abstract class Mod { + @Binds + @IntoSet + abstract fun bind(entry: UpgradeNavigation): NavigationEntry + } +} diff --git a/app/src/gplay/java/eu/darken/capod/upgrade/ui/UpgradeScreen.kt b/app/src/gplay/java/eu/darken/capod/upgrade/ui/UpgradeScreen.kt new file mode 100644 index 00000000..815ff49f --- /dev/null +++ b/app/src/gplay/java/eu/darken/capod/upgrade/ui/UpgradeScreen.kt @@ -0,0 +1,254 @@ +package eu.darken.capod.upgrade.ui + +import android.app.Activity +import android.widget.Toast +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.Stars +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.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +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) + + val context = LocalContext.current + val activity = context as? Activity + + LaunchedEffect(Unit) { + vm.events.collect { event -> + when (event) { + UpgradeViewModel.UpgradeEvent.RestoreFailed -> { + Toast.makeText( + context, + R.string.upgrades_no_purchases_found_check_account, + Toast.LENGTH_LONG + ).show() + } + } + } + } + + UpgradeScreen( + onNavigateUp = { vm.navUp() }, + onUpgrade = { activity?.let { vm.startPurchase(it) } }, + onRestore = { vm.restorePurchase() }, + ) +} + +private data class Benefit(val icon: ImageVector, val textRes: Int) + +@Composable +fun UpgradeScreen( + onNavigateUp: () -> Unit, + onUpgrade: () -> Unit, + onRestore: () -> 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.primaryContainer.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_tertiary), fontWeight = FontWeight.Bold)) { + append("Pro") + } + }, + 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_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.primaryContainer, + modifier = Modifier.size(28.dp), + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + imageVector = benefit.icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + 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 = onUpgrade, + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + shape = RoundedCornerShape(12.dp), + ) { + Icon( + imageVector = Icons.TwoTone.Stars, + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.general_upgrade_action), + style = MaterialTheme.typography.titleMedium, + ) + } + + TextButton( + onClick = onRestore, + modifier = Modifier.padding(top = 4.dp), + ) { + Text(text = stringResource(R.string.upgrade_restore_action)) + } + + 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 = {}, + onUpgrade = {}, + onRestore = {}, + ) +} diff --git a/app/src/gplay/java/eu/darken/capod/upgrade/ui/UpgradeViewModel.kt b/app/src/gplay/java/eu/darken/capod/upgrade/ui/UpgradeViewModel.kt new file mode 100644 index 00000000..56762e5c --- /dev/null +++ b/app/src/gplay/java/eu/darken/capod/upgrade/ui/UpgradeViewModel.kt @@ -0,0 +1,72 @@ +package eu.darken.capod.upgrade.ui + +import android.app.Activity +import dagger.hilt.android.lifecycle.HiltViewModel +import eu.darken.capod.common.coroutine.DispatcherProvider +import eu.darken.capod.common.debug.logging.log +import eu.darken.capod.common.debug.logging.logTag +import eu.darken.capod.common.flow.SingleEventFlow +import eu.darken.capod.common.uix.ViewModel4 +import eu.darken.capod.common.upgrade.core.CapodSku +import eu.darken.capod.common.upgrade.core.UpgradeRepoGplay +import eu.darken.capod.common.upgrade.core.data.BillingDataRepo +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@HiltViewModel +class UpgradeViewModel @Inject constructor( + private val dispatcherProvider: DispatcherProvider, + private val upgradeRepo: UpgradeRepoGplay, + private val billingDataRepo: BillingDataRepo, +) : ViewModel4(dispatcherProvider) { + + sealed interface UpgradeEvent { + data object RestoreFailed : UpgradeEvent + } + + val events = SingleEventFlow() + + init { + upgradeRepo.upgradeInfo + .onEach { info -> + if (info.isPro) { + log(TAG) { "User is now pro, navigating back" } + navUp() + } + } + .launchIn(vmScope) + } + + fun startPurchase(activity: Activity) = launch { + try { + withContext(dispatcherProvider.Main) { + billingDataRepo.startIapFlow(activity, CapodSku.PRO_UPGRADE.sku) + } + } catch (e: Exception) { + log(TAG) { "startIapFlow failed: $e" } + errorEvents.emitBlocking(e) + } + } + + fun restorePurchase() = launch { + try { + val data = billingDataRepo.getIapData() + log(TAG) { "Restore check: $data" } + if (data.purchasedSkus.any { it.sku == CapodSku.PRO_UPGRADE.sku }) { + log(TAG) { "Pro purchase found" } + } else { + log(TAG) { "No pro purchase found" } + events.tryEmit(UpgradeEvent.RestoreFailed) + } + } catch (e: Exception) { + log(TAG) { "Restore failed: $e" } + errorEvents.emitBlocking(e) + } + } + + companion object { + private val TAG = logTag("Upgrade", "VM") + } +} diff --git a/app/src/gplay/res/values/strings.xml b/app/src/gplay/res/values/strings.xml index fab2eaea..9cc72e70 100644 --- a/app/src/gplay/res/values/strings.xml +++ b/app/src/gplay/res/values/strings.xml @@ -6,4 +6,5 @@ There was an error in Google Play. Please try again later or reboot your phone.\n\nError: %s Google Play Billing Error There was an error when asking Google Play for your purchase details. Clear Google Play cache and reboot your phone.\n\nError %s + Restore purchase \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/common/navigation/Nav.kt b/app/src/main/java/eu/darken/capod/common/navigation/Nav.kt index 84b2e509..0b0a3c18 100644 --- a/app/src/main/java/eu/darken/capod/common/navigation/Nav.kt +++ b/app/src/main/java/eu/darken/capod/common/navigation/Nav.kt @@ -18,6 +18,9 @@ object Nav { @Serializable data object TroubleShooter : Main + + @Serializable + data object Upgrade : Main } sealed interface Settings : NavigationDestination { diff --git a/app/src/main/java/eu/darken/capod/common/upgrade/UpgradeRepo.kt b/app/src/main/java/eu/darken/capod/common/upgrade/UpgradeRepo.kt index b5bfd513..2a7ade6f 100644 --- a/app/src/main/java/eu/darken/capod/common/upgrade/UpgradeRepo.kt +++ b/app/src/main/java/eu/darken/capod/common/upgrade/UpgradeRepo.kt @@ -1,14 +1,11 @@ package eu.darken.capod.common.upgrade -import android.app.Activity import kotlinx.coroutines.flow.Flow import java.time.Instant interface UpgradeRepo { val upgradeInfo: Flow - fun launchBillingFlow(activity: Activity) - fun getSponsorUrl(): String? = null interface Info { diff --git a/app/src/main/java/eu/darken/capod/main/ui/MainActivity.kt b/app/src/main/java/eu/darken/capod/main/ui/MainActivity.kt index df0d7c25..3f802c6e 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/MainActivity.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/MainActivity.kt @@ -1,10 +1,12 @@ package eu.darken.capod.main.ui +import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -53,6 +55,10 @@ class MainActivity : Activity2() { val backStack = rememberNavBackStack(startDestination) navCtrl.setup(backStack) + LaunchedEffect(Unit) { + consumeUpgradeExtra(intent) + } + CapodTheme(state = themeState) { val backgroundColor = MaterialTheme.colorScheme.background val useDarkIcons = backgroundColor.luminance() > 0.5f @@ -85,7 +91,22 @@ class MainActivity : Activity2() { } } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + consumeUpgradeExtra(intent) + } + + private fun consumeUpgradeExtra(intent: Intent?) { + if (intent?.getBooleanExtra(EXTRA_NAVIGATE_TO_UPGRADE, false) == true) { + intent.removeExtra(EXTRA_NAVIGATE_TO_UPGRADE) + if (generalSettings.isOnboardingDone.value) { + navCtrl.goTo(Nav.Main.Upgrade) + } + } + } + companion object { + const val EXTRA_NAVIGATE_TO_UPGRADE = "navigate_to_upgrade" private val TAG = logTag("MainActivity") } } diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewScreen.kt index 1c5efdd0..7596bd79 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewScreen.kt @@ -1,6 +1,5 @@ package eu.darken.capod.main.ui.overview -import android.app.Activity import android.content.Intent import android.provider.Settings import androidx.activity.compose.rememberLauncherForActivityResult @@ -67,7 +66,6 @@ fun OverviewScreenHost(vm: OverviewViewModel = hiltViewModel()) { NavigationEventHandler(vm) val context = LocalContext.current - val activity = context as? Activity // Collect workerAutolaunch passively to keep it active LaunchedEffect(Unit) { @@ -122,13 +120,6 @@ fun OverviewScreenHost(vm: OverviewViewModel = hiltViewModel()) { } } - // Handle upgrade flow events - LaunchedEffect(Unit) { - vm.launchUpgradeFlow.collect { action -> - activity?.let { action(it) } - } - } - val state by vm.state.collectAsStateWithLifecycle(initialValue = null) val currentState = state ?: return diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt index 43d29213..bbad5e23 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt @@ -1,6 +1,5 @@ package eu.darken.capod.main.ui.overview -import android.app.Activity import dagger.hilt.android.lifecycle.HiltViewModel import eu.darken.capod.common.bluetooth.BluetoothManager2 import eu.darken.capod.common.coroutine.DispatcherProvider @@ -50,7 +49,6 @@ class OverviewViewModel @Inject constructor( ) : ViewModel4(dispatcherProvider) { val requestPermissionEvent = SingleEventFlow() - val launchUpgradeFlow = SingleEventFlow<(Activity) -> Unit>() private val showUnmatchedDevices = MutableStateFlow(false) @@ -141,8 +139,8 @@ class OverviewViewModel @Inject constructor( navTo(Nav.Main.DeviceManager) } - fun onUpgrade() = launch { - launchUpgradeFlow.tryEmit { upgradeRepo.launchBillingFlow(it) } + fun onUpgrade() { + navTo(Nav.Main.Upgrade) } fun toggleUnmatchedDevices() { diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsScreen.kt index db798cee..477044ba 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsScreen.kt @@ -94,11 +94,11 @@ fun SettingsScreen( }, actions = { val sponsorUrl = state.sponsorUrl - if (sponsorUrl != null) { + if (state.isPro && sponsorUrl != null) { IconButton(onClick = { onSponsor(sponsorUrl) }) { Icon( imageVector = Icons.TwoTone.Favorite, - contentDescription = "Sponsor development", + contentDescription = null, ) } } @@ -190,7 +190,7 @@ fun SettingsScreen( @Composable private fun SettingsScreenPreview() = PreviewWrapper { SettingsScreen( - state = SettingsViewModel.State(sponsorUrl = "https://example.com"), + state = SettingsViewModel.State(isPro = true, sponsorUrl = "https://example.com"), onNavigateUp = {}, onGeneralSettings = {}, onDeviceManager = {}, diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsViewModel.kt index 71ef5d1d..b97777ad 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/SettingsViewModel.kt @@ -18,11 +18,12 @@ class SettingsViewModel @Inject constructor( ) : ViewModel4(dispatcherProvider) { data class State( + val isPro: Boolean, val sponsorUrl: String?, ) val state = upgradeRepo.upgradeInfo - .map { State(sponsorUrl = upgradeRepo.getSponsorUrl()) } + .map { State(isPro = it.isPro, sponsorUrl = upgradeRepo.getSponsorUrl()) } .asLiveState() fun openUrl(url: String) { diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsScreen.kt index 99891f55..3c280373 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsScreen.kt @@ -1,6 +1,5 @@ package eu.darken.capod.main.ui.settings.general -import android.app.Activity import android.os.Build import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -35,14 +34,12 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp @@ -69,11 +66,6 @@ fun GeneralSettingsScreenHost(vm: GeneralSettingsViewModel = hiltViewModel()) { ErrorEventHandler(vm) NavigationEventHandler(vm) - val activity = LocalContext.current as? Activity - LaunchedEffect(Unit) { - vm.launchUpgradeFlow.collect { action -> activity?.let { action(it) } } - } - val state by vm.state.collectAsStateWithLifecycle(initialValue = null) state?.let { GeneralSettingsScreen( diff --git a/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsViewModel.kt index ae76cacb..afb34297 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/settings/general/GeneralSettingsViewModel.kt @@ -1,11 +1,9 @@ package eu.darken.capod.main.ui.settings.general -import android.app.Activity import dagger.hilt.android.lifecycle.HiltViewModel import eu.darken.capod.common.bluetooth.ScannerMode import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.debug.logging.logTag -import eu.darken.capod.common.flow.SingleEventFlow import eu.darken.capod.common.navigation.Nav import eu.darken.capod.common.theming.ThemeColor import eu.darken.capod.common.theming.ThemeMode @@ -42,8 +40,6 @@ class GeneralSettingsViewModel @Inject constructor( private val isPro = upgradeRepo.upgradeInfo.map { it.isPro }.asLiveState() - val launchUpgradeFlow = SingleEventFlow<(Activity) -> Unit>() - val state = combine( combine( generalSettings.monitorMode.flow, @@ -110,7 +106,7 @@ class GeneralSettingsViewModel @Inject constructor( if (isPro.first()) { generalSettings.themeMode.value = mode } else { - launchUpgradeFlow.tryEmit { upgradeRepo.launchBillingFlow(it) } + navTo(Nav.Main.Upgrade) } } @@ -118,7 +114,7 @@ class GeneralSettingsViewModel @Inject constructor( if (isPro.first()) { generalSettings.themeStyle.value = style } else { - launchUpgradeFlow.tryEmit { upgradeRepo.launchBillingFlow(it) } + navTo(Nav.Main.Upgrade) } } @@ -126,12 +122,12 @@ class GeneralSettingsViewModel @Inject constructor( if (isPro.first()) { generalSettings.themeColor.value = color } else { - launchUpgradeFlow.tryEmit { upgradeRepo.launchBillingFlow(it) } + navTo(Nav.Main.Upgrade) } } - fun launchUpgrade() = launch { - launchUpgradeFlow.tryEmit { upgradeRepo.launchBillingFlow(it) } + fun launchUpgrade() { + navTo(Nav.Main.Upgrade) } fun goToDebugSettings() { diff --git a/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationActivity.kt b/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationActivity.kt index 878a50b5..4de33873 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationActivity.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/widget/WidgetConfigurationActivity.kt @@ -22,7 +22,7 @@ import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.theming.CapodTheme import eu.darken.capod.common.uix.Activity2 -import eu.darken.capod.common.upgrade.UpgradeRepo +import eu.darken.capod.main.ui.MainActivity import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.main.core.currentThemeState import eu.darken.capod.main.core.themeState @@ -34,7 +34,6 @@ class WidgetConfigurationActivity : Activity2() { private val vm: WidgetConfigurationViewModel by viewModels() - @Inject lateinit var upgradeRepo: UpgradeRepo @Inject lateinit var generalSettings: GeneralSettings @ApplicationContext @Inject lateinit var appContext: Context @@ -87,7 +86,11 @@ class WidgetConfigurationActivity : Activity2() { if (currentState.isPro) { confirmSelection() } else { - upgradeRepo.launchBillingFlow(this@WidgetConfigurationActivity) + startActivity( + Intent(this@WidgetConfigurationActivity, MainActivity::class.java).apply { + putExtra(MainActivity.EXTRA_NAVIGATE_TO_UPGRADE, true) + } + ) } }, onCancel = { finish() }, diff --git a/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsScreen.kt b/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsScreen.kt index 66ff6b8f..fd497720 100644 --- a/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsScreen.kt +++ b/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsScreen.kt @@ -1,6 +1,5 @@ package eu.darken.capod.reaction.ui -import android.app.Activity import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth @@ -16,6 +15,8 @@ import androidx.compose.material.icons.automirrored.twotone.Message import androidx.compose.material.icons.twotone.PauseCircle import androidx.compose.material.icons.twotone.PlayCircle import androidx.compose.material.icons.twotone.QuestionMark +import androidx.compose.material.icons.twotone.Stars +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.twotone.Workspaces import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api @@ -35,7 +36,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp @@ -56,19 +56,18 @@ fun ReactionSettingsScreenHost(vm: ReactionSettingsViewModel = hiltViewModel()) NavigationEventHandler(vm) val state by vm.state.collectAsStateWithLifecycle(initialValue = null) - val activity = LocalContext.current as Activity state?.let { ReactionSettingsScreen( state = it, onNavigateUp = { vm.navUp() }, onOnePodModeChanged = { enabled -> vm.setOnePodMode(enabled) }, - onAutoPlayChanged = { enabled -> vm.setAutoPlay(enabled, activity) }, - onAutoPauseChanged = { enabled -> vm.setAutoPause(enabled, activity) }, - onAutoConnectChanged = { enabled -> vm.setAutoConnect(enabled, activity) }, + onAutoPlayChanged = { enabled -> vm.setAutoPlay(enabled) }, + onAutoPauseChanged = { enabled -> vm.setAutoPause(enabled) }, + onAutoConnectChanged = { enabled -> vm.setAutoConnect(enabled) }, onAutoConnectConditionSelected = { condition -> vm.setAutoConnectCondition(condition) }, - onShowPopUpOnCaseOpenChanged = { enabled -> vm.setShowPopUpOnCaseOpen(enabled, activity) }, - onShowPopUpOnConnectionChanged = { enabled -> vm.setShowPopUpOnConnection(enabled, activity) }, + onShowPopUpOnCaseOpenChanged = { enabled -> vm.setShowPopUpOnCaseOpen(enabled) }, + onShowPopUpOnConnectionChanged = { enabled -> vm.setShowPopUpOnConnection(enabled) }, ) } } @@ -131,11 +130,7 @@ fun ReactionSettingsScreen( icon = Icons.TwoTone.PlayCircle, onClick = { onAutoPlayChanged(!state.autoPlay) }, trailingContent = { - Switch( - checked = state.autoPlay, - onCheckedChange = onAutoPlayChanged, - modifier = Modifier.padding(start = 16.dp), - ) + ProGatedToggle(isPro = state.isPro, checked = state.autoPlay, onCheckedChange = onAutoPlayChanged) }, ) } @@ -146,11 +141,7 @@ fun ReactionSettingsScreen( icon = Icons.TwoTone.PauseCircle, onClick = { onAutoPauseChanged(!state.autoPause) }, trailingContent = { - Switch( - checked = state.autoPause, - onCheckedChange = onAutoPauseChanged, - modifier = Modifier.padding(start = 16.dp), - ) + ProGatedToggle(isPro = state.isPro, checked = state.autoPause, onCheckedChange = onAutoPauseChanged) }, ) } @@ -173,11 +164,7 @@ fun ReactionSettingsScreen( icon = Icons.TwoTone.BluetoothConnected, onClick = { onAutoConnectChanged(!state.autoConnect) }, trailingContent = { - Switch( - checked = state.autoConnect, - onCheckedChange = onAutoConnectChanged, - modifier = Modifier.padding(start = 16.dp), - ) + ProGatedToggle(isPro = state.isPro, checked = state.autoConnect, onCheckedChange = onAutoConnectChanged) }, ) } @@ -200,11 +187,7 @@ fun ReactionSettingsScreen( icon = Icons.AutoMirrored.TwoTone.Message, onClick = { onShowPopUpOnCaseOpenChanged(!state.showPopUpOnCaseOpen) }, trailingContent = { - Switch( - checked = state.showPopUpOnCaseOpen, - onCheckedChange = onShowPopUpOnCaseOpenChanged, - modifier = Modifier.padding(start = 16.dp), - ) + ProGatedToggle(isPro = state.isPro, checked = state.showPopUpOnCaseOpen, onCheckedChange = onShowPopUpOnCaseOpenChanged) }, ) } @@ -215,11 +198,7 @@ fun ReactionSettingsScreen( icon = Icons.AutoMirrored.TwoTone.Message, onClick = { onShowPopUpOnConnectionChanged(!state.showPopUpOnConnection) }, trailingContent = { - Switch( - checked = state.showPopUpOnConnection, - onCheckedChange = onShowPopUpOnConnectionChanged, - modifier = Modifier.padding(start = 16.dp), - ) + ProGatedToggle(isPro = state.isPro, checked = state.showPopUpOnConnection, onCheckedChange = onShowPopUpOnConnectionChanged) }, ) } @@ -270,6 +249,28 @@ fun ReactionSettingsScreen( } } +@Composable +private fun ProGatedToggle( + isPro: Boolean, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + if (isPro) { + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + modifier = Modifier.padding(start = 16.dp), + ) + } else { + Icon( + imageVector = Icons.TwoTone.Stars, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(start = 16.dp).size(24.dp), + ) + } +} + @Preview2 @Composable private fun ReactionSettingsScreenPreview() = PreviewWrapper { diff --git a/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsViewModel.kt b/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsViewModel.kt index 453056c3..a3599a54 100644 --- a/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsViewModel.kt +++ b/app/src/main/java/eu/darken/capod/reaction/ui/ReactionSettingsViewModel.kt @@ -1,10 +1,9 @@ package eu.darken.capod.reaction.ui -import android.app.Activity import dagger.hilt.android.lifecycle.HiltViewModel import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.debug.logging.logTag - +import eu.darken.capod.common.navigation.Nav import eu.darken.capod.common.uix.ViewModel4 import eu.darken.capod.common.upgrade.UpgradeRepo import eu.darken.capod.main.core.GeneralSettings @@ -63,7 +62,7 @@ class ReactionSettingsViewModel @Inject constructor( reactionSettings.onePodMode.value = enabled } - fun setAutoPlay(enabled: Boolean, activity: Activity) = launch { + fun setAutoPlay(enabled: Boolean) = launch { if (!enabled) { reactionSettings.autoPlay.value = false return@launch @@ -71,11 +70,11 @@ class ReactionSettingsViewModel @Inject constructor( if (isPro.first()) { reactionSettings.autoPlay.value = true } else { - upgradeRepo.launchBillingFlow(activity) + navTo(Nav.Main.Upgrade) } } - fun setAutoPause(enabled: Boolean, activity: Activity) = launch { + fun setAutoPause(enabled: Boolean) = launch { if (!enabled) { reactionSettings.autoPause.value = false return@launch @@ -83,11 +82,11 @@ class ReactionSettingsViewModel @Inject constructor( if (isPro.first()) { reactionSettings.autoPause.value = true } else { - upgradeRepo.launchBillingFlow(activity) + navTo(Nav.Main.Upgrade) } } - fun setAutoConnect(enabled: Boolean, activity: Activity) = launch { + fun setAutoConnect(enabled: Boolean) = launch { if (!enabled) { reactionSettings.autoConnect.value = false return@launch @@ -96,7 +95,7 @@ class ReactionSettingsViewModel @Inject constructor( reactionSettings.autoConnect.value = true generalSettings.monitorMode.value = MonitorMode.ALWAYS } else { - upgradeRepo.launchBillingFlow(activity) + navTo(Nav.Main.Upgrade) } } @@ -104,7 +103,7 @@ class ReactionSettingsViewModel @Inject constructor( reactionSettings.autoConnectCondition.value = condition } - fun setShowPopUpOnCaseOpen(enabled: Boolean, activity: Activity) = launch { + fun setShowPopUpOnCaseOpen(enabled: Boolean) = launch { if (!enabled) { reactionSettings.showPopUpOnCaseOpen.value = false return@launch @@ -112,11 +111,11 @@ class ReactionSettingsViewModel @Inject constructor( if (isPro.first()) { reactionSettings.showPopUpOnCaseOpen.value = true } else { - upgradeRepo.launchBillingFlow(activity) + navTo(Nav.Main.Upgrade) } } - fun setShowPopUpOnConnection(enabled: Boolean, activity: Activity) = launch { + fun setShowPopUpOnConnection(enabled: Boolean) = launch { if (!enabled) { reactionSettings.showPopUpOnConnection.value = false return@launch @@ -124,7 +123,7 @@ class ReactionSettingsViewModel @Inject constructor( if (isPro.first()) { reactionSettings.showPopUpOnConnection.value = true } else { - upgradeRepo.launchBillingFlow(activity) + navTo(Nav.Main.Upgrade) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c3565bfe..1ffc4588 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,6 +19,13 @@ Upgrade CAPod Get additional features and support the developer. + CAPod is developed by a single person. Upgrading unlocks extra features and helps keep the app alive. + Theme customization + Auto-play & auto-pause + Auto-connect + Popup on case open & connection + Home screen widgets + Support the developer Monitor mode Under which circumstances this app monitors Bluetooth data.