mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-03 15:11:42 +01: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.BillingProduct
|
||||||
import org.signal.core.util.billing.BillingPurchaseResult
|
import org.signal.core.util.billing.BillingPurchaseResult
|
||||||
import org.signal.core.util.billing.BillingPurchaseState
|
import org.signal.core.util.billing.BillingPurchaseState
|
||||||
|
import org.signal.core.util.billing.BillingResponseCode
|
||||||
import org.signal.core.util.money.FiatMoney
|
import org.signal.core.util.money.FiatMoney
|
||||||
import org.signal.donations.InAppPaymentType
|
import org.signal.donations.InAppPaymentType
|
||||||
import org.thoughtcrime.securesms.backup.DeletionState
|
import org.thoughtcrime.securesms.backup.DeletionState
|
||||||
@@ -67,7 +68,7 @@ class BackupSubscriptionCheckJobTest {
|
|||||||
every { RemoteConfig.messageBackups } returns true
|
every { RemoteConfig.messageBackups } returns true
|
||||||
every { RemoteConfig.internalUser } 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.queryPurchases() } returns mockk()
|
||||||
coEvery { AppDependencies.billingApi.queryProduct() } returns null
|
coEvery { AppDependencies.billingApi.queryProduct() } returns null
|
||||||
|
|
||||||
@@ -143,7 +144,7 @@ class BackupSubscriptionCheckJobTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun givenBillingApiNotAvailable_whenIRun_thenIExpectSuccessAndEarlyExit() {
|
fun givenBillingApiNotAvailable_whenIRun_thenIExpectSuccessAndEarlyExit() {
|
||||||
coEvery { AppDependencies.billingApi.isApiAvailable() } returns false
|
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.BILLING_UNAVAILABLE
|
||||||
|
|
||||||
val job = BackupSubscriptionCheckJob.create()
|
val job = BackupSubscriptionCheckJob.create()
|
||||||
val result = job.run()
|
val result = job.run()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.junit.Test
|
|||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.signal.core.util.billing.BillingPurchaseResult
|
import org.signal.core.util.billing.BillingPurchaseResult
|
||||||
import org.signal.core.util.billing.BillingPurchaseState
|
import org.signal.core.util.billing.BillingPurchaseState
|
||||||
|
import org.signal.core.util.billing.BillingResponseCode
|
||||||
import org.signal.core.util.deleteAll
|
import org.signal.core.util.deleteAll
|
||||||
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
|
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
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()
|
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
|
coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.None
|
||||||
|
|
||||||
val job = GooglePlayBillingPurchaseTokenMigrationJob()
|
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(
|
coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.Success(
|
||||||
purchaseState = BillingPurchaseState.PURCHASED,
|
purchaseState = BillingPurchaseState.PURCHASED,
|
||||||
purchaseToken = "purchaseToken",
|
purchaseToken = "purchaseToken",
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ import java.io.File
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
import java.math.BigDecimal
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.util.Currency
|
import java.util.Currency
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -1876,20 +1877,11 @@ object BackupRepository {
|
|||||||
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()?.activeSubscription?.let {
|
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()?.activeSubscription?.let {
|
||||||
FiatMoney.fromSignalNetworkAmount(it.amount, Currency.getInstance(it.currency))
|
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.")
|
Log.d(TAG, "Accessing price via billing api.")
|
||||||
AppDependencies.billingApi.queryProduct()?.price
|
AppDependencies.billingApi.queryProduct()?.price
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Billing API is not available on this device. Accessing price via subscription configuration.")
|
FiatMoney(BigDecimal.ZERO, Currency.getInstance(Locale.getDefault()))
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (productPrice == null) {
|
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.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.google.android.gms.common.GoogleApiAvailability
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.asFlowable
|
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.database.InAppPaymentTable
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||||
|
import org.thoughtcrime.securesms.util.PlayStoreUtil
|
||||||
import org.thoughtcrime.securesms.util.Util
|
import org.thoughtcrime.securesms.util.Util
|
||||||
import org.thoughtcrime.securesms.util.storage.AndroidCredentialRepository
|
import org.thoughtcrime.securesms.util.storage.AndroidCredentialRepository
|
||||||
import org.thoughtcrime.securesms.util.viewModel
|
import org.thoughtcrime.securesms.util.viewModel
|
||||||
@@ -63,7 +65,10 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val viewModel: MessageBackupsFlowViewModel by viewModel {
|
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()
|
private val errorHandler = InAppPaymentCheckoutDelegate.ErrorHandler()
|
||||||
@@ -97,6 +102,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
viewModel.refreshCurrentTier()
|
viewModel.refreshCurrentTier()
|
||||||
|
viewModel.setGooglePlayApiAvailability(GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -181,12 +187,21 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
onNextClicked = viewModel::goToNextStage,
|
onNextClicked = viewModel::goToNextStage,
|
||||||
isBillingApiAvailable = state.isBillingApiAvailable,
|
googlePlayServicesAvailability = state.googlePlayApiAvailability,
|
||||||
|
googlePlayBillingAvailability = state.googlePlayBillingAvailability,
|
||||||
onLearnMoreAboutWhyUserCanNotUpgrade = {
|
onLearnMoreAboutWhyUserCanNotUpgrade = {
|
||||||
CommunicationActions.openBrowserLink(
|
CommunicationActions.openBrowserLink(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
getString(R.string.backup_support_url)
|
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
|
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import org.signal.core.util.billing.BillingResponseCode
|
||||||
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||||
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState
|
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState
|
||||||
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
import org.thoughtcrime.securesms.database.InAppPaymentTable
|
||||||
@@ -17,7 +18,8 @@ data class MessageBackupsFlowState(
|
|||||||
val selectedMessageBackupTier: MessageBackupTier? = SignalStore.backup.backupTier,
|
val selectedMessageBackupTier: MessageBackupTier? = SignalStore.backup.backupTier,
|
||||||
val currentMessageBackupTier: MessageBackupTier? = null,
|
val currentMessageBackupTier: MessageBackupTier? = null,
|
||||||
val allBackupTypes: List<MessageBackupsType> = emptyList(),
|
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 inAppPayment: InAppPaymentTable.InAppPayment? = null,
|
||||||
val startScreen: MessageBackupsStage,
|
val startScreen: MessageBackupsStage,
|
||||||
val stage: MessageBackupsStage = startScreen,
|
val stage: MessageBackupsStage = startScreen,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
|
|
||||||
class MessageBackupsFlowViewModel(
|
class MessageBackupsFlowViewModel(
|
||||||
private val initialTierSelection: MessageBackupTier?,
|
private val initialTierSelection: MessageBackupTier?,
|
||||||
|
googlePlayApiAvailability: Int,
|
||||||
startScreen: MessageBackupsStage = if (SignalStore.backup.backupTier == null) MessageBackupsStage.EDUCATION else MessageBackupsStage.TYPE_SELECTION
|
startScreen: MessageBackupsStage = if (SignalStore.backup.backupTier == null) MessageBackupsStage.EDUCATION else MessageBackupsStage.TYPE_SELECTION
|
||||||
) : ViewModel(), BackupKeyCredentialManagerHandler {
|
) : ViewModel(), BackupKeyCredentialManagerHandler {
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ class MessageBackupsFlowViewModel(
|
|||||||
private val internalStateFlow = MutableStateFlow(
|
private val internalStateFlow = MutableStateFlow(
|
||||||
MessageBackupsFlowState(
|
MessageBackupsFlowState(
|
||||||
allBackupTypes = emptyList(),
|
allBackupTypes = emptyList(),
|
||||||
|
googlePlayApiAvailability = GooglePlayServicesAvailability.fromCode(googlePlayApiAvailability),
|
||||||
currentMessageBackupTier = SignalStore.backup.backupTier,
|
currentMessageBackupTier = SignalStore.backup.backupTier,
|
||||||
selectedMessageBackupTier = resolveSelectedTier(initialTierSelection, SignalStore.backup.backupTier),
|
selectedMessageBackupTier = resolveSelectedTier(initialTierSelection, SignalStore.backup.backupTier),
|
||||||
startScreen = startScreen
|
startScreen = startScreen
|
||||||
@@ -105,7 +107,6 @@ class MessageBackupsFlowViewModel(
|
|||||||
internalStateFlow.update { state ->
|
internalStateFlow.update { state ->
|
||||||
state.copy(
|
state.copy(
|
||||||
allBackupTypes = allBackupTypes,
|
allBackupTypes = allBackupTypes,
|
||||||
isBillingApiAvailable = AppDependencies.billingApi.isApiAvailable(),
|
|
||||||
selectedMessageBackupTier = if (state.selectedMessageBackupTier in allBackupTypes.map { it.tier }) state.selectedMessageBackupTier else allBackupTypes.firstOrNull()?.tier
|
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() {
|
fun refreshCurrentTier() {
|
||||||
val tier = SignalStore.backup.backupTier
|
val tier = SignalStore.backup.backupTier
|
||||||
if (tier == MessageBackupTier.PAID) {
|
if (tier == MessageBackupTier.PAID) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
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.Scaffolds
|
||||||
import org.signal.core.ui.compose.SignalPreview
|
import org.signal.core.ui.compose.SignalPreview
|
||||||
import org.signal.core.ui.compose.theme.SignalTheme
|
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.bytes
|
||||||
import org.signal.core.util.money.FiatMoney
|
import org.signal.core.util.money.FiatMoney
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
@@ -75,13 +77,16 @@ fun MessageBackupsTypeSelectionScreen(
|
|||||||
currentBackupTier: MessageBackupTier?,
|
currentBackupTier: MessageBackupTier?,
|
||||||
selectedBackupTier: MessageBackupTier?,
|
selectedBackupTier: MessageBackupTier?,
|
||||||
allBackupTypes: List<MessageBackupsType>,
|
allBackupTypes: List<MessageBackupsType>,
|
||||||
isBillingApiAvailable: Boolean,
|
googlePlayServicesAvailability: GooglePlayServicesAvailability,
|
||||||
|
googlePlayBillingAvailability: BillingResponseCode,
|
||||||
isNextEnabled: Boolean,
|
isNextEnabled: Boolean,
|
||||||
onMessageBackupsTierSelected: (MessageBackupTier) -> Unit,
|
onMessageBackupsTierSelected: (MessageBackupTier) -> Unit,
|
||||||
onNavigationClick: () -> Unit,
|
onNavigationClick: () -> Unit,
|
||||||
onReadMoreClicked: () -> Unit,
|
onReadMoreClicked: () -> Unit,
|
||||||
onNextClicked: () -> Unit,
|
onNextClicked: () -> Unit,
|
||||||
onLearnMoreAboutWhyUserCanNotUpgrade: () -> Unit
|
onLearnMoreAboutWhyUserCanNotUpgrade: () -> Unit,
|
||||||
|
onMakeGooglePlayServicesAvailable: () -> Unit,
|
||||||
|
onOpenPlayStore: () -> Unit
|
||||||
) {
|
) {
|
||||||
Scaffolds.Settings(
|
Scaffolds.Settings(
|
||||||
title = "",
|
title = "",
|
||||||
@@ -161,28 +166,26 @@ fun MessageBackupsTypeSelectionScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val hasCurrentBackupTier = currentBackupTier != null
|
val hasCurrentBackupTier = currentBackupTier != null
|
||||||
var displayNotAvailableDialog by remember { mutableStateOf(false) }
|
val paidTierNotAvailableDialogState = remember { PaidTierNotAvailableDialogState() }
|
||||||
val onSubscribeButtonClick = remember(isBillingApiAvailable, selectedBackupTier) {
|
val onSubscribeButtonClick = remember(googlePlayServicesAvailability, googlePlayBillingAvailability, selectedBackupTier) {
|
||||||
{
|
{
|
||||||
if (selectedBackupTier == MessageBackupTier.PAID && !isBillingApiAvailable) {
|
if (selectedBackupTier == MessageBackupTier.PAID && googlePlayServicesAvailability != GooglePlayServicesAvailability.SUCCESS) {
|
||||||
displayNotAvailableDialog = true
|
paidTierNotAvailableDialogState.displayGooglePlayApiErrorDialog = true
|
||||||
|
} else if (selectedBackupTier == MessageBackupTier.PAID && !googlePlayBillingAvailability.isSuccess) {
|
||||||
|
paidTierNotAvailableDialogState.displayGooglePlayBillingErrorDialog = true
|
||||||
} else {
|
} else {
|
||||||
onNextClicked()
|
onNextClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (displayNotAvailableDialog) {
|
PaidTierNotAvailableDialogs(
|
||||||
UpgradeNotAvailableDialog(
|
state = paidTierNotAvailableDialogState,
|
||||||
onConfirm = {
|
onOpenPlayStore = onOpenPlayStore,
|
||||||
displayNotAvailableDialog = false
|
onLearnMoreAboutWhyUserCanNotUpgrade = onLearnMoreAboutWhyUserCanNotUpgrade,
|
||||||
},
|
onMakeGooglePlayServicesAvailable = onMakeGooglePlayServicesAvailable,
|
||||||
onDismiss = onLearnMoreAboutWhyUserCanNotUpgrade,
|
googlePlayServicesAvailability = googlePlayServicesAvailability
|
||||||
onDismissRequest = {
|
)
|
||||||
displayNotAvailableDialog = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Buttons.LargeTonal(
|
Buttons.LargeTonal(
|
||||||
onClick = onSubscribeButtonClick,
|
onClick = onSubscribeButtonClick,
|
||||||
@@ -193,7 +196,9 @@ fun MessageBackupsTypeSelectionScreen(
|
|||||||
.padding(vertical = if (hasCurrentBackupTier) 10.dp else 16.dp)
|
.padding(vertical = if (hasCurrentBackupTier) 10.dp else 16.dp)
|
||||||
) {
|
) {
|
||||||
val text: String = if (currentBackupTier == null) {
|
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 paidTier = allBackupTypes.first { it.tier == MessageBackupTier.PAID } as MessageBackupsType.Paid
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -207,6 +212,8 @@ fun MessageBackupsTypeSelectionScreen(
|
|||||||
} else {
|
} else {
|
||||||
stringResource(R.string.MessageBackupsTypeSelectionScreen__subscribe)
|
stringResource(R.string.MessageBackupsTypeSelectionScreen__subscribe)
|
||||||
}
|
}
|
||||||
|
} else if (selectedBackupTier == MessageBackupTier.PAID && (googlePlayServicesAvailability != GooglePlayServicesAvailability.SUCCESS || !googlePlayBillingAvailability.isSuccess)) {
|
||||||
|
stringResource(R.string.MessageBackupsTypeSelectionScreen__more_about_this_plan)
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.MessageBackupsTypeSelectionScreen__change_backup_type)
|
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
|
@Composable
|
||||||
private fun UpgradeNotAvailableDialog(
|
fun PaidTierNotAvailableDialogs(
|
||||||
onConfirm: () -> Unit,
|
state: PaidTierNotAvailableDialogState,
|
||||||
onDismiss: () -> Unit,
|
googlePlayServicesAvailability: GooglePlayServicesAvailability,
|
||||||
onDismissRequest: () -> Unit
|
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(
|
Dialogs.SimpleAlertDialog(
|
||||||
title = stringResource(R.string.MessageBackupsTypeSelectionScreen__cant_upgrade_plan),
|
title = stringResource(R.string.GooglePlayServicesAvailability__service_disabled_title),
|
||||||
body = stringResource(R.string.MessageBackupsTypeSelectionScreen__to_subscribe_to_signal_secure_backups),
|
body = "To subscribe to Signal Secure Backups, please sign into the Google Play store.",
|
||||||
confirm = stringResource(android.R.string.ok),
|
onConfirm = onOpenPlayStore,
|
||||||
dismiss = stringResource(R.string.MessageBackupsTypeSelectionScreen__learn_more),
|
onDismiss = onDismissRequest,
|
||||||
onConfirm = onConfirm,
|
onDismissRequest = onDismissRequest,
|
||||||
onDismiss = onDismiss,
|
confirm = "Open Play Store",
|
||||||
onDismissRequest = onDismissRequest
|
dismiss = stringResource(android.R.string.cancel)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,8 +293,11 @@ private fun MessageBackupsTypeSelectionScreenPreview() {
|
|||||||
onReadMoreClicked = {},
|
onReadMoreClicked = {},
|
||||||
onNextClicked = {},
|
onNextClicked = {},
|
||||||
onLearnMoreAboutWhyUserCanNotUpgrade = {},
|
onLearnMoreAboutWhyUserCanNotUpgrade = {},
|
||||||
|
onMakeGooglePlayServicesAvailable = {},
|
||||||
|
onOpenPlayStore = {},
|
||||||
currentBackupTier = null,
|
currentBackupTier = null,
|
||||||
isBillingApiAvailable = true,
|
googlePlayServicesAvailability = GooglePlayServicesAvailability.SUCCESS,
|
||||||
|
googlePlayBillingAvailability = BillingResponseCode.OK,
|
||||||
isNextEnabled = true
|
isNextEnabled = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -278,25 +318,16 @@ private fun MessageBackupsTypeSelectionScreenWithCurrentTierPreview() {
|
|||||||
onReadMoreClicked = {},
|
onReadMoreClicked = {},
|
||||||
onNextClicked = {},
|
onNextClicked = {},
|
||||||
onLearnMoreAboutWhyUserCanNotUpgrade = {},
|
onLearnMoreAboutWhyUserCanNotUpgrade = {},
|
||||||
|
onMakeGooglePlayServicesAvailable = {},
|
||||||
|
onOpenPlayStore = {},
|
||||||
currentBackupTier = MessageBackupTier.PAID,
|
currentBackupTier = MessageBackupTier.PAID,
|
||||||
isBillingApiAvailable = true,
|
googlePlayServicesAvailability = GooglePlayServicesAvailability.SUCCESS,
|
||||||
|
googlePlayBillingAvailability = BillingResponseCode.OK,
|
||||||
isNextEnabled = true
|
isNextEnabled = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SignalPreview
|
|
||||||
@Composable
|
|
||||||
private fun UpgradeNotAvailableDialogPreview() {
|
|
||||||
Previews.Preview {
|
|
||||||
UpgradeNotAvailableDialog(
|
|
||||||
onConfirm = {},
|
|
||||||
onDismiss = {},
|
|
||||||
onDismissRequest = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MessageBackupsTypeBlock(
|
fun MessageBackupsTypeBlock(
|
||||||
messageBackupsType: MessageBackupsType,
|
messageBackupsType: MessageBackupsType,
|
||||||
@@ -382,8 +413,12 @@ private fun getFormattedPricePerMonth(messageBackupsType: MessageBackupsType): S
|
|||||||
return when (messageBackupsType) {
|
return when (messageBackupsType) {
|
||||||
is MessageBackupsType.Free -> stringResource(id = R.string.MessageBackupsTypeSelectionScreen__free)
|
is MessageBackupsType.Free -> stringResource(id = R.string.MessageBackupsTypeSelectionScreen__free)
|
||||||
is MessageBackupsType.Paid -> {
|
is MessageBackupsType.Paid -> {
|
||||||
val formattedAmount = FiatMoneyUtil.format(LocalContext.current.resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
|
if (messageBackupsType.pricePerMonth.amount == BigDecimal.ZERO) {
|
||||||
stringResource(id = R.string.MessageBackupsTypeSelectionScreen__s_month, formattedAmount)
|
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.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import com.google.android.gms.common.GoogleApiAvailability
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.asFlowable
|
import kotlinx.coroutines.rx3.asFlowable
|
||||||
@@ -59,6 +60,7 @@ abstract class UpgradeToPaidTierBottomSheet : ComposeBottomSheetDialogFragment()
|
|||||||
private val viewModel: MessageBackupsFlowViewModel by viewModel {
|
private val viewModel: MessageBackupsFlowViewModel by viewModel {
|
||||||
MessageBackupsFlowViewModel(
|
MessageBackupsFlowViewModel(
|
||||||
initialTierSelection = MessageBackupTier.PAID,
|
initialTierSelection = MessageBackupTier.PAID,
|
||||||
|
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()),
|
||||||
startScreen = MessageBackupsStage.TYPE_SELECTION
|
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
|
@Composable
|
||||||
override fun SheetContent() {
|
override fun SheetContent() {
|
||||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val isBillingApiAvailable = AppDependencies.billingApi.isApiAvailable()
|
val isBillingApiAvailable = AppDependencies.billingApi.getApiAvailability().isSuccess
|
||||||
if (isBillingApiAvailable) {
|
if (isBillingApiAvailable) {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(isPaidTierPricingAvailable = true)
|
it.copy(isPaidTierPricingAvailable = true)
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class ManageStorageSettingsViewModel : ViewModel() {
|
|||||||
|
|
||||||
private suspend fun getOnDeviceStorageOptimizationState(): OnDeviceStorageOptimizationState {
|
private suspend fun getOnDeviceStorageOptimizationState(): OnDeviceStorageOptimizationState {
|
||||||
return when {
|
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.backupTier != MessageBackupTier.PAID -> OnDeviceStorageOptimizationState.REQUIRES_PAID_TIER
|
||||||
SignalStore.backup.optimizeStorage -> OnDeviceStorageOptimizationState.ENABLED
|
SignalStore.backup.optimizeStorage -> OnDeviceStorageOptimizationState.ENABLED
|
||||||
else -> OnDeviceStorageOptimizationState.DISABLED
|
else -> OnDeviceStorageOptimizationState.DISABLED
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
|
|||||||
return Result.success()
|
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)
|
Log.i(TAG, "Google Play Billing API is not available on this device. Clearing mismatch value and exiting.", true)
|
||||||
SignalStore.backup.subscriptionStateMismatchDetected = false
|
SignalStore.backup.subscriptionStateMismatchDetected = false
|
||||||
return Result.success()
|
return Result.success()
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class InAppPaymentPurchaseTokenJob private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun linkPurchaseToken(): Result {
|
private suspend fun linkPurchaseToken(): Result {
|
||||||
if (!AppDependencies.billingApi.isApiAvailable()) {
|
if (!AppDependencies.billingApi.getApiAvailability().isSuccess) {
|
||||||
warning("Billing API is not available on this device. Exiting.")
|
warning("Billing API is not available on this device. Exiting.")
|
||||||
return Result.failure()
|
return Result.failure()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ class PostRegistrationBackupRedemptionJob : CoroutineJob {
|
|||||||
val emptyPrice = FiatMoney(BigDecimal.ZERO, Currency.getInstance(Locale.getDefault()))
|
val emptyPrice = FiatMoney(BigDecimal.ZERO, Currency.getInstance(Locale.getDefault()))
|
||||||
val price: FiatMoney = if (subscription != null) {
|
val price: FiatMoney = if (subscription != null) {
|
||||||
FiatMoney.fromSignalNetworkAmount(subscription.amount, Currency.getInstance(subscription.currency))
|
FiatMoney.fromSignalNetworkAmount(subscription.amount, Currency.getInstance(subscription.currency))
|
||||||
} else if (AppDependencies.billingApi.isApiAvailable()) {
|
} else if (AppDependencies.billingApi.getApiAvailability().isSuccess) {
|
||||||
AppDependencies.billingApi.queryProduct()?.price ?: emptyPrice
|
AppDependencies.billingApi.queryProduct()?.price ?: emptyPrice
|
||||||
} else {
|
} else {
|
||||||
emptyPrice
|
emptyPrice
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class LogSectionRemoteBackups : LogSection {
|
|||||||
output.append("\n -- Subscription State\n")
|
output.append("\n -- Subscription State\n")
|
||||||
|
|
||||||
val backupSubscriptionId = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)
|
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)
|
val inAppPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP)
|
||||||
|
|
||||||
output.append("Has backup subscription id: ${backupSubscriptionId != null}\n")
|
output.append("Has backup subscription id: ${backupSubscriptionId != null}\n")
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ internal class GooglePlayBillingPurchaseTokenMigrationJob private constructor(
|
|||||||
|
|
||||||
if (backupSubscriber.iapSubscriptionId?.purchaseToken == "-") {
|
if (backupSubscriber.iapSubscriptionId?.purchaseToken == "-") {
|
||||||
val purchaseResult: BillingPurchaseResult.Success? = runBlocking {
|
val purchaseResult: BillingPurchaseResult.Success? = runBlocking {
|
||||||
if (AppDependencies.billingApi.isApiAvailable()) {
|
if (AppDependencies.billingApi.getApiAvailability().isSuccess) {
|
||||||
val purchase = AppDependencies.billingApi.queryPurchases()
|
val purchase = AppDependencies.billingApi.queryPurchases()
|
||||||
|
|
||||||
if (purchase is BillingPurchaseResult.Success) {
|
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) {
|
private static void openPlayStore(@NonNull Context context) {
|
||||||
String packageName = context.getPackageName();
|
String packageName = context.getPackageName();
|
||||||
|
|
||||||
|
|||||||
@@ -8041,6 +8041,32 @@
|
|||||||
<!-- Row title to change or cancel subscription -->
|
<!-- Row title to change or cancel subscription -->
|
||||||
<string name="BackupsTypeSettingsFragment__change_or_cancel_subscription">Change or cancel subscription</string>
|
<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 -->
|
<!-- ManageStorageSettingsFragment -->
|
||||||
<!-- Settings section title header show above manage settings options for automatic chat history management (deleting) -->
|
<!-- Settings section title header show above manage settings options for automatic chat history management (deleting) -->
|
||||||
<string name="ManageStorageSettingsFragment_chat_limit">Chat limits</string>
|
<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>
|
<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 -->
|
<!-- Primary action button label when selecting a backup tier with a current selection -->
|
||||||
<string name="MessageBackupsTypeSelectionScreen__change_backup_type">Change backup type</string>
|
<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 -->
|
<!-- Secondary action button label when selecting a backup tier with a current selection -->
|
||||||
<string name="MessageBackupsTypeSelectionScreen__cancel_subscription">Cancel subscription</string>
|
<string name="MessageBackupsTypeSelectionScreen__cancel_subscription">Cancel subscription</string>
|
||||||
<!-- MessageBackupsType block amount for free tier -->
|
<!-- MessageBackupsType block amount for free tier -->
|
||||||
<string name="MessageBackupsTypeSelectionScreen__free">Free</string>
|
<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. -->
|
<!-- MessageBackupsType block amount for paid tier. Placeholder is formatted currency amount. -->
|
||||||
<string name="MessageBackupsTypeSelectionScreen__s_month">%1$s/month</string>
|
<string name="MessageBackupsTypeSelectionScreen__s_month">%1$s/month</string>
|
||||||
<!-- Title for free tier -->
|
<!-- 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
|
* 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
|
* to out-of-date Google Play API
|
||||||
*/
|
*/
|
||||||
override suspend fun isApiAvailable(): Boolean {
|
override suspend fun getApiAvailability(): org.signal.core.util.billing.BillingResponseCode {
|
||||||
return try {
|
return try {
|
||||||
doOnConnectionReady("isApiAvailable") {
|
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) {
|
} catch (e: BillingError) {
|
||||||
Log.e(TAG, "Failed to connect to Google Play Billing", e)
|
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()
|
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
|
* 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