diff --git a/app/src/main/kotlin/net/primal/android/premium/buying/PremiumBuyingContract.kt b/app/src/main/kotlin/net/primal/android/premium/buying/PremiumBuyingContract.kt index b5481df3e..e5214b348 100644 --- a/app/src/main/kotlin/net/primal/android/premium/buying/PremiumBuyingContract.kt +++ b/app/src/main/kotlin/net/primal/android/premium/buying/PremiumBuyingContract.kt @@ -10,6 +10,7 @@ interface PremiumBuyingContract { val subscriptions: List = emptyList(), val stage: PremiumStage = PremiumStage.Home, val primalName: String? = null, + val hasActiveSubscription: Boolean = false, val profile: ProfileDetailsUi? = null, val promoCodeValidity: Boolean? = null, @@ -23,6 +24,8 @@ interface PremiumBuyingContract { data class ApplyPromoCode(val promoCode: String) : UiEvent() data object ClearPromoCodeValidity : UiEvent() + data object RestoreSubscription : UiEvent() + data class RequestPurchase( val activity: Activity, val subscriptionProduct: SubscriptionProduct, diff --git a/app/src/main/kotlin/net/primal/android/premium/buying/PremiumBuyingViewModel.kt b/app/src/main/kotlin/net/primal/android/premium/buying/PremiumBuyingViewModel.kt index 9f8502007..00f46392b 100644 --- a/app/src/main/kotlin/net/primal/android/premium/buying/PremiumBuyingViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/premium/buying/PremiumBuyingViewModel.kt @@ -21,6 +21,7 @@ import net.primal.android.profile.repository.ProfileRepository import net.primal.android.user.accounts.active.ActiveAccountStore import net.primal.android.wallet.store.PrimalBillingClient import net.primal.android.wallet.store.domain.InAppPurchaseException +import net.primal.android.wallet.store.domain.SubscriptionPurchase import timber.log.Timber @HiltViewModel @@ -31,6 +32,8 @@ class PremiumBuyingViewModel @Inject constructor( private val activeAccountStore: ActiveAccountStore, ) : ViewModel() { + private var purchase: SubscriptionPurchase? = null + private val _state = MutableStateFlow(UiState()) val state = _state.asStateFlow() private fun setState(reducer: UiState.() -> UiState) = _state.getAndUpdate { it.reducer() } @@ -42,10 +45,10 @@ class PremiumBuyingViewModel @Inject constructor( observeEvents() observePurchases() observeActiveProfile() - fetchSubscriptionProducts() + initBillingClient() } - private fun fetchSubscriptionProducts() { + private fun initBillingClient() { viewModelScope.launch { if (isGoogleBuild()) { val subscriptionProducts = primalBillingClient.querySubscriptionProducts() @@ -54,6 +57,16 @@ class PremiumBuyingViewModel @Inject constructor( premiumRepository.fetchMembershipProducts() setState { copy(loading = false) } } + + fetchActiveSubscription() + } + } + + private fun fetchActiveSubscription() { + viewModelScope.launch { + val purchases = primalBillingClient.queryActiveSubscriptions() + purchase = purchases.firstOrNull() + setState { copy(hasActiveSubscription = purchase != null) } } } @@ -66,6 +79,7 @@ class PremiumBuyingViewModel @Inject constructor( is UiEvent.ApplyPromoCode -> tryApplyPromoCode(it.promoCode) UiEvent.ClearPromoCodeValidity -> setState { copy(promoCodeValidity = null) } is UiEvent.RequestPurchase -> launchBillingFlow(it) + UiEvent.RestoreSubscription -> restorePurchase() } } } @@ -95,6 +109,8 @@ class PremiumBuyingViewModel @Inject constructor( setState { copy(stage = PremiumBuyingContract.PremiumStage.Success) } } catch (error: WssException) { Timber.e(error) + this@PremiumBuyingViewModel.purchase = purchase + setState { copy(hasActiveSubscription = true) } } } } @@ -118,4 +134,22 @@ class PremiumBuyingViewModel @Inject constructor( Timber.w(error) } } + + private fun restorePurchase() = + viewModelScope.launch { + val primalName = _state.value.primalName + val existingPurchase = purchase + if (primalName != null && existingPurchase != null) { + try { + premiumRepository.purchaseMembership( + userId = activeAccountStore.activeUserId(), + primalName = primalName, + purchase = existingPurchase, + ) + setState { copy(stage = PremiumBuyingContract.PremiumStage.Success) } + } catch (error: WssException) { + Timber.e(error) + } + } + } } diff --git a/app/src/main/kotlin/net/primal/android/premium/buying/purchase/PremiumPurchaseStage.kt b/app/src/main/kotlin/net/primal/android/premium/buying/purchase/PremiumPurchaseStage.kt index 5b7bfb90f..e0b143e03 100644 --- a/app/src/main/kotlin/net/primal/android/premium/buying/purchase/PremiumPurchaseStage.kt +++ b/app/src/main/kotlin/net/primal/android/premium/buying/purchase/PremiumPurchaseStage.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -141,8 +140,9 @@ fun PremiumPurchaseStage( if (activity != null) { BuyPremiumButtons( modifier = Modifier.padding(horizontal = 12.dp), + hasActiveSubscription = state.hasActiveSubscription, subscriptions = state.subscriptions, - onClick = { subscription -> + onBuySubscription = { subscription -> eventPublisher( PremiumBuyingContract.UiEvent.RequestPurchase( activity = activity, @@ -150,6 +150,9 @@ fun PremiumPurchaseStage( ), ) }, + onRestoreSubscription = { + eventPublisher(PremiumBuyingContract.UiEvent.RestoreSubscription) + }, ) TOSNotice() } @@ -190,21 +193,47 @@ private fun MoreInfoPromoCodeRow( @Composable fun BuyPremiumButtons( - modifier: Modifier = Modifier, + modifier: Modifier, + hasActiveSubscription: Boolean, subscriptions: List, - onClick: (SubscriptionProduct) -> Unit, + onBuySubscription: (SubscriptionProduct) -> Unit, + onRestoreSubscription: () -> Unit, ) { Column( modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp), ) { - subscriptions.forEach { - BuyPremiumButton( - startText = it.toGetSubscriptionString(), - endText = it.toPricingString(), - onClick = { onClick(it) }, + if (hasActiveSubscription) { + Text( + modifier = Modifier.padding(horizontal = 32.dp), + text = stringResource(R.string.premium_purchase_restore_subscription_explanation), + textAlign = TextAlign.Center, + style = AppTheme.typography.bodySmall, + color = AppTheme.extraColorScheme.onSurfaceVariantAlt2, ) + + PrimalFilledButton( + modifier = Modifier.fillMaxWidth(), + height = 64.dp, + shape = RoundedCornerShape(percent = 100), + onClick = onRestoreSubscription, + ) { + Text( + text = stringResource(R.string.premium_purchase_restore_subscription), + style = AppTheme.typography.bodyLarge, + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + ) + } + } else { + subscriptions.forEach { + BuyPremiumButton( + startText = it.toGetSubscriptionString(), + endText = it.toPricingString(), + onClick = { onBuySubscription(it) }, + ) + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fa499d97c..4ad50f2c0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -119,6 +119,8 @@ Have a promo code? Get Annual Plan Get Monthly Plan + Restore Your Subscription + Your Google Play account already has an active Primal Premium subscription, connected to a different Nostr account. You can restore it below: By proceeding you acknowledge that\nyou agree to our Terms of Service