Skip to content

Commit 3a8aa48

Browse files
fix the payments
1 parent c2ecbea commit 3a8aa48

16 files changed

+993
-207
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +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("boolean", "ENABLE_LOGGING", "true")
6970

7071
}
7172

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: 271 additions & 29 deletions
Large diffs are not rendered by default.

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

Lines changed: 14 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,14 @@ package com.blurr.voice
33
import android.app.Application
44
import android.content.Context
55
import android.content.Intent
6-
import android.util.Log
6+
import com.blurr.voice.utilities.Logger
77
import com.android.billingclient.api.*
88
import com.blurr.voice.intents.IntentRegistry
99
import com.blurr.voice.intents.impl.DialIntent
1010
import com.blurr.voice.intents.impl.EmailComposeIntent
1111
import com.blurr.voice.intents.impl.ShareTextIntent
1212
import com.blurr.voice.intents.impl.ViewUrlIntent
1313
import com.blurr.voice.triggers.TriggerMonitoringService
14-
import com.google.firebase.auth.ktx.auth
15-
import com.google.firebase.firestore.ktx.firestore
16-
import com.google.firebase.ktx.Firebase
1714
import kotlinx.coroutines.*
1815
import kotlinx.coroutines.flow.MutableStateFlow
1916
import kotlinx.coroutines.flow.StateFlow
@@ -65,25 +62,24 @@ class MyApplication : Application(), PurchasesUpdatedListener {
6562

6663
private fun connectToBillingService() {
6764
if (billingClient.isReady) {
68-
Log.d("MyApplication", "BillingClient is already connected.")
65+
Logger.d("MyApplication", "BillingClient is already connected.")
6966
return
7067
}
7168
billingClient.startConnection(object : BillingClientStateListener {
7269
override fun onBillingSetupFinished(billingResult: BillingResult) {
7370
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
74-
Log.d("MyApplication", "BillingClient setup successfully.")
71+
Logger.d("MyApplication", "BillingClient setup successfully.")
7572
_isBillingClientReady.value = true
7673
reconnectAttempts = 0
77-
queryPurchases()
7874
} else {
79-
Log.e("MyApplication", "BillingClient setup failed: ${billingResult.debugMessage}")
75+
Logger.e("MyApplication", "BillingClient setup failed: ${billingResult.debugMessage}")
8076
_isBillingClientReady.value = false
8177
retryConnectionWithBackoff()
8278
}
8379
}
8480

8581
override fun onBillingServiceDisconnected() {
86-
Log.w("MyApplication", "Billing service disconnected. Retrying...")
82+
Logger.w("MyApplication", "Billing service disconnected. Retrying...")
8783
_isBillingClientReady.value = false
8884
retryConnectionWithBackoff()
8985
}
@@ -95,89 +91,21 @@ class MyApplication : Application(), PurchasesUpdatedListener {
9591
val delay = initialReconnectDelayMs * (2.0.pow(reconnectAttempts)).toLong()
9692
applicationScope.launch {
9793
delay(delay)
98-
Log.d("MyApplication", "Retrying connection, attempt #${reconnectAttempts + 1}")
94+
reconnectAttempts++
95+
Logger.d("MyApplication", "Retrying connection, attempt #$reconnectAttempts")
9996
connectToBillingService()
10097
}
101-
reconnectAttempts++
10298
} else {
103-
Log.e("MyApplication", "Max reconnect attempts reached. Will not retry further.")
104-
}
105-
}
106-
107-
private fun queryPurchases() {
108-
if (!_isBillingClientReady.value) {
109-
Log.e("MyApplication", "queryPurchases: BillingClient is not ready")
110-
return
111-
}
112-
applicationScope.launch {
113-
val params = QueryPurchasesParams.newBuilder()
114-
.setProductType(BillingClient.ProductType.SUBS)
115-
.build()
116-
val purchasesResult = billingClient.queryPurchasesAsync(params)
117-
val billingResult = purchasesResult.billingResult
118-
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
119-
purchasesResult.purchasesList.forEach { purchase ->
120-
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
121-
handlePurchase(purchase)
122-
}
123-
}
124-
} else {
125-
Log.e("MyApplication", "Failed to query purchases: ${billingResult.debugMessage}")
126-
}
99+
Logger.e("MyApplication", "Max reconnect attempts reached. Will not retry further.")
127100
}
128101
}
129102

130103
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: MutableList<Purchase>?) {
131-
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
132-
for (purchase in purchases) {
133-
handlePurchase(purchase)
134-
}
135-
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
136-
Log.d("MyApplication", "User cancelled the purchase.")
137-
} else {
138-
Log.e("MyApplication", "Purchase error: ${billingResult.debugMessage}")
139-
}
140-
}
141-
142-
private fun handlePurchase(purchase: Purchase) {
143-
applicationScope.launch(Dispatchers.IO) {
144-
try {
145-
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
146-
if (!purchase.isAcknowledged) {
147-
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
148-
.setPurchaseToken(purchase.purchaseToken)
149-
.build()
150-
billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult ->
151-
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
152-
Log.d("MyApplication", "Purchase acknowledged: ${purchase.orderId}")
153-
updateUserToPro()
154-
} else {
155-
Log.e("MyApplication", "Failed to acknowledge purchase: ${billingResult.debugMessage}")
156-
}
157-
}
158-
} else {
159-
// Purchase already acknowledged, ensure backend is updated.
160-
updateUserToPro()
161-
}
162-
}
163-
} catch (e: Exception) {
164-
Log.e("MyApplication", "Error handling purchase", e)
165-
}
166-
}
167-
}
168-
169-
private fun updateUserToPro() {
170-
val auth = Firebase.auth
171-
val db = Firebase.firestore
172-
auth.currentUser?.uid?.let { uid ->
173-
val userDocRef = db.collection("users").document(uid)
174-
userDocRef.update("plan", "pro")
175-
.addOnSuccessListener {
176-
Log.d("MyApplication", "User plan updated to 'pro' in Firestore.")
177-
}
178-
.addOnFailureListener { e ->
179-
Log.e("MyApplication", "Failed to update user plan in Firestore.", e)
180-
}
181-
} ?: Log.e("MyApplication", "Cannot update user to pro: current user is null.")
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)
182110
}
183111
}
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)