Skip to content

Commit 834776c

Browse files
Initial implementation of google play billing (#1503)
1 parent 9c17b3e commit 834776c

File tree

12 files changed

+221
-19
lines changed

12 files changed

+221
-19
lines changed

app/build.gradle.kts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ val getGitHash = providers
4848

4949
val firebaseEnabledVariants = listOf("play", "fdroid")
5050
val nonPlayVariants = listOf("fdroid", "website") + if (huaweiEnabled) listOf("huawei") else emptyList()
51-
val nonDebugBuildTypes = listOf("release", "qa", "automaticQa")
51+
val nonDebugBuildTypes = listOf("release", "releaseWithDebugMenu", "qa", "automaticQa")
5252

5353
fun VariantDimension.devNetDefaultOn(defaultOn: Boolean) {
5454
val fqEnumClass = "org.session.libsession.utilities.Environment"
@@ -169,6 +169,12 @@ android {
169169
setAuthorityPostfix("")
170170
}
171171

172+
create("releaseWithDebugMenu") {
173+
initWith(getByName("release"))
174+
175+
matchingFallbacks += "release"
176+
}
177+
172178
create("qa") {
173179
initWith(getByName("release"))
174180

@@ -473,6 +479,9 @@ dependencies {
473479

474480
implementation(libs.androidx.biometric)
475481

482+
playImplementation(libs.android.billing)
483+
playImplementation(libs.android.billing.ktx)
484+
476485
debugImplementation(libs.sqlite.web.viewer)
477486
}
478487

app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,20 @@ fun DebugMenu(
235235
"Session Pro",
236236
verticalArrangement = Arrangement.spacedBy(0.dp)) {
237237
Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing))
238+
239+
Text(text = "Purchase a plan")
240+
Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
241+
242+
DropDown(
243+
selected = null,
244+
modifier = modifier,
245+
values = uiState.debugProPlans,
246+
onValueSelected = { sendCommand(DebugMenuViewModel.Commands.PurchaseDebugPlan(it)) },
247+
labeler = { it?.label ?: "Select a plan to buy" }
248+
)
249+
250+
Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing))
251+
238252
DebugSwitchRow(
239253
text = "Set current user as Pro",
240254
checked = uiState.forceCurrentUserAsPro,
@@ -735,7 +749,8 @@ fun PreviewDebugMenu() {
735749
messageProFeature = setOf(ProStatusManager.MessageProFeature.AnimatedAvatar),
736750
dbInspectorState = DebugMenuViewModel.DatabaseInspectorState.STARTED,
737751
debugSubscriptionStatuses = setOf(DebugMenuViewModel.DebugSubscriptionStatus.AUTO_GOOGLE),
738-
selectedDebugSubscriptionStatus = DebugMenuViewModel.DebugSubscriptionStatus.AUTO_GOOGLE
752+
selectedDebugSubscriptionStatus = DebugMenuViewModel.DebugSubscriptionStatus.AUTO_GOOGLE,
753+
debugProPlans = emptyList(),
739754
),
740755
sendCommand = {},
741756
onClose = {}

app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.database.RecipientSettingsDatabase
3838
import org.thoughtcrime.securesms.database.model.ThreadRecord
3939
import org.thoughtcrime.securesms.dependencies.ConfigFactory
4040
import org.thoughtcrime.securesms.pro.ProStatusManager
41+
import org.thoughtcrime.securesms.pro.subscription.SubscriptionManager
4142
import org.thoughtcrime.securesms.repository.ConversationRepository
4243
import org.thoughtcrime.securesms.tokenpage.TokenPageNotificationManager
4344
import org.thoughtcrime.securesms.util.ClearDataUtils
@@ -58,6 +59,7 @@ class DebugMenuViewModel @Inject constructor(
5859
private val attachmentDatabase: AttachmentDatabase,
5960
private val conversationRepository: ConversationRepository,
6061
private val databaseInspector: DatabaseInspector,
62+
subscriptionManagers: Set<@JvmSuppressWildcards SubscriptionManager>,
6163
) : ViewModel() {
6264
private val TAG = "DebugMenu"
6365

@@ -90,6 +92,9 @@ class DebugMenuViewModel @Inject constructor(
9092
DebugSubscriptionStatus.EXPIRED,
9193
),
9294
selectedDebugSubscriptionStatus = textSecurePreferences.getDebugSubscriptionType() ?: DebugSubscriptionStatus.AUTO_GOOGLE,
95+
debugProPlans = subscriptionManagers.asSequence()
96+
.flatMap { it.availablePlans.asSequence().map { plan -> DebugProPlan(it, plan) } }
97+
.toList(),
9398
)
9499
)
95100
val uiState: StateFlow<UIState>
@@ -295,6 +300,10 @@ class DebugMenuViewModel @Inject constructor(
295300
it.copy(selectedDebugSubscriptionStatus = command.status)
296301
}
297302
}
303+
304+
is Commands.PurchaseDebugPlan -> {
305+
command.plan.apply { manager.purchasePlan(plan) }
306+
}
298307
}
299308
}
300309

@@ -400,6 +409,7 @@ class DebugMenuViewModel @Inject constructor(
400409
val dbInspectorState: DatabaseInspectorState,
401410
val debugSubscriptionStatuses: Set<DebugSubscriptionStatus>,
402411
val selectedDebugSubscriptionStatus: DebugSubscriptionStatus,
412+
val debugProPlans: List<DebugProPlan>,
403413
)
404414

405415
enum class DatabaseInspectorState {
@@ -440,5 +450,6 @@ class DebugMenuViewModel @Inject constructor(
440450
data class GenerateContacts(val prefix: String, val count: Int): Commands()
441451
data object ToggleDatabaseInspector : Commands()
442452
data class SetDebugSubscriptionStatus(val status: DebugSubscriptionStatus) : Commands()
453+
data class PurchaseDebugPlan(val plan: DebugProPlan) : Commands()
443454
}
444455
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.thoughtcrime.securesms.debugmenu
2+
3+
import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration
4+
import org.thoughtcrime.securesms.pro.subscription.SubscriptionManager
5+
6+
data class DebugProPlan(
7+
val manager: SubscriptionManager,
8+
val plan: ProSubscriptionDuration
9+
) {
10+
val label: String get() = "${manager.id}-${plan.name}"
11+
}

app/src/main/java/org/thoughtcrime/securesms/dependencies/OnAppStartupComponents.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.notifications.BackgroundPollManager
2222
import org.thoughtcrime.securesms.notifications.PushRegistrationHandler
2323
import org.thoughtcrime.securesms.pro.ProStatusManager
2424
import org.thoughtcrime.securesms.pro.subscription.SubscriptionCoordinator
25+
import org.thoughtcrime.securesms.pro.subscription.SubscriptionManager
2526
import org.thoughtcrime.securesms.service.ExpiringMessageManager
2627
import org.thoughtcrime.securesms.tokenpage.TokenDataManager
2728
import org.thoughtcrime.securesms.util.AppVisibilityManager
@@ -68,6 +69,7 @@ class OnAppStartupComponents private constructor(
6869
subscriptionCoordinator: SubscriptionCoordinator,
6970
avatarUploadManager: AvatarUploadManager,
7071
configToDatabaseSync: ConfigToDatabaseSync,
72+
subscriptionManagers: Set<@JvmSuppressWildcards SubscriptionManager>,
7173
): this(
7274
components = listOf(
7375
configUploader,
@@ -99,6 +101,6 @@ class OnAppStartupComponents private constructor(
99101
subscriptionCoordinator,
100102
avatarUploadManager,
101103
configToDatabaseSync,
102-
)
104+
) + subscriptionManagers
103105
)
104106
}

app/src/main/java/org/thoughtcrime/securesms/pro/subscription/NoOpSubscriptionManager.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class NoOpSubscriptionManager @Inject constructor() : SubscriptionManager {
1212
override val iconRes = null
1313

1414
override fun purchasePlan(subscriptionDuration: ProSubscriptionDuration) {}
15+
override val availablePlans: List<ProSubscriptionDuration>
16+
get() = emptyList()
1517

1618
//todo PRO test out build type with no subscription providers available - What do we show on the Pro Settings page?
1719
}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package org.thoughtcrime.securesms.pro.subscription
22

3+
import org.thoughtcrime.securesms.dependencies.OnAppStartupComponent
4+
35
/**
46
* Represents the implementation details of a given subscription provider
57
*/
6-
interface SubscriptionManager {
8+
interface SubscriptionManager: OnAppStartupComponent {
79
val id: String
810
val displayName: String
911
val description: String
1012
val iconRes: Int?
1113

14+
val availablePlans: List<ProSubscriptionDuration>
15+
1216
fun purchasePlan(subscriptionDuration: ProSubscriptionDuration)
1317
}

app/src/main/java/org/thoughtcrime/securesms/ui/components/DropDown.kt

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,30 @@ import org.thoughtcrime.securesms.ui.theme.LocalType
2323
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
2424
import org.thoughtcrime.securesms.ui.theme.bold
2525

26-
@OptIn(ExperimentalMaterial3Api::class)
2726
@Composable
2827
fun DropDown(
2928
modifier: Modifier = Modifier,
3029
selectedText: String,
3130
values: List<String>,
3231
onValueSelected: (String) -> Unit
32+
) {
33+
DropDown(
34+
modifier = modifier,
35+
selected = selectedText,
36+
values = values,
37+
onValueSelected = onValueSelected,
38+
labeler = { it.orEmpty() }
39+
)
40+
}
41+
42+
@OptIn(ExperimentalMaterial3Api::class)
43+
@Composable
44+
fun <T> DropDown(
45+
modifier: Modifier = Modifier,
46+
selected: T?,
47+
values: List<T>,
48+
onValueSelected: (T) -> Unit,
49+
labeler: (T?) -> String,
3350
) {
3451
var expanded by remember { mutableStateOf(false) }
3552

@@ -41,7 +58,7 @@ fun DropDown(
4158
}
4259
) {
4360
TextField(
44-
value = selectedText,
61+
value = labeler(selected),
4562
onValueChange = {},
4663
readOnly = true,
4764
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
@@ -80,7 +97,7 @@ fun DropDown(
8097
DropdownMenuItem(
8198
text = {
8299
Text(
83-
text = item,
100+
text = labeler(item),
84101
style = LocalType.current.base
85102
)
86103
},
@@ -97,6 +114,7 @@ fun DropDown(
97114
}
98115
}
99116

117+
100118
@Preview
101119
@Composable
102120
fun PreviewDropDown() {
Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,141 @@
11
package org.thoughtcrime.securesms.pro.subscription
22

3+
import android.app.Application
4+
import android.widget.Toast
5+
import com.android.billingclient.api.BillingClient
6+
import com.android.billingclient.api.BillingClientStateListener
7+
import com.android.billingclient.api.BillingFlowParams
8+
import com.android.billingclient.api.BillingResult
9+
import com.android.billingclient.api.PendingPurchasesParams
10+
import com.android.billingclient.api.QueryProductDetailsParams
11+
import com.android.billingclient.api.queryProductDetails
12+
import kotlinx.coroutines.CancellationException
13+
import kotlinx.coroutines.CoroutineScope
14+
import kotlinx.coroutines.Dispatchers
15+
import kotlinx.coroutines.launch
16+
import kotlinx.coroutines.withContext
17+
import org.session.libsignal.utilities.Log
18+
import org.thoughtcrime.securesms.dependencies.ManagerScope
19+
import org.thoughtcrime.securesms.util.CurrentActivityObserver
320
import javax.inject.Inject
421

522
/**
623
* The Google Play Store implementation of our subscription manager
724
*/
8-
class PlayStoreSubscriptionManager @Inject constructor(): SubscriptionManager {
25+
class PlayStoreSubscriptionManager @Inject constructor(
26+
private val application: Application,
27+
@param:ManagerScope private val scope: CoroutineScope,
28+
private val currentActivityObserver: CurrentActivityObserver,
29+
) : SubscriptionManager {
930
override val id = "google_play_store"
1031
override val displayName = ""
1132
override val description = ""
1233
override val iconRes = null
1334

35+
private val billingClient by lazy {
36+
BillingClient.newBuilder(application)
37+
.setListener { result, purchases ->
38+
Log.d(TAG, "onPurchasesUpdated: $result, $purchases")
39+
}
40+
.enableAutoServiceReconnection()
41+
.enablePendingPurchases(
42+
PendingPurchasesParams.newBuilder()
43+
.enableOneTimeProducts()
44+
.enablePrepaidPlans()
45+
.build()
46+
)
47+
.build()
48+
}
49+
50+
override val availablePlans: List<ProSubscriptionDuration> =
51+
ProSubscriptionDuration.entries.toList()
52+
1453
override fun purchasePlan(subscriptionDuration: ProSubscriptionDuration) {
15-
//todo PRO implement
54+
scope.launch {
55+
try {
56+
val activity = checkNotNull(currentActivityObserver.currentActivity.value) {
57+
"No current activity available to launch the billing flow"
58+
}
59+
60+
val result = billingClient.queryProductDetails(
61+
QueryProductDetailsParams.newBuilder()
62+
.setProductList(
63+
listOf(
64+
QueryProductDetailsParams.Product.newBuilder()
65+
.setProductId("session_pro")
66+
.setProductType(BillingClient.ProductType.SUBS)
67+
.build()
68+
)
69+
)
70+
.build()
71+
)
72+
73+
check(result.billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
74+
"Failed to query product details. Reason: ${result.billingResult}"
75+
}
76+
77+
val productDetails = checkNotNull(result.productDetailsList?.firstOrNull()) {
78+
"Unable to get the product: product for given id is null"
79+
}
80+
81+
val planId = subscriptionDuration.planId
82+
83+
val offerDetails = checkNotNull(productDetails.subscriptionOfferDetails
84+
?.firstOrNull { it.basePlanId == planId }) {
85+
"Unable to find a plan with id $planId"
86+
}
87+
88+
val billingResult = billingClient.launchBillingFlow(
89+
activity, BillingFlowParams.newBuilder()
90+
.setProductDetailsParamsList(
91+
listOf(
92+
BillingFlowParams.ProductDetailsParams.newBuilder()
93+
.setProductDetails(productDetails)
94+
.setOfferToken(offerDetails.offerToken)
95+
.build()
96+
)
97+
)
98+
.build()
99+
)
100+
101+
check(billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
102+
"Unable to launch the billing flow. Reason: ${billingResult.debugMessage}"
103+
}
104+
105+
} catch (e: CancellationException) {
106+
throw e
107+
} catch (e: Exception) {
108+
Log.e(TAG, "Error purchase plan", e)
109+
110+
withContext(Dispatchers.Main) {
111+
Toast.makeText(application, e.message, Toast.LENGTH_LONG).show()
112+
}
113+
}
114+
}
115+
}
116+
117+
private val ProSubscriptionDuration.planId: String
118+
get() = when (this) {
119+
ProSubscriptionDuration.ONE_MONTH -> "session-pro-1-month"
120+
ProSubscriptionDuration.THREE_MONTHS -> "session-pro-3-months"
121+
ProSubscriptionDuration.TWELVE_MONTHS -> "session-pro-12-months"
122+
}
123+
124+
override fun onPostAppStarted() {
125+
super.onPostAppStarted()
126+
127+
billingClient.startConnection(object : BillingClientStateListener {
128+
override fun onBillingServiceDisconnected() {
129+
Log.w(TAG, "onBillingServiceDisconnected")
130+
}
131+
132+
override fun onBillingSetupFinished(result: BillingResult) {
133+
Log.d(TAG, "onBillingSetupFinished with $result")
134+
}
135+
})
136+
}
137+
138+
companion object {
139+
private const val TAG = "PlayStoreSubscriptionManager"
16140
}
17141
}

0 commit comments

Comments
 (0)