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.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import org.tasks.BuildConfig
import org.tasks.analytics.Firebase
import org.tasks.jobs.WorkManager
@ -46,6 +47,29 @@ class BillingClientImpl(
private var connected = false
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 {
executeServiceRequest {
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) {
connect()
}
runnable()
return runnable()
}
override suspend fun consume(sku: String) {

@ -12,4 +12,5 @@ interface BillingClient {
oldPurchase: Purchase? = null
)
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) },
setNameYourPrice = { viewModel.setNameYourPrice(it) },
onBack = { finish() },
skus = state.skus,
snackbarHostState = snackbarHostState,
)
LaunchedEffect(state.error) {

@ -33,6 +33,7 @@ class PurchaseActivityViewModel @Inject constructor(
val price: Float = -1f,
val subscription: Purchase? = null,
val error: String? = null,
val skus: List<Sku> = emptyList(),
)
private val purchaseReceiver = object : BroadcastReceiver() {
@ -72,6 +73,19 @@ class PurchaseActivityViewModel @Inject constructor(
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)
@ -83,7 +97,7 @@ class PurchaseActivityViewModel @Inject constructor(
}
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 {
billingClient.initiatePurchaseFlow(
activity,
@ -109,5 +123,8 @@ class PurchaseActivityViewModel @Inject constructor(
companion object {
const val EXTRA_GITHUB = "extra_github"
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 kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.billing.Sku
import org.tasks.compose.Constants.HALF_KEYLINE
import org.tasks.compose.Constants.KEYLINE_FIRST
import org.tasks.compose.PurchaseText.SubscriptionScreen
@ -145,6 +146,7 @@ object PurchaseText {
setPrice: (Float) -> Unit,
setNameYourPrice: (Boolean) -> Unit,
subscribe: (Int, Boolean) -> Unit,
skus: List<Sku>,
onBack: () -> Unit,
) {
Scaffold(
@ -221,6 +223,7 @@ object PurchaseText {
setNameYourPrice = setNameYourPrice,
setPrice = setPrice,
subscribe = subscribe,
skus = skus,
)
}
}
@ -271,6 +274,7 @@ object PurchaseText {
setNameYourPrice: (Boolean) -> Unit,
setPrice: (Float) -> Unit,
subscribe: (Int, Boolean) -> Unit,
skus: List<Sku>,
) {
Column(
modifier = Modifier.fillMaxWidth(),
@ -282,9 +286,13 @@ object PurchaseText {
sliderPosition = sliderPosition,
setPrice = setPrice,
subscribe = subscribe,
skus = skus,
)
} else {
TasksAccount(subscribe)
TasksAccount(
skus = skus,
subscribe = subscribe
)
}
Spacer(Modifier.height(KEYLINE_FIRST))
val scope = rememberCoroutineScope()
@ -373,7 +381,10 @@ object PurchaseText {
}
@Composable
fun TasksAccount(subscribe: (Int, Boolean) -> Unit) {
fun TasksAccount(
skus: List<Sku>,
subscribe: (Int, Boolean) -> Unit,
) {
Column(
modifier = Modifier
.fillMaxWidth()
@ -385,15 +396,19 @@ object PurchaseText {
horizontalArrangement = Arrangement.Center
) {
PurchaseButton(
price = 30,
price = remember(skus) {
skus.find { it.productId == "annual_30" }?.price ?: "$30"
},
popperText = "${stringResource(R.string.save_percent, 16)} $POPPER",
onClick = subscribe
onClick = { subscribe(30, false) },
)
Spacer(Modifier.width(KEYLINE_FIRST))
PurchaseButton(
price = 3,
price = remember (skus) {
skus.find { it.productId == "monthly_03" }?.price ?: "$3"
},
monthly = true,
onClick = subscribe
onClick = { subscribe(3, true) },
)
}
}
@ -401,21 +416,21 @@ object PurchaseText {
@Composable
fun PurchaseButton(
price: Int,
price: String,
monthly: Boolean = false,
popperText: String = "",
onClick: (Int, Boolean) -> Unit,
onClick: () -> Unit,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Button(
onClick = { onClick(price, monthly) },
onClick = { onClick() },
colors = ButtonDefaults.textButtonColors(
containerColor = MaterialTheme.colorScheme.secondary
)
) {
Text(
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
),
color = MaterialTheme.colorScheme.onSecondary,
@ -435,6 +450,7 @@ object PurchaseText {
sliderPosition: Float,
setPrice: (Float) -> Unit,
subscribe: (Int, Boolean) -> Unit,
skus: List<Sku>,
) {
Column(
modifier = Modifier.fillMaxWidth(),
@ -460,21 +476,32 @@ object PurchaseText {
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
val price = sliderPosition.toInt()
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)
"${stringResource(R.string.above_average, 16)} $POPPER"
else
"",
onClick = subscribe
onClick = { subscribe(sliderPosition.toInt(), false) },
)
if (sliderPosition.toInt() < 3) {
Spacer(Modifier.width(KEYLINE_FIRST))
PurchaseButton(
price = sliderPosition.toInt(),
price = remember (skus, price) {
skus
.find { it.productId == "monthly_${price.toString().padStart(2, '0')}" }
?.price
?: "$$price"
},
monthly = true,
popperText = "${stringResource(R.string.above_average)} $POPPER",
onClick = subscribe
onClick = { subscribe(price, true) },
)
}
}
@ -494,6 +521,7 @@ private fun PurchaseDialogPreview() {
sliderPosition = 1f,
setPrice = {},
setNameYourPrice = {},
skus = emptyList(),
)
}
}
@ -510,6 +538,7 @@ private fun NameYourPricePreview() {
sliderPosition = 4f,
setPrice = {},
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="price_per_year">$%s/year</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="follow_reddit">Join r/tasks</string>
<string name="follow_twitter">Follow @tasks_org</string>

Loading…
Cancel
Save