diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index 32af07859c..587d11f584 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -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() } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutSheet.kt index c785c793de..14c61d12d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutSheet.kt @@ -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, sheetState: SheetState, onDismissRequest: () -> Unit, @@ -78,7 +76,7 @@ fun MessageBackupsCheckoutSheet( @Composable private fun SheetContent( - messageBackupsType: MessageBackupsType, + messageBackupsType: MessageBackupsType.Paid, availablePaymentGateways: List, 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 = {} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt index 62b0e0bc45..e0f888feaa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowFragment.kt @@ -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().first { it.tier == state.selectedMessageBackupTier!! }, availablePaymentMethods = state.availablePaymentMethods, sheetState = checkoutSheetState, onDismissRequest = { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt index e6b12d47cb..ffa0f48d4c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowState.kt @@ -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 = emptyList(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt index 3e5ef0e4a2..811e3088f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt @@ -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!!, diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsType.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsType.kt index e808a72b4c..5083134fc8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsType.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsType.kt @@ -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 -) +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 + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt index 1794f572a1..b1358fc888 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsTypeSelectionScreen.kt @@ -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 { + 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 { 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 ) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsFragment.kt index 9cb0716712..f1b0dd332e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/RemoteBackupsSettingsFragment.kt @@ -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 = {} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsFragment.kt index 8b0ef8a528..9c00a21228 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/chats/backups/type/BackupsTypeSettingsFragment.kt @@ -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 {} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cc9c632965..613115ee46 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7458,6 +7458,30 @@ Change backup type Cancel subscription + + Free + + %1$s/month + + Text + all your media + + + Text + %1$d day of media + Text + %1$d days of media + + + Full text message backup + + Full media backup + + %1$s of storage (%2$s photos) + + Thanks for supporting Signal! + + + Last %1$d day of media + Last %1$d days of media + diff --git a/core-util-jvm/src/main/java/org/signal/core/util/ByteExtensions.kt b/core-util-jvm/src/main/java/org/signal/core/util/ByteExtensions.kt index 5b8f5b9a8b..ad7ec621c3 100644 --- a/core-util-jvm/src/main/java/org/signal/core/util/ByteExtensions.kt +++ b/core-util-jvm/src/main/java/org/signal/core/util/ByteExtensions.kt @@ -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 { + 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") + } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/SubscriptionsConfiguration.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/SubscriptionsConfiguration.java index 27de4d719a..9953f4c5a0 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/SubscriptionsConfiguration.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/SubscriptionsConfiguration.java @@ -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 backupLevelConfigurationMap; + + @JsonProperty("backupFreeTierMediaDays") + private int freeTierMediaDays; + + public Map getBackupLevelConfigurationMap() { + return backupLevelConfigurationMap; + } + + public int getFreeTierMediaDays() { + return freeTierMediaDays; + } + } + + public static class BackupLevelConfiguration { + @JsonProperty("storageAllowanceBytes") + private long storageAllowanceBytes; + + public long getStorageAllowanceBytes() { + return storageAllowanceBytes; + } + } + public Map getCurrencies() { return currencies; } @@ -99,4 +127,8 @@ public class SubscriptionsConfiguration { public BigDecimal getSepaMaximumEuros() { return sepaMaximumEuros; } + + public BackupConfiguration getBackupConfiguration() { + return backupConfiguration; + } } \ No newline at end of file