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.billing.Inventory
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.SignInDialog
import org.tasks.dialogs.DialogBuilder

@ -1,138 +1,55 @@
package org.tasks.billing
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.lifecycleScope
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
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.extensions.Context.toast
import org.tasks.preferences.Preferences
import org.tasks.extensions.Context.findActivity
import org.tasks.themes.TasksTheme
import org.tasks.themes.Theme
import java.util.Locale
import javax.inject.Inject
@AndroidEntryPoint
class PurchaseActivity : AppCompatActivity(), OnPurchasesUpdated {
@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?) {
super.onCreate(savedInstanceState)
val github = intent?.extras?.getBoolean(EXTRA_GITHUB) ?: false
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 {
TasksTheme(theme = theme.themeBase.index) {
BackHandler {
finish()
}
val viewModel: PurchaseActivityViewModel = viewModel()
val state = viewModel.viewState.collectAsStateWithLifecycle().value
val context = LocalContext.current
SubscriptionScreen(
nameYourPrice = nameYourPrice,
sliderPosition = sliderPosition,
github = github,
subscribe = this::purchase,
nameYourPrice = state.nameYourPrice,
sliderPosition = state.price,
github = state.isGithub,
subscribe = { price, isMonthly ->
context.findActivity()?.let { viewModel.purchase(it, price, isMonthly) }
},
setPrice = { viewModel.setPrice(it) },
setNameYourPrice = { viewModel.setNameYourPrice(it) },
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) {
if (success) {
setResult(RESULT_OK)
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 org.tasks.billing.Inventory
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.IconPickerViewModel
import org.tasks.themes.TasksTheme

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

Loading…
Cancel
Save