Read and use backups data to structure tier feature sets.

This commit is contained in:
Alex Hart 2024-08-19 15:35:49 -03:00 committed by mtang-signal
parent 478e3a7233
commit fd31bc60b2
12 changed files with 258 additions and 130 deletions

View file

@ -6,7 +6,6 @@
package org.thoughtcrime.securesms.backup.v2
import androidx.annotation.WorkerThread
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
@ -25,7 +24,6 @@ import org.signal.libsignal.messagebackup.MessageBackupKey
import org.signal.libsignal.protocol.ServiceId.Aci
import org.signal.libsignal.zkgroup.backups.BackupLevel
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
@ -46,7 +44,6 @@ import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeature
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
@ -83,7 +80,6 @@ import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.math.BigDecimal
import java.time.ZonedDateTime
import java.util.Currency
import java.util.Locale
@ -896,30 +892,29 @@ object BackupRepository {
suspend fun getBackupsType(tier: MessageBackupTier): MessageBackupsType {
val backupCurrency = SignalStore.inAppPayments.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.BACKUP)
return when (tier) {
MessageBackupTier.FREE -> getFreeType(backupCurrency)
MessageBackupTier.FREE -> getFreeType()
MessageBackupTier.PAID -> getPaidType(backupCurrency)
}
}
private fun getFreeType(currency: Currency): MessageBackupsType {
return MessageBackupsType(
tier = MessageBackupTier.FREE,
pricePerMonth = FiatMoney(BigDecimal.ZERO, currency),
title = "Text + 30 days of media", // TODO [message-backups] Finalize text (does this come from server?)
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup" // TODO [message-backups] Finalize text (does this come from server?)
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Last 30 days of media" // TODO [message-backups] Finalize text (does this come from server?)
)
)
private suspend fun getFreeType(): MessageBackupsType {
val config = getSubscriptionsConfiguration()
return MessageBackupsType.Free(
mediaRetentionDays = config.backupConfiguration.freeTierMediaDays
)
}
private suspend fun getPaidType(currency: Currency): MessageBackupsType {
val config = getSubscriptionsConfiguration()
return MessageBackupsType.Paid(
pricePerMonth = FiatMoney(config.currencies[currency.currencyCode.lowercase()]!!.backupSubscription[SubscriptionsConfiguration.BACKUPS_LEVEL]!!, currency),
storageAllowanceBytes = config.backupConfiguration.backupLevelConfigurationMap[SubscriptionsConfiguration.BACKUPS_LEVEL]!!.storageAllowanceBytes
)
}
private suspend fun getSubscriptionsConfiguration(): SubscriptionsConfiguration {
val serviceResponse = withContext(Dispatchers.IO) {
AppDependencies
.donationsService
@ -938,31 +933,7 @@ object BackupRepository {
error("Unhandled error occurred while downloading configuration.")
}
val config = serviceResponse.result.get()
return MessageBackupsType(
tier = MessageBackupTier.PAID,
pricePerMonth = FiatMoney(config.currencies[currency.currencyCode.lowercase()]!!.backupSubscription[SubscriptionsConfiguration.BACKUPS_LEVEL]!!, currency),
title = "Text + All your media", // TODO [message-backups] Finalize text (does this come from server?)
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup" // TODO [message-backups] Finalize text (does this come from server?)
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Full media backup" // TODO [message-backups] Finalize text (does this come from server?)
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "1TB of storage (~250K photos)" // TODO [message-backups] Finalize text (does this come from server?)
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
label = "Thanks for supporting Signal!" // TODO [message-backups] Finalize text (does this come from server?)
)
)
)
return serviceResponse.result.get()
}
/**

View file

@ -32,13 +32,11 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.updateLayoutParams
import kotlinx.collections.immutable.persistentListOf
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.databinding.PaypalButtonBinding
@ -49,7 +47,7 @@ import java.util.Currency
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MessageBackupsCheckoutSheet(
messageBackupsType: MessageBackupsType,
messageBackupsType: MessageBackupsType.Paid,
availablePaymentMethods: List<InAppPaymentData.PaymentMethodType>,
sheetState: SheetState,
onDismissRequest: () -> Unit,
@ -78,7 +76,7 @@ fun MessageBackupsCheckoutSheet(
@Composable
private fun SheetContent(
messageBackupsType: MessageBackupsType,
messageBackupsType: MessageBackupsType.Paid,
availablePaymentGateways: List<InAppPaymentData.PaymentMethodType>,
onPaymentGatewaySelected: (InAppPaymentData.PaymentMethodType) -> Unit
) {
@ -244,11 +242,9 @@ private fun MessageBackupsCheckoutSheetPreview() {
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
SheetContent(
messageBackupsType = MessageBackupsType(
tier = MessageBackupTier.FREE,
title = "Free",
messageBackupsType = MessageBackupsType.Paid(
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
features = persistentListOf()
storageAllowanceBytes = 107374182400
),
availablePaymentGateways = availablePaymentGateways,
onPaymentGatewaySelected = {}

View file

@ -19,6 +19,7 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.fragment.findNavController
import io.reactivex.rxjava3.processors.PublishProcessor
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentCheckoutDelegate
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.InAppPaymentProcessorAction
import org.thoughtcrime.securesms.compose.ComposeFragment
@ -112,7 +113,15 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
currentBackupTier = state.currentMessageBackupTier,
selectedBackupTier = state.selectedMessageBackupTier,
availableBackupTypes = state.availableBackupTypes,
onMessageBackupsTierSelected = viewModel::onMessageBackupTierUpdated,
onMessageBackupsTierSelected = { tier ->
val type = state.availableBackupTypes.first { it.tier == tier }
val label = when (type) {
is MessageBackupsType.Free -> requireContext().resources.getQuantityString(R.plurals.MessageBackupsTypeSelectionScreen__text_plus_d_days_of_media, type.mediaRetentionDays, type.mediaRetentionDays)
is MessageBackupsType.Paid -> requireContext().getString(R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media)
}
viewModel.onMessageBackupTierUpdated(tier, label)
},
onNavigationClick = viewModel::goToPreviousScreen,
onReadMoreClicked = {},
onCancelSubscriptionClicked = viewModel::displayCancellationDialog,
@ -121,7 +130,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
if (state.screen == MessageBackupsScreen.CHECKOUT_SHEET) {
MessageBackupsCheckoutSheet(
messageBackupsType = state.availableBackupTypes.first { it.tier == state.selectedMessageBackupTier!! },
messageBackupsType = state.availableBackupTypes.filterIsInstance<MessageBackupsType.Paid>().first { it.tier == state.selectedMessageBackupTier!! },
availablePaymentMethods = state.availablePaymentMethods,
sheetState = checkoutSheetState,
onDismissRequest = {

View file

@ -12,6 +12,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
data class MessageBackupsFlowState(
val selectedMessageBackupTierLabel: String? = null,
val selectedMessageBackupTier: MessageBackupTier? = SignalStore.backup.backupTier,
val currentMessageBackupTier: MessageBackupTier? = SignalStore.backup.backupTier,
val availableBackupTypes: List<MessageBackupsType> = emptyList(),

View file

@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.util.money.FiatMoney
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
@ -25,6 +26,7 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaym
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayOrderStrategy
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
@ -33,6 +35,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.RemoteConfig
import org.whispersystems.signalservice.api.kbs.PinHashUtil.verifyLocalPinHash
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import java.math.BigDecimal
class MessageBackupsFlowViewModel : ViewModel() {
private val internalStateFlow = MutableStateFlow(
@ -125,8 +128,13 @@ class MessageBackupsFlowViewModel : ViewModel() {
internalStateFlow.update { it.copy(selectedPaymentMethod = paymentMethod) }
}
fun onMessageBackupTierUpdated(messageBackupTier: MessageBackupTier) {
internalStateFlow.update { it.copy(selectedMessageBackupTier = messageBackupTier) }
fun onMessageBackupTierUpdated(messageBackupTier: MessageBackupTier, messageBackupTierLabel: String) {
internalStateFlow.update {
it.copy(
selectedMessageBackupTier = messageBackupTier,
selectedMessageBackupTierLabel = messageBackupTierLabel
)
}
}
fun onCancellationComplete() {
@ -187,6 +195,8 @@ class MessageBackupsFlowViewModel : ViewModel() {
internalStateFlow.update { it.copy(inAppPayment = null) }
}
val currency = SignalStore.inAppPayments.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.BACKUP)
SignalDatabase.inAppPayments.clearCreated()
val id = SignalDatabase.inAppPayments.insert(
type = InAppPaymentType.RECURRING_BACKUP,
@ -195,8 +205,8 @@ class MessageBackupsFlowViewModel : ViewModel() {
endOfPeriod = null,
inAppPaymentData = InAppPaymentData(
badge = null,
label = backupsType.title,
amount = backupsType.pricePerMonth.toFiatValue(),
label = state.selectedMessageBackupTierLabel!!,
amount = if (backupsType is MessageBackupsType.Paid) backupsType.pricePerMonth.toFiatValue() else FiatMoney(BigDecimal.ZERO, currency).toFiatValue(),
level = SubscriptionsConfiguration.BACKUPS_LEVEL.toLong(),
recipientId = Recipient.self().id.serialize(),
paymentMethodType = state.selectedPaymentMethod!!,

View file

@ -6,7 +6,6 @@
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import androidx.compose.runtime.Stable
import kotlinx.collections.immutable.ImmutableList
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
@ -14,9 +13,20 @@ import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
* Represents a type of backup a user can select.
*/
@Stable
data class MessageBackupsType(
val tier: MessageBackupTier,
val pricePerMonth: FiatMoney,
val title: String,
val features: ImmutableList<MessageBackupsTypeFeature>
)
sealed interface MessageBackupsType {
val tier: MessageBackupTier
data class Paid(
val pricePerMonth: FiatMoney,
val storageAllowanceBytes: Long
) : MessageBackupsType {
override val tier: MessageBackupTier = MessageBackupTier.PAID
}
data class Free(
val mediaRetentionDays: Int
) : MessageBackupsType {
override val tier: MessageBackupTier = MessageBackupTier.FREE
}
}

View file

@ -35,6 +35,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.SpanStyle
@ -49,10 +50,12 @@ import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.SignalPreview
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.bytes
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.ByteUnit
import java.math.BigDecimal
import java.util.Currency
@ -252,12 +255,15 @@ fun MessageBackupsTypeBlock(
) {
Column {
Text(
text = formatCostPerMonth(messageBackupsType.pricePerMonth),
text = getFormattedPricePerMonth(messageBackupsType),
style = MaterialTheme.typography.titleSmall
)
Text(
text = messageBackupsType.title,
text = when (messageBackupsType) {
is MessageBackupsType.Free -> pluralStringResource(id = R.plurals.MessageBackupsTypeSelectionScreen__text_plus_d_days_of_media, messageBackupsType.mediaRetentionDays, messageBackupsType.mediaRetentionDays)
is MessageBackupsType.Paid -> stringResource(id = R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media)
},
style = MaterialTheme.typography.titleMedium
)
@ -273,7 +279,7 @@ fun MessageBackupsTypeBlock(
.padding(top = 8.dp)
.padding(horizontal = 16.dp)
) {
messageBackupsType.features.forEach {
getFeatures(messageBackupsType = messageBackupsType).forEach {
MessageBackupsTypeFeatureRow(messageBackupsTypeFeature = it, iconTint = featureIconTint)
}
}
@ -290,53 +296,73 @@ fun MessageBackupsTypeBlock(
}
@Composable
private fun formatCostPerMonth(pricePerMonth: FiatMoney): String {
return if (pricePerMonth.amount == BigDecimal.ZERO) {
"Free"
} else {
"${FiatMoneyUtil.format(LocalContext.current.resources, pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())}/month"
private fun getFormattedPricePerMonth(messageBackupsType: MessageBackupsType): String {
return when (messageBackupsType) {
is MessageBackupsType.Free -> stringResource(id = R.string.MessageBackupsTypeSelectionScreen__free)
is MessageBackupsType.Paid -> {
val formattedAmount = FiatMoneyUtil.format(LocalContext.current.resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
stringResource(id = R.string.MessageBackupsTypeSelectionScreen__s_month, formattedAmount)
}
}
}
@Composable
private fun getFeatures(messageBackupsType: MessageBackupsType): List<MessageBackupsTypeFeature> {
return when (messageBackupsType) {
is MessageBackupsType.Free -> persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = stringResource(id = R.string.MessageBackupsTypeSelectionScreen__full_text_message_backup)
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = pluralStringResource(
id = R.plurals.MessageBackupsTypeSelectionScreen__last_d_days_of_media,
count = messageBackupsType.mediaRetentionDays,
messageBackupsType.mediaRetentionDays
)
)
)
is MessageBackupsType.Paid -> {
val photoCount = messageBackupsType.storageAllowanceBytes / ByteUnit.MEGABYTES.toBytes(2)
val photoCountThousands = photoCount / 1000
val (count, size) = messageBackupsType.storageAllowanceBytes.bytes.getLargestNonZeroValue()
persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = stringResource(id = R.string.MessageBackupsTypeSelectionScreen__full_text_message_backup)
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = stringResource(id = R.string.MessageBackupsTypeSelectionScreen__full_media_backup)
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = stringResource(
id = R.string.MessageBackupsTypeSelectionScreen__s_of_storage_s_photos,
"${count}${size.label}",
"~${photoCountThousands}K"
)
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
label = stringResource(id = R.string.MessageBackupsTypeSelectionScreen__thanks_for_supporting_signal)
)
)
}
}
}
fun testBackupTypes(): List<MessageBackupsType> {
return listOf(
MessageBackupsType(
tier = MessageBackupTier.FREE,
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
title = "Text + 30 days of media",
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Last 30 days of media"
)
)
MessageBackupsType.Free(
mediaRetentionDays = 30
),
MessageBackupsType(
tier = MessageBackupTier.PAID,
MessageBackupsType.Paid(
pricePerMonth = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")),
title = "Text + All your media",
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Full media backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "1TB of storage (~250K photos)"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
label = "Thanks for supporting Signal!"
)
)
storageAllowanceBytes = 107374182400
)
)
}

View file

@ -42,13 +42,13 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import androidx.fragment.app.setFragmentResultListener
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import kotlinx.collections.immutable.persistentListOf
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@ -66,19 +66,19 @@ import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.BackupFrequency
import org.thoughtcrime.securesms.backup.v2.BackupV2Event
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.components.settings.app.chats.backups.type.BackupsTypeSettingsFragment
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentCheckoutLauncher.createBackupsCheckoutLauncher
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.conversation.v2.registerForLifecycle
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.viewModel
import java.math.BigDecimal
import java.util.Currency
import java.util.Locale
/**
@ -418,14 +418,29 @@ private fun BackupTypeRow(
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} else {
} else if (messageBackupsType is MessageBackupsType.Paid) {
val localResources = LocalContext.current.resources
val formattedCurrency = remember(messageBackupsType.pricePerMonth) {
FiatMoneyUtil.format(localResources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
}
Text(
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__s_dot_s_per_month, messageBackupsType.title, formattedCurrency)
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__s_dot_s_per_month, stringResource(id = R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media), formattedCurrency)
)
} else {
val retentionDays = (messageBackupsType as MessageBackupsType.Free).mediaRetentionDays
val localResources = LocalContext.current.resources
val formattedCurrency = remember {
val currency = SignalStore.inAppPayments.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.BACKUP)
FiatMoneyUtil.format(localResources, FiatMoney(BigDecimal.ZERO, currency), FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
}
Text(
text = stringResource(
id = R.string.RemoteBackupsSettingsFragment__s_dot_s_per_month,
pluralStringResource(id = R.plurals.MessageBackupsTypeSelectionScreen__text_plus_d_days_of_media, retentionDays, retentionDays),
formattedCurrency
)
)
}
}
@ -680,11 +695,8 @@ private fun RemoteBackupsSettingsContentPreview() {
private fun BackupTypeRowPreview() {
Previews.Preview {
BackupTypeRow(
messageBackupsType = MessageBackupsType(
tier = MessageBackupTier.FREE,
title = "Free",
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
features = persistentListOf()
messageBackupsType = MessageBackupsType.Free(
mediaRetentionDays = 30
),
onChangeBackupsTypeClick = {},
onEnableBackupsClick = {}

View file

@ -20,11 +20,11 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import androidx.navigation.fragment.findNavController
import kotlinx.collections.immutable.persistentListOf
import org.signal.core.ui.Previews
import org.signal.core.ui.Rows
import org.signal.core.ui.Scaffolds
@ -33,16 +33,16 @@ import org.signal.core.util.money.FiatMoney
import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentCheckoutLauncher.createBackupsCheckoutLauncher
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.viewModel
import java.math.BigDecimal
import java.util.Currency
import java.util.Locale
/**
@ -161,8 +161,18 @@ private fun BackupsTypeRow(
nextRenewalTimestamp: Long
) {
val resources = LocalContext.current.resources
val formattedAmount = remember(messageBackupsType.pricePerMonth) {
FiatMoneyUtil.format(resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
val formattedAmount = remember(messageBackupsType) {
val amount = when (messageBackupsType) {
is MessageBackupsType.Paid -> messageBackupsType.pricePerMonth
else -> FiatMoney(BigDecimal.ZERO, SignalStore.inAppPayments.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.BACKUP))
}
FiatMoneyUtil.format(resources, amount, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
}
val title = when (messageBackupsType) {
is MessageBackupsType.Paid -> stringResource(id = R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media)
is MessageBackupsType.Free -> pluralStringResource(id = R.plurals.MessageBackupsTypeSelectionScreen__text_plus_d_days_of_media, count = messageBackupsType.mediaRetentionDays, messageBackupsType.mediaRetentionDays)
}
val renewal = remember(nextRenewalTimestamp) {
@ -171,7 +181,7 @@ private fun BackupsTypeRow(
Rows.TextRow(text = {
Column {
Text(text = messageBackupsType.title)
Text(text = title)
Text(
text = stringResource(id = R.string.BackupsTypeSettingsFragment__s_month_renews_s, formattedAmount, renewal),
style = MaterialTheme.typography.bodyMedium,
@ -212,11 +222,8 @@ private fun BackupsTypeSettingsContentPreview() {
Previews.Preview {
BackupsTypeSettingsContent(
state = BackupsTypeSettingsState(
messageBackupsType = MessageBackupsType(
tier = MessageBackupTier.FREE,
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
title = "Free",
features = persistentListOf()
messageBackupsType = MessageBackupsType.Free(
mediaRetentionDays = 30
)
),
contentCallbacks = object : ContentCallbacks {}

View file

@ -7458,6 +7458,30 @@
<string name="MessageBackupsTypeSelectionScreen__change_backup_type">Change backup type</string>
<!-- Secondary action button label when selecting a backup tier with a current selection -->
<string name="MessageBackupsTypeSelectionScreen__cancel_subscription">Cancel subscription</string>
<!-- MessageBackupsType block amount for free tier -->
<string name="MessageBackupsTypeSelectionScreen__free">Free</string>
<!-- MessageBackupsType block amount for paid tier. Placeholder is formatted currency amount. -->
<string name="MessageBackupsTypeSelectionScreen__s_month">%1$s/month</string>
<!-- Title for free tier -->
<string name="MessageBackupsTypeSelectionScreen__text_plus_all_your_media">Text + all your media</string>
<!-- Title for paid tier. Placeholder is days of media retention. -->
<plurals name="MessageBackupsTypeSelectionScreen__text_plus_d_days_of_media">
<item quantity="one">Text + %1$d day of media</item>
<item quantity="other">Text + %1$d days of media</item>
</plurals>
<!-- Description text for text history feature -->
<string name="MessageBackupsTypeSelectionScreen__full_text_message_backup">Full text message backup</string>
<!-- Description text for paid tier media retention -->
<string name="MessageBackupsTypeSelectionScreen__full_media_backup">Full media backup</string>
<!-- Description text for storage space for paid tier media. Placeholder 1 is for byte amount, placeholder 2 is for a photo count estimate -->
<string name="MessageBackupsTypeSelectionScreen__s_of_storage_s_photos">%1$s of storage (%2$s photos)</string>
<!-- Description text for thanks -->
<string name="MessageBackupsTypeSelectionScreen__thanks_for_supporting_signal">Thanks for supporting Signal!</string>
<!-- Description text for free tier media retention. Placeholder is retention day count. -->
<plurals name="MessageBackupsTypeSelectionScreen__last_d_days_of_media">
<item quantity="one">Last %1$d day of media</item>
<item quantity="other">Last %1$d days of media</item>
</plurals>
<!-- ConfirmBackupCancellationDialog -->
<!-- Dialog title -->

View file

@ -29,6 +29,12 @@ inline val Long.gibiBytes: ByteSize
inline val Int.gibiBytes: ByteSize
get() = (this * 1024).mebiBytes
inline val Long.tebiBytes: ByteSize
get() = (this * 1024).gibiBytes
inline val Int.tebiBytes: ByteSize
get() = (this * 1024).gibiBytes
class ByteSize(val bytes: Long) {
val inWholeBytes: Long
get() = bytes
@ -42,6 +48,9 @@ class ByteSize(val bytes: Long) {
val inWholeGibiBytes: Long
get() = inWholeMebiBytes / 1024
val inWholeTebiBytes: Long
get() = inWholeGibiBytes / 1024
val inKibiBytes: Float
get() = bytes / 1024f
@ -50,4 +59,25 @@ class ByteSize(val bytes: Long) {
val inGibiBytes: Float
get() = inMebiBytes / 1024f
val inTebiBytes: Float
get() = inGibiBytes / 1024f
fun getLargestNonZeroValue(): Pair<Long, Size> {
return when {
inWholeTebiBytes > 0L -> inWholeTebiBytes to Size.TEBIBYTE
inWholeGibiBytes > 0L -> inWholeGibiBytes to Size.GIBIBYTE
inWholeMebiBytes > 0L -> inWholeMebiBytes to Size.MEBIBYTE
inWholeKibiBytes > 0L -> inWholeKibiBytes to Size.KIBIBYTE
else -> inWholeBytes to Size.BYTE
}
}
enum class Size(val label: String) {
BYTE("B"),
KIBIBYTE("KB"),
MEBIBYTE("MB"),
GIBIBYTE("GB"),
TEBIBYTE("TB")
}
}

View file

@ -35,6 +35,9 @@ public class SubscriptionsConfiguration {
@JsonProperty("sepaMaximumEuros")
private BigDecimal sepaMaximumEuros;
@JsonProperty("backup")
private BackupConfiguration backupConfiguration;
public static class CurrencyConfiguration {
@JsonProperty("minimum")
private BigDecimal minimum;
@ -88,6 +91,31 @@ public class SubscriptionsConfiguration {
}
}
public static class BackupConfiguration {
@JsonProperty("levels")
private Map<Integer, BackupLevelConfiguration> backupLevelConfigurationMap;
@JsonProperty("backupFreeTierMediaDays")
private int freeTierMediaDays;
public Map<Integer, BackupLevelConfiguration> getBackupLevelConfigurationMap() {
return backupLevelConfigurationMap;
}
public int getFreeTierMediaDays() {
return freeTierMediaDays;
}
}
public static class BackupLevelConfiguration {
@JsonProperty("storageAllowanceBytes")
private long storageAllowanceBytes;
public long getStorageAllowanceBytes() {
return storageAllowanceBytes;
}
}
public Map<String, CurrencyConfiguration> getCurrencies() {
return currencies;
}
@ -99,4 +127,8 @@ public class SubscriptionsConfiguration {
public BigDecimal getSepaMaximumEuros() {
return sepaMaximumEuros;
}
public BackupConfiguration getBackupConfiguration() {
return backupConfiguration;
}
}