mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 20:48:43 +00:00
Add "Backups Subscription not found" states.
This commit is contained in:
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user