Skip to content

Commit

Permalink
Merge branch 'release/3.1.9'
Browse files Browse the repository at this point in the history
  • Loading branch information
benoitletondor committed Feb 20, 2024
2 parents e8fa974 + de5cd42 commit d6b57a1
Show file tree
Hide file tree
Showing 15 changed files with 308 additions and 50 deletions.
4 changes: 2 additions & 2 deletions Android/EasyBudget/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ android {
compileSdk = 34
minSdk = 21
targetSdk = 34
versionCode = 127
versionName = "3.1.8"
versionCode = 128
versionName = "3.1.9"
vectorDrawables.useSupportLibrary = true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,13 @@ interface Iab {
suspend fun isUserPremium(): Boolean
suspend fun isUserPro(): Boolean
fun updateIAPStatusIfNeeded()
suspend fun fetchPricing(): Pricing
suspend fun launchPremiumSubscriptionFlow(activity: Activity): PurchaseFlowResult
suspend fun launchProSubscriptionFlow(activity: Activity): PurchaseFlowResult
}

data class Pricing(val premiumPricing: String, val proPricing: String)

sealed class PurchaseFlowResult {
data object Cancelled : PurchaseFlowResult()
data class Success(val purchaseType: PurchaseType) : PurchaseFlowResult()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,64 @@ class IabImpl(
}
}

override suspend fun fetchPricing(): Pricing {
if(iabStatusMutableFlow.value == PremiumCheckStatus.INITIALIZING || iabStatusMutableFlow.value == PremiumCheckStatus.ERROR) {
throw IllegalStateException("IAB is not setup")
}

val skuList = listOf(
SKU_PREMIUM_SUBSCRIPTION,
SKU_PRO_SUBSCRIPTION,
)

val productList = skuList.map { productId ->
QueryProductDetailsParams.Product
.newBuilder()
.setProductId(productId)
.setProductType(BillingClient.ProductType.SUBS)
.build()
}

val (billingResult, skuDetailsList) = billingClient.queryProductDetails(
QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build()
)

if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
throw IllegalStateException("Unable to connect to reach PlayStore (response code: " + billingResult.responseCode + "). Please restart the app and try again")
}

if(skuDetailsList == null) {
throw IllegalStateException("Unable to get details from PlayStore. Please restart the app and try again")
}

val premiumSubscriptionPrice = skuDetailsList
.first { it.productId == SKU_PREMIUM_SUBSCRIPTION }
.subscriptionOfferDetails
?.first()
?.pricingPhases
?.pricingPhaseList
?.first()
?.formattedPrice
?: throw IllegalArgumentException("No price for premium subscription")

val proSubscriptionPrice = skuDetailsList
.first { it.productId == SKU_PRO_SUBSCRIPTION }
.subscriptionOfferDetails
?.first()
?.pricingPhases
?.pricingPhaseList
?.first()
?.formattedPrice
?: throw IllegalArgumentException("No price for pro subscription")

return Pricing(
premiumPricing = premiumSubscriptionPrice,
proPricing = proSubscriptionPrice,
)
}

private fun queryPurchases() {
queryPurchasesJob?.cancel()
queryPurchasesJob = scope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.benoitletondor.easybudgetapp.helper.setStatusBarColor
import com.benoitletondor.easybudgetapp.iab.PurchaseFlowResult
import com.benoitletondor.easybudgetapp.theme.AppTheme
import com.benoitletondor.easybudgetapp.theme.easyBudgetGreenColor
import com.benoitletondor.easybudgetapp.view.premium.view.ErrorView
import com.benoitletondor.easybudgetapp.view.premium.view.LoadingView
import com.benoitletondor.easybudgetapp.view.premium.view.SubscribeView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
Expand All @@ -63,18 +64,13 @@ class PremiumActivity : AppCompatActivity() {
) {
val state by viewModel.userSubscriptionStatus.collectAsState(PremiumViewModel.SubscriptionStatus.Verifying)

when (state) {
PremiumViewModel.SubscriptionStatus.Error,
PremiumViewModel.SubscriptionStatus.NotSubscribed,
PremiumViewModel.SubscriptionStatus.PremiumSubscribed,
PremiumViewModel.SubscriptionStatus.ProSubscribed -> SubscribeView(
viewModel,
when (val currentState = state) {
is PremiumViewModel.WithPricing -> SubscribeView(
currentState.pricing,
showProByDefault = intent.getBooleanExtra(EXTRA_SHOW_PRO, false),
premiumSubscribed = state == PremiumViewModel.SubscriptionStatus.PremiumSubscribed || state == PremiumViewModel.SubscriptionStatus.ProSubscribed,
proSubscribed = state == PremiumViewModel.SubscriptionStatus.ProSubscribed,
onCancelButtonClicked = {
finish()
},
premiumSubscribed = currentState is PremiumViewModel.SubscriptionStatus.PremiumSubscribed || currentState is PremiumViewModel.SubscriptionStatus.ProSubscribed,
proSubscribed = currentState is PremiumViewModel.SubscriptionStatus.ProSubscribed,
onCancelButtonClicked = this@PremiumActivity::finish,
onBuyPremiumButtonClicked = {
viewModel.onBuyPremiumClicked(this@PremiumActivity)
},
Expand All @@ -84,6 +80,10 @@ class PremiumActivity : AppCompatActivity() {
)

PremiumViewModel.SubscriptionStatus.Verifying -> LoadingView()
PremiumViewModel.SubscriptionStatus.Error -> ErrorView(
onRetryButtonPressed = viewModel::onRetryButtonPressed,
onCloseButtonPressed = viewModel::onCloseButtonPressed,
)
}
}
}
Expand Down Expand Up @@ -133,6 +133,12 @@ class PremiumActivity : AppCompatActivity() {
}
}
}

lifecycleScope.launchCollect(viewModel.eventFlow) { event ->
when(event) {
PremiumViewModel.Event.Finish -> finish()
}
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,53 @@ import com.benoitletondor.easybudgetapp.helper.Logger
import com.benoitletondor.easybudgetapp.helper.MutableLiveFlow
import com.benoitletondor.easybudgetapp.iab.Iab
import com.benoitletondor.easybudgetapp.iab.PremiumCheckStatus
import com.benoitletondor.easybudgetapp.iab.Pricing
import com.benoitletondor.easybudgetapp.iab.PurchaseFlowResult
import com.benoitletondor.easybudgetapp.iab.PurchaseType
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.retryWhen
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class PremiumViewModel @Inject constructor(
private val iab: Iab,
) : ViewModel() {
val userSubscriptionStatus: Flow<SubscriptionStatus> = iab.iabStatusFlow
.map { iabStatus ->
return@map when(iabStatus) {
PremiumCheckStatus.INITIALIZING, PremiumCheckStatus.CHECKING -> SubscriptionStatus.Verifying
PremiumCheckStatus.ERROR -> SubscriptionStatus.Error
PremiumCheckStatus.NOT_PREMIUM -> SubscriptionStatus.NotSubscribed
PremiumCheckStatus.LEGACY_PREMIUM,
PremiumCheckStatus.PREMIUM_SUBSCRIBED -> SubscriptionStatus.PremiumSubscribed
PremiumCheckStatus.PRO_SUBSCRIBED -> SubscriptionStatus.ProSubscribed
}
private val errorRetryMutableSharedFlow = MutableSharedFlow<Unit>()

private val eventMutableSharedFlow = MutableSharedFlow<Event>()
val eventFlow: Flow<Event> = eventMutableSharedFlow

@OptIn(ExperimentalCoroutinesApi::class)
val userSubscriptionStatus: Flow<SubscriptionStatus> = flow { emit(iab.fetchPricing()) }
.flatMapLatest { pricing ->
iab.iabStatusFlow
.map { iabStatus ->
return@map when(iabStatus) {
PremiumCheckStatus.INITIALIZING, PremiumCheckStatus.CHECKING -> SubscriptionStatus.Verifying
PremiumCheckStatus.ERROR -> SubscriptionStatus.Error
PremiumCheckStatus.NOT_PREMIUM -> SubscriptionStatus.NotSubscribed(pricing)
PremiumCheckStatus.LEGACY_PREMIUM,
PremiumCheckStatus.PREMIUM_SUBSCRIBED -> SubscriptionStatus.PremiumSubscribed(pricing)
PremiumCheckStatus.PRO_SUBSCRIBED -> SubscriptionStatus.ProSubscribed(pricing)
}
} }
.retryWhen { cause, _ ->
Logger.error("Error while fetching subscription pricing", cause)
emit(SubscriptionStatus.Error)

errorRetryMutableSharedFlow.first()

true
}

private val premiumPurchaseStatusMutableFlow = MutableStateFlow(PurchaseFlowStatus.NOT_STARTED)
Expand All @@ -60,6 +84,18 @@ class PremiumViewModel @Inject constructor(
private val proPurchaseEventMutableFlow = MutableLiveFlow<PurchaseFlowResult>()
val proPurchaseEventFlow: Flow<PurchaseFlowResult> = proPurchaseEventMutableFlow

fun onRetryButtonPressed() {
viewModelScope.launch {
errorRetryMutableSharedFlow.emit(Unit)
}
}

fun onCloseButtonPressed() {
viewModelScope.launch {
eventMutableSharedFlow.emit(Event.Finish)
}
}

fun onBuyPremiumClicked(activity: Activity) {
premiumPurchaseStatusMutableFlow.value = PurchaseFlowStatus.LOADING

Expand Down Expand Up @@ -117,9 +153,17 @@ class PremiumViewModel @Inject constructor(
sealed class SubscriptionStatus {
data object Verifying : SubscriptionStatus()
data object Error : SubscriptionStatus()
data object NotSubscribed : SubscriptionStatus()
data object PremiumSubscribed : SubscriptionStatus()
data object ProSubscribed : SubscriptionStatus()
data class NotSubscribed(override val pricing: Pricing) : SubscriptionStatus(), WithPricing
data class PremiumSubscribed(override val pricing: Pricing) : SubscriptionStatus(), WithPricing
data class ProSubscribed(override val pricing: Pricing) : SubscriptionStatus(), WithPricing
}

sealed interface WithPricing {
val pricing: Pricing
}

sealed class Event {
data object Finish : Event()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.benoitletondor.easybudgetapp.view.premium.view

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
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.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.benoitletondor.easybudgetapp.R
import com.benoitletondor.easybudgetapp.theme.AppTheme
import com.benoitletondor.easybudgetapp.theme.easyBudgetGreenColor

@Composable
fun ErrorView(
onRetryButtonPressed: () -> Unit,
onCloseButtonPressed: () -> Unit,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(horizontal = 20.dp, vertical = 16.dp),
verticalArrangement = Arrangement.Center,
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.premium_screen_error_loading_title),
textAlign = TextAlign.Center,
fontWeight = FontWeight.SemiBold,
color = Color.White,
fontSize = 16.sp,
)

Spacer(modifier = Modifier.height(10.dp))

Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.premium_screen_error_loading_message),
color = Color.White,
fontSize = 16.sp,
)

Spacer(modifier = Modifier.height(16.dp))

Button(
modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = onRetryButtonPressed,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary,
contentColor = MaterialTheme.colorScheme.onSecondary,
),
) {
Text(stringResource(R.string.manage_account_error_cta))
}

Spacer(modifier = Modifier.height(50.dp))

Button(
modifier = Modifier.align(Alignment.CenterHorizontally),
onClick = onCloseButtonPressed,
) {
Text(stringResource(R.string.premium_screen_error_close_cta))
}
}
}

@Composable
@Preview(showSystemUi = true)
private fun Preview() {
AppTheme {
Box(
modifier = Modifier
.background(easyBudgetGreenColor)
.fillMaxWidth()
.fillMaxHeight()
) {
ErrorView(
onRetryButtonPressed = {},
onCloseButtonPressed = {},
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color

@Composable
fun BoxScope.LoadingView() {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center),
color = Color.White,
)
}
Loading

0 comments on commit d6b57a1

Please sign in to comment.