Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ val getGitHash = providers

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

fun VariantDimension.devNetDefaultOn(defaultOn: Boolean) {
val fqEnumClass = "org.session.libsession.utilities.Environment"
Expand Down Expand Up @@ -169,6 +169,12 @@ android {
setAuthorityPostfix("")
}

create("releaseWithDebugMenu") {
initWith(getByName("release"))

matchingFallbacks += "release"
}

create("qa") {
initWith(getByName("release"))

Expand Down Expand Up @@ -473,6 +479,9 @@ dependencies {

implementation(libs.androidx.biometric)

playImplementation(libs.android.billing)
playImplementation(libs.android.billing.ktx)

debugImplementation(libs.sqlite.web.viewer)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,20 @@ fun DebugMenu(
"Session Pro",
verticalArrangement = Arrangement.spacedBy(0.dp)) {
Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing))

Text(text = "Purchase a plan")
Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))

DropDown(
selected = null,
modifier = modifier,
values = uiState.debugProPlans,
onValueSelected = { sendCommand(DebugMenuViewModel.Commands.PurchaseDebugPlan(it)) },
labeler = { it?.label ?: "Select a plan to buy" }
)

Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing))

DebugSwitchRow(
text = "Set current user as Pro",
checked = uiState.forceCurrentUserAsPro,
Expand Down Expand Up @@ -735,7 +749,8 @@ fun PreviewDebugMenu() {
messageProFeature = setOf(ProStatusManager.MessageProFeature.AnimatedAvatar),
dbInspectorState = DebugMenuViewModel.DatabaseInspectorState.STARTED,
debugSubscriptionStatuses = setOf(DebugMenuViewModel.DebugSubscriptionStatus.AUTO_GOOGLE),
selectedDebugSubscriptionStatus = DebugMenuViewModel.DebugSubscriptionStatus.AUTO_GOOGLE
selectedDebugSubscriptionStatus = DebugMenuViewModel.DebugSubscriptionStatus.AUTO_GOOGLE,
debugProPlans = emptyList(),
),
sendCommand = {},
onClose = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.database.RecipientSettingsDatabase
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.pro.ProStatusManager
import org.thoughtcrime.securesms.pro.subscription.SubscriptionManager
import org.thoughtcrime.securesms.repository.ConversationRepository
import org.thoughtcrime.securesms.tokenpage.TokenPageNotificationManager
import org.thoughtcrime.securesms.util.ClearDataUtils
Expand All @@ -58,6 +59,7 @@ class DebugMenuViewModel @Inject constructor(
private val attachmentDatabase: AttachmentDatabase,
private val conversationRepository: ConversationRepository,
private val databaseInspector: DatabaseInspector,
subscriptionManagers: Set<@JvmSuppressWildcards SubscriptionManager>,
) : ViewModel() {
private val TAG = "DebugMenu"

Expand Down Expand Up @@ -90,6 +92,9 @@ class DebugMenuViewModel @Inject constructor(
DebugSubscriptionStatus.EXPIRED,
),
selectedDebugSubscriptionStatus = textSecurePreferences.getDebugSubscriptionType() ?: DebugSubscriptionStatus.AUTO_GOOGLE,
debugProPlans = subscriptionManagers.asSequence()
.flatMap { it.availablePlans.asSequence().map { plan -> DebugProPlan(it, plan) } }
.toList(),
)
)
val uiState: StateFlow<UIState>
Expand Down Expand Up @@ -295,6 +300,10 @@ class DebugMenuViewModel @Inject constructor(
it.copy(selectedDebugSubscriptionStatus = command.status)
}
}

is Commands.PurchaseDebugPlan -> {
command.plan.apply { manager.purchasePlan(plan) }
}
}
}

Expand Down Expand Up @@ -400,6 +409,7 @@ class DebugMenuViewModel @Inject constructor(
val dbInspectorState: DatabaseInspectorState,
val debugSubscriptionStatuses: Set<DebugSubscriptionStatus>,
val selectedDebugSubscriptionStatus: DebugSubscriptionStatus,
val debugProPlans: List<DebugProPlan>,
)

enum class DatabaseInspectorState {
Expand Down Expand Up @@ -440,5 +450,6 @@ class DebugMenuViewModel @Inject constructor(
data class GenerateContacts(val prefix: String, val count: Int): Commands()
data object ToggleDatabaseInspector : Commands()
data class SetDebugSubscriptionStatus(val status: DebugSubscriptionStatus) : Commands()
data class PurchaseDebugPlan(val plan: DebugProPlan) : Commands()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.thoughtcrime.securesms.debugmenu

import org.thoughtcrime.securesms.pro.subscription.ProSubscriptionDuration
import org.thoughtcrime.securesms.pro.subscription.SubscriptionManager

data class DebugProPlan(
val manager: SubscriptionManager,
val plan: ProSubscriptionDuration
) {
val label: String get() = "${manager.id}-${plan.name}"
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.notifications.BackgroundPollManager
import org.thoughtcrime.securesms.notifications.PushRegistrationHandler
import org.thoughtcrime.securesms.pro.ProStatusManager
import org.thoughtcrime.securesms.pro.subscription.SubscriptionCoordinator
import org.thoughtcrime.securesms.pro.subscription.SubscriptionManager
import org.thoughtcrime.securesms.service.ExpiringMessageManager
import org.thoughtcrime.securesms.tokenpage.TokenDataManager
import org.thoughtcrime.securesms.util.AppVisibilityManager
Expand Down Expand Up @@ -68,6 +69,7 @@ class OnAppStartupComponents private constructor(
subscriptionCoordinator: SubscriptionCoordinator,
avatarUploadManager: AvatarUploadManager,
configToDatabaseSync: ConfigToDatabaseSync,
subscriptionManagers: Set<@JvmSuppressWildcards SubscriptionManager>,
): this(
components = listOf(
configUploader,
Expand Down Expand Up @@ -99,6 +101,6 @@ class OnAppStartupComponents private constructor(
subscriptionCoordinator,
avatarUploadManager,
configToDatabaseSync,
)
) + subscriptionManagers
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class NoOpSubscriptionManager @Inject constructor() : SubscriptionManager {
override val iconRes = null

override fun purchasePlan(subscriptionDuration: ProSubscriptionDuration) {}
override val availablePlans: List<ProSubscriptionDuration>
get() = emptyList()

//todo PRO test out build type with no subscription providers available - What do we show on the Pro Settings page?
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package org.thoughtcrime.securesms.pro.subscription

import org.thoughtcrime.securesms.dependencies.OnAppStartupComponent

/**
* Represents the implementation details of a given subscription provider
*/
interface SubscriptionManager {
interface SubscriptionManager: OnAppStartupComponent {
val id: String
val displayName: String
val description: String
val iconRes: Int?

val availablePlans: List<ProSubscriptionDuration>

fun purchasePlan(subscriptionDuration: ProSubscriptionDuration)
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,30 @@ import org.thoughtcrime.securesms.ui.theme.LocalType
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.bold

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DropDown(
modifier: Modifier = Modifier,
selectedText: String,
values: List<String>,
onValueSelected: (String) -> Unit
) {
DropDown(
modifier = modifier,
selected = selectedText,
values = values,
onValueSelected = onValueSelected,
labeler = { it.orEmpty() }
)
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun <T> DropDown(
modifier: Modifier = Modifier,
selected: T?,
values: List<T>,
onValueSelected: (T) -> Unit,
labeler: (T?) -> String,
) {
var expanded by remember { mutableStateOf(false) }

Expand All @@ -41,7 +58,7 @@ fun DropDown(
}
) {
TextField(
value = selectedText,
value = labeler(selected),
onValueChange = {},
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
Expand Down Expand Up @@ -80,7 +97,7 @@ fun DropDown(
DropdownMenuItem(
text = {
Text(
text = item,
text = labeler(item),
style = LocalType.current.base
)
},
Expand All @@ -97,6 +114,7 @@ fun DropDown(
}
}


@Preview
@Composable
fun PreviewDropDown() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,141 @@
package org.thoughtcrime.securesms.pro.subscription

import android.app.Application
import android.widget.Toast
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.PendingPurchasesParams
import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.queryProductDetails
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.dependencies.ManagerScope
import org.thoughtcrime.securesms.util.CurrentActivityObserver
import javax.inject.Inject

/**
* The Google Play Store implementation of our subscription manager
*/
class PlayStoreSubscriptionManager @Inject constructor(): SubscriptionManager {
class PlayStoreSubscriptionManager @Inject constructor(
private val application: Application,
@param:ManagerScope private val scope: CoroutineScope,
private val currentActivityObserver: CurrentActivityObserver,
) : SubscriptionManager {
override val id = "google_play_store"
override val displayName = ""
override val description = ""
override val iconRes = null

private val billingClient by lazy {
BillingClient.newBuilder(application)
.setListener { result, purchases ->
Log.d(TAG, "onPurchasesUpdated: $result, $purchases")
}
.enableAutoServiceReconnection()
.enablePendingPurchases(
PendingPurchasesParams.newBuilder()
.enableOneTimeProducts()
.enablePrepaidPlans()
.build()
)
.build()
}

override val availablePlans: List<ProSubscriptionDuration> =
ProSubscriptionDuration.entries.toList()

override fun purchasePlan(subscriptionDuration: ProSubscriptionDuration) {
//todo PRO implement
scope.launch {
try {
val activity = checkNotNull(currentActivityObserver.currentActivity.value) {
"No current activity available to launch the billing flow"
}

val result = billingClient.queryProductDetails(
QueryProductDetailsParams.newBuilder()
.setProductList(
listOf(
QueryProductDetailsParams.Product.newBuilder()
.setProductId("session_pro")
.setProductType(BillingClient.ProductType.SUBS)
.build()
)
)
.build()
)

check(result.billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
"Failed to query product details. Reason: ${result.billingResult}"
}

val productDetails = checkNotNull(result.productDetailsList?.firstOrNull()) {
"Unable to get the product: product for given id is null"
}

val planId = subscriptionDuration.planId

val offerDetails = checkNotNull(productDetails.subscriptionOfferDetails
?.firstOrNull { it.basePlanId == planId }) {
"Unable to find a plan with id $planId"
}

val billingResult = billingClient.launchBillingFlow(
activity, BillingFlowParams.newBuilder()
.setProductDetailsParamsList(
listOf(
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(offerDetails.offerToken)
.build()
)
)
.build()
)

check(billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
"Unable to launch the billing flow. Reason: ${billingResult.debugMessage}"
}

} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Log.e(TAG, "Error purchase plan", e)

withContext(Dispatchers.Main) {
Toast.makeText(application, e.message, Toast.LENGTH_LONG).show()
}
}
}
}

private val ProSubscriptionDuration.planId: String
get() = when (this) {
ProSubscriptionDuration.ONE_MONTH -> "session-pro-1-month"
ProSubscriptionDuration.THREE_MONTHS -> "session-pro-3-months"
ProSubscriptionDuration.TWELVE_MONTHS -> "session-pro-12-months"
}

override fun onPostAppStarted() {
super.onPostAppStarted()

billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingServiceDisconnected() {
Log.w(TAG, "onBillingServiceDisconnected")
}

override fun onBillingSetupFinished(result: BillingResult) {
Log.d(TAG, "onBillingSetupFinished with $result")
}
})
}

companion object {
private const val TAG = "PlayStoreSubscriptionManager"
}
}
Loading