Show subscription prices in local currency

pull/3203/head
Alex Baker 12 months ago
parent efa664dd70
commit d2a0dda6c5

@ -24,6 +24,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import org.tasks.BuildConfig import org.tasks.BuildConfig
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.jobs.WorkManager import org.tasks.jobs.WorkManager
@ -46,6 +47,29 @@ class BillingClientImpl(
private var connected = false private var connected = false
private var onPurchasesUpdated: OnPurchasesUpdated? = null private var onPurchasesUpdated: OnPurchasesUpdated? = null
override suspend fun getSkus(skus: List<String>): List<Sku> =
executeServiceRequest {
val skuDetailsResult = withContext(Dispatchers.IO) {
billingClient.querySkuDetails(
SkuDetailsParams
.newBuilder()
.setType(SkuType.SUBS)
.setSkusList(skus)
.build()
)
}
skuDetailsResult.billingResult.let {
if (!it.success) {
throw IllegalStateException(it.responseCodeString)
}
}
val json = Json { ignoreUnknownKeys = true }
skuDetailsResult
.skuDetailsList
?.map { json.decodeFromString<Sku>(it.originalJson) }
?: emptyList()
}
override suspend fun queryPurchases(throwError: Boolean) = try { override suspend fun queryPurchases(throwError: Boolean) = try {
executeServiceRequest { executeServiceRequest {
withContext(Dispatchers.IO + NonCancellable) { withContext(Dispatchers.IO + NonCancellable) {
@ -174,11 +198,11 @@ class BillingClientImpl(
) )
} }
private suspend fun executeServiceRequest(runnable: suspend () -> Unit) { private suspend fun <T> executeServiceRequest(runnable: suspend () -> T): T {
if (!connected) { if (!connected) {
connect() connect()
} }
runnable() return runnable()
} }
override suspend fun consume(sku: String) { override suspend fun consume(sku: String) {

@ -12,4 +12,5 @@ interface BillingClient {
oldPurchase: Purchase? = null oldPurchase: Purchase? = null
) )
suspend fun acknowledge(purchase: Purchase) suspend fun acknowledge(purchase: Purchase)
} suspend fun getSkus(skus: List<String>): List<Sku>
}

@ -47,6 +47,7 @@ class PurchaseActivity : AppCompatActivity(), OnPurchasesUpdated {
setPrice = { viewModel.setPrice(it) }, setPrice = { viewModel.setPrice(it) },
setNameYourPrice = { viewModel.setNameYourPrice(it) }, setNameYourPrice = { viewModel.setNameYourPrice(it) },
onBack = { finish() }, onBack = { finish() },
skus = state.skus,
snackbarHostState = snackbarHostState, snackbarHostState = snackbarHostState,
) )
LaunchedEffect(state.error) { LaunchedEffect(state.error) {

@ -33,6 +33,7 @@ class PurchaseActivityViewModel @Inject constructor(
val price: Float = -1f, val price: Float = -1f,
val subscription: Purchase? = null, val subscription: Purchase? = null,
val error: String? = null, val error: String? = null,
val skus: List<Sku> = emptyList(),
) )
private val purchaseReceiver = object : BroadcastReceiver() { private val purchaseReceiver = object : BroadcastReceiver() {
@ -72,6 +73,19 @@ class PurchaseActivityViewModel @Inject constructor(
it.copy(error = e.message) it.copy(error = e.message)
} }
} }
try {
val skus =
(1..25).map { it.toSku(false) }
.plus((1..3).map { it.toSku(true) })
.plus(30.toSku(false))
_viewState.update {
it.copy(skus = billingClient.getSkus(skus))
}
} catch (e: Exception) {
_viewState.update {
it.copy(error = e.message)
}
}
} }
firebase.logEvent(R.string.event_showed_purchase_dialog) firebase.logEvent(R.string.event_showed_purchase_dialog)
@ -83,7 +97,7 @@ class PurchaseActivityViewModel @Inject constructor(
} }
fun purchase(activity: Activity, price: Int, monthly: Boolean) = viewModelScope.launch { fun purchase(activity: Activity, price: Int, monthly: Boolean) = viewModelScope.launch {
val newSku = String.format(Locale.US, "%s_%02d", if (monthly) "monthly" else "annual", price) val newSku = price.toSku(monthly)
try { try {
billingClient.initiatePurchaseFlow( billingClient.initiatePurchaseFlow(
activity, activity,
@ -109,5 +123,8 @@ class PurchaseActivityViewModel @Inject constructor(
companion object { companion object {
const val EXTRA_GITHUB = "extra_github" const val EXTRA_GITHUB = "extra_github"
const val EXTRA_NAME_YOUR_PRICE = "extra_name_your_price" const val EXTRA_NAME_YOUR_PRICE = "extra_name_your_price"
fun Int.toSku(monthly: Boolean) =
String.format(Locale.US, "%s_%02d", if (monthly) "monthly" else "annual", this)
} }
} }

@ -0,0 +1,9 @@
package org.tasks.billing
import kotlinx.serialization.Serializable
@Serializable
data class Sku(
val productId: String,
val price: String,
)

@ -60,6 +60,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.R import org.tasks.R
import org.tasks.billing.Sku
import org.tasks.compose.Constants.HALF_KEYLINE import org.tasks.compose.Constants.HALF_KEYLINE
import org.tasks.compose.Constants.KEYLINE_FIRST import org.tasks.compose.Constants.KEYLINE_FIRST
import org.tasks.compose.PurchaseText.SubscriptionScreen import org.tasks.compose.PurchaseText.SubscriptionScreen
@ -145,6 +146,7 @@ object PurchaseText {
setPrice: (Float) -> Unit, setPrice: (Float) -> Unit,
setNameYourPrice: (Boolean) -> Unit, setNameYourPrice: (Boolean) -> Unit,
subscribe: (Int, Boolean) -> Unit, subscribe: (Int, Boolean) -> Unit,
skus: List<Sku>,
onBack: () -> Unit, onBack: () -> Unit,
) { ) {
Scaffold( Scaffold(
@ -221,6 +223,7 @@ object PurchaseText {
setNameYourPrice = setNameYourPrice, setNameYourPrice = setNameYourPrice,
setPrice = setPrice, setPrice = setPrice,
subscribe = subscribe, subscribe = subscribe,
skus = skus,
) )
} }
} }
@ -271,6 +274,7 @@ object PurchaseText {
setNameYourPrice: (Boolean) -> Unit, setNameYourPrice: (Boolean) -> Unit,
setPrice: (Float) -> Unit, setPrice: (Float) -> Unit,
subscribe: (Int, Boolean) -> Unit, subscribe: (Int, Boolean) -> Unit,
skus: List<Sku>,
) { ) {
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -282,9 +286,13 @@ object PurchaseText {
sliderPosition = sliderPosition, sliderPosition = sliderPosition,
setPrice = setPrice, setPrice = setPrice,
subscribe = subscribe, subscribe = subscribe,
skus = skus,
) )
} else { } else {
TasksAccount(subscribe) TasksAccount(
skus = skus,
subscribe = subscribe
)
} }
Spacer(Modifier.height(KEYLINE_FIRST)) Spacer(Modifier.height(KEYLINE_FIRST))
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -373,7 +381,10 @@ object PurchaseText {
} }
@Composable @Composable
fun TasksAccount(subscribe: (Int, Boolean) -> Unit) { fun TasksAccount(
skus: List<Sku>,
subscribe: (Int, Boolean) -> Unit,
) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -385,15 +396,19 @@ object PurchaseText {
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
PurchaseButton( PurchaseButton(
price = 30, price = remember(skus) {
skus.find { it.productId == "annual_30" }?.price ?: "$30"
},
popperText = "${stringResource(R.string.save_percent, 16)} $POPPER", popperText = "${stringResource(R.string.save_percent, 16)} $POPPER",
onClick = subscribe onClick = { subscribe(30, false) },
) )
Spacer(Modifier.width(KEYLINE_FIRST)) Spacer(Modifier.width(KEYLINE_FIRST))
PurchaseButton( PurchaseButton(
price = 3, price = remember (skus) {
skus.find { it.productId == "monthly_03" }?.price ?: "$3"
},
monthly = true, monthly = true,
onClick = subscribe onClick = { subscribe(3, true) },
) )
} }
} }
@ -401,21 +416,21 @@ object PurchaseText {
@Composable @Composable
fun PurchaseButton( fun PurchaseButton(
price: Int, price: String,
monthly: Boolean = false, monthly: Boolean = false,
popperText: String = "", popperText: String = "",
onClick: (Int, Boolean) -> Unit, onClick: () -> Unit,
) { ) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button( Button(
onClick = { onClick(price, monthly) }, onClick = { onClick() },
colors = ButtonDefaults.textButtonColors( colors = ButtonDefaults.textButtonColors(
containerColor = MaterialTheme.colorScheme.secondary containerColor = MaterialTheme.colorScheme.secondary
) )
) { ) {
Text( Text(
text = stringResource( text = stringResource(
if (monthly) R.string.price_per_month else R.string.price_per_year, if (monthly) R.string.price_per_month_with_currency else R.string.price_per_year_with_currency,
price price
), ),
color = MaterialTheme.colorScheme.onSecondary, color = MaterialTheme.colorScheme.onSecondary,
@ -435,6 +450,7 @@ object PurchaseText {
sliderPosition: Float, sliderPosition: Float,
setPrice: (Float) -> Unit, setPrice: (Float) -> Unit,
subscribe: (Int, Boolean) -> Unit, subscribe: (Int, Boolean) -> Unit,
skus: List<Sku>,
) { ) {
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -460,21 +476,32 @@ object PurchaseText {
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center horizontalArrangement = Arrangement.Center
) { ) {
val price = sliderPosition.toInt()
PurchaseButton( PurchaseButton(
price = sliderPosition.toInt(), price = remember (skus, price) {
skus
.find { it.productId == "annual_${price.toString().padStart(2, '0')}" }
?.price
?: "$$price"
},
popperText = if (sliderPosition.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(sliderPosition.toInt(), false) },
) )
if (sliderPosition.toInt() < 3) { if (sliderPosition.toInt() < 3) {
Spacer(Modifier.width(KEYLINE_FIRST)) Spacer(Modifier.width(KEYLINE_FIRST))
PurchaseButton( PurchaseButton(
price = sliderPosition.toInt(), price = remember (skus, price) {
skus
.find { it.productId == "monthly_${price.toString().padStart(2, '0')}" }
?.price
?: "$$price"
},
monthly = true, monthly = true,
popperText = "${stringResource(R.string.above_average)} $POPPER", popperText = "${stringResource(R.string.above_average)} $POPPER",
onClick = subscribe onClick = { subscribe(price, true) },
) )
} }
} }
@ -494,6 +521,7 @@ private fun PurchaseDialogPreview() {
sliderPosition = 1f, sliderPosition = 1f,
setPrice = {}, setPrice = {},
setNameYourPrice = {}, setNameYourPrice = {},
skus = emptyList(),
) )
} }
} }
@ -510,6 +538,7 @@ private fun NameYourPricePreview() {
sliderPosition = 4f, sliderPosition = 4f,
setPrice = {}, setPrice = {},
setNameYourPrice = {}, setNameYourPrice = {},
skus = emptyList(),
) )
} }
} }

@ -634,6 +634,8 @@ File %1$s contained %2$s.\n\n
<string name="no_google_play_subscription">No eligible Google Play subscription found</string> <string name="no_google_play_subscription">No eligible Google Play subscription found</string>
<string name="price_per_year">$%s/year</string> <string name="price_per_year">$%s/year</string>
<string name="price_per_month">$%s/month</string> <string name="price_per_month">$%s/month</string>
<string name="price_per_month_with_currency">%s/month</string>
<string name="price_per_year_with_currency">%s/year</string>
<string name="current_subscription">Current subscription: %s</string> <string name="current_subscription">Current subscription: %s</string>
<string name="follow_reddit">Join r/tasks</string> <string name="follow_reddit">Join r/tasks</string>
<string name="follow_twitter">Follow @tasks_org</string> <string name="follow_twitter">Follow @tasks_org</string>

Loading…
Cancel
Save