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
)
)
}