Add PurchaseActivityViewModel

pull/3199/head
Alex Baker 12 months ago
parent 93231f3c10
commit 04a7eef174

@ -43,7 +43,7 @@ import org.tasks.TasksApplication.Companion.IS_GENERIC
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.billing.PurchaseActivity import org.tasks.billing.PurchaseActivity
import org.tasks.billing.PurchaseActivity.Companion.EXTRA_GITHUB import org.tasks.billing.PurchaseActivityViewModel.Companion.EXTRA_GITHUB
import org.tasks.compose.ConsentDialog import org.tasks.compose.ConsentDialog
import org.tasks.compose.SignInDialog import org.tasks.compose.SignInDialog
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder

@ -1,138 +1,55 @@
package org.tasks.billing package org.tasks.billing
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext
import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.viewmodel.compose.viewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.analytics.Firebase
import org.tasks.compose.PurchaseText.SubscriptionScreen import org.tasks.compose.PurchaseText.SubscriptionScreen
import org.tasks.extensions.Context.toast import org.tasks.extensions.Context.findActivity
import org.tasks.preferences.Preferences
import org.tasks.themes.TasksTheme import org.tasks.themes.TasksTheme
import org.tasks.themes.Theme import org.tasks.themes.Theme
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class PurchaseActivity : AppCompatActivity(), OnPurchasesUpdated { class PurchaseActivity : AppCompatActivity(), OnPurchasesUpdated {
@Inject lateinit var theme: Theme @Inject lateinit var theme: Theme
@Inject lateinit var billingClient: BillingClient
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var inventory: Inventory
@Inject lateinit var preferences: Preferences
@Inject lateinit var firebase: Firebase
private var currentSubscription: Purchase? = null
private val purchaseReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
setup()
}
}
private val nameYourPrice = mutableStateOf(false)
private val sliderPosition = mutableStateOf(-1f)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val github = intent?.extras?.getBoolean(EXTRA_GITHUB) ?: false
theme.applyToContext(this) theme.applyToContext(this)
if (savedInstanceState == null) {
nameYourPrice.value = intent?.extras?.getBoolean(EXTRA_NAME_YOUR_PRICE) ?: firebase.nameYourPrice
} else {
nameYourPrice.value = savedInstanceState.getBoolean(EXTRA_NAME_YOUR_PRICE)
sliderPosition.value = savedInstanceState.getFloat(EXTRA_PRICE)
}
setContent { setContent {
TasksTheme(theme = theme.themeBase.index) { TasksTheme(theme = theme.themeBase.index) {
BackHandler { BackHandler {
finish() finish()
} }
val viewModel: PurchaseActivityViewModel = viewModel()
val state = viewModel.viewState.collectAsStateWithLifecycle().value
val context = LocalContext.current
SubscriptionScreen( SubscriptionScreen(
nameYourPrice = nameYourPrice, nameYourPrice = state.nameYourPrice,
sliderPosition = sliderPosition, sliderPosition = state.price,
github = github, github = state.isGithub,
subscribe = this::purchase, subscribe = { price, isMonthly ->
context.findActivity()?.let { viewModel.purchase(it, price, isMonthly) }
},
setPrice = { viewModel.setPrice(it) },
setNameYourPrice = { viewModel.setNameYourPrice(it) },
onBack = { finish() }, onBack = { finish() },
) )
LaunchedEffect(key1 = Unit) {
firebase.logEvent(R.string.event_showed_purchase_dialog)
}
} }
} }
} }
override fun onStart() {
super.onStart()
localBroadcastManager.registerPurchaseReceiver(purchaseReceiver)
lifecycleScope.launch {
try {
billingClient.queryPurchases(throwError = true)
} catch (e: Exception) {
toast(e.message)
}
}
}
override fun onStop() {
super.onStop()
localBroadcastManager.unregisterReceiver(purchaseReceiver)
}
private fun setup() {
currentSubscription = inventory.subscription.value
if (sliderPosition.value < 0) {
sliderPosition.value =
currentSubscription
?.subscriptionPrice
?.coerceAtMost(25)
?.toFloat() ?: 10f
}
}
private fun purchase(price: Int, monthly: Boolean) = lifecycleScope.launch {
val newSku = String.format(Locale.US, "%s_%02d", if (monthly) "monthly" else "annual", price)
try {
billingClient.initiatePurchaseFlow(
this@PurchaseActivity,
newSku,
BillingClientImpl.TYPE_SUBS,
currentSubscription?.takeIf { it.sku != newSku }
)
} catch (e: Exception) {
this@PurchaseActivity.toast(e.message)
}
}
override fun onPurchasesUpdated(success: Boolean) { override fun onPurchasesUpdated(success: Boolean) {
if (success) { if (success) {
setResult(RESULT_OK) setResult(RESULT_OK)
finish() finish()
} }
} }
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putFloat(EXTRA_PRICE, sliderPosition.value)
outState.putBoolean(EXTRA_NAME_YOUR_PRICE, nameYourPrice.value)
}
companion object {
const val EXTRA_GITHUB = "extra_github"
private const val EXTRA_PRICE = "extra_price"
const val EXTRA_NAME_YOUR_PRICE = "extra_name_your_price"
}
} }

@ -0,0 +1,109 @@
package org.tasks.billing
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.analytics.Firebase
import java.util.Locale
import javax.inject.Inject
@HiltViewModel
class PurchaseActivityViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val inventory: Inventory,
private val billingClient: BillingClient,
private val localBroadcastManager: LocalBroadcastManager,
firebase: Firebase,
) : ViewModel() {
data class ViewState(
val nameYourPrice: Boolean,
val isGithub: Boolean,
val price: Float = -1f,
val subscription: Purchase? = null,
val error: String? = null,
)
private val purchaseReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val subscription = inventory.subscription.value
_viewState.update { state ->
state.copy(
subscription = subscription,
price = state.price.takeIf { it > 0 }
?: subscription?.subscriptionPrice?.coerceAtMost(25)?.toFloat()
?: 10f
)
}
}
}
fun setPrice(price: Float) {
_viewState.update { it.copy(price = price) }
}
private val _viewState = MutableStateFlow(
ViewState(
nameYourPrice = savedStateHandle.get<Boolean>(EXTRA_NAME_YOUR_PRICE) ?: firebase.nameYourPrice,
isGithub = savedStateHandle.get<Boolean>(EXTRA_GITHUB) ?: false,
)
)
val viewState: StateFlow<ViewState> = _viewState
init {
localBroadcastManager.registerPurchaseReceiver(purchaseReceiver)
viewModelScope.launch {
try {
billingClient.queryPurchases(throwError = true)
} catch (e: Exception) {
_viewState.update {
it.copy(error = e.message)
}
}
}
firebase.logEvent(R.string.event_showed_purchase_dialog)
}
override fun onCleared() {
super.onCleared()
localBroadcastManager.unregisterReceiver(purchaseReceiver)
}
fun purchase(activity: Activity, price: Int, monthly: Boolean) = viewModelScope.launch {
val newSku = String.format(Locale.US, "%s_%02d", if (monthly) "monthly" else "annual", price)
try {
billingClient.initiatePurchaseFlow(
activity,
newSku,
BillingClientImpl.TYPE_SUBS,
_viewState.value.subscription?.takeIf { it.sku != newSku },
)
} catch (e: Exception) {
_viewState.update {
it.copy(error = e.message)
}
}
}
fun setNameYourPrice(nameYourPrice: Boolean) {
_viewState.update { it.copy(nameYourPrice = nameYourPrice) }
}
companion object {
const val EXTRA_GITHUB = "extra_github"
const val EXTRA_NAME_YOUR_PRICE = "extra_name_your_price"
}
}

@ -24,7 +24,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.billing.PurchaseActivity import org.tasks.billing.PurchaseActivity
import org.tasks.billing.PurchaseActivity.Companion.EXTRA_NAME_YOUR_PRICE import org.tasks.billing.PurchaseActivityViewModel.Companion.EXTRA_NAME_YOUR_PRICE
import org.tasks.compose.pickers.IconPicker import org.tasks.compose.pickers.IconPicker
import org.tasks.compose.pickers.IconPickerViewModel import org.tasks.compose.pickers.IconPickerViewModel
import org.tasks.themes.TasksTheme import org.tasks.themes.TasksTheme

@ -39,9 +39,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -138,9 +135,11 @@ object PurchaseText {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SubscriptionScreen( fun SubscriptionScreen(
nameYourPrice: MutableState<Boolean> = mutableStateOf(false), nameYourPrice: Boolean,
sliderPosition: MutableState<Float> = mutableStateOf(0f), sliderPosition: Float,
github: Boolean = false, github: Boolean = false,
setPrice: (Float) -> Unit,
setNameYourPrice: (Boolean) -> Unit,
subscribe: (Int, Boolean) -> Unit, subscribe: (Int, Boolean) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
) { ) {
@ -186,7 +185,7 @@ object PurchaseText {
state = pagerState // Optional: to control the pager's state state = pagerState // Optional: to control the pager's state
) { index -> ) { index ->
val item = featureList[index] val item = featureList[index]
PagerItem(item, nameYourPrice.value && index == 0) PagerItem(item, nameYourPrice && index == 0)
} }
Row( Row(
Modifier Modifier
@ -214,6 +213,8 @@ object PurchaseText {
nameYourPrice = nameYourPrice, nameYourPrice = nameYourPrice,
sliderPosition = sliderPosition, sliderPosition = sliderPosition,
pagerState = pagerState, pagerState = pagerState,
setNameYourPrice = setNameYourPrice,
setPrice = setPrice,
subscribe = subscribe, subscribe = subscribe,
) )
} }
@ -259,9 +260,11 @@ object PurchaseText {
@Composable @Composable
fun GooglePlayButtons( fun GooglePlayButtons(
nameYourPrice: MutableState<Boolean>, nameYourPrice: Boolean,
sliderPosition: MutableState<Float>, sliderPosition: Float,
pagerState: PagerState, pagerState: PagerState,
setNameYourPrice: (Boolean) -> Unit,
setPrice: (Float) -> Unit,
subscribe: (Int, Boolean) -> Unit, subscribe: (Int, Boolean) -> Unit,
) { ) {
Column( Column(
@ -269,8 +272,12 @@ object PurchaseText {
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
HorizontalDivider(modifier = Modifier.padding(vertical = KEYLINE_FIRST)) HorizontalDivider(modifier = Modifier.padding(vertical = KEYLINE_FIRST))
if (nameYourPrice.value) { if (nameYourPrice) {
NameYourPrice(sliderPosition, subscribe) NameYourPrice(
sliderPosition = sliderPosition,
setPrice = setPrice,
subscribe = subscribe,
)
} else { } else {
TasksAccount(subscribe) TasksAccount(subscribe)
} }
@ -278,7 +285,7 @@ object PurchaseText {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
OutlinedButton( OutlinedButton(
onClick = { onClick = {
nameYourPrice.value = !nameYourPrice.value setNameYourPrice(!nameYourPrice)
scope.launch { scope.launch {
pagerState.animateScrollToPage(0) pagerState.animateScrollToPage(0)
} }
@ -419,7 +426,11 @@ object PurchaseText {
} }
@Composable @Composable
fun NameYourPrice(sliderPosition: MutableState<Float>, subscribe: (Int, Boolean) -> Unit) { fun NameYourPrice(
sliderPosition: Float,
setPrice: (Float) -> Unit,
subscribe: (Int, Boolean) -> Unit,
) {
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@ -427,8 +438,8 @@ object PurchaseText {
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
Slider( Slider(
modifier = Modifier.padding(KEYLINE_FIRST, 0.dp, KEYLINE_FIRST, HALF_KEYLINE), modifier = Modifier.padding(KEYLINE_FIRST, 0.dp, KEYLINE_FIRST, HALF_KEYLINE),
value = sliderPosition.value, value = sliderPosition,
onValueChange = { sliderPosition.value = it }, onValueChange = { setPrice(it) },
valueRange = 1f..25f, valueRange = 1f..25f,
steps = 25, steps = 25,
colors = SliderDefaults.colors( colors = SliderDefaults.colors(
@ -445,17 +456,17 @@ object PurchaseText {
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
PurchaseButton( PurchaseButton(
price = sliderPosition.value.toInt(), price = sliderPosition.toInt(),
popperText = if (sliderPosition.value.toInt() >= 7) popperText = if (sliderPosition.toInt() >= 7)
"${stringResource(R.string.above_average, 16)} $POPPER" "${stringResource(R.string.above_average, 16)} $POPPER"
else else
"", "",
onClick = subscribe onClick = subscribe
) )
if (sliderPosition.value.toInt() < 3) { if (sliderPosition.toInt() < 3) {
Spacer(Modifier.width(KEYLINE_FIRST)) Spacer(Modifier.width(KEYLINE_FIRST))
PurchaseButton( PurchaseButton(
price = sliderPosition.value.toInt(), price = sliderPosition.toInt(),
monthly = true, monthly = true,
popperText = "${stringResource(R.string.above_average)} $POPPER", popperText = "${stringResource(R.string.above_average)} $POPPER",
onClick = subscribe onClick = subscribe
@ -474,6 +485,10 @@ private fun PurchaseDialogPreview() {
SubscriptionScreen( SubscriptionScreen(
subscribe = { _, _ -> }, subscribe = { _, _ -> },
onBack = {}, onBack = {},
nameYourPrice = false,
sliderPosition = 1f,
setPrice = {},
setNameYourPrice = {},
) )
} }
} }
@ -484,9 +499,12 @@ private fun PurchaseDialogPreview() {
private fun NameYourPricePreview() { private fun NameYourPricePreview() {
TasksTheme { TasksTheme {
SubscriptionScreen( SubscriptionScreen(
nameYourPrice = remember { mutableStateOf(true) },
subscribe = { _, _ -> }, subscribe = { _, _ -> },
onBack = {}, onBack = {},
nameYourPrice = true,
sliderPosition = 4f,
setPrice = {},
setNameYourPrice = {},
) )
} }
} }

Loading…
Cancel
Save