mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Add Billing API and Google API availability error dialogs.
This commit is contained in:
committed by
Greyson Parrelli
parent
0713a88ddb
commit
2a90809ba3
@@ -24,6 +24,7 @@ import org.junit.runner.RunWith
|
||||
import org.signal.core.util.billing.BillingProduct
|
||||
import org.signal.core.util.billing.BillingPurchaseResult
|
||||
import org.signal.core.util.billing.BillingPurchaseState
|
||||
import org.signal.core.util.billing.BillingResponseCode
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.backup.DeletionState
|
||||
@@ -67,7 +68,7 @@ class BackupSubscriptionCheckJobTest {
|
||||
every { RemoteConfig.messageBackups } returns true
|
||||
every { RemoteConfig.internalUser } returns true
|
||||
|
||||
coEvery { AppDependencies.billingApi.isApiAvailable() } returns true
|
||||
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.OK
|
||||
coEvery { AppDependencies.billingApi.queryPurchases() } returns mockk()
|
||||
coEvery { AppDependencies.billingApi.queryProduct() } returns null
|
||||
|
||||
@@ -143,7 +144,7 @@ class BackupSubscriptionCheckJobTest {
|
||||
|
||||
@Test
|
||||
fun givenBillingApiNotAvailable_whenIRun_thenIExpectSuccessAndEarlyExit() {
|
||||
coEvery { AppDependencies.billingApi.isApiAvailable() } returns false
|
||||
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.BILLING_UNAVAILABLE
|
||||
|
||||
val job = BackupSubscriptionCheckJob.create()
|
||||
val result = job.run()
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.billing.BillingPurchaseResult
|
||||
import org.signal.core.util.billing.BillingPurchaseState
|
||||
import org.signal.core.util.billing.BillingResponseCode
|
||||
import org.signal.core.util.deleteAll
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
@@ -94,7 +95,7 @@ class GooglePlayBillingPurchaseTokenMigrationJobTest {
|
||||
)
|
||||
)
|
||||
|
||||
coEvery { AppDependencies.billingApi.isApiAvailable() } returns false
|
||||
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.BILLING_UNAVAILABLE
|
||||
|
||||
val job = GooglePlayBillingPurchaseTokenMigrationJob()
|
||||
|
||||
@@ -118,7 +119,7 @@ class GooglePlayBillingPurchaseTokenMigrationJobTest {
|
||||
)
|
||||
)
|
||||
|
||||
coEvery { AppDependencies.billingApi.isApiAvailable() } returns true
|
||||
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.OK
|
||||
coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.None
|
||||
|
||||
val job = GooglePlayBillingPurchaseTokenMigrationJob()
|
||||
@@ -143,7 +144,7 @@ class GooglePlayBillingPurchaseTokenMigrationJobTest {
|
||||
)
|
||||
)
|
||||
|
||||
coEvery { AppDependencies.billingApi.isApiAvailable() } returns true
|
||||
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.OK
|
||||
coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.Success(
|
||||
purchaseState = BillingPurchaseState.PURCHASED,
|
||||
purchaseToken = "purchaseToken",
|
||||
|
||||
@@ -164,6 +164,7 @@ import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.math.BigDecimal
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
@@ -1876,20 +1877,11 @@ object BackupRepository {
|
||||
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()?.activeSubscription?.let {
|
||||
FiatMoney.fromSignalNetworkAmount(it.amount, Currency.getInstance(it.currency))
|
||||
}
|
||||
} else if (AppDependencies.billingApi.isApiAvailable()) {
|
||||
} else if (AppDependencies.billingApi.getApiAvailability().isSuccess) {
|
||||
Log.d(TAG, "Accessing price via billing api.")
|
||||
AppDependencies.billingApi.queryProduct()?.price
|
||||
} else {
|
||||
Log.d(TAG, "Billing API is not available on this device. Accessing price via subscription configuration.")
|
||||
val configurationResult = AppDependencies.donationsService.getDonationsConfiguration(Locale.getDefault()).toNetworkResult()
|
||||
val currency = Currency.getInstance(Locale.getDefault())
|
||||
|
||||
when (configurationResult) {
|
||||
is NetworkResult.Success -> configurationResult.result.currencies[currency.currencyCode.lowercase()]?.backupSubscription[SubscriptionsConfiguration.BACKUPS_LEVEL]?.let {
|
||||
FiatMoney(it, currency)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
FiatMoney(BigDecimal.ZERO, Currency.getInstance(Locale.getDefault()))
|
||||
}
|
||||
|
||||
if (productPrice == null) {
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import org.signal.core.ui.compose.Dialogs
|
||||
import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Represents the availability status of Google Play Services on the device.
|
||||
*
|
||||
* Maps Google Play Services ConnectionResult codes to enum values for easier handling
|
||||
* in the application. Each enum value corresponds to a specific state that determines
|
||||
* what dialog or action should be presented to the user.
|
||||
*
|
||||
* @param code The corresponding ConnectionResult code from Google Play Services
|
||||
*/
|
||||
enum class GooglePlayServicesAvailability(val code: Int) {
|
||||
/** An unknown code. Possibly due to an update on Google's end */
|
||||
UNKNOWN(code = Int.MIN_VALUE),
|
||||
|
||||
/** Google Play Services is available and ready to use */
|
||||
SUCCESS(code = ConnectionResult.SUCCESS),
|
||||
|
||||
/** Google Play Services is not installed on the device */
|
||||
SERVICE_MISSING(code = ConnectionResult.SERVICE_MISSING),
|
||||
|
||||
/** Google Play Services is currently being updated */
|
||||
SERVICE_UPDATING(code = ConnectionResult.SERVICE_UPDATING),
|
||||
|
||||
/** Google Play Services requires an update to a newer version */
|
||||
SERVICE_VERSION_UPDATE_REQUIRED(code = ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED),
|
||||
|
||||
/** Google Play Services is installed but disabled by the user */
|
||||
SERVICE_DISABLED(code = ConnectionResult.SERVICE_DISABLED),
|
||||
|
||||
/** Google Play Services installation is invalid or corrupted */
|
||||
SERVICE_INVALID(code = ConnectionResult.SERVICE_INVALID);
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = Log.tag(GooglePlayServicesAvailability::class)
|
||||
|
||||
/**
|
||||
* Converts a Google Play Services ConnectionResult code to the corresponding enum value.
|
||||
*
|
||||
* @param code The ConnectionResult code from Google Play Services
|
||||
* @return The matching GooglePlayServicesAvailability enum value
|
||||
*/
|
||||
fun fromCode(code: Int): GooglePlayServicesAvailability {
|
||||
val availability = entries.firstOrNull { it.code == code } ?: UNKNOWN
|
||||
if (availability == UNKNOWN) {
|
||||
Log.w(TAG, "Unknown availability code: $code")
|
||||
}
|
||||
|
||||
return availability
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a dialog based on the Google Play Services availability status.
|
||||
*
|
||||
* Shows different dialogs with appropriate messages and actions depending on whether
|
||||
* Google Play Services is missing, updating, requires an update, is disabled, or invalid.
|
||||
* When availability is SUCCESS, automatically calls onDismissRequest to dismiss any dialog.
|
||||
*
|
||||
* @param onDismissRequest Callback invoked when the dialog is dismissed or when SUCCESS status is received
|
||||
* @param onLearnMoreClick Callback invoked when the "Learn More" action is selected
|
||||
* @param onMakeServicesAvailableClick Callback invoked when an action to make services
|
||||
* available is selected (e.g., install or update)
|
||||
* @param googlePlayServicesAvailability The current availability status of Google Play Services
|
||||
*/
|
||||
@Composable
|
||||
fun GooglePlayServicesAvailabilityDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onLearnMoreClick: () -> Unit,
|
||||
onMakeServicesAvailableClick: () -> Unit,
|
||||
googlePlayServicesAvailability: GooglePlayServicesAvailability
|
||||
) {
|
||||
when (googlePlayServicesAvailability) {
|
||||
GooglePlayServicesAvailability.SUCCESS -> {
|
||||
LaunchedEffect(Unit) {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
GooglePlayServicesAvailability.SERVICE_MISSING, GooglePlayServicesAvailability.UNKNOWN -> {
|
||||
ServiceMissingDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onInstallPlayServicesClick = onMakeServicesAvailableClick
|
||||
)
|
||||
}
|
||||
GooglePlayServicesAvailability.SERVICE_UPDATING -> {
|
||||
ServiceUpdatingDialog(onDismissRequest = onDismissRequest)
|
||||
}
|
||||
GooglePlayServicesAvailability.SERVICE_VERSION_UPDATE_REQUIRED -> {
|
||||
ServiceVersionUpdateRequiredDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onUpdateClick = onMakeServicesAvailableClick
|
||||
)
|
||||
}
|
||||
GooglePlayServicesAvailability.SERVICE_DISABLED -> {
|
||||
ServiceDisabledDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onLearnMoreClick = onLearnMoreClick
|
||||
)
|
||||
}
|
||||
GooglePlayServicesAvailability.SERVICE_INVALID -> {
|
||||
ServiceInvalidDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onLearnMoreClick = onLearnMoreClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServiceMissingDialog(onDismissRequest: () -> Unit, onInstallPlayServicesClick: () -> Unit) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.GooglePlayServicesAvailability__service_missing_title),
|
||||
body = stringResource(R.string.GooglePlayServicesAvailability__service_missing_message),
|
||||
confirm = stringResource(R.string.GooglePlayServicesAvailability__install_play_services),
|
||||
dismiss = stringResource(android.R.string.cancel),
|
||||
onConfirm = {},
|
||||
onDeny = onInstallPlayServicesClick,
|
||||
onDismiss = onDismissRequest,
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServiceUpdatingDialog(onDismissRequest: () -> Unit) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.GooglePlayServicesAvailability__service_updating_title),
|
||||
body = stringResource(R.string.GooglePlayServicesAvailability__service_updating_message),
|
||||
confirm = stringResource(android.R.string.ok),
|
||||
onConfirm = {},
|
||||
onDismiss = onDismissRequest,
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServiceVersionUpdateRequiredDialog(onDismissRequest: () -> Unit, onUpdateClick: () -> Unit) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.GooglePlayServicesAvailability__service_update_required_title),
|
||||
body = stringResource(R.string.GooglePlayServicesAvailability__service_update_required_message),
|
||||
confirm = stringResource(R.string.GooglePlayServicesAvailability__update),
|
||||
dismiss = stringResource(android.R.string.cancel),
|
||||
onConfirm = onUpdateClick,
|
||||
onDismiss = onDismissRequest,
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServiceDisabledDialog(onDismissRequest: () -> Unit, onLearnMoreClick: () -> Unit) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.GooglePlayServicesAvailability__service_disabled_title),
|
||||
body = stringResource(R.string.GooglePlayServicesAvailability__service_disabled_message),
|
||||
confirm = stringResource(android.R.string.ok),
|
||||
dismiss = stringResource(R.string.GooglePlayServicesAvailability__learn_more),
|
||||
onConfirm = onDismissRequest,
|
||||
onDeny = onLearnMoreClick,
|
||||
onDismiss = onDismissRequest,
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServiceInvalidDialog(onDismissRequest: () -> Unit, onLearnMoreClick: () -> Unit) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.GooglePlayServicesAvailability__service_disabled_title),
|
||||
body = stringResource(R.string.GooglePlayServicesAvailability__service_invalid_message),
|
||||
confirm = stringResource(android.R.string.ok),
|
||||
dismiss = stringResource(R.string.GooglePlayServicesAvailability__learn_more),
|
||||
onConfirm = {},
|
||||
onDeny = onLearnMoreClick,
|
||||
onDismiss = onDismissRequest,
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun ServiceMissingDialogPreview() {
|
||||
Previews.Preview {
|
||||
ServiceMissingDialog({}, {})
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun ServiceUpdatingDialogPreview() {
|
||||
Previews.Preview {
|
||||
ServiceUpdatingDialog({})
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun ServiceVersionUpdateRequiredDialogPreview() {
|
||||
Previews.Preview {
|
||||
ServiceVersionUpdateRequiredDialog({}, {})
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun ServiceDisabledDialogPreview() {
|
||||
Previews.Preview {
|
||||
ServiceDisabledDialog({}, {})
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun ServiceInvalidDialogPreview() {
|
||||
Previews.Preview {
|
||||
ServiceInvalidDialog({}, {})
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.asFlowable
|
||||
@@ -40,6 +41,7 @@ import org.thoughtcrime.securesms.compose.Nav
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.storage.AndroidCredentialRepository
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
@@ -63,7 +65,10 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
}
|
||||
|
||||
private val viewModel: MessageBackupsFlowViewModel by viewModel {
|
||||
MessageBackupsFlowViewModel(requireArguments().getSerializableCompat(TIER, MessageBackupTier::class.java))
|
||||
MessageBackupsFlowViewModel(
|
||||
initialTierSelection = requireArguments().getSerializableCompat(TIER, MessageBackupTier::class.java),
|
||||
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext())
|
||||
)
|
||||
}
|
||||
|
||||
private val errorHandler = InAppPaymentCheckoutDelegate.ErrorHandler()
|
||||
@@ -97,6 +102,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refreshCurrentTier()
|
||||
viewModel.setGooglePlayApiAvailability(GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()))
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -181,12 +187,21 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
||||
)
|
||||
},
|
||||
onNextClicked = viewModel::goToNextStage,
|
||||
isBillingApiAvailable = state.isBillingApiAvailable,
|
||||
googlePlayServicesAvailability = state.googlePlayApiAvailability,
|
||||
googlePlayBillingAvailability = state.googlePlayBillingAvailability,
|
||||
onLearnMoreAboutWhyUserCanNotUpgrade = {
|
||||
CommunicationActions.openBrowserLink(
|
||||
requireContext(),
|
||||
getString(R.string.backup_support_url)
|
||||
)
|
||||
},
|
||||
onMakeGooglePlayServicesAvailable = {
|
||||
GoogleApiAvailability.getInstance().makeGooglePlayServicesAvailable(requireActivity()).addOnSuccessListener {
|
||||
viewModel.setGooglePlayApiAvailability(GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()))
|
||||
}
|
||||
},
|
||||
onOpenPlayStore = {
|
||||
PlayStoreUtil.openPlayStoreHome(requireContext())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import org.signal.core.util.billing.BillingResponseCode
|
||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||
@@ -17,7 +18,8 @@ data class MessageBackupsFlowState(
|
||||
val selectedMessageBackupTier: MessageBackupTier? = SignalStore.backup.backupTier,
|
||||
val currentMessageBackupTier: MessageBackupTier? = null,
|
||||
val allBackupTypes: List<MessageBackupsType> = emptyList(),
|
||||
val isBillingApiAvailable: Boolean = false,
|
||||
val googlePlayApiAvailability: GooglePlayServicesAvailability = GooglePlayServicesAvailability.SUCCESS,
|
||||
val googlePlayBillingAvailability: BillingResponseCode = BillingResponseCode.FEATURE_NOT_SUPPORTED,
|
||||
val inAppPayment: InAppPaymentTable.InAppPayment? = null,
|
||||
val startScreen: MessageBackupsStage,
|
||||
val stage: MessageBackupsStage = startScreen,
|
||||
|
||||
@@ -53,6 +53,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class MessageBackupsFlowViewModel(
|
||||
private val initialTierSelection: MessageBackupTier?,
|
||||
googlePlayApiAvailability: Int,
|
||||
startScreen: MessageBackupsStage = if (SignalStore.backup.backupTier == null) MessageBackupsStage.EDUCATION else MessageBackupsStage.TYPE_SELECTION
|
||||
) : ViewModel(), BackupKeyCredentialManagerHandler {
|
||||
|
||||
@@ -64,6 +65,7 @@ class MessageBackupsFlowViewModel(
|
||||
private val internalStateFlow = MutableStateFlow(
|
||||
MessageBackupsFlowState(
|
||||
allBackupTypes = emptyList(),
|
||||
googlePlayApiAvailability = GooglePlayServicesAvailability.fromCode(googlePlayApiAvailability),
|
||||
currentMessageBackupTier = SignalStore.backup.backupTier,
|
||||
selectedMessageBackupTier = resolveSelectedTier(initialTierSelection, SignalStore.backup.backupTier),
|
||||
startScreen = startScreen
|
||||
@@ -105,7 +107,6 @@ class MessageBackupsFlowViewModel(
|
||||
internalStateFlow.update { state ->
|
||||
state.copy(
|
||||
allBackupTypes = allBackupTypes,
|
||||
isBillingApiAvailable = AppDependencies.billingApi.isApiAvailable(),
|
||||
selectedMessageBackupTier = if (state.selectedMessageBackupTier in allBackupTypes.map { it.tier }) state.selectedMessageBackupTier else allBackupTypes.firstOrNull()?.tier
|
||||
)
|
||||
}
|
||||
@@ -158,6 +159,12 @@ class MessageBackupsFlowViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun setGooglePlayApiAvailability(googlePlayApiAvailability: Int) {
|
||||
internalStateFlow.update {
|
||||
it.copy(googlePlayApiAvailability = GooglePlayServicesAvailability.fromCode(googlePlayApiAvailability))
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshCurrentTier() {
|
||||
val tier = SignalStore.backup.backupTier
|
||||
if (tier == MessageBackupTier.PAID) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -52,6 +53,7 @@ import org.signal.core.ui.compose.Previews
|
||||
import org.signal.core.ui.compose.Scaffolds
|
||||
import org.signal.core.ui.compose.SignalPreview
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.util.billing.BillingResponseCode
|
||||
import org.signal.core.util.bytes
|
||||
import org.signal.core.util.money.FiatMoney
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -75,13 +77,16 @@ fun MessageBackupsTypeSelectionScreen(
|
||||
currentBackupTier: MessageBackupTier?,
|
||||
selectedBackupTier: MessageBackupTier?,
|
||||
allBackupTypes: List<MessageBackupsType>,
|
||||
isBillingApiAvailable: Boolean,
|
||||
googlePlayServicesAvailability: GooglePlayServicesAvailability,
|
||||
googlePlayBillingAvailability: BillingResponseCode,
|
||||
isNextEnabled: Boolean,
|
||||
onMessageBackupsTierSelected: (MessageBackupTier) -> Unit,
|
||||
onNavigationClick: () -> Unit,
|
||||
onReadMoreClicked: () -> Unit,
|
||||
onNextClicked: () -> Unit,
|
||||
onLearnMoreAboutWhyUserCanNotUpgrade: () -> Unit
|
||||
onLearnMoreAboutWhyUserCanNotUpgrade: () -> Unit,
|
||||
onMakeGooglePlayServicesAvailable: () -> Unit,
|
||||
onOpenPlayStore: () -> Unit
|
||||
) {
|
||||
Scaffolds.Settings(
|
||||
title = "",
|
||||
@@ -161,28 +166,26 @@ fun MessageBackupsTypeSelectionScreen(
|
||||
}
|
||||
|
||||
val hasCurrentBackupTier = currentBackupTier != null
|
||||
var displayNotAvailableDialog by remember { mutableStateOf(false) }
|
||||
val onSubscribeButtonClick = remember(isBillingApiAvailable, selectedBackupTier) {
|
||||
val paidTierNotAvailableDialogState = remember { PaidTierNotAvailableDialogState() }
|
||||
val onSubscribeButtonClick = remember(googlePlayServicesAvailability, googlePlayBillingAvailability, selectedBackupTier) {
|
||||
{
|
||||
if (selectedBackupTier == MessageBackupTier.PAID && !isBillingApiAvailable) {
|
||||
displayNotAvailableDialog = true
|
||||
if (selectedBackupTier == MessageBackupTier.PAID && googlePlayServicesAvailability != GooglePlayServicesAvailability.SUCCESS) {
|
||||
paidTierNotAvailableDialogState.displayGooglePlayApiErrorDialog = true
|
||||
} else if (selectedBackupTier == MessageBackupTier.PAID && !googlePlayBillingAvailability.isSuccess) {
|
||||
paidTierNotAvailableDialogState.displayGooglePlayBillingErrorDialog = true
|
||||
} else {
|
||||
onNextClicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (displayNotAvailableDialog) {
|
||||
UpgradeNotAvailableDialog(
|
||||
onConfirm = {
|
||||
displayNotAvailableDialog = false
|
||||
},
|
||||
onDismiss = onLearnMoreAboutWhyUserCanNotUpgrade,
|
||||
onDismissRequest = {
|
||||
displayNotAvailableDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
PaidTierNotAvailableDialogs(
|
||||
state = paidTierNotAvailableDialogState,
|
||||
onOpenPlayStore = onOpenPlayStore,
|
||||
onLearnMoreAboutWhyUserCanNotUpgrade = onLearnMoreAboutWhyUserCanNotUpgrade,
|
||||
onMakeGooglePlayServicesAvailable = onMakeGooglePlayServicesAvailable,
|
||||
googlePlayServicesAvailability = googlePlayServicesAvailability
|
||||
)
|
||||
|
||||
Buttons.LargeTonal(
|
||||
onClick = onSubscribeButtonClick,
|
||||
@@ -193,7 +196,9 @@ fun MessageBackupsTypeSelectionScreen(
|
||||
.padding(vertical = if (hasCurrentBackupTier) 10.dp else 16.dp)
|
||||
) {
|
||||
val text: String = if (currentBackupTier == null) {
|
||||
if (selectedBackupTier == MessageBackupTier.PAID && allBackupTypes.map { it.tier }.contains(selectedBackupTier)) {
|
||||
if (selectedBackupTier == MessageBackupTier.PAID && (googlePlayServicesAvailability != GooglePlayServicesAvailability.SUCCESS || !googlePlayBillingAvailability.isSuccess)) {
|
||||
stringResource(R.string.MessageBackupsTypeSelectionScreen__more_about_this_plan)
|
||||
} else if (selectedBackupTier == MessageBackupTier.PAID && allBackupTypes.map { it.tier }.contains(selectedBackupTier)) {
|
||||
val paidTier = allBackupTypes.first { it.tier == MessageBackupTier.PAID } as MessageBackupsType.Paid
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -207,6 +212,8 @@ fun MessageBackupsTypeSelectionScreen(
|
||||
} else {
|
||||
stringResource(R.string.MessageBackupsTypeSelectionScreen__subscribe)
|
||||
}
|
||||
} else if (selectedBackupTier == MessageBackupTier.PAID && (googlePlayServicesAvailability != GooglePlayServicesAvailability.SUCCESS || !googlePlayBillingAvailability.isSuccess)) {
|
||||
stringResource(R.string.MessageBackupsTypeSelectionScreen__more_about_this_plan)
|
||||
} else {
|
||||
stringResource(R.string.MessageBackupsTypeSelectionScreen__change_backup_type)
|
||||
}
|
||||
@@ -224,20 +231,50 @@ fun MessageBackupsTypeSelectionScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class PaidTierNotAvailableDialogState {
|
||||
var displayGooglePlayBillingErrorDialog: Boolean by mutableStateOf(false)
|
||||
var displayGooglePlayApiErrorDialog: Boolean by mutableStateOf(false)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpgradeNotAvailableDialog(
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
onDismissRequest: () -> Unit
|
||||
fun PaidTierNotAvailableDialogs(
|
||||
state: PaidTierNotAvailableDialogState,
|
||||
googlePlayServicesAvailability: GooglePlayServicesAvailability,
|
||||
onLearnMoreAboutWhyUserCanNotUpgrade: () -> Unit,
|
||||
onMakeGooglePlayServicesAvailable: () -> Unit,
|
||||
onOpenPlayStore: () -> Unit
|
||||
) {
|
||||
if (state.displayGooglePlayApiErrorDialog) {
|
||||
GooglePlayServicesAvailabilityDialog(
|
||||
onDismissRequest = { state.displayGooglePlayApiErrorDialog = false },
|
||||
googlePlayServicesAvailability = googlePlayServicesAvailability,
|
||||
onLearnMoreClick = onLearnMoreAboutWhyUserCanNotUpgrade,
|
||||
onMakeServicesAvailableClick = onMakeGooglePlayServicesAvailable
|
||||
)
|
||||
}
|
||||
|
||||
if (state.displayGooglePlayBillingErrorDialog) {
|
||||
UserNotSignedInDialog(
|
||||
onDismissRequest = { state.displayGooglePlayBillingErrorDialog = false },
|
||||
onOpenPlayStore = onOpenPlayStore
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserNotSignedInDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onOpenPlayStore: () -> Unit
|
||||
) {
|
||||
Dialogs.SimpleAlertDialog(
|
||||
title = stringResource(R.string.MessageBackupsTypeSelectionScreen__cant_upgrade_plan),
|
||||
body = stringResource(R.string.MessageBackupsTypeSelectionScreen__to_subscribe_to_signal_secure_backups),
|
||||
confirm = stringResource(android.R.string.ok),
|
||||
dismiss = stringResource(R.string.MessageBackupsTypeSelectionScreen__learn_more),
|
||||
onConfirm = onConfirm,
|
||||
onDismiss = onDismiss,
|
||||
onDismissRequest = onDismissRequest
|
||||
title = stringResource(R.string.GooglePlayServicesAvailability__service_disabled_title),
|
||||
body = "To subscribe to Signal Secure Backups, please sign into the Google Play store.",
|
||||
onConfirm = onOpenPlayStore,
|
||||
onDismiss = onDismissRequest,
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirm = "Open Play Store",
|
||||
dismiss = stringResource(android.R.string.cancel)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -256,8 +293,11 @@ private fun MessageBackupsTypeSelectionScreenPreview() {
|
||||
onReadMoreClicked = {},
|
||||
onNextClicked = {},
|
||||
onLearnMoreAboutWhyUserCanNotUpgrade = {},
|
||||
onMakeGooglePlayServicesAvailable = {},
|
||||
onOpenPlayStore = {},
|
||||
currentBackupTier = null,
|
||||
isBillingApiAvailable = true,
|
||||
googlePlayServicesAvailability = GooglePlayServicesAvailability.SUCCESS,
|
||||
googlePlayBillingAvailability = BillingResponseCode.OK,
|
||||
isNextEnabled = true
|
||||
)
|
||||
}
|
||||
@@ -278,25 +318,16 @@ private fun MessageBackupsTypeSelectionScreenWithCurrentTierPreview() {
|
||||
onReadMoreClicked = {},
|
||||
onNextClicked = {},
|
||||
onLearnMoreAboutWhyUserCanNotUpgrade = {},
|
||||
onMakeGooglePlayServicesAvailable = {},
|
||||
onOpenPlayStore = {},
|
||||
currentBackupTier = MessageBackupTier.PAID,
|
||||
isBillingApiAvailable = true,
|
||||
googlePlayServicesAvailability = GooglePlayServicesAvailability.SUCCESS,
|
||||
googlePlayBillingAvailability = BillingResponseCode.OK,
|
||||
isNextEnabled = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@SignalPreview
|
||||
@Composable
|
||||
private fun UpgradeNotAvailableDialogPreview() {
|
||||
Previews.Preview {
|
||||
UpgradeNotAvailableDialog(
|
||||
onConfirm = {},
|
||||
onDismiss = {},
|
||||
onDismissRequest = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MessageBackupsTypeBlock(
|
||||
messageBackupsType: MessageBackupsType,
|
||||
@@ -382,8 +413,12 @@ private fun getFormattedPricePerMonth(messageBackupsType: MessageBackupsType): S
|
||||
return when (messageBackupsType) {
|
||||
is MessageBackupsType.Free -> stringResource(id = R.string.MessageBackupsTypeSelectionScreen__free)
|
||||
is MessageBackupsType.Paid -> {
|
||||
val formattedAmount = FiatMoneyUtil.format(LocalContext.current.resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
stringResource(id = R.string.MessageBackupsTypeSelectionScreen__s_month, formattedAmount)
|
||||
if (messageBackupsType.pricePerMonth.amount == BigDecimal.ZERO) {
|
||||
stringResource(R.string.MessageBackupsTypeSelectionScreen__paid)
|
||||
} else {
|
||||
val formattedAmount = FiatMoneyUtil.format(LocalContext.current.resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
||||
stringResource(id = R.string.MessageBackupsTypeSelectionScreen__s_month, formattedAmount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.asFlowable
|
||||
@@ -59,6 +60,7 @@ abstract class UpgradeToPaidTierBottomSheet : ComposeBottomSheetDialogFragment()
|
||||
private val viewModel: MessageBackupsFlowViewModel by viewModel {
|
||||
MessageBackupsFlowViewModel(
|
||||
initialTierSelection = MessageBackupTier.PAID,
|
||||
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()),
|
||||
startScreen = MessageBackupsStage.TYPE_SELECTION
|
||||
)
|
||||
}
|
||||
@@ -93,6 +95,12 @@ abstract class UpgradeToPaidTierBottomSheet : ComposeBottomSheetDialogFragment()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refreshCurrentTier()
|
||||
viewModel.setGooglePlayApiAvailability(GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()))
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun SheetContent() {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
@@ -83,7 +83,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val isBillingApiAvailable = AppDependencies.billingApi.isApiAvailable()
|
||||
val isBillingApiAvailable = AppDependencies.billingApi.getApiAvailability().isSuccess
|
||||
if (isBillingApiAvailable) {
|
||||
_state.update {
|
||||
it.copy(isPaidTierPricingAvailable = true)
|
||||
|
||||
@@ -135,7 +135,7 @@ class ManageStorageSettingsViewModel : ViewModel() {
|
||||
|
||||
private suspend fun getOnDeviceStorageOptimizationState(): OnDeviceStorageOptimizationState {
|
||||
return when {
|
||||
!RemoteConfig.messageBackups || !SignalStore.backup.areBackupsEnabled || !AppDependencies.billingApi.isApiAvailable() || (!RemoteConfig.internalUser && !Environment.IS_STAGING) -> OnDeviceStorageOptimizationState.FEATURE_NOT_AVAILABLE
|
||||
!RemoteConfig.messageBackups || !SignalStore.backup.areBackupsEnabled || !AppDependencies.billingApi.getApiAvailability().isSuccess || (!RemoteConfig.internalUser && !Environment.IS_STAGING) -> OnDeviceStorageOptimizationState.FEATURE_NOT_AVAILABLE
|
||||
SignalStore.backup.backupTier != MessageBackupTier.PAID -> OnDeviceStorageOptimizationState.REQUIRES_PAID_TIER
|
||||
SignalStore.backup.optimizeStorage -> OnDeviceStorageOptimizationState.ENABLED
|
||||
else -> OnDeviceStorageOptimizationState.DISABLED
|
||||
|
||||
@@ -90,7 +90,7 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (!AppDependencies.billingApi.isApiAvailable()) {
|
||||
if (!AppDependencies.billingApi.getApiAvailability().isSuccess) {
|
||||
Log.i(TAG, "Google Play Billing API is not available on this device. Clearing mismatch value and exiting.", true)
|
||||
SignalStore.backup.subscriptionStateMismatchDetected = false
|
||||
return Result.success()
|
||||
|
||||
@@ -87,7 +87,7 @@ class InAppPaymentPurchaseTokenJob private constructor(
|
||||
}
|
||||
|
||||
private suspend fun linkPurchaseToken(): Result {
|
||||
if (!AppDependencies.billingApi.isApiAvailable()) {
|
||||
if (!AppDependencies.billingApi.getApiAvailability().isSuccess) {
|
||||
warning("Billing API is not available on this device. Exiting.")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ class PostRegistrationBackupRedemptionJob : CoroutineJob {
|
||||
val emptyPrice = FiatMoney(BigDecimal.ZERO, Currency.getInstance(Locale.getDefault()))
|
||||
val price: FiatMoney = if (subscription != null) {
|
||||
FiatMoney.fromSignalNetworkAmount(subscription.amount, Currency.getInstance(subscription.currency))
|
||||
} else if (AppDependencies.billingApi.isApiAvailable()) {
|
||||
} else if (AppDependencies.billingApi.getApiAvailability().isSuccess) {
|
||||
AppDependencies.billingApi.queryProduct()?.price ?: emptyPrice
|
||||
} else {
|
||||
emptyPrice
|
||||
|
||||
@@ -44,7 +44,7 @@ class LogSectionRemoteBackups : LogSection {
|
||||
output.append("\n -- Subscription State\n")
|
||||
|
||||
val backupSubscriptionId = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
val hasGooglePlayBilling = runBlocking { AppDependencies.billingApi.isApiAvailable() }
|
||||
val hasGooglePlayBilling = runBlocking { AppDependencies.billingApi.getApiAvailability().isSuccess }
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP)
|
||||
|
||||
output.append("Has backup subscription id: ${backupSubscriptionId != null}\n")
|
||||
|
||||
@@ -54,7 +54,7 @@ internal class GooglePlayBillingPurchaseTokenMigrationJob private constructor(
|
||||
|
||||
if (backupSubscriber.iapSubscriptionId?.purchaseToken == "-") {
|
||||
val purchaseResult: BillingPurchaseResult.Success? = runBlocking {
|
||||
if (AppDependencies.billingApi.isApiAvailable()) {
|
||||
if (AppDependencies.billingApi.getApiAvailability().isSuccess) {
|
||||
val purchase = AppDependencies.billingApi.queryPurchases()
|
||||
|
||||
if (purchase is BillingPurchaseResult.Success) {
|
||||
|
||||
@@ -21,6 +21,16 @@ public final class PlayStoreUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static void openPlayStoreHome(@NonNull Context context) {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.android.vending"));
|
||||
intent.setPackage("com.android.vending");
|
||||
context.startActivity(intent);
|
||||
} catch (android.content.ActivityNotFoundException e) {
|
||||
CommunicationActions.openBrowserLink(context, "https://play.google.com/store/apps/");
|
||||
}
|
||||
}
|
||||
|
||||
private static void openPlayStore(@NonNull Context context) {
|
||||
String packageName = context.getPackageName();
|
||||
|
||||
|
||||
@@ -8041,6 +8041,32 @@
|
||||
<!-- Row title to change or cancel subscription -->
|
||||
<string name="BackupsTypeSettingsFragment__change_or_cancel_subscription">Change or cancel subscription</string>
|
||||
|
||||
<!-- GooglePlayServicesAvailability -->
|
||||
<!-- Dialog title when Google Play services are missing from the device -->
|
||||
<string name="GooglePlayServicesAvailability__service_missing_title">Google Play services missing</string>
|
||||
<!-- Dialog message explaining that Google Play services are required for Signal Secure backups subscription -->
|
||||
<string name="GooglePlayServicesAvailability__service_missing_message">To subscribe to Signal Secure backups, you need to install Google Play services on your phone.</string>
|
||||
<!-- Dialog button text to install Google Play services -->
|
||||
<string name="GooglePlayServicesAvailability__install_play_services">Install Play Services</string>
|
||||
<!-- Dialog title when Google Play services are currently updating -->
|
||||
<string name="GooglePlayServicesAvailability__service_updating_title">Google Play services are updating</string>
|
||||
<!-- Dialog message asking user to wait while Google Play services finish updating -->
|
||||
<string name="GooglePlayServicesAvailability__service_updating_message">To subscribe to Signal Secure backups please wait for Google Play services to finish updating.</string>
|
||||
<!-- Dialog title when Google Play services need to be updated -->
|
||||
<string name="GooglePlayServicesAvailability__service_update_required_title">Update Google Play services</string>
|
||||
<!-- Dialog message explaining that Google Play services need to be updated for Signal Secure backups -->
|
||||
<string name="GooglePlayServicesAvailability__service_update_required_message">To subscribe to Signal Secure Backups, update Google Play service on your phone.</string>
|
||||
<!-- Dialog button text to update Google Play services -->
|
||||
<string name="GooglePlayServicesAvailability__update">Update</string>
|
||||
<!-- Dialog title when Google Play services are disabled -->
|
||||
<string name="GooglePlayServicesAvailability__service_disabled_title">Paid subscriptions require Google Play</string>
|
||||
<!-- Dialog message explaining that Google Play services must be enabled for subscriptions -->
|
||||
<string name="GooglePlayServicesAvailability__service_disabled_message">To subscribe to Signal Secure Backups, enable Google Play services on your phone and sign into the Google Play store. </string>
|
||||
<!-- Dialog button text to learn more about enabling Google Play services -->
|
||||
<string name="GooglePlayServicesAvailability__learn_more">Learn more</string>
|
||||
<!-- Dialog message shown when Google Play services is invalid and subscriptions are not available -->
|
||||
<string name="GooglePlayServicesAvailability__service_invalid_message">You can\'t subscribe to Signal Secure backups because your phone does not support Google Play services. Contact the phone manufacturer for assistance.</string>
|
||||
|
||||
<!-- ManageStorageSettingsFragment -->
|
||||
<!-- Settings section title header show above manage settings options for automatic chat history management (deleting) -->
|
||||
<string name="ManageStorageSettingsFragment_chat_limit">Chat limits</string>
|
||||
@@ -8481,10 +8507,14 @@
|
||||
<string name="MessageBackupsTypeSelectionScreen__subscribe_for_x_month">Subscribe for %1$s/month</string>
|
||||
<!-- Primary action button label when selecting a backup tier with a current selection -->
|
||||
<string name="MessageBackupsTypeSelectionScreen__change_backup_type">Change backup type</string>
|
||||
<!-- Primary action button label when selecting paid tier when Google Play API isn't available -->
|
||||
<string name="MessageBackupsTypeSelectionScreen__more_about_this_plan">More about this plan</string>
|
||||
<!-- Secondary action button label when selecting a backup tier with a current selection -->
|
||||
<string name="MessageBackupsTypeSelectionScreen__cancel_subscription">Cancel subscription</string>
|
||||
<!-- MessageBackupsType block amount for free tier -->
|
||||
<string name="MessageBackupsTypeSelectionScreen__free">Free</string>
|
||||
<!-- MessageBackupsType block amount for paid tier when no pricing is available -->
|
||||
<string name="MessageBackupsTypeSelectionScreen__paid">Paid</string>
|
||||
<!-- MessageBackupsType block amount for paid tier. Placeholder is formatted currency amount. -->
|
||||
<string name="MessageBackupsTypeSelectionScreen__s_month">%1$s/month</string>
|
||||
<!-- Title for free tier -->
|
||||
|
||||
@@ -268,14 +268,14 @@ internal class BillingApiImpl(
|
||||
* Returns whether or not subscriptions are supported by a user's device. Lack of subscription support is generally due
|
||||
* to out-of-date Google Play API
|
||||
*/
|
||||
override suspend fun isApiAvailable(): Boolean {
|
||||
override suspend fun getApiAvailability(): org.signal.core.util.billing.BillingResponseCode {
|
||||
return try {
|
||||
doOnConnectionReady("isApiAvailable") {
|
||||
billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS).responseCode == BillingResponseCode.OK
|
||||
org.signal.core.util.billing.BillingResponseCode.fromBillingLibraryResponseCode(billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS).responseCode)
|
||||
}
|
||||
} catch (e: BillingError) {
|
||||
Log.e(TAG, "Failed to connect to Google Play Billing", e)
|
||||
false
|
||||
org.signal.core.util.billing.BillingResponseCode.fromBillingLibraryResponseCode(e.billingResponseCode)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ interface BillingApi {
|
||||
*/
|
||||
fun getBillingPurchaseResults(): Flow<BillingPurchaseResult> = emptyFlow()
|
||||
|
||||
suspend fun isApiAvailable(): Boolean = false
|
||||
suspend fun getApiAvailability(): BillingResponseCode = BillingResponseCode.FEATURE_NOT_SUPPORTED
|
||||
|
||||
/**
|
||||
* Queries the Billing API for product pricing. This value should be cached by
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.core.util.billing
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
|
||||
enum class BillingResponseCode(val code: Int) {
|
||||
UNKNOWN(code = Int.MIN_VALUE),
|
||||
SERVICE_TIMEOUT(code = -3),
|
||||
FEATURE_NOT_SUPPORTED(code = -2),
|
||||
SERVICE_DISCONNECTED(code = -1),
|
||||
OK(code = 0),
|
||||
USER_CANCELED(code = 1),
|
||||
SERVICE_UNAVAILABLE(code = 2),
|
||||
BILLING_UNAVAILABLE(code = 3),
|
||||
ITEM_UNAVAILABLE(code = 4),
|
||||
DEVELOPER_ERROR(code = 5),
|
||||
ERROR(code = 6),
|
||||
ITEM_ALREADY_OWNED(code = 7),
|
||||
ITEM_NOT_OWNED(code = 8),
|
||||
NETWORK_ERROR(code = 12);
|
||||
|
||||
val isSuccess: Boolean get() = this == OK
|
||||
|
||||
companion object {
|
||||
|
||||
private val TAG = Log.tag(BillingResponseCode::class)
|
||||
|
||||
fun fromBillingLibraryResponseCode(responseCode: Int): BillingResponseCode {
|
||||
val code = BillingResponseCode.entries.firstOrNull { responseCode == it.code } ?: UNKNOWN
|
||||
|
||||
if (code == UNKNOWN) {
|
||||
Log.w(TAG, "Unknown response code: $code")
|
||||
}
|
||||
|
||||
return code
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user