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.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -26,6 +28,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
@@ -119,7 +122,7 @@ class AppSettingsFragment : ComposeFragment(), Callbacks {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
viewModel.refreshExpiredGiftBadge() viewModel.refresh()
viewModel.refreshDeprecatedOrUnregistered() 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 { item {
Rows.TextRow( Rows.TextRow(
text = stringResource(R.string.AccountSettingsFragment__account), text = stringResource(R.string.AccountSettingsFragment__account),
@@ -555,7 +596,8 @@ private fun AppSettingsContentPreview() {
showInternalPreferences = true, showInternalPreferences = true,
showPayments = true, showPayments = true,
showAppUpdates = true, showAppUpdates = true,
showBackups = true showBackups = true,
backupFailureState = BackupFailureState.SUBSCRIPTION_STATE_MISMATCH
), ),
bannerManager = BannerManager( bannerManager = BannerManager(
banners = listOf(TestBanner()) banners = listOf(TestBanner())

View File

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

View File

@@ -60,7 +60,20 @@ class AppSettingsViewModel : ViewModel() {
} }
} }
fun refreshExpiredGiftBadge() { fun refresh() {
store.update { it.copy(hasExpiredGiftBadge = SignalStore.inAppPayments.getExpiredGiftBadge() != null) } 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.animation.AnimatedContent
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.AlertDialogDefaults
@@ -45,8 +48,10 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.dimensionResource
@@ -94,6 +99,8 @@ import org.thoughtcrime.securesms.util.viewModel
import java.math.BigDecimal import java.math.BigDecimal
import java.util.Currency import java.util.Currency
import java.util.Locale import java.util.Locale
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
/** /**
@@ -199,6 +206,18 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
override fun onSkipMediaRestore() { override fun onSkipMediaRestore() {
// TODO - [backups] Skip media restoration // 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() { private fun displayBackupKey() {
@@ -275,6 +294,9 @@ private interface ContentCallbacks {
fun onViewBackupKeyClick() = Unit fun onViewBackupKeyClick() = Unit
fun onSkipMediaRestore() = Unit fun onSkipMediaRestore() = Unit
fun onCancelMediaRestore() = Unit fun onCancelMediaRestore() = Unit
fun onRenewLostSubscription() = Unit
fun onLearnMoreAboutLostSubscription() = Unit
fun onContactSupport() = Unit
} }
@Composable @Composable
@@ -313,13 +335,23 @@ private fun RemoteBackupsSettingsContent(
is RemoteBackupsSettingsState.BackupState.Loading -> { is RemoteBackupsSettingsState.BackupState.Loading -> {
LoadingCard() LoadingCard()
} }
is RemoteBackupsSettingsState.BackupState.Error -> { is RemoteBackupsSettingsState.BackupState.Error -> {
ErrorCard() ErrorCard()
} }
is RemoteBackupsSettingsState.BackupState.Pending -> { is RemoteBackupsSettingsState.BackupState.Pending -> {
PendingCard(state.price) PendingCard(state.price)
} }
is RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay -> {
SubscriptionMismatchMissingGooglePlayCard(
state = state,
onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription,
onRenewClick = contentCallbacks::onRenewLostSubscription
)
}
RemoteBackupsSettingsState.BackupState.None -> Unit RemoteBackupsSettingsState.BackupState.None -> Unit
is RemoteBackupsSettingsState.BackupState.WithTypeAndRenewalTime -> { is RemoteBackupsSettingsState.BackupState.WithTypeAndRenewalTime -> {
@@ -404,6 +436,13 @@ private fun RemoteBackupsSettingsContent(
RemoteBackupsSettingsState.Dialog.DOWNLOADING_YOUR_BACKUP -> { RemoteBackupsSettingsState.Dialog.DOWNLOADING_YOUR_BACKUP -> {
DownloadingYourBackupDialog(onDismiss = contentCallbacks::onDialogDismissed) DownloadingYourBackupDialog(onDismiss = contentCallbacks::onDialogDismissed)
} }
RemoteBackupsSettingsState.Dialog.SUBSCRIPTION_NOT_FOUND -> {
SubscriptionNotFoundBottomSheet(
onDismiss = contentCallbacks::onDialogDismissed,
onContactSupport = contentCallbacks::onContactSupport
)
}
} }
val snackbarMessageId = remember(requestedSnackbar) { 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 @Composable
private fun InProgressBackupRow( private fun InProgressBackupRow(
progress: Int?, 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 @SignalPreview
@Composable @Composable
private fun BackupCardPreview() { private fun BackupCardPreview() {

View File

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

View File

@@ -20,6 +20,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.asFlow import kotlinx.coroutines.reactive.asFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.signal.core.util.billing.BillingPurchaseResult
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney import org.signal.core.util.money.FiatMoney
import org.signal.donations.InAppPaymentType import org.signal.donations.InAppPaymentType
@@ -153,6 +154,44 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
return 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) { when (tier) {
MessageBackupTier.PAID -> { MessageBackupTier.PAID -> {
Log.d(TAG, "Attempting to retrieve subscription details for active PAID backup.") 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.billing.BillingPurchaseResult
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.donations.InAppPaymentType import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository 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 { override suspend fun doRun(): Result {
if (!SignalStore.account.isRegistered) { 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() return Result.success()
} }
if (!RemoteConfig.messageBackups) { 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() return Result.success()
} }
if (!SignalStore.backup.areBackupsEnabled) { 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() return Result.success()
} }
if (!AppDependencies.billingApi.isApiAvailable()) { 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() return Result.success()
} }
@@ -81,41 +86,35 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
val hasActivePurchase = purchase is BillingPurchaseResult.Success && purchase.isAcknowledged && purchase.isWithinTheLastMonth() val hasActivePurchase = purchase is BillingPurchaseResult.Success && purchase.isAcknowledged && purchase.isWithinTheLastMonth()
synchronized(InAppPaymentSubscriberRecord.Type.BACKUP) { 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) 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() val activeSubscription = RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()
if (activeSubscription?.isActive == true && tier != MessageBackupTier.PAID && inAppPayment?.state != InAppPaymentTable.State.PENDING) { val hasActiveSignalSubscription = activeSubscription?.isActive == true
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? Log.i(TAG, "Synchronizing backup tier with value from server.")
return Result.success() BackupRepository.getBackupTier().runIfSuccessful {
SignalStore.backup.backupTier = it
} }
if (activeSubscription?.isActive != true && tier == MessageBackupTier.PAID) { val hasActivePaidBackupTier = SignalStore.backup.backupTier == MessageBackupTier.PAID
Log.w(TAG, "User subscription is inactive or does not exist. User will need to cancel and resubscribe.") val hasValidActiveState = hasActivePaidBackupTier && hasActiveSignalSubscription && hasActivePurchase
// TODO [message-backups] Set UI hint? 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() 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_ARCHIVE_UPLOAD_STATE = "backup.archiveUploadState"
private const val KEY_BACKUP_UPLOADED = "backup.backupUploaded" private const val KEY_BACKUP_UPLOADED = "backup.backupUploaded"
private const val KEY_SUBSCRIPTION_STATE_MISMATCH = "backup.subscriptionStateMismatch"
private val cachedCdnCredentialsExpiresIn: Duration = 12.hours 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) 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 * 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 * 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() markDonationManuallyCancelled()
} else { } else {
SignalStore.backup.subscriptionStateMismatchDetected = false
markBackupSubscriptionpManuallyCancelled() markBackupSubscriptionpManuallyCancelled()
SignalStore.backup.disableBackups() SignalStore.backup.disableBackups()
@@ -513,6 +514,7 @@ class InAppPaymentValues internal constructor(store: KeyValueStore) : SignalStor
} else { } else {
clearBackupSubscriptionManuallyCancelled() clearBackupSubscriptionManuallyCancelled()
SignalStore.backup.subscriptionStateMismatchDetected = false
SignalStore.backup.backupTier = MessageBackupTier.PAID SignalStore.backup.backupTier = MessageBackupTier.PAID
SignalStore.uiHints.markHasEverEnabledRemoteBackups() SignalStore.uiHints.markHasEverEnabledRemoteBackups()
} }

View File

@@ -43,6 +43,13 @@
app:exitAnim="@anim/fragment_open_exit" app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter" app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" /> 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 <action
android:id="@+id/action_appSettingsFragment_to_linkDeviceFragment" android:id="@+id/action_appSettingsFragment_to_linkDeviceFragment"
app:destination="@id/linkDeviceFragment" app:destination="@id/linkDeviceFragment"

View File

@@ -4964,6 +4964,9 @@
<string name="QualitySelectorBottomSheetDialog__media_quality">Media quality</string> <string name="QualitySelectorBottomSheetDialog__media_quality">Media quality</string>
<!-- AppSettingsFragment --> <!-- 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 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 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> <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> <string name="RemoteBackupsSettingsFragment__downloading_your_backup">Downloading your backup</string>
<!-- Dialog message for dialog which alerts user their optimized media will be downloaded --> <!-- 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> <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 --> <!-- MessageBackupsEducationScreen -->
<!-- Screen subtitle underneath large headline title --> <!-- Screen subtitle underneath large headline title -->