Skip to content

Commit a8602c6

Browse files
Merge pull request #356 from Ayush0Chaudhary/fix-billing
Fix billing
2 parents e0ef406 + ee252c1 commit a8602c6

16 files changed

+1104
-130
lines changed

app/build.gradle.kts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,7 @@ android {
6666
buildConfigField("String", "GCLOUD_GATEWAY_URL", "\"$googlecloudGatewayURL\"")
6767
buildConfigField("String", "GCLOUD_PROXY_URL", "\"$googlecloudProxyURL\"")
6868
buildConfigField("String", "GCLOUD_PROXY_URL_KEY", "\"$googlecloudProxyURLKey\"")
69-
buildConfigField("String", "REVENUE_CAT_PUBLIC_URL", "\"$revenueCatSDK\"")
70-
buildConfigField("String", "REVENUECAT_API_KEY", "\"$revenueCatApiKey\"")
69+
buildConfigField("boolean", "ENABLE_LOGGING", "true")
7170

7271
}
7372

@@ -153,8 +152,7 @@ dependencies {
153152
implementation("com.google.firebase:firebase-crashlytics-ndk")
154153
implementation(libs.firebase.firestore)
155154
implementation("androidx.recyclerview:recyclerview:1.3.2")
156-
implementation("com.revenuecat.purchases:purchases:9.7.0")
157-
implementation("com.revenuecat.purchases:purchases-ui:9.7.0")
155+
implementation("com.android.billingclient:billing-ktx:7.0.0")
158156
}
159157

160158
// Task to increment version for release builds

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
<activity android:name=".triggers.ui.TriggersActivity" android:exported="false" android:label="Triggers" android:theme="@style/Theme.Blurr" />
7777
<activity android:name=".triggers.ui.CreateTriggerActivity" android:exported="false" android:label="Create Trigger" android:theme="@style/Theme.Blurr" android:windowSoftInputMode="adjustResize" />
7878
<activity android:name=".triggers.ui.ChooseTriggerTypeActivity" android:exported="false" android:label="Choose Trigger Type" android:theme="@style/Theme.Blurr" />
79+
<activity android:name=".ProPurchaseActivity" android:exported="false" android:label="Upgrade to Pro" android:theme="@style/Theme.Blurr" />
7980

8081

8182
<!-- Your Other Services -->

app/src/main/java/com/blurr/voice/MainActivity.kt

Lines changed: 304 additions & 38 deletions
Large diffs are not rendered by default.
Lines changed: 80 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,111 @@
1-
package com.blurr.voice // Use your app's package name
1+
package com.blurr.voice
22

33
import android.app.Application
44
import android.content.Context
5-
import com.blurr.voice.intents.IntentRegistry
65
import android.content.Intent
6+
import com.blurr.voice.utilities.Logger
7+
import com.android.billingclient.api.*
8+
import com.blurr.voice.intents.IntentRegistry
79
import com.blurr.voice.intents.impl.DialIntent
810
import com.blurr.voice.intents.impl.EmailComposeIntent
911
import com.blurr.voice.intents.impl.ShareTextIntent
1012
import com.blurr.voice.intents.impl.ViewUrlIntent
1113
import com.blurr.voice.triggers.TriggerMonitoringService
12-
import com.revenuecat.purchases.LogLevel
13-
import com.revenuecat.purchases.Purchases
14-
import com.revenuecat.purchases.PurchasesConfiguration
14+
import kotlinx.coroutines.*
15+
import kotlinx.coroutines.flow.MutableStateFlow
16+
import kotlinx.coroutines.flow.StateFlow
17+
import kotlinx.coroutines.flow.asStateFlow
18+
import kotlin.math.pow
1519

16-
class MyApplication : Application() {
20+
class MyApplication : Application(), PurchasesUpdatedListener {
21+
22+
private val applicationScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
23+
private var reconnectAttempts = 0
24+
private val maxReconnectAttempts = 5
25+
private val initialReconnectDelayMs = 1000L
1726

1827
companion object {
1928
lateinit var appContext: Context
20-
private set // Make the setter private to ensure it's not changed elsewhere
29+
private set
30+
31+
lateinit var billingClient: BillingClient
32+
private set
33+
34+
private val _isBillingClientReady = MutableStateFlow(false)
35+
val isBillingClientReady: StateFlow<Boolean> = _isBillingClientReady.asStateFlow()
2136
}
2237

2338
override fun onCreate() {
2439
super.onCreate()
2540
appContext = applicationContext
2641

27-
Purchases.logLevel = LogLevel.DEBUG
28-
Purchases.configure(
29-
PurchasesConfiguration.Builder(this, BuildConfig.REVENUECAT_API_KEY).build()
30-
)
42+
billingClient = BillingClient.newBuilder(this)
43+
.setListener(this)
44+
.enablePendingPurchases()
45+
.build()
46+
47+
connectToBillingService()
3148

32-
// Register built-in app intents (plug-and-play extensions can add their own here)
3349
IntentRegistry.register(DialIntent())
3450
IntentRegistry.register(ViewUrlIntent())
3551
IntentRegistry.register(ShareTextIntent())
3652
IntentRegistry.register(EmailComposeIntent())
37-
// Optional: initialize registry scanning for additional implementations
3853
IntentRegistry.init(this)
3954

40-
// Start the trigger monitoring service
4155
val serviceIntent = Intent(this, TriggerMonitoringService::class.java)
4256
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
4357
startForegroundService(serviceIntent)
4458
} else {
4559
startService(serviceIntent)
46-
} }
47-
}
60+
}
61+
}
62+
63+
private fun connectToBillingService() {
64+
if (billingClient.isReady) {
65+
Logger.d("MyApplication", "BillingClient is already connected.")
66+
return
67+
}
68+
billingClient.startConnection(object : BillingClientStateListener {
69+
override fun onBillingSetupFinished(billingResult: BillingResult) {
70+
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
71+
Logger.d("MyApplication", "BillingClient setup successfully.")
72+
_isBillingClientReady.value = true
73+
reconnectAttempts = 0
74+
} else {
75+
Logger.e("MyApplication", "BillingClient setup failed: ${billingResult.debugMessage}")
76+
_isBillingClientReady.value = false
77+
retryConnectionWithBackoff()
78+
}
79+
}
80+
81+
override fun onBillingServiceDisconnected() {
82+
Logger.w("MyApplication", "Billing service disconnected. Retrying...")
83+
_isBillingClientReady.value = false
84+
retryConnectionWithBackoff()
85+
}
86+
})
87+
}
88+
89+
private fun retryConnectionWithBackoff() {
90+
if (reconnectAttempts < maxReconnectAttempts) {
91+
val delay = initialReconnectDelayMs * (2.0.pow(reconnectAttempts)).toLong()
92+
applicationScope.launch {
93+
delay(delay)
94+
reconnectAttempts++
95+
Logger.d("MyApplication", "Retrying connection, attempt #$reconnectAttempts")
96+
connectToBillingService()
97+
}
98+
} else {
99+
Logger.e("MyApplication", "Max reconnect attempts reached. Will not retry further.")
100+
}
101+
}
102+
103+
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: MutableList<Purchase>?) {
104+
Logger.d("MyApplication", "Purchase update received")
105+
// Send broadcast to MainActivity to handle the purchase update
106+
val intent = Intent("com.blurr.voice.PURCHASE_UPDATED")
107+
intent.putExtra("response_code", billingResult.responseCode)
108+
intent.putExtra("debug_message", billingResult.debugMessage)
109+
appContext.sendBroadcast(intent)
110+
}
111+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package com.blurr.voice
2+
3+
import android.os.Bundle
4+
import android.util.Log
5+
import android.view.View
6+
import android.widget.Button
7+
import android.widget.ProgressBar
8+
import android.widget.TextView
9+
import android.widget.Toast
10+
import androidx.appcompat.app.AppCompatActivity
11+
import androidx.lifecycle.lifecycleScope
12+
import com.android.billingclient.api.*
13+
import com.blurr.voice.MyApplication
14+
import kotlinx.coroutines.flow.first
15+
import kotlinx.coroutines.launch
16+
import kotlinx.coroutines.withTimeoutOrNull
17+
18+
class ProPurchaseActivity : AppCompatActivity(), PurchasesUpdatedListener {
19+
20+
private lateinit var priceTextView: TextView
21+
private lateinit var purchaseButton: Button
22+
private lateinit var loadingProgressBar: ProgressBar
23+
private lateinit var featuresTextView: TextView
24+
private lateinit var backButton: View
25+
26+
private val billingClient: BillingClient = MyApplication.billingClient
27+
private var productDetails: ProductDetails? = null
28+
29+
companion object {
30+
private const val PRO_SKU = "panda_premium_monthly"
31+
private const val TAG = "ProPurchaseActivity"
32+
}
33+
34+
override fun onCreate(savedInstanceState: Bundle?) {
35+
super.onCreate(savedInstanceState)
36+
setContentView(R.layout.activity_pro_purchase)
37+
38+
initializeViews()
39+
setupClickListeners()
40+
loadProductDetails()
41+
}
42+
43+
private fun initializeViews() {
44+
priceTextView = findViewById(R.id.price_text)
45+
purchaseButton = findViewById(R.id.purchase_button)
46+
loadingProgressBar = findViewById(R.id.loading_progress)
47+
// featuresTextView = findViewById(R.id.features_text)
48+
backButton = findViewById(R.id.back_button)
49+
50+
// Initially hide purchase button and show loading
51+
purchaseButton.visibility = View.GONE
52+
loadingProgressBar.visibility = View.VISIBLE
53+
}
54+
55+
private fun setupClickListeners() {
56+
backButton.setOnClickListener {
57+
finish()
58+
}
59+
60+
purchaseButton.setOnClickListener {
61+
launchPurchaseFlow()
62+
}
63+
}
64+
65+
private fun loadProductDetails() {
66+
lifecycleScope.launch {
67+
try {
68+
// Wait for billing client to be ready
69+
val isReady = withTimeoutOrNull(10000L) {
70+
MyApplication.isBillingClientReady.first { it }
71+
}
72+
73+
if (isReady != true) {
74+
showError("Unable to connect to Play Store. Please try again later.")
75+
return@launch
76+
}
77+
78+
val productList = listOf(
79+
QueryProductDetailsParams.Product.newBuilder()
80+
.setProductId(PRO_SKU)
81+
.setProductType(BillingClient.ProductType.SUBS)
82+
.build()
83+
)
84+
85+
val params = QueryProductDetailsParams.newBuilder()
86+
.setProductList(productList)
87+
.build()
88+
89+
val productDetailsResult = billingClient.queryProductDetails(params)
90+
91+
if (productDetailsResult.billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
92+
val productDetailsList = productDetailsResult.productDetailsList
93+
Log.d(TAG, "Product details: $productDetailsList")
94+
if (productDetailsList?.isNotEmpty() == true) {
95+
productDetails = productDetailsList[0]
96+
updateUIWithProductDetails()
97+
} else {
98+
showError("Pro subscription not available")
99+
}
100+
} else {
101+
showError("Failed to load pricing information")
102+
}
103+
104+
} catch (e: Exception) {
105+
Log.e(TAG, "Error loading product details", e)
106+
showError("Error loading pricing information")
107+
}
108+
}
109+
}
110+
111+
private fun updateUIWithProductDetails() {
112+
productDetails?.let { details ->
113+
val subscriptionOfferDetails = details.subscriptionOfferDetails?.firstOrNull()
114+
val pricingPhase = subscriptionOfferDetails?.pricingPhases?.pricingPhaseList?.firstOrNull()
115+
116+
if (pricingPhase != null) {
117+
val formattedPrice = pricingPhase.formattedPrice
118+
val billingPeriod = pricingPhase.billingPeriod
119+
120+
// Convert billing period to readable format
121+
val periodText = when {
122+
billingPeriod.contains("P1M") -> "month"
123+
billingPeriod.contains("P1Y") -> "year"
124+
billingPeriod.contains("P1W") -> "week"
125+
else -> "billing period"
126+
}
127+
128+
priceTextView.text = "$formattedPrice/$periodText"
129+
130+
// Show purchase button and hide loading
131+
loadingProgressBar.visibility = View.GONE
132+
purchaseButton.visibility = View.VISIBLE
133+
134+
} else {
135+
showError("Pricing information not available")
136+
}
137+
}
138+
}
139+
140+
private fun launchPurchaseFlow() {
141+
productDetails?.let { details ->
142+
val subscriptionOfferDetails = details.subscriptionOfferDetails?.firstOrNull()
143+
if (subscriptionOfferDetails != null) {
144+
val productDetailsParamsList = listOf(
145+
BillingFlowParams.ProductDetailsParams.newBuilder()
146+
.setProductDetails(details)
147+
.setOfferToken(subscriptionOfferDetails.offerToken)
148+
.build()
149+
)
150+
151+
val billingFlowParams = BillingFlowParams.newBuilder()
152+
.setProductDetailsParamsList(productDetailsParamsList)
153+
.build()
154+
155+
val billingResult = billingClient.launchBillingFlow(this, billingFlowParams)
156+
157+
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
158+
Toast.makeText(this, "Failed to launch purchase flow", Toast.LENGTH_SHORT).show()
159+
}
160+
}
161+
}
162+
}
163+
164+
private fun showError(message: String) {
165+
loadingProgressBar.visibility = View.GONE
166+
priceTextView.text = "Pricing unavailable"
167+
purchaseButton.visibility = View.GONE
168+
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
169+
}
170+
171+
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: MutableList<Purchase>?) {
172+
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
173+
for (purchase in purchases) {
174+
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
175+
Toast.makeText(this, "Purchase successful! Welcome to Pro!", Toast.LENGTH_LONG).show()
176+
finish() // Close the purchase activity
177+
}
178+
}
179+
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
180+
Toast.makeText(this, "Purchase cancelled", Toast.LENGTH_SHORT).show()
181+
} else {
182+
Toast.makeText(this, "Purchase failed: ${billingResult.debugMessage}", Toast.LENGTH_LONG).show()
183+
}
184+
}
185+
}

0 commit comments

Comments
 (0)