Add "Backups Subscription not found" states.

This commit is contained in:
Alex Hart
2024-10-30 12:13:03 -03:00
committed by GitHub
parent d51fe5fe81
commit ddcb9564bb
13 changed files with 513 additions and 39 deletions

View File

@@ -11,10 +11,12 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -26,6 +28,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.colorResource
@@ -119,7 +122,7 @@ class AppSettingsFragment : ComposeFragment(), Callbacks {
override fun onResume() {
super.onResume()
viewModel.refreshExpiredGiftBadge()
viewModel.refresh()
viewModel.refreshDeprecatedOrUnregistered()
}
@@ -184,6 +187,44 @@ private fun AppSettingsContent(
)
}
if (state.backupFailureState != BackupFailureState.NONE) {
item {
Dividers.Default()
}
item {
Rows.TextRow(
text = {
Text(text = stringResource(R.string.AppSettingsFragment__renew_your_signal_backups_subscription))
},
icon = {
Box {
Icon(
painter = painterResource(R.drawable.symbol_backup_24),
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = null
)
Box(
modifier = Modifier
.absoluteOffset(3.dp, (-2).dp)
.background(color = Color(0xFFFFCC00), shape = CircleShape)
.size(12.dp)
.align(Alignment.TopEnd)
)
}
},
onClick = {
callbacks.navigate(R.id.action_appSettingsFragment_to_remoteBackupsSettingsFragment)
}
)
}
item {
Dividers.Default()
}
}
item {
Rows.TextRow(
text = stringResource(R.string.AccountSettingsFragment__account),
@@ -555,7 +596,8 @@ private fun AppSettingsContentPreview() {
showInternalPreferences = true,
showPayments = true,
showAppUpdates = true,
showBackups = true
showBackups = true,
backupFailureState = BackupFailureState.SUBSCRIPTION_STATE_MISMATCH
),
bannerManager = BannerManager(
banners = listOf(TestBanner())

View File

@@ -15,7 +15,8 @@ data class AppSettingsState(
val showInternalPreferences: Boolean = RemoteConfig.internalUser,
val showPayments: Boolean = SignalStore.payments.paymentsAvailability.showPaymentsMenu(),
val showAppUpdates: Boolean = Environment.IS_NIGHTLY,
val showBackups: Boolean = RemoteConfig.messageBackups
val showBackups: Boolean = RemoteConfig.messageBackups,
val backupFailureState: BackupFailureState = BackupFailureState.NONE
) {
fun isRegisteredAndUpToDate(): Boolean {
return !userUnregistered && !clientDeprecated

View File

@@ -60,7 +60,20 @@ class AppSettingsViewModel : ViewModel() {
}
}
fun refreshExpiredGiftBadge() {
store.update { it.copy(hasExpiredGiftBadge = SignalStore.inAppPayments.getExpiredGiftBadge() != null) }
fun refresh() {
store.update {
it.copy(
hasExpiredGiftBadge = SignalStore.inAppPayments.getExpiredGiftBadge() != null,
backupFailureState = getBackupFailureState()
)
}
}
private fun getBackupFailureState(): BackupFailureState {
return if (SignalStore.backup.subscriptionStateMismatchDetected) {
BackupFailureState.SUBSCRIPTION_STATE_MISMATCH
} else {
BackupFailureState.NONE
}
}
}

View File

@@ -0,0 +1,14 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app
/**
* Describes the current backup failure state.
*/
enum class BackupFailureState {
NONE,
SUBSCRIPTION_STATE_MISMATCH
}

View File

@@ -14,7 +14,9 @@ import androidx.biometric.BiometricPrompt
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -26,6 +28,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
@@ -45,8 +48,10 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
@@ -94,6 +99,8 @@ import org.thoughtcrime.securesms.util.viewModel
import java.math.BigDecimal
import java.util.Currency
import java.util.Locale
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/**
@@ -199,6 +206,18 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
override fun onSkipMediaRestore() {
// TODO - [backups] Skip media restoration
}
override fun onLearnMoreAboutLostSubscription() {
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.SUBSCRIPTION_NOT_FOUND)
}
override fun onRenewLostSubscription() {
// TODO - [backups] Need process here (cancel first?)
}
override fun onContactSupport() {
// TODO - [backups] Need to contact support.
}
}
private fun displayBackupKey() {
@@ -275,6 +294,9 @@ private interface ContentCallbacks {
fun onViewBackupKeyClick() = Unit
fun onSkipMediaRestore() = Unit
fun onCancelMediaRestore() = Unit
fun onRenewLostSubscription() = Unit
fun onLearnMoreAboutLostSubscription() = Unit
fun onContactSupport() = Unit
}
@Composable
@@ -313,13 +335,23 @@ private fun RemoteBackupsSettingsContent(
is RemoteBackupsSettingsState.BackupState.Loading -> {
LoadingCard()
}
is RemoteBackupsSettingsState.BackupState.Error -> {
ErrorCard()
}
is RemoteBackupsSettingsState.BackupState.Pending -> {
PendingCard(state.price)
}
is RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay -> {
SubscriptionMismatchMissingGooglePlayCard(
state = state,
onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription,
onRenewClick = contentCallbacks::onRenewLostSubscription
)
}
RemoteBackupsSettingsState.BackupState.None -> Unit
is RemoteBackupsSettingsState.BackupState.WithTypeAndRenewalTime -> {
@@ -404,6 +436,13 @@ private fun RemoteBackupsSettingsContent(
RemoteBackupsSettingsState.Dialog.DOWNLOADING_YOUR_BACKUP -> {
DownloadingYourBackupDialog(onDismiss = contentCallbacks::onDialogDismissed)
}
RemoteBackupsSettingsState.Dialog.SUBSCRIPTION_NOT_FOUND -> {
SubscriptionNotFoundBottomSheet(
onDismiss = contentCallbacks::onDialogDismissed,
onContactSupport = contentCallbacks::onContactSupport
)
}
}
val snackbarMessageId = remember(requestedSnackbar) {
@@ -727,6 +766,85 @@ private fun PendingCard(
}
}
@Composable
private fun SubscriptionMismatchMissingGooglePlayCard(
state: RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay,
onRenewClick: () -> Unit = {},
onLearnMoreClick: () -> Unit = {}
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp)
.background(color = SignalTheme.colors.colorSurface2, shape = RoundedCornerShape(12.dp))
.padding(24.dp)
) {
val days by rememberUpdatedState((state.renewalTime - System.currentTimeMillis().milliseconds).inWholeDays)
Row {
Text(
text = pluralStringResource(R.plurals.RemoteBackupsSettingsFragment__your_subscription_on_this_device_is_valid, days.toInt(), days),
modifier = Modifier
.weight(1f)
.padding(end = 13.dp)
)
Box {
Image(
painter = painterResource(R.drawable.image_signal_backups),
contentDescription = null,
modifier = Modifier.size(64.dp)
)
Box(
modifier = Modifier
.size(22.dp)
.background(
color = Color(0xFFFFCC00),
shape = CircleShape
)
.border(5.dp, color = SignalTheme.colors.colorSurface2, shape = CircleShape)
.align(Alignment.TopEnd)
)
}
}
Row(
horizontalArrangement = spacedBy(16.dp)
) {
Buttons.LargeTonal(
onClick = onRenewClick,
colors = ButtonDefaults.filledTonalButtonColors().copy(
containerColor = SignalTheme.colors.colorTransparent5,
contentColor = colorResource(R.color.signal_light_colorOnSurface)
),
modifier = Modifier
.padding(top = 24.dp)
.weight(1f)
) {
Text(
text = stringResource(R.string.RemoteBackupsSettingsFragment__renew)
)
}
Buttons.LargeTonal(
onClick = onLearnMoreClick,
colors = ButtonDefaults.filledTonalButtonColors().copy(
containerColor = SignalTheme.colors.colorTransparent5,
contentColor = colorResource(R.color.signal_light_colorOnSurface)
),
modifier = Modifier
.padding(top = 24.dp)
.weight(1f)
) {
Text(
text = stringResource(R.string.RemoteBackupsSettingsFragment__learn_more)
)
}
}
}
}
@Composable
private fun InProgressBackupRow(
progress: Int?,
@@ -995,6 +1113,22 @@ private fun PendingCardPreview() {
}
}
@SignalPreview
@Composable
private fun SubscriptionMismatchMissingGooglePlayCardPreview() {
Previews.Preview {
SubscriptionMismatchMissingGooglePlayCard(
state = RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay(
messageBackupsType = MessageBackupsType.Paid(
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("CAD")),
storageAllowanceBytes = 100_000_000
),
renewalTime = System.currentTimeMillis().milliseconds + 30.days
)
)
}
}
@SignalPreview
@Composable
private fun BackupCardPreview() {

View File

@@ -91,6 +91,14 @@ data class RemoteBackupsSettingsState(
override val renewalTime: Duration
) : WithTypeAndRenewalTime
/**
* Subscription mismatch detected.
*/
data class SubscriptionMismatchMissingGooglePlay(
override val messageBackupsType: MessageBackupsType,
override val renewalTime: Duration
) : WithTypeAndRenewalTime
/**
* An error occurred retrieving the network state
*/
@@ -103,7 +111,8 @@ data class RemoteBackupsSettingsState(
BACKUP_FREQUENCY,
PROGRESS_SPINNER,
DOWNLOADING_YOUR_BACKUP,
TURN_OFF_FAILED
TURN_OFF_FAILED,
SUBSCRIPTION_NOT_FOUND
}
enum class Snackbar {

View File

@@ -20,6 +20,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.asFlow
import kotlinx.coroutines.withContext
import org.signal.core.util.billing.BillingPurchaseResult
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.donations.InAppPaymentType
@@ -153,6 +154,44 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
return
}
if (SignalStore.backup.subscriptionStateMismatchDetected) {
Log.d(TAG, "[subscriptionStateMismatchDetected] A mismatch was detected.")
val hasActiveGooglePlayBillingSubscription = when (val purchaseResult = AppDependencies.billingApi.queryPurchases()) {
is BillingPurchaseResult.Success -> purchaseResult.isAcknowledged && purchaseResult.isWithinTheLastMonth()
else -> false
}
Log.d(TAG, "[subscriptionStateMismatchDetected] hasActiveGooglePlayBillingSubscription: $hasActiveGooglePlayBillingSubscription")
val activeSubscription = withContext(Dispatchers.IO) {
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()
}
val hasActiveSignalSubscription = activeSubscription?.isActive == true
Log.d(TAG, "[subscriptionStateMismatchDetected] hasActiveSignalSubscription: $hasActiveSignalSubscription")
val type = withContext(Dispatchers.IO) {
BackupRepository.getBackupsType(MessageBackupTier.PAID) as MessageBackupsType.Paid
}
if (hasActiveSignalSubscription && !hasActiveGooglePlayBillingSubscription) {
_state.update {
it.copy(
backupState = RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay(
messageBackupsType = type,
renewalTime = activeSubscription!!.activeSubscription.endOfCurrentPeriod.seconds
)
)
}
}
// TODO [backups] - handle other cases.
return
}
when (tier) {
MessageBackupTier.PAID -> {
Log.d(TAG, "Attempting to retrieve subscription details for active PAID backup.")

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.backups.remote
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.signal.core.ui.horizontalGutters
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.R
/**
* Displayed after user taps "Learn more" when being notified that their subscription
* could not be found. This state is entered when a user has a Signal service backups
* subscription that is active but no on-device (Google Play Billing) subscription.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SubscriptionNotFoundBottomSheet(
onDismiss: () -> Unit,
onContactSupport: () -> Unit
) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = onDismiss,
dragHandle = { BottomSheets.Handle() }
) {
SubscriptionNotFoundContent(
onDismiss = onDismiss,
onContactSupport = onContactSupport
)
}
}
@Composable
private fun ColumnScope.SubscriptionNotFoundContent(
onDismiss: () -> Unit = {},
onContactSupport: () -> Unit = {}
) {
Text(
text = stringResource(R.string.SubscriptionNotFoundBottomSheet__subscription_not_found),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 28.dp)
.horizontalGutters()
.align(Alignment.CenterHorizontally)
)
Text(
text = stringResource(R.string.SubscriptionNotFoundBottomSheet__your_subscription_couldnt_be_restored),
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 12.dp)
.horizontalGutters()
.align(Alignment.CenterHorizontally)
)
Text(
text = stringResource(R.string.SubscriptionNotFoundBottomSheet__this_could_happen_if),
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier
.horizontalGutters()
.padding(bottom = 12.dp)
.align(Alignment.CenterHorizontally)
)
SubscriptionNotFoundReason(stringResource(R.string.SubscriptionNotFoundBottomSheet__youre_signed_into_the_play_store_with_a_different_google_account))
SubscriptionNotFoundReason(stringResource(R.string.SubscriptionNotFoundBottomSheet__you_transferred_from_an_iphone))
SubscriptionNotFoundReason(stringResource(R.string.SubscriptionNotFoundBottomSheet__your_subscription_recently_expired))
Text(
text = stringResource(R.string.SubscriptionNotFoundBottomSheet__if_you_have_an_active_subscription_on),
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 24.dp)
.horizontalGutters()
.align(Alignment.CenterHorizontally)
)
Buttons.LargeTonal(
onClick = onDismiss,
modifier = Modifier
.defaultMinSize(minWidth = 220.dp)
.padding(top = 36.dp)
.align(Alignment.CenterHorizontally)
) {
Text(text = stringResource(R.string.SubscriptionNotFoundBottomSheet__got_it))
}
TextButton(
onClick = onContactSupport,
modifier = Modifier
.defaultMinSize(minWidth = 220.dp)
.padding(top = 16.dp, bottom = 48.dp)
.align(Alignment.CenterHorizontally)
) {
Text(
text = stringResource(R.string.SubscriptionNotFoundBottomSheet__contact_support)
)
}
}
@Composable
private fun SubscriptionNotFoundReason(text: String) {
Row(
modifier = Modifier
.height(IntrinsicSize.Min)
.padding(horizontal = 36.dp)
.padding(top = 12.dp)
) {
Box(
modifier = Modifier
.padding(end = 12.dp)
.fillMaxHeight()
.padding(vertical = 2.dp)
.width(4.dp)
.background(
color = SignalTheme.colors.colorTransparentInverse2,
shape = RoundedCornerShape(2.dp)
)
)
Text(
text = text,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.weight(1f)
)
}
}
@SignalPreview
@Composable
private fun SubscriptionNotFoundContentPreview() {
Previews.BottomSheetPreview {
Column {
SubscriptionNotFoundContent()
}
}
}

View File

@@ -9,6 +9,7 @@ import androidx.annotation.VisibleForTesting
import org.signal.core.util.billing.BillingPurchaseResult
import org.signal.core.util.logging.Log
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
@@ -58,22 +59,26 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
override suspend fun doRun(): Result {
if (!SignalStore.account.isRegistered) {
Log.i(TAG, "User is not registered. Exiting.")
Log.i(TAG, "User is not registered. Clearing mismatch value and exiting.")
SignalStore.backup.subscriptionStateMismatchDetected = false
return Result.success()
}
if (!RemoteConfig.messageBackups) {
Log.i(TAG, "Message backups are not enabled. Exiting.")
Log.i(TAG, "Message backups are not enabled. Clearing mismatch value and exiting.")
SignalStore.backup.subscriptionStateMismatchDetected = false
return Result.success()
}
if (!SignalStore.backup.areBackupsEnabled) {
Log.i(TAG, "Backups are not enabled on this device. Exiting.")
Log.i(TAG, "Backups are not enabled on this device. Clearing mismatch value and exiting.")
SignalStore.backup.subscriptionStateMismatchDetected = false
return Result.success()
}
if (!AppDependencies.billingApi.isApiAvailable()) {
Log.i(TAG, "Google Play Billing API is not available on this device. Exiting.")
Log.i(TAG, "Google Play Billing API is not available on this device. Clearing mismatch value and exiting.")
SignalStore.backup.subscriptionStateMismatchDetected = false
return Result.success()
}
@@ -81,41 +86,35 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
val hasActivePurchase = purchase is BillingPurchaseResult.Success && purchase.isAcknowledged && purchase.isWithinTheLastMonth()
synchronized(InAppPaymentSubscriberRecord.Type.BACKUP) {
val subscriberId = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)
if (subscriberId == null && hasActivePurchase) {
Log.w(TAG, "User has active Google Play Billing purchase but no subscriber id! User should cancel backup and resubscribe.")
// TODO [message-backups] Set UI flag hint here to launch sheet (designs pending)
return Result.success()
}
val tier = SignalStore.backup.backupTier
if (subscriberId == null && tier == MessageBackupTier.PAID) {
Log.w(TAG, "User has no subscriber id but PAID backup tier. User will need to cancel and resubscribe.")
// TODO [message-backups] Set UI flag hint here to launch sheet (designs pending)
return Result.success()
}
val inAppPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP)
if (inAppPayment?.state == InAppPaymentTable.State.PENDING) {
Log.i(TAG, "User has a pending in-app payment. Clearing mismatch value and re-checking later.")
SignalStore.backup.subscriptionStateMismatchDetected = false
return Result.success()
}
val activeSubscription = RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()
if (activeSubscription?.isActive == true && tier != MessageBackupTier.PAID && inAppPayment?.state != InAppPaymentTable.State.PENDING) {
Log.w(TAG, "User has an active subscription but no backup tier and no pending redemption.")
// TODO [message-backups] Set UI flag hint here to launch error sheet?
return Result.success()
val hasActiveSignalSubscription = activeSubscription?.isActive == true
Log.i(TAG, "Synchronizing backup tier with value from server.")
BackupRepository.getBackupTier().runIfSuccessful {
SignalStore.backup.backupTier = it
}
if (activeSubscription?.isActive != true && tier == MessageBackupTier.PAID) {
Log.w(TAG, "User subscription is inactive or does not exist. User will need to cancel and resubscribe.")
// TODO [message-backups] Set UI hint?
val hasActivePaidBackupTier = SignalStore.backup.backupTier == MessageBackupTier.PAID
val hasValidActiveState = hasActivePaidBackupTier && hasActiveSignalSubscription && hasActivePurchase
val hasValidInactiveState = !hasActivePaidBackupTier && !hasActiveSignalSubscription && !hasActivePurchase
if (hasValidActiveState || hasValidInactiveState) {
Log.i(TAG, "Valid state: (hasValidActiveState: $hasValidActiveState, hasValidInactiveState: $hasValidInactiveState). Clearing mismatch value and exiting.", true)
SignalStore.backup.subscriptionStateMismatchDetected = false
return Result.success()
} else {
Log.w(TAG, "State mismatch: (hasActivePaidBackupTier: $hasActivePaidBackupTier, hasActiveSignalSubscription: $hasActiveSignalSubscription, hasActivePurchase: $hasActivePurchase). Setting mismatch value and exiting.", true)
SignalStore.backup.subscriptionStateMismatchDetected = true
return Result.success()
}
if (activeSubscription?.isActive != true && hasActivePurchase) {
Log.w(TAG, "User subscription is inactive but user has a recent purchase. User will need to cancel and resubscribe.")
// TODO [message-backups] Set UI hint?
return Result.success()
}
return Result.success()
}
}

View File

@@ -43,6 +43,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
private const val KEY_ARCHIVE_UPLOAD_STATE = "backup.archiveUploadState"
private const val KEY_BACKUP_UPLOADED = "backup.backupUploaded"
private const val KEY_SUBSCRIPTION_STATE_MISMATCH = "backup.subscriptionStateMismatch"
private val cachedCdnCredentialsExpiresIn: Duration = 12.hours
}
@@ -73,6 +74,12 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
*/
val latestBackupTier: MessageBackupTier? by enumValue(KEY_LATEST_BACKUP_TIER, null, MessageBackupTier.Serializer)
/**
* Denotes if there was a mismatch detected between the user's Signal subscription, on-device Google Play subscription,
* and what zk authorization we think we have.
*/
var subscriptionStateMismatchDetected: Boolean by booleanValue(KEY_SUBSCRIPTION_STATE_MISMATCH, false)
/**
* When seting the backup tier, we also want to write to the latestBackupTier, as long as
* the value is non-null. This gives us a 1-deep history of the selected backup tier for

View File

@@ -469,6 +469,7 @@ class InAppPaymentValues internal constructor(store: KeyValueStore) : SignalStor
}
markDonationManuallyCancelled()
} else {
SignalStore.backup.subscriptionStateMismatchDetected = false
markBackupSubscriptionpManuallyCancelled()
SignalStore.backup.disableBackups()
@@ -513,6 +514,7 @@ class InAppPaymentValues internal constructor(store: KeyValueStore) : SignalStor
} else {
clearBackupSubscriptionManuallyCancelled()
SignalStore.backup.subscriptionStateMismatchDetected = false
SignalStore.backup.backupTier = MessageBackupTier.PAID
SignalStore.uiHints.markHasEverEnabledRemoteBackups()
}

View File

@@ -43,6 +43,13 @@
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
<action
android:id="@+id/action_appSettingsFragment_to_remoteBackupsSettingsFragment"
app:destination="@id/remoteBackupsSettingsFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
<action
android:id="@+id/action_appSettingsFragment_to_linkDeviceFragment"
app:destination="@id/linkDeviceFragment"

View File

@@ -4964,6 +4964,9 @@
<string name="QualitySelectorBottomSheetDialog__media_quality">Media quality</string>
<!-- AppSettingsFragment -->
<!-- String alerting user that something is wrong with their backups subscription -->
<string name="AppSettingsFragment__renew_your_signal_backups_subscription">Renew your Signal Backups subscription</string>
<!-- String displayed telling user to invite their friends to Signal -->
<string name="AppSettingsFragment__invite_your_friends">Invite your friends</string>
<!-- String displayed in a toast when we successfully copy the donations subscriber id to the clipboard -->
<string name="AppSettingsFragment__copied_donor_subscriber_id_to_clipboard">Copied donor subscriber id to clipboard</string>
@@ -7682,6 +7685,35 @@
<string name="RemoteBackupsSettingsFragment__downloading_your_backup">Downloading your backup</string>
<!-- Dialog message for dialog which alerts user their optimized media will be downloaded -->
<string name="RemoteBackupsSettingsFragment__depending_on_the_size">Depending on the size of your backup, this could take a long time. You can use your phone as you normally do while the download takes place.</string>
<!-- Displayed in card when user has a signal subscription but the device doesn't see a google play billing subscription. Placeholder is days until subscription expiration. -->
<plurals name="RemoteBackupsSettingsFragment__your_subscription_on_this_device_is_valid">
<item quantity="one">Your subscription on this device is valid for the next %1$d day. Renew to continue using Signal Backups</item>
<item quantity="other">Your subscription on this device is valid for the next %1$d days. Renew to continue using Signal Backups</item>
</plurals>
<!-- Button label to start subscription renewal -->
<string name="RemoteBackupsSettingsFragment__renew">Renew</string>
<!-- Button label to learn more about why subscription disappeared -->
<string name="RemoteBackupsSettingsFragment__learn_more">Learn more</string>
<!-- SubscriptionNotFoundBottomSheet -->
<!-- Displayed as a bottom sheet title -->
<string name="SubscriptionNotFoundBottomSheet__subscription_not_found">Subscription not found</string>
<!-- Displayed below title on bottom sheet -->
<string name="SubscriptionNotFoundBottomSheet__your_subscription_couldnt_be_restored">Your subscription couldn\'t be restored.</string>
<!-- Displayed below title on bottom sheet -->
<string name="SubscriptionNotFoundBottomSheet__this_could_happen_if">This could happen if:</string>
<!-- First item in a bulleted list of reasons -->
<string name="SubscriptionNotFoundBottomSheet__youre_signed_into_the_play_store_with_a_different_google_account">You\'re signed into the Play Store with a different Google account.</string>
<!-- Second item in a bulleted list of reasons -->
<string name="SubscriptionNotFoundBottomSheet__you_transferred_from_an_iphone">You transferred from an iPhone.</string>
<!-- Third item in a bulleted list of reasons -->
<string name="SubscriptionNotFoundBottomSheet__your_subscription_recently_expired">Your subscription recently expired.</string>
<!-- Note underneath bullet points -->
<string name="SubscriptionNotFoundBottomSheet__if_you_have_an_active_subscription_on">If you have an active subscription on your old phone consider canceling it before it renews.</string>
<!-- Action button label to acknowledge and dismiss sheet -->
<string name="SubscriptionNotFoundBottomSheet__got_it">Got it</string>
<!-- Action button label to contact support -->
<string name="SubscriptionNotFoundBottomSheet__contact_support">Contact support</string>
<!-- MessageBackupsEducationScreen -->
<!-- Screen subtitle underneath large headline title -->